Adds the server-side seams the tournament engine will plug into:
- Dispatch ReportMatchResult / AdvanceTournament /
TournamentSettingsSelect in processGameCommand; base implementations
return RespContextError so regular games reject them safely
- Server_AbstractPlayer::setDeck lets a backend install a deck into a
player slot directly
- Server_Room::getUserInterfaceByName resolves a room user by name for
automated game setup
Took 24 minutes
The client already learns a game is a tournament from Event_GameJoined's
ServerInfo_Game.is_tournament, so mirroring it on every Event_GameStateChanged
is dead wire data — nothing on the client reads it. Removed.
- TournamentPlayer/TournamentPairing/TournamentSettings move into
serverinfo_tournament.proto, imported by both the tournament event and
the tournament game commands, so Command_CreateGame reaches them without
crossing the command/event boundary
- Command_CreateGame carries the full TournamentSettings instead of a
duplicate games_per_match field, keeping game creation and mid-game
changes on one wire representation
- draw results are explicit: Command_ReportMatchResult and
TournamentPairing gain is_draw, so a report stays distinguishable from
an unset -1 winner
- pairing match-wins are uint32 like the other game counts
Adds the protocol layer for the tournament game mode:
- Event_TournamentState (2027) carrying phase, round counters, player
standings, pairings, and settings (games_per_match)
- Command_ReportMatchResult / Command_AdvanceTournament /
Command_TournamentSettingsSelect game command extensions (1037-1039)
- is_tournament flag on Command_CreateGame and ServerInfo_Game;
is_tournament and parent_game_id on Event_GameStateChanged
Took 13 minutes
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>
* [Security] Add per-address rate limiting for auth endpoints
Introduce a thread-safe RateLimiter that tracks attempts per key
(IP address) within a sliding time window, and wire it into the
authentication endpoints:
- Login: failed login attempts from an address are counted; once the
configured maximum is exceeded within the window, further logins
from that address are rejected with RespTooManyRequests. A
successful login clears the failed attempts for that address.
- Registration: implement the previously stubbed
tooManyRegistrationAttempts, limiting how many accounts can be
created per address per window.
- Forgot-password: throttle both the email-request and the
email-challenge paths per address.
New [security] settings with defaults:
max_login_attempts_per_ip=5 / login_attempt_window_seconds=900
max_registrations_per_ip=2 / registration_window_seconds=3600
max_forgot_password_requests_per_ip=3 / forgot_password_window_seconds=3600
Adds unit tests for the RateLimiter (window limit, over-limit
blocking, clearing, per-key independence).
Took 3 minutes
* [Server] Fix rate limiter: disable semantics, draining window, bounded keys, pre-work login gate
- recordAttempt()/isBlocked() return false when maxAttempts <= 0 or windowSeconds <= 0, so the documented 'set to 0 to disable' actually disables instead of blocking every address permanently
- Keys at their limit are rejected without recording, so repeated attempts at the attacker's pace cannot keep sliding the window and holding the lockout open; a block now clears once the recorded attempts age out of windowSeconds
- Attempts map is bounded: an opportunistic prune (every 60s) erases keys whose attempts have fully aged out
- Login is throttled before the work: isLoginRateLimited() (a new virtual on Server, backed by isBlocked()) gates loginUser(), so a locked-out address no longer burns a database round trip and password verification per attempt
- recordAttemptAt()/isBlockedAt() time seams make the sliding-window behaviour deterministically testable; tests rewritten for the real semantics (no reliance on the maxAttempts=0 bug, no sleeps)
* [Server] Inform client of login/registration rate limit timeout
- Add retryAfterSeconds/retryAfterSecondsAt to RateLimiter to compute
seconds until oldest attempt exits the sliding window
- Expose loginRateLimitRemainingSeconds via Server/Servatrice
- Include denied_end_time (absolute epoch) in RespTooManyRequests for
login pre-auth and when transitioning to lockout after failed attempt
- Include denied_end_time in registration rate limit response
- Client: show retry time for login RespTooManyRequests
- Client: simplify registration RespTooManyRequests message to show
retry time when provided
- Add test for retryAfterSecondsAt
* [Client] Restore registration guidance and drop duplicated rate limit reason
The registration RespTooManyRequests case now only shows the retry-after
copy when the server actually sent a denied_end_time; the per-email account
cap path (no end time) falls back to the previous guidance about already
having an account and contacting the server operator.
Drop the canned denied_reason_str from the new login rate limit branches so
the client no longer prints the same sentence twice.
* [Server] Exempt trusted_sources from auth rate limits
Addresses in security/trusted_sources bypass per-address limits, matching
the existing max_users_per_address bypass in initSession. Without this,
Webatrice behind local nginx (all clients as 127.0.0.1) would lock out
every web user after 5 mistyped passwords, and cap the whole server at
2 registrations / 3 forgot-password requests per window.
Adds Servatrice::isRateLimitExempt helper; recordFailedLogin,
isLoginRateLimited, recordRegistrationAttempt and
recordForgotPasswordAttempt all check it.
# Conflicts:
# servatrice/src/servatrice.cpp
* [Server] Key login rate limit by (address, username)
A successful login cleared the whole per-IP failure history, so 4 bad
guesses plus 1 self-login (or guest UnknownUser login) from the same
address bypassed the limiter. Failures are now bucketed per
(address, lowercase username) and a success clears only its own pair.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Completer] Minimum size popup, fall back to compact design if not possible.
Took 15 minutes
Took 6 seconds
Took 5 minutes
* [Completer] Fix popup going offscreen by clamping in screen coordinates
The position clamping in applyPopupWidth() was comparing popup->pos()
(parent coordinates) against screen->availableGeometry() (screen
coordinates). When the parent widget is not at the screen origin the
clamping computed a position in the wrong coordinate space, placing the
popup offscreen. Convert to global coordinates first, clamp, then map
back to parent coordinates.
* [Completer] Clamp popup from geometry() in screen coordinates
setMinimumWidth() already widens the popup synchronously, so the old
width() < minWidth guard never ran and the popup grew past the right
screen edge. The popup is also a top-level Qt::Popup window, so move()
takes global coordinates and the mapFromGlobal round-trip moved it to
the top-left of the screen. Clamp x from geometry() after
setMinimumWidth() and move(QPoint(clampedX, popup->y())) directly.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle] Don't report a cancelled card database update as successful (#3470)
Took 3 minutes
* [Oracle] Only report cancel when nothing was saved yet
Rejecting the wizard after SaveSetsPage or a download page already
wrote the database to disk still replaced cards.xml on disk, but
wasCancelled() forced exit code 1 so Cockatrice skipped the reload.
Track successful saves with OracleWizard::markDatabaseSaved() and only
treat a cancel as a failure when nothing was saved.
* [Oracle] Restore updater stderr in card update error dialog
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Style] Confirm broken Windows styles with a dialog offering Fusion
* [Style] Address review nits on broken style dialog
Extract Fusion index lookup into findFusionIndex helper and add all
message box buttons before choosing the default button.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Tests] Scope loader matcher settings to the process to fix parallel-run flakes
* [Tests] Clean up per-process settings sandbox after loader matcher run
* [Tests] Skip loader matcher sandbox cleanup during gtest discovery
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Raise rubber band above count label and collapse multi-selection on click
Raise rubber band on start and collapse multi-selection to clicked card when left-clicking an already-selected card with no modifiers.
* [Game] Collapse multi-selection on release instead of press
Collapsing at press time dropped the selection before the drag started,
which broke dragging multiple cards. Keep the selection on press and
collapse it on release when the pointer stayed within the drag
distance.
* [Game] Defer selection collapse until the double-click window passes
A release without drag collapsed the multi-selection immediately, so a
double-click that followed (playSelected acts on scene()->selectedItems())
only ever saw the single clicked card. When the card handles
double-clicks on the selection, arm the collapse on a single-shot timer
for QApplication::doubleClickInterval() instead and cancel it from the
double-click handler; a new press disarms the other cards and a
selection-count guard covers rubber-band/ctrl changes meanwhile.
* [Game] Keep sideboard multi-selection on double-click
DeckViewCard overrides doubleClickActsOnSelection to preserve the
selection, restoring master behavior where double-clicking moves all
selected cards between main and side. DeckView cancels any pending
collapse before reading the selection.
* [Game] Remove duplicated inline selection-collapse declarations
* [Game] Fix DeckView collapse call visibility and multi-selection collapse
Make cancelPendingSelectionCollapse() public so DeckView can call it on
selected DeckViewCards. Always arm the deferred collapse on release
instead of skipping cards whose double-click acts on the selection;
the double-click-interval timer plus the cancel calls in
CardItem::mouseDoubleClickEvent and DeckView::mouseDoubleClickEvent
already preserve the selection for double-clicks.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The A/B run measured Ninja against the Visual Studio generator across
ten builds: Ninja medians sit at ~17 min against ~21 for MSBuild, and
more importantly Ninja already matches the Linux and macOS legs, runs
the compiler itself and parallelizes the whole graph instead of one
MSBuild node at a time. Single runs on shared 4-core hosts swing by
minutes either way (VS and Ninja legs even ran in different Azure
regions inside one matrix), so this switches on medians and
engineering coherence, not on any one timing.
The Ninja leg is now the release leg: it keeps the -Win10 suffix and
the vcpkg scope (named after the vs2026 toolchain, whose cl.exe is
unchanged, so the warm archives keep restoring), uploads PDBs from the
single-config layout and ships installers to releases. The experiment
scaffolding goes away with the generator it measured.
cmake --build only passes a job count to MSBuild when it is asked for
one, and .ci/compile.sh never asked, so every Visual Studio generator
build ran single-node: projects were built one after another and MTT
only ever parallelized the files of whichever project happened to be
building. Ask for one node per core, overridable with
BUILD_PARALLEL_LEVEL, and fall back to a single node when the core
count is not exposed.
Alongside it set EnforceProcessCountAcrossBuilds and CL_MPCount, the
knobs Microsoft's build throughput write-up pairs with MTT: without
them the node count and MTT multiply into more cl.exe processes than
the runner has cores for.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The Windows job spends about 28 minutes inside one "Build project"
group, with nothing in it that says where the time goes: MSBuild runs
single-node because no --parallel is passed, so projects build one
after another and it is not visible which projects, tasks or phases are
responsible.
Add a --profile flag that makes the Windows build write
-flp:PerformanceSummary and -bl, repeat the summary in the job log so
the numbers need no download, and report the core count, which decides
how much parallelism is worth asking for. The binary log is uploaded as
an artifact only on the runs that asked for it.
The workflow drives it from a build_profile matrix key that is empty by
default, so no build changes behaviour until someone sets it.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CI] Key the vcpkg binary cache per ABI and let pull requests heal it
The cache had no prefix, and TAServers/vcpkg-cache restores every entry it
finds under the prefix rather than only the hashes vcpkg asks for. With all
legs sharing the single vcpkg/ namespace, each of the five of them downloads
all 66 archives of all five on every run. Each restore also refreshes the
entry's last access date, so a dead ABI generation stays fresh forever and
GitHub's least recently used eviction can never reach it.
Name the scope in the key after the platform, target and toolchain it belongs
to, so a leg restores only its own archives. macOS 14, 15 and 26 on the same
Xcode build identical binaries when they share the lowest deployment target,
so point all three at 14 and let them share one scope instead of three.
A pull request cannot write the shared cache either. Its entries are scoped to
refs/pull/<n>/merge, so nothing it saves is ever readable again, and letting
it write anyway only burns the repository's quota, which is what the previous
attempt at this did. Keep the build read-only and hand the archives it had to
build itself to a new workflow running in the default branch scope, which
saves them where master can restore them. Forks stay read-only, they are the
one case whose archives must not be trusted.
Because the prefix changes every key, add a job that deletes the entries of
retired scopes, which is the one-time clean up of the current flat namespace,
and the per-run ones pull requests leave behind, which age based eviction
also never reaches.
Also pin the Linux ccache cache to its own subdirectory, since the directory
is shared with the docker build context, and drop the clang-format install
from the build images, which format nothing at runtime.
* [CI] Create the vcpkg cache directory when a scope is still empty
* [CI] Let a pull request write the vcpkg archives it builds
* [CI] Upload the vcpkg handover as one archive instead of loose files
* [CI] Hand the vcpkg cache over from one leg per scope
* [CI] Gate vcpkg cache writes to master pushes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Command_DumpZone.number_cards is an unbounded sint32 supplied by the client, and
cmdDumpZone used it as an index without ever putting it in range.
for (int i = 0; (i < cards.size()) && (i < numberCards || numberCards == -1); ++i) {
const auto &findId = cmd.is_reversed() ? cards.size() - numberCards + i : i;
Server_Card *card = cards[findId];
A forward request happened to be safe, because the loop stopped at the zone's own size.
A reversed one was not. number_cards is documented by the client as -1 for "the whole
zone", and cards.size() - numberCards is therefore cardCount + 1, one past the last card.
A request larger than the zone puts the offset at a negative index instead. Both read
out of bounds, then dereference the result as a Server_Card, so either adjacent heap
memory or a crash.
Any player can reach it: the zone only has to be a public one, or the requester's own,
and both number_cards and is_reversed come straight off the wire. A reversed dump of -1
is enough on any public zone with cards in it.
Resolve the request to a half-open span up front. -1 and any positive count are clamped
to what the zone actually holds, any other negative count resolves to nothing, and a
reversed request takes the span from the end rather than computing an offset from a value
that may not be in range. cards.at() then carries the bound as a second line of defence,
and qsizetype keeps the loop and the list's own indexing type aligned.
Also report card_count as the zone's real size. It was
numberCards < cards.size() ? cards.size() : numberCards, which agrees with the zone size
for every request that was in range and reported the client's own oversized number
otherwise. The client uses card_count to draw placeholders when a zone lists no cards, so
overstating it invents cards that were never there.
Put the resolution in its own translation unit. server_abstract_player.cpp refers to the
RNG global that only servatrice's main.cpp defines, so anything linking that object needs
the whole server binary behind it, which is why no test could reach this code before.
server_dump_zone_test covers each request shape and then asserts the span is contained in
[0, cardCount) across the edges of the request domain, INT_MIN and INT_MAX included.
Reverting the arithmetic makes three of them fail, the two out-of-bounds shapes and the
containment sweep.
The client's own copy of this convention at view_zone.cpp:93 already clamps the way this
now does. Leaving that duplication for a follow-up, since the client reaching the same
answer by a different route is not a defect.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [GameSelector] Persist quick-filter toolbar edits and repair filter settings migration
- Save game-filter parameters on toolbar changes, debounced with flush on close
- Repair pre-camelCase gamefilters.ini keys in place on startup
- Map the legacy creator-name filter to hostNameFilter
- Balance begin/endFilterChange in GamesProxyModel::refresh
- Align the hideIgnoredUserGames default between model and settings
- Cover the migration with regression tests
* [Review] Address RickyRister comments on filter persistence
- Rename game-type prefix constants to SCREAMING_SNAKE to match
MIGRATION_SENTINEL_KEY and project style
- Document why GameSelector destructor flushes a pending debounced
filter save (toolbar path has no other writer)
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CardDB] Reload when the first run enables all sets
checkUnknownSets() enabled every set when none was enabled, but the
database had already been parsed under that empty enablement, so the
parsers had stripped every printing. It only notified the models and never
reloaded, leaving the deck editor's card database display empty until the
next launch.
Route the all-sets-enabled case through the same reload the new-sets path
uses, and skip caching a snapshot whose sets are all disabled.
* [CardDB] Own checkUnknownSets after startup and fix the cache skip
The loader's initialLoadComplete guard skipped checkUnknownSets() on the
first successful load of the session, so a download that landed after
startupConfigCheck() and the onboarding wizard had already run (e.g.
skipping the wizard download and fetching later from the menu) left every
set disabled and the deck editor blank until the next launch. The guard
also let the check run on the worker before the queued snapshot swap, and
alongside reloadCardDatabasesAndNotify()'s own check, double-emitting
cardDatabaseNewSetsFound on manual reloads.
Move the check to the callers that load after startup: the post-update,
custom-import, and spoiler paths run it via a QFutureWatcher once the
finished snapshot has been swapped in, and the loader's synchronous
path-change reload runs it directly.
Judge the cache skip by the parsed sets' own enablement, what the parsers
actually saw, instead of getEnabledSetNames(), which reports empty for the
noop controller even though it enables every set. Wait for the first-run
reparse before opening Manage Sets so the dialog cannot bind to the
pre-reload set instances.
* [CardDB] Queue post-reload check and unblock first-run Manage Sets
reloadCardDatabasesAndNotify() no longer emits cardDatabaseEnabledSetsChanged after the swap's Reset already rebuilt the models (second 36k rebuild froze startup), and its follow-up checkUnknownSets() is queued to the GUI thread so it sees swapped-in data. cardDatabaseAllNewSetsEnabled() chains Welcome/Manage Sets off a QFutureWatcher instead of waitForFinished() blocking the event loop during the reparse.
* [CardDB] Batch the first-run enable-all into a single settings flush
checkUnknownSets() enabled every set with ~3 QSettings rewrite cycles per set (sort key, known, enabled), and each setValue re-opens and re-syncs the whole cardDatabase.ini. With a full database (874 sets) that froze the GUI thread for ~9s after the database download (LagMonitor: event loop stalled for 9129 ms, measured offscreen with a real 59MB cards.xml), reading as a total application hang that recovers on its own.
Persist the enable-all through the controller's batched saveSets() in a single flush instead, mirroring what SetsModel::save() already does: CardSetList::enableAllBatched() assigns fresh sort keys and marks every set known+enabled in memory, then writes all records at once. SetSaveData gains the isKnown flag so the batch covers all three options, and SetsModel::save() passes the sets' known state through (unchanged behavior there).
Also drop the cardDatabaseEnabledSetsChanged emission from the all-sets-enabled branch: the reload chained off cardDatabaseAllNewSetsEnabled rebuilds the models from the re-parsed snapshot via cardDatabaseReset, so emitting both rebuilt the 36k-card models twice on the GUI thread.
Adds a cache-invalidation test driving the real checkUnknownSets() first-run path.
* [CardDB] Batch the new-sets-found enable-all the same way
The first-run batching fixed the 874-set case, but the new-sets-found dialog path (enableAllUnknownSets/markAllSetsAsKnown) still wrote per set and per option, each setValue re-opening and re-syncing the whole cardDatabase.ini. Users with hundreds of custom sets hit the same multi-second GUI stall when answering the dialog.
Persist both through the controller's batched saveSets() in a single flush, writing only changed sets (so the common no-op check skips the flush entirely). Removes the now-unused write-through CardSetList methods (enableAll, enableAllUnknown, markAllAsKnown, guessSortKeys) so the batched flush is the only way to persist bulk set changes.
Extends the test controller mock with per-write/batched-flush counters and adds tests asserting both dialog paths persist with exactly one flush and zero individual writes.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Third-party deck tools (e.g. Delver Lens) export decks as tab-separated rows
such as "2 Card Name Set Name". Card names never contain tab characters,
so a leading numeric column marks a tabular record: the first two columns are read
as the quantity and card name, and any further columns are discarded as
printer/set metadata.
- deck_list: parse tab-delimited rows before whitespace collapsing
- tests: cover tab-separated mainboard and sideboard imports
- docs: document the accepted plain-text import format (fills the TODO)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [SMTP] Add reliable email delivery with retry and database-backed queue
Rework the SMTP client from fire-and-forget sending into a persistent
queue with exponential backoff and per-message retry budget, backed by
the existing database rows as the durable source of truth.
- Keep pending emails in an in-memory queue with attempts/backoff
tracking; enqueue without deleting database rows, and only remove the
row once delivery is confirmed via the mailDelivered signal.
- Add exponential backoff (doubling, capped at 30 min) between delivery
attempts, bounded by new smtp/maxretries and smtp/retrydelay settings.
- Track connection-layer failures (connect/auth/encrypt) with their own
backoff; route failures to recordFailed/recordConnectionFailure so
undeliverable mail eventually drops from the queue instead of
retrying forever.
- Treat rejected recipients/senders as permanent failures.
- Rebuild the SMTP buffer from the authoritative queue before each send
via a new QxtSmtp::reset() so reconnects never duplicate deliveries.
- Start the idle timeout after authentication, not on TCP connect, so
slow SSL/STARTTLS handshakes are not cut short.
* [SMTP] Fix retry accounting, backoff reset, and token dedupe
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Spectator join/leave events were triggering the game tab's attention
indicator (exclamation mark and taskbar alert) for players who often
leave the tab open while waiting for an opponent. This made spectator
activity a false alarm.
Only emit the user event when an actual player joins or leaves, keeping
spectator joins/leaves quiet.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Show loaded deck messages to players joining a game (#552)
The "Player X has loaded a deck" message is only broadcast when a deck is
selected, so players and spectators who join a game afterwards never see it.
On game join, replay a deck-select event context privately to the joining
participant for every player that already has a deck loaded, reusing the
existing Event_PlayerPropertiesChanged + Context_DeckSelect mechanism so no
protocol version bump is needed.
- Server_AbstractPlayer: expose the sideboard-locked state
- Server_Game: synthesize deck-select contexts in createGameJoinedEvent
* [Server] Skip deck replay on rejoin and after start
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Invalidate view when hiding replaced card menu
Hide any visible card menu before replacing and invalidate viewport.
* [Game] Fix viewport update compile error in setCardMenu
QWidget::update has no QRectF overload, and sceneRect is in scene
coordinates rather than viewport coordinates anyway, so invalidate the
whole viewport instead.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
An open [[ card link now takes precedence over a space-preceded @
mention, so typing an @ before a card no longer shadows the card
completer.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
- Remove the third close option (Resolve with note...) so closing a
report is one of two clear actions
- Make Resolve always prompt for the optional resolution note, matching
Dismiss, and rename the button to Resolve...
- Halve the resolveReport signature now that every caller prompts
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Report] Add report reopen action for closed reports
- New Command_ReportReopen moderator command (extension 1206) with server
handler that reopens resolved/dismissed reports, resets resolution state
and notified, and records the reopening note in the audit log
- Reopen... button in the report queue action bar, gated to resolved and
dismissed reports, with optional note prompt and tooltip
- Success confirmations for Assign, Resolve/Dismiss and Reopen now survive
the list refresh via a pending status message instead of being clobbered
- Dispatch/auth coverage in server_moderator_role_test
* [Report] Surface resolution audit info in report details
- Add resolved_by_name to ServerInfo_Report and join resolved_by users in
the report list, my-list and details queries
- Render resolution note and resolved-at/by in the report detail pane
instead of duplicating the description into the chat log context
* [Report] Add context menu to report queue rows
- Open the standard UserContextMenu on right-click/Menu key for the
reported user, selecting the row first so the details panel matches
the menu target
- Carry the reported user's level in ServerInfo_Report so the menu
shows the same actions as the user list, with an IsUser fallback for
deleted or unregistered users
- Wire the Private chat action through TabReport to TabSupervisor and
retranslate the menu on live language switches
* [Server] Fix reported_user_level column indices in report queries
The SELECT lists resolved_by_name at index 10, shifting room_id to 11,
replay_id to 12, reported_user_id to 13 and reported_user_admin to 14.
The code read 12/13, so 'user exists' meant 'a replay exists' and the
admin flags came from the user id (e.g. id 5 -> admin+judge; users
without a replay reported as unregistered, hiding registered-only
actions). Fixed in cmdReportList and cmdReportMyList, plus the column
comment.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
- Add resolved_by_name to ServerInfo_Report and join resolved_by users in
the report list, my-list and details queries
- Render resolution note and resolved-at/by in the report detail pane
instead of duplicating the description into the chat log context
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
- New Command_ReportReopen moderator command (extension 1206) with server
handler that reopens resolved/dismissed reports, resets resolution state
and notified, and records the reopening note in the audit log
- Reopen... button in the report queue action bar, gated to resolved and
dismissed reports, with optional note prompt and tooltip
- Success confirmations for Assign, Resolve/Dismiss and Reopen now survive
the list refresh via a pending status message instead of being clobbered
- Dispatch/auth coverage in server_moderator_role_test
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The executables precompile Qt since #7235, but the libcockatrice_*
static libraries still reparse the full Qt Core stack in every TU.
Measured over the tree after the IWYU pass: 380 MB of header parse in
the libraries, 92% of it reachable from qtcore_pch.h.
- Apply qtcore_pch.h to the 14 Qt Core static library targets
(skips libcockatrice_rng: its SFMT.c is C and cannot use a C++
precompiled header).
- Grow qtcore_pch.h with 25 QtCore headers that IWYU shows are
re-parsed across many TUs and were not already reachable from the
existing list (JSON/QUuid cluster, QSettings, item models,
threading, XmlStream, QTemporaryDir, ...).
- Grow qtwidgets_pch.h with 30 QtGui/QtWidgets headers (dialog
cluster, item views, QImageReader, ...).
Only stable Qt headers go into the PCH, so it rarely invalidates.
Test executables stay PCH-less: 1-3 TUs per target does not amortize
the extra precompile.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Tests] Register each test case with CTest instead of each test binary
Registering a test cost four edits across a dozen CMakeLists files: add_test,
add_executable, a conditional add_dependencies on gtest, and target_link_libraries.
The conditional branch was the trap -- it is only needed when GTest was fetched and
add_subdirectory'd, and omitting it yields undefined gtest symbols at link time --
so it was easy to leave out and impossible to notice until the link failed.
Fold all four steps into cockatrice_add_test() and migrate every test directory
onto it, with a cockatrice_add_manual_test() wrapper for the three benchmarks that
are built but deliberately kept out of ctest. The calls now read as a declaration
of what a test is rather than a transcript of how to register one.
While rewriting the registrations, replace add_test(NAME x COMMAND x) with
gtest_discover_tests(). Naming the binary as a single CTest entry meant a CI failure
reported only which binary failed, and ctest -R Suite.Case could not select an
individual case. Discovery raises the suite from 35 opaque entries to 469 real ones:
ctest -R ThemeMigrationTest.IsIdempotent now runs exactly that case, and
ctest -L server / -LE bench work off labels applied at registration.
Give every test a 60s TIMEOUT. CTest's implicit default is 1500s, so one hung
socket wait would stall CI for 25 minutes, and single_instance_manager_test already
does real QLocalSocket waits.
Dropping per-case granularity costs wall-clock time, since the suite becomes
process-spawn bound rather than CPU bound: serial ctest went from 3.8s to 30.8s. Run
it with -j in CI to recover it -- 7.6s at -j4, and 3.5s at -j8, both at or below
the old serial time. The binaries are parallel-safe: the only test claiming a fixed
resource scopes its local socket name to its own pid, so concurrent copies cannot
collide.
Also delete dummy_test, which asserted 1 == 1 and only proved the harness was
wired up -- something the other 468 cases already establish.
No test is added, removed, or changed by this commit; the 469 cases are the same
ones the 35 binaries contained.
* [Tests] Make QtTest available to test targets
QtTest was not among the modules test targets could link, so the Qt model and
signal contracts had no way to be verified from a test: QAbstractItemModelTester
and QSignalSpy both live there.
Add Test to _TEST_NEEDED in cmake/FindQtRuntime.cmake. That is the single place the
union of test-only Qt modules is declared, so this is the only file that needs to
change to make the module reachable from every test target.
Nothing uses it yet. This lands the capability ahead of the model-layer tests that
need it, so those can be added as tests alone rather than as test plus build-system
plumbing.
* [Tests] Correct the note on the oracle test compression defines
add_definitions() populates the directory's COMPILE_DEFINITIONS property, which is
consumed at generate time, so it reaches every target in the directory no matter
where it is called relative to add_executable(). The previous note claimed the
flags stayed confined to the benchmark because the find_package block came after
it; oracle_importer_test, created above that block, carries both defines too.
Verified harmless rather than a live bug: HAS_ZLIB and HAS_LZMA gate code only in
oracle/src/pages.cpp and oracle/src/pagetemplates.cpp, and no test target compiles
either. The ordering and the leaked defines are both pre-existing, so this only
corrects the explanation.
* [Tests] Carry one CTest label per test instead of several
gtest_discover_tests() can hand CTest exactly one label. GoogleTestAddTests.cmake
accumulates its generated script with `foreach(arg ${ARGN})`, so a label list reaches
the CTestTestfile as bare `LABELS ci server`, and CTest reads only the first value of a
property and drops the rest without a word.
The `ci` label that is appended to every registered test was therefore absent from every
test that also asked for `server` or `bench`: `ctest -L ci` reported 400 of 469 cases,
and `ctest --print-labels` never listed a second label anywhere in the build.
Reduce each test to exactly one of `ci`, `server`, or `bench`, which is all the
selections need. The three now partition the suite exactly, 400 + 64 + 5 = 469, and
`ctest -LE bench` still means everything but benchmarks. A second label is now a
configure-time FATAL_ERROR rather than a line that quietly means something else.
Anything orthogonal to a test's kind belongs in a helper flag beside the label, so it
composes with the group instead of competing for the single slot.
Also raise DISCOVERY_TIMEOUT off GoogleTestAddTests' 5s default. A plain debug binary
answers --gtest_list_tests in 0.04s, but that budget is thin once several binaries
discover concurrently behind a -j build, and being over it fails at the *link* step with
a bare "Process terminated due to timeout" that points nowhere near the cause.
* [Tests] Link gtest_main on the fetched-gtest path
The downloaded-gtest branch set GTEST_BOTH_LIBRARIES to `gtest` alone, while
FindGTest sets it to both halves. Every test target links that list rather than
picking an entry point, so on this path nothing provides main() and any test
source that does not define its own fails at link with `undefined reference to
main` pointing at crt1.o, naming neither gtest nor the line responsible.
That stayed latent while every test carried its own four-line main(). A test that
omits it now fails, and the error points at the C runtime rather than at gtest,
which is a long way from the cause. Restoring both halves makes the two paths
agree and drops the boilerplate requirement on new test sources.
The 18 sources that still define main() keep linking: gtest_main.cc defines
nothing but main, so the linker leaves its object in the archive unused rather
than reporting a duplicate.
* [Build] Add an ENABLE_SANITIZERS option for ASan and UBSan
Adds -DENABLE_SANITIZERS, off by default, building with AddressSanitizer and
UndefinedBehaviorSanitizer. The bulk of the work is in the three ways the obvious
implementation silently does the wrong thing.
Probe the flags on the link step, not just the compile step. check_cxx_compiler_flag()
only compiles, so every sanitizer flag comes back unsupported, because linking an ASan
object without -fsanitize=address leaves __asan_init undefined. The result is not a
build failure but a build that appears instrumented and is not, which is worse. Use
check_cxx_source_compiles() with CMAKE_REQUIRED_LINK_OPTIONS instead, and treat an
unsupported sanitizer pair as fatal since the cosmetic flags are allowed to degrade.
Opt vendored sources out per source, through a cockatrice_disable_sanitizers_for()
helper rather than a list of set_source_files_properties() calls at the root. The
helper has to exist because CMake resolves a source-file property against the directory
scope its target was declared in: setting the property from the root with absolute paths
is a no-op, silently, which is why the call sites sit next to the add_library and
add_executable that pull the vendored code in. SFMT, minizip, the LZMA SDK and Qxt SMTP
are excluded. oracle/src/qt-json is in the history but not in the tree, removed in
#7214.
Also mark those sources SKIP_PRECOMPILE_HEADERS. The PCH is built with the sanitizer
flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with
-fno-sanitize=all does not and GCC then refuses the PCH, which is a fatal
-Werror=invalid-pch in the servatrice and oracle builds.
-fno-sanitize=vptr is not optional. Qt constructs QObject subclasses into raw storage
and writes the vptr afterwards, so the vptr check reports on essentially every QObject
instantiation, and it has no value here because nothing outside moc-generated code
inherits QObject.
Verified on the resulting build: our own translation units carry ASan and UBSan calls,
SFMT.o carries neither, and the full suite of 464 cases passes clean under
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 with UBSAN_OPTIONS=halt_on_error=0.
The test suite needs two adjustments to make this option usable at all, both of which
land here because they are consequences of COCKATRICE_SANITIZERS existing rather than
independent changes.
DISCOVERY_TIMEOUT rises from 60s to 300s under sanitizers. An instrumented binary takes
2.8s to answer --gtest_list_tests against 0.04s uninstrumented, and discovery runs
several binaries at once behind a -j build.
One target is excluded via a NO_SANITIZE flag, server_developer_role_test. Reaching
google::protobuf::TextFormat::Printer's destructor aborts on an absl assertion inside
system protobuf 33.4 whenever the consumer translation unit is built without NDEBUG,
which a Debug sanitizer build is. This reproduces with no Cockatrice code at all, just
"TextFormat::Printer p; p.PrintToString(msg, &out);" built with -fsanitize=address, so it
is upstream rather than ours. It is a flag and not a fourth label because "skip this in
the sanitizer leg" has to compose with a test's group rather than compete for the single
label slot, and because a flag excludes by not registering the cases at all, which
cannot rot the way a label silently can. The target still builds and still runs on the
legs without sanitizers.
* [CI] Run the suite once under ASan and UBSan
Adds --sanitize to .ci/compile.sh, mirroring --test, and turns it on for one entry of
the existing Linux Docker matrix: Ubuntu 26.04, which already had a test-enabled leg.
That reuses the image, the ccache layer and the existing step shape, so the CI diff is a
matrix key plus the argument plumbing. Debian 13 and Fedora 44 keep covering the
uninstrumented suite, so nothing is lost by instrumenting one leg.
Sanitizers stay off the release and package legs, which build with --release for
packaging: instrumenting those would change the shipped binaries' optimization profile
and build time for no benefit. macOS and Windows stay uninstrumented too, since ASan is
unreliable against the Qt build there and MSVC has no equivalent without /fsanitize=address.
--sanitize does not imply --test, so an instrumented build can be produced locally
without paying for ctest. When both are given, ctest runs with detect_leaks=0 because Qt
and the sanitizer runtimes both leak at exit by design and LeakSanitizer's default buries
real findings under hundreds of process-teardown reports, and with UBSan halt_on_error=0 so
one run collects many findings instead of stopping at the first. ASan keeps
abort_on_error=1, since a bad access leaves the process unusable and continuing only
produces a second, more confusing report from the wreckage.
* [CI] Publish a gcovr line-coverage report from the test run
Sanitizers tell us what a run did wrong; they say nothing about what it never
reached. This adds the other half: an instrumented build whose report says which
of our own lines no test executes.
Add an ENABLE_COVERAGE option that compiles with --coverage -O0 -g, mirroring the
ENABLE_SANITIZERS opt-in including the per-source carve-out that keeps vendored
trees out of the denominator. -O0 is deliberate: at higher optimisation levels GCC
merges and relocates lines, so the report would describe code that no longer exists
in that form. Only GCC is supported, since Clang's intermediate format needs
llvm-cov; asking for coverage under Clang is a hard error rather than a build that
silently produces unreadable .gcno files.
Keep the filters in a committed gcovr.cfg rather than in the invocation. The tests
themselves are excluded because they are compiled with the same instrumentation
and are only ever executed by the run doing the measuring, so leaving them in would
report a healthier project than we have. The process entry points and the client's
window chrome go too: each needs a display, an interactive terminal or a live
database before it does anything a headless test could observe, so they would
otherwise sit at 0% forever and bury real movement.
Give coverage its own matrix leg rather than a second flag on the sanitizer leg.
The two answer different questions, and combining them slows both down without
telling us more. The new variant key gives the leg its own Docker image and ccache
entry so the two legs cannot race on the same tag.
The leg publishes an HTML report with per-file pages, LCOV, Cobertura XML and a
JSON summary. gcovr is a report-time dependency rather than a build dependency, so
a missing one warns and skips instead of failing an otherwise green build.
* [CI] Resolve the coverage source root before the build dir cd
The previous commit computed SRC_ROOT after `cd "$BUILD_DIR"`, under a comment
claiming it ran before. `$0` is relative to the directory the script was invoked
from, so from inside the build dir `dirname "$0"` no longer resolves and the
subshell's `cd` fails. Under `set -e` that is fatal rather than an empty variable,
which took down every leg in the matrix, coverage or not, including the ones this
change does not otherwise touch.
Move it above the cd and guard it on MAKE_COVERAGE, since the coverage report is
the only consumer. Resolving it unconditionally meant a coverage-only detail was
paid for by every build in the fleet, and gave every one of them a way to fail.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Tests] Register each test case with CTest instead of each test binary
Registering a test cost four edits across a dozen CMakeLists files: add_test,
add_executable, a conditional add_dependencies on gtest, and target_link_libraries.
The conditional branch was the trap -- it is only needed when GTest was fetched and
add_subdirectory'd, and omitting it yields undefined gtest symbols at link time --
so it was easy to leave out and impossible to notice until the link failed.
Fold all four steps into cockatrice_add_test() and migrate every test directory
onto it, with a cockatrice_add_manual_test() wrapper for the three benchmarks that
are built but deliberately kept out of ctest. The calls now read as a declaration
of what a test is rather than a transcript of how to register one.
While rewriting the registrations, replace add_test(NAME x COMMAND x) with
gtest_discover_tests(). Naming the binary as a single CTest entry meant a CI failure
reported only which binary failed, and ctest -R Suite.Case could not select an
individual case. Discovery raises the suite from 35 opaque entries to 469 real ones:
ctest -R ThemeMigrationTest.IsIdempotent now runs exactly that case, and
ctest -L server / -LE bench work off labels applied at registration.
Give every test a 60s TIMEOUT. CTest's implicit default is 1500s, so one hung
socket wait would stall CI for 25 minutes, and single_instance_manager_test already
does real QLocalSocket waits.
Dropping per-case granularity costs wall-clock time, since the suite becomes
process-spawn bound rather than CPU bound: serial ctest went from 3.8s to 30.8s. Run
it with -j in CI to recover it -- 7.6s at -j4, and 3.5s at -j8, both at or below
the old serial time. The binaries are parallel-safe: the only test claiming a fixed
resource scopes its local socket name to its own pid, so concurrent copies cannot
collide.
Also delete dummy_test, which asserted 1 == 1 and only proved the harness was
wired up -- something the other 468 cases already establish.
No test is added, removed, or changed by this commit; the 469 cases are the same
ones the 35 binaries contained.
* [Tests] Make QtTest available to test targets
QtTest was not among the modules test targets could link, so the Qt model and
signal contracts had no way to be verified from a test: QAbstractItemModelTester
and QSignalSpy both live there.
Add Test to _TEST_NEEDED in cmake/FindQtRuntime.cmake. That is the single place the
union of test-only Qt modules is declared, so this is the only file that needs to
change to make the module reachable from every test target.
Nothing uses it yet. This lands the capability ahead of the model-layer tests that
need it, so those can be added as tests alone rather than as test plus build-system
plumbing.
* [Tests] Correct the note on the oracle test compression defines
add_definitions() populates the directory's COMPILE_DEFINITIONS property, which is
consumed at generate time, so it reaches every target in the directory no matter
where it is called relative to add_executable(). The previous note claimed the
flags stayed confined to the benchmark because the find_package block came after
it; oracle_importer_test, created above that block, carries both defines too.
Verified harmless rather than a live bug: HAS_ZLIB and HAS_LZMA gate code only in
oracle/src/pages.cpp and oracle/src/pagetemplates.cpp, and no test target compiles
either. The ordering and the leaked defines are both pre-existing, so this only
corrects the explanation.
* [Tests] Carry one CTest label per test instead of several
gtest_discover_tests() can hand CTest exactly one label. GoogleTestAddTests.cmake
accumulates its generated script with `foreach(arg ${ARGN})`, so a label list reaches
the CTestTestfile as bare `LABELS ci server`, and CTest reads only the first value of a
property and drops the rest without a word.
The `ci` label that is appended to every registered test was therefore absent from every
test that also asked for `server` or `bench`: `ctest -L ci` reported 400 of 469 cases,
and `ctest --print-labels` never listed a second label anywhere in the build.
Reduce each test to exactly one of `ci`, `server`, or `bench`, which is all the
selections need. The three now partition the suite exactly, 400 + 64 + 5 = 469, and
`ctest -LE bench` still means everything but benchmarks. A second label is now a
configure-time FATAL_ERROR rather than a line that quietly means something else.
Anything orthogonal to a test's kind belongs in a helper flag beside the label, so it
composes with the group instead of competing for the single slot.
Also raise DISCOVERY_TIMEOUT off GoogleTestAddTests' 5s default. A plain debug binary
answers --gtest_list_tests in 0.04s, but that budget is thin once several binaries
discover concurrently behind a -j build, and being over it fails at the *link* step with
a bare "Process terminated due to timeout" that points nowhere near the cause.
* [Tests] Link gtest_main on the fetched-gtest path
The downloaded-gtest branch set GTEST_BOTH_LIBRARIES to `gtest` alone, while
FindGTest sets it to both halves. Every test target links that list rather than
picking an entry point, so on this path nothing provides main() and any test
source that does not define its own fails at link with `undefined reference to
main` pointing at crt1.o, naming neither gtest nor the line responsible.
That stayed latent while every test carried its own four-line main(). A test that
omits it now fails, and the error points at the C runtime rather than at gtest,
which is a long way from the cause. Restoring both halves makes the two paths
agree and drops the boilerplate requirement on new test sources.
The 18 sources that still define main() keep linking: gtest_main.cc defines
nothing but main, so the linker leaves its object in the archive unused rather
than reporting a duplicate.
* [Build] Run the test suite under ASan and UBSan (#7400)
* [Build] Add an ENABLE_SANITIZERS option for ASan and UBSan
Adds -DENABLE_SANITIZERS, off by default, building with AddressSanitizer and
UndefinedBehaviorSanitizer. The bulk of the work is in the three ways the obvious
implementation silently does the wrong thing.
Probe the flags on the link step, not just the compile step. check_cxx_compiler_flag()
only compiles, so every sanitizer flag comes back unsupported, because linking an ASan
object without -fsanitize=address leaves __asan_init undefined. The result is not a
build failure but a build that appears instrumented and is not, which is worse. Use
check_cxx_source_compiles() with CMAKE_REQUIRED_LINK_OPTIONS instead, and treat an
unsupported sanitizer pair as fatal since the cosmetic flags are allowed to degrade.
Opt vendored sources out per source, through a cockatrice_disable_sanitizers_for()
helper rather than a list of set_source_files_properties() calls at the root. The
helper has to exist because CMake resolves a source-file property against the directory
scope its target was declared in: setting the property from the root with absolute paths
is a no-op, silently, which is why the call sites sit next to the add_library and
add_executable that pull the vendored code in. SFMT, minizip, the LZMA SDK and Qxt SMTP
are excluded. oracle/src/qt-json is in the history but not in the tree, removed in
#7214.
Also mark those sources SKIP_PRECOMPILE_HEADERS. The PCH is built with the sanitizer
flags, so it defines __SANITIZE_ADDRESS__; a translation unit compiled with
-fno-sanitize=all does not and GCC then refuses the PCH, which is a fatal
-Werror=invalid-pch in the servatrice and oracle builds.
-fno-sanitize=vptr is not optional. Qt constructs QObject subclasses into raw storage
and writes the vptr afterwards, so the vptr check reports on essentially every QObject
instantiation, and it has no value here because nothing outside moc-generated code
inherits QObject.
Verified on the resulting build: our own translation units carry ASan and UBSan calls,
SFMT.o carries neither, and the full suite of 464 cases passes clean under
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 with UBSAN_OPTIONS=halt_on_error=0.
The test suite needs two adjustments to make this option usable at all, both of which
land here because they are consequences of COCKATRICE_SANITIZERS existing rather than
independent changes.
DISCOVERY_TIMEOUT rises from 60s to 300s under sanitizers. An instrumented binary takes
2.8s to answer --gtest_list_tests against 0.04s uninstrumented, and discovery runs
several binaries at once behind a -j build.
One target is excluded via a NO_SANITIZE flag, server_developer_role_test. Reaching
google::protobuf::TextFormat::Printer's destructor aborts on an absl assertion inside
system protobuf 33.4 whenever the consumer translation unit is built without NDEBUG,
which a Debug sanitizer build is. This reproduces with no Cockatrice code at all, just
"TextFormat::Printer p; p.PrintToString(msg, &out);" built with -fsanitize=address, so it
is upstream rather than ours. It is a flag and not a fourth label because "skip this in
the sanitizer leg" has to compose with a test's group rather than compete for the single
label slot, and because a flag excludes by not registering the cases at all, which
cannot rot the way a label silently can. The target still builds and still runs on the
legs without sanitizers.
* [CI] Run the suite once under ASan and UBSan
Adds --sanitize to .ci/compile.sh, mirroring --test, and turns it on for one entry of
the existing Linux Docker matrix: Ubuntu 26.04, which already had a test-enabled leg.
That reuses the image, the ccache layer and the existing step shape, so the CI diff is a
matrix key plus the argument plumbing. Debian 13 and Fedora 44 keep covering the
uninstrumented suite, so nothing is lost by instrumenting one leg.
Sanitizers stay off the release and package legs, which build with --release for
packaging: instrumenting those would change the shipped binaries' optimization profile
and build time for no benefit. macOS and Windows stay uninstrumented too, since ASan is
unreliable against the Qt build there and MSVC has no equivalent without /fsanitize=address.
--sanitize does not imply --test, so an instrumented build can be produced locally
without paying for ctest. When both are given, ctest runs with detect_leaks=0 because Qt
and the sanitizer runtimes both leak at exit by design and LeakSanitizer's default buries
real findings under hundreds of process-teardown reports, and with UBSan halt_on_error=0 so
one run collects many findings instead of stopping at the first. ASan keeps
abort_on_error=1, since a bad access leaves the process unusable and continuing only
produces a second, more confusing report from the wreckage.
* [Build] Fix sanitizer flag leak into release package and make UBSan fail CI
- compile.sh always passes -DENABLE_SANITIZERS explicitly so the
release-package reconfigure in the same build dir cannot inherit ON
from the sanitized debug step (which shipped libasan/libubsan DEB deps).
- Sanitizers.cmake resets COCKATRICE_SANITIZERS to OFF when the option is
off (CACHE INTERNAL otherwise sticks forever, keeping NO_SANITIZE skips
and the 300s discovery timeout).
- UBSan now fails the job: -fno-sanitize-recover=undefined at compile time
plus UBSAN_OPTIONS halt_on_error=1 (recover mode exits 0, so the old
halt_on_error=0 could never fail).
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [UI] Fix auto-completion acceptance on Space/Tab/Enter
* [UI] Accept current completion when no item selected in popup
* [UI] Also accept completion on focus out when none selected
* [UI] Keep focus in chat on Tab completion acceptance
* [UI] Prevent Tab from moving focus when no completion popup
* [UI] Refocus on Tab when completing
* [UI] Let focusOut handle Tab completion acceptance
* [UI] Restore Tab behavior to match original (handle in focusOut)
* [UI] Defer refocus on Tab to avoid focus chain issues
* [UI] Auto-select first completion match in popup
* [UI] Auto-select first match when completer popup resets
* Revert "[UI] Auto-select first match when completer popup resets"
This reverts commit c7a3de3595.
* Revert "[UI] Auto-select first completion match in popup"
This reverts commit ae0dfb8622.
* [UI] Ensure completion selection updates on model changes
* Revert "[UI] Ensure completion selection updates on model changes"
This reverts commit 686bbef5b2.
* [UI] Highlight the selected completion in chat popups
The card completer's above-variant moved the current row with
NoUpdate, so the delegate never painted the selection border, and it
used Select which accumulated highlights while navigating. Use
ClearAndSelect so only the current row is highlighted and the old one is
cleared.
The mention popup opened with no current row at all, so nothing was
highlighted and there was no visual hint of what Enter would insert.
Select the row currentCompletion() uses, and install a delegate that
mirrors the active highlight into the inactive group so the selection
stays visible when the transient popup is not the active window.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
getPreferredPrinting returns a null PrintingInfo when a deck card has no
database entry or printings. SetProviderIdToPreferred dereferenced
getSet() unconditionally, crashing on custom or unresolved cards.
Guard against a null preferred set and leave the card's cleared printing
data as is.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Windows] Purge every runtime DLL on upgrade instead of a named few
A user updating from 3.0.2 to 3.1 got "abseil_dll.dll is either not designed
to run on Windows or it contains an error" and could not start the client.
abseil_dll.dll comes from vcpkg, not Qt. It rides into the install directory
on the "*.dll" glob in cockatrice/CMakeLists.txt, alongside libprotobuf.dll,
liblzma.dll and z.dll, and none of those were named in the upgrade purge. So
on an upgrade the old uninstaller's RMDir /r skipped whichever of them was
locked, the purge did not look for them, HasStaleRuntimeFiles did not look
for them either, and the installer reported success - having written the new
executables next to the previous version's protobuf runtime. Windows then
refused to load it.
The mask list was also already wrong where it existed: it matches zlib*.dll
while vcpkg ships z.dll.
Delete every top-level DLL instead of naming library families. The payload is
written after the purge, so a DLL the new build still needs is put straight
back; what the purge has to guarantee is only that nothing from the previous
build is left to be loaded next to the new executables. Enumerated families
cannot do that - every transitive dependency that arrives between two
releases is one more name to forget, and the three previous rounds of this
work each reasoned about Qt because that is what the reports named at the
time.
HasStaleRuntimeFiles collapses to the matching *.dll probe, so the abort
fires on any survivor rather than only on the families it was told about.
IsExistingCockatriceInstall stays on Qt6*.dll on purpose: it decides whether a
directory is ours at all, and a folder that merely holds some DLLs of its own
must not be purged. Qt6*.dll is enough even when only the protobuf runtime
survived, because the whole Qt set is mapped by the same process that maps
abseil_dll.dll and so is locked or free with it.
* [CI] Assert the whole Windows runtime, not just the Qt modules
required-qt-runtime.txt listed only the Qt modules COCKATRICE_QT_MODULES
names, so it could not see any other runtime go missing. The three previous
rounds of installer work all reasoned about Qt*.dll because that was all the
build knew how to ask about, and the one runtime that actually broke the
reported upgrade - abseil_dll.dll - was outside the set entirely.
- Rename it to required-runtime.txt and take the third party runtime names
from the vcpkg bin directory the app-local copies come from, so a
dependency bump cannot leave the list behind and a versioned name such as
libcrypto-3-x64.dll becoming libcrypto-4-x64.dll cannot either
- Union the three applications' module lists rather than the client's alone:
Servatrice contributes Qt6Sql.dll, servatrice.exe is installed next to
cockatrice.exe and cannot start without it, and nothing asserted it
- Add Core to the client's module list. It is the one module nothing can do
without and it only ever arrived transitively, so Qt6Core.dll was shipped
and never checked - the new unknown-runtime check flagged it on the first
run
- Fail the build on a missing runtime, as before, and warn on a runtime
nothing requires. Only the test framework is a hard failure there: "no
shipped executable links gtest" is a fact, whereas a runtime missing from
the manifest may be one the manifest failed to derive, and a check that has
never run before should not be able to block a release
The warning is meant to be read. Qt6::Quick pulls in Qml, QmlModels,
QmlWorkerScript, QuickBasic, QuickTemplates2 and OpenGL transitively and none
are named, so the manifest is known to be a partial list; the first Windows
run reports the real one, which is evidence rather than a guess about what Qt
links. Fold that back in when it lands.
* [CI] Stop deriving the required runtime from the vcpkg bin directory
The first Windows run of this check failed, and the seven files it named as
missing are not runtime at all: gmock.dll, gmock_main.dll, legacy.dll,
libprotobuf-lite.dll, libprotoc.dll and pthreadVC3/VCE3/VSE3.dll. None of them
is linked by any shipped executable, so a correct package leaves them out.
vcpkg_installed/x64-windows/bin is everything vcpkg installed, not everything
the applications import. The protoc compiler, the GoogleMock library, a port
tool and three CMake FindThreads probes sit beside the genuine runtimes, and a
directory listing cannot tell them apart. Asserting it fails the build over a
package that is right - which is what happened, and it is the same mistake as
the one that produced this bug report: reasoning about a list of names instead
of about the build.
The real source of truth is the three executables' import tables, which
fixup_bundle already walks to decide what to copy. Until CMake can read that
result, the Qt half is what can be derived honestly, from the same module lists
the targets are built against. So assert that, and report the rest rather than
require it:
- Drop the vcpkg bin glob, its gtest filter and the vcpkg-runtime-known marker.
All three existed only to support the glob.
- Report top level DLLs the manifest does not name without failing. These are
expected: protobuf, Abseil, OpenSSL, liblzma, ffmpeg and the Qt debug DLLs all
arrive on the build output glob because nothing in the build declares them by
name. The list is a report of what the package carries, not a verdict, and it
is how the missing Qt6Qml/Quick/OpenGL entries above became visible.
- Report a leaked gtest or gmock instead of failing on it. "No shipped
executable links this" is a fact, but a check that has never passed on a real
package has not earned the right to block a release, and the first run of an
enforcing version is exactly when its own wrong assumptions show up.
- Skip DLLs under a subdirectory, which are platform plugins rather than
imported runtimes, and the NSIS plugins, which extract to $PLUGINSDIR and so
reach the comparison looking like payload.
Verified against the paths 7-Zip actually listed in the failing package: the
check passes on it, still fails when Qt6Sql.dll or the qwindows plugin is
removed, and still reports gtest.dll and gmock.dll if either is added.
The purge in cmake/NSIS.template.in covers the runtimes the manifest does not
name. That fix is independent of any list and is what makes the reported update
failure go away.
* [Installer] Fix the /R upgrade aborting before it does anything
WaitForLauncherToExit read the wrong registers. System.dll writes its
.rN outputs to $N and its .RN outputs to $RN, but the wait used
p.r0/i.r1 while testing $R1. $R1 therefore stayed empty, and the "did
not exit" test compared "" with "0" - true every time. Every in-app
update passes /PID=, so every upgrade hit AbortInstall before writing
anything; because /R also sets silent, the override just vanished with
exit code 1 and no message. The bug came in with #7372 and is inherited
by this branch, not introduced by it.
Use the uppercase .RN forms, and treat a null or plugin-failed
OpenProcess as the launcher already being gone - the case the old
${Errors} check never caught, because System.dll does not set the NSIS
error flag from the callee's return value.
Also restore the abort dialog for the in-app updater: AbortInstall and
the two inline warnings gated their message box on ${IfNot} ${Silent},
which is false for a /R upgrade, so any abort on that path was equally
silent. The unattended case is a /S install without /R, not /R, so show
the box for $ReinstallMode = 1 or an interactive install. The
stale-runtime abort now goes through AbortInstall so that decision
lives in one place.
* [Installer] Log every upgrade step to %TEMP%\Cockatrice-installer.log
* [Installer] Actually append to the upgrade log (FileSeek END)
* [Installer] Wait for the old uninstaller before replacing files
An NSIS uninstaller copies itself to %TEMP% and runs from there so that it
can delete its own directory; the process the installer started exits
immediately. A plain ExecWait therefore returns before the uninstall has
run, and the temp copy's "RMDir /r $INSTDIR" then executes concurrently
with the install section, deleting files the payload just wrote. That is
why an upgrade could leave Qt6Core/Gui/Multimedia and the other
alphabetically-first DLLs missing while the tail of the directory survived:
a recursive delete marching from the front while the final listing ran.
Pass _?=$INSTDIR to the old uninstaller so it runs in place and ExecWait
actually blocks until the uninstall completes. _?= must be the last
argument and unquoted, even for paths with spaces. Applied to both the
silent /R path (AutoUninstallIfNeeded) and the interactive path
(componentsPagePre).
* [Installer] Tighten the wording of the upgrade comments
Drop the dash-heavy phrasing from the comments this PR adds, and say plainly
in the _?= comment that ExecWait returns before the old uninstaller has run
and the temp copy then deletes the payload the section is still writing.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Improve card database update visibility
Addresses Cockatrice/Cockatrice#7431.
Cockatrice previously offered no in-client feedback while the card
database updater ran: the only progress UI lived in the first-run
wizard, stderr/stdout were not parsed in interactive runs, and starting
a second update with Oracle already open or with a hidden background run
active just showed a confusing 'already running' guard - even though
Oracle is launched as a separate process with no lifecycle tracking.
- Add a status-bar widget (CardDatabaseUpdateStatusBar) that appears
while an update runs and shows the Downloading/Parsing/Importing
stages with percent progress; it is also used in the quit-confirm
prompt so users see an unfinished update before cancelling it.
- Parse the updater's PROGRESS lines (new CardUpdateProgressParser,
unit-tested) in both foreground and background runs and log stage
changes to the new card_database.update category, throttled to at
most one line per second; forward Oracle stderr to the logs.
- Show a tray balloon when a background update starts and on finish or
failure instead of staying silent.
- Request a running Oracle instance to raise itself (QLocalServer
CockatriceOracleRaise-<user>) when the user starts another update, and
demote the 'already running' guard to an informative message only when
no running instance can be found.
- Emit download/scan/import progress unconditionally from Oracle so
interactive and background runs behave the same.
- Document the new log category in the troubleshooting guide.
* [Client] Address card database update review feedback
* [Client] Add setting to control card database update tray notifications
* [Client] Address remaining card database update review nits
- Rename card_update_progress_parser.{h,cpp} to card_update_progress.{h,cpp}
- Store the CardUpdateProgress struct in the status bar widget
* Lint.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Style] Fall back to the built-in windows style instead of windowsvista
The Windows Vista native style is broken the same way as the Windows 11 one,
so the sanitizer now catches both and falls back to the built-in QWindowsStyle
instead of the Vista plugin. The style picker no longer offers either.
* [Style] Keep Windows native styles selectable but warn about them
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The parsers keep dropping printings for disabled sets so that disabling a
set hides them everywhere, not just in deck editor search. The cached parse
output therefore depends on enablement, which the source hash did not cover:
the first parse froze whatever enablement state existed at that moment and
later toggles hit the stale cache.
- Fold a parser schema version and the sorted enabled set names into the
binary cache source hash so toggling a set or enabling newly discovered
sets (including custom ones) forces a re-parse on the next load.
- Harden the legacy set migration to only write keys that exist so missing
legacy values can no longer clobber cardDatabase.ini and silently disable
sets on upgrade.
- Make CardDatabase::reloadCardDatabasesAndNotify() bypass the binary cache
so an explicit reload always re-reads the files on disk, even when their
size/mtime hash still matches.
- Cover cache invalidation end to end: sandboxed loads assert that disabling
a set, adding a custom database and enabling a fresh set each rewrite the
cache, that a no-op load keeps it, and that an explicit reload picks up
edits the source hash cannot see.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Use native title bar height for ZoneViewWidget close button
Compute title bar height from style (PM_TitleBarHeight) instead of hardcoded 24.0 to align hit box with native close button.
* [Game] Fix ZoneViewWidget title bar height compile errors
Drop the redundant static on the anonymous namespace helper, stop
redeclaring titleBarHeight's parameter and reuse tbh in the fallback
path, and restore the position clamp that still referenced the removed
kTitleBarHeight constant.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [CardDB] Drop rebalanced-card filtering from the v4 parser
Rebalanced cards no longer exist, so the includeRebalancedCards
preference has no data to filter. Always keep printings marked
isRebalanced and remove the now-unused ICardPreferenceProvider
plumbing from CockatriceXml4Parser and CardDatabaseLoader.
The setting itself (CardsDisplaySettings, Manage Sets checkbox,
Oracle attribute mapping) is removed in a follow-up.
* [CardDB] Drop the rebalanced-cards setting entirely
Rebalanced Alchemy cards no longer exist, so remove the
includeRebalancedCards preference surface: the CardsDisplaySettings
getter/setter/signal, the ICardPreferenceProvider and
ICardsDisplaySettingsProvider methods, the Manage Sets checkbox, the
legacy settings migration mapping, the isRebalanced attribute mapping
in the Oracle importer, and the isRebalanced attribute in the v4
schema.
Builds on #7448, which already stopped the v4 parser from filtering
on the preference.
* [CardDB] Drop the unused isOnlineOnly printing property
The Arena-card detection switched from isOnlineOnly to isRebalanced in
#5778, so nothing has read this printing property since; it only
round-trips from the Oracle importer into cards.xml. Remove the
importer mapping, the benchmark fixture field and the v4 schema
attribute so regenerated databases stop carrying it.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
- TCP client connections: reject messages whose declared length is
negative or exceeds a 1.5MB cap immediately instead of buffering
indefinitely, preventing unbounded memory growth and the previously
stuck state caused by negative lengths overflowing the int header.
- ISL connections: apply the same length validation with a 4MB cap,
disconnecting the peer on an implausible length.
- Websockets: only trust the forwarded client-address header (e.g.
X-Forwarded-For) when the websocket connection originates from an
address in security/trusted_sources. Previously any client could
spoof its address to bypass bans, user limits and rate limiting.
Took 3 minutes
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Completer] Stop line edit rewrite on popup highlight
* [Completer] Extract the completion index into a variable
Make the popup selection update readable at a glance.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [DeckList] Apply include-what-you-use to libcockatrice_deck_list
* [IWYU] Apply include-what-you-use across the tree
Run one whole-tree IWYU pass over the libcockatrice_* libraries, the
Cockatrice client, Oracle, Servatrice and the test suite, so each
translation unit includes what it uses directly and forward-declares
what it does not need.
IWYU only analyses real translation units, never the moc code CMake
generates from Q_OBJECT headers, so it strips includes that moc output
silently relies on. Where a type is passed by value (or nested in a
QList/QMap) in a signal or slot signature, the generated moc code needs
the complete type; those includes are kept with an explanatory
`// IWYU pragma: keep`. The same holds for Q_DECLARE_METATYPE targets
such as QSqlDatabase.
A handful of includes are added by hand where callers brace-initialize a
const-reference parameter, which also requires the complete type.
Applied as a single commit on purpose: stripping includes from the
libcockatrice_* headers is precisely what breaks the client, test and
server translation units, and the fixes for those live in the same
translation units. Splitting by component would therefore leave
intermediate commits that do not compile.
All 426 modified Q_OBJECT/Q_GADGET headers were verified to be
moc-self-contained by compiling each generated moc_*.cpp standalone;
several built only because of moc include ordering in
mocs_compilation.cpp before the keep-includes were added.
* [Tooling] Emit public Qt headers and fix IWYU fallout CI found
Three problems the local build could not see, all because a green local
build only proves the configuration it was run under.
Private Qt headers. IWYU emitted private, lowercase Qt headers, and
qtypes.h does not exist at all before Qt 6.9, so the Debian 12 and
Ubuntu 24.04 jobs on Qt 6.4.2 failed to compile. The generator only
emitted a mapping when the capital header name mirrored the private one
(QAbstractAnimation for qabstractanimation.h), so qtypes.h, which is
wrapped by QtTypes instead, got no mapping and kept the private
spelling. Map it explicitly to <QtTypes>, which is public since Qt 5 and
provides the same typedefs either way, and rewrite the 100 affected
includes.
Do not generalize this to "prefer any Qt* wrapper". The table is
generated from whichever Qt is installed, and the wrappers are not
equally old: QtLogging only appeared in Qt 6.8 and QtMinMax in 6.5, so
that heuristic trades one breakage for another. Every other lowercase
header still in the tree has shipped publicly since Qt 5, so those are
left alone.
Platform-guarded includes. IWYU only sees the preprocessor configuration
it is handed, so it dropped an include that only the macOS branch of
main.cpp needs, and the macOS jobs failed on an undeclared
UrlSchemeEventFilter. Restore the include. Force-compiling every file
that carries a platform guard under Q_OS_MAC, Q_OS_MACOS and __APPLE__
reports nothing else, so the macOS branches are covered rather than
assumed.
shellcheck. Drop a leftover variable and stop assigning the
include-what-you-use lookup, which was only ever an existence check.
* [Tooling] Map Qt 6.5+ split headers to QtGlobal for Qt 6.4 compatibility
IWYU emitted QtTypes, QtPreprocessorSupport, QtVersionChecks, QtAssert,
QtMinMax, QtClassHelperMacros, QtEnvironmentVariables and QtTranslation.
Qt 6.5 split those declarations out of qglobal.h into separate headers, so
neither the Qt* wrapper nor the q*.h private name exists on the oldest Qt we
support (6.4.2), and Debian 12 / Ubuntu 24.04 fail with "No such file or
directory". qtypes.h is worse still: it only appeared in Qt 6.9.
QtGlobal is the one spelling that works across the whole supported range. It
has been public since Qt 5, defines these declarations itself up to Qt 6.4, and
includes the split headers from Qt 6.5 on, so redirect all eight private
headers to it and collapse the affected includes in the tree.
Curated overrides now take precedence over the name-mirroring rule, which
cannot know about Qt version floors because the mapping is generated from
whatever Qt is installed. That rule was mapping qtversionchecks.h to
<QtVersionChecks> even with an override present, because the override was only
consulted as a fallback. A curated key that matches nothing is now a hard
error rather than a silent fallback, and the redirected headers are no longer
advertised as the home of a symbol.
Verified every Qt include in the tree resolves against the real
qt6-base-dev 6.4.2 package: 15 distinct includes, 0 missing.
* [Tooling] Map QOverload to QtGlobal and drop the QMetaMethodArgument include
Two more headers IWYU emitted do not exist on Qt 6.4.2:
QOverload was split out of qglobal.h in Qt 6.5, so on 6.4.2 it is declared in
qglobal.h directly and the standalone header is a hard error. QtGlobal covers
both, so map qoverload.h to it like the other 6.5+ split headers.
QMetaMethodArgument is never referenced anywhere in the tree. It is a bare
forwarder for qobjectdefs.h that our generator advertised as a class symbol,
which is how IWYU came to add the include at all. The symbol mappings are
generated from header names, so they cannot tell a forwarder that happens to be
capitalised from a real class; removing the unused include is the fix, and the
generic case stays a known limitation of the generator.
Extending the audit to every Q* header (an earlier pass only matched headers
starting with "Qt" or lowercase q*.h, so it missed all of them) against the real
qt6-base-dev, qt6-multimedia-dev, qt6-declarative-dev, qt6-svg-dev and
qt6-websockets-dev 6.4.2 packages: 341 distinct Qt includes, 0 missing.
* [Game] Give moc the complete CardItem type in player_actions.h
player_actions.h forward declares CardItem but passes QList<CardItem *> by
value in actPlay/actPlayFacedown/actHide. moc instantiates the metatype for
those parameters, and on Qt 6.4 that expands to sizeof(CardItem), so Ubuntu
24.04 fails with "invalid application of 'sizeof' to incomplete type
'CardItem'". Newer Qt tolerates the incomplete type, which is why only the
6.4.2 jobs failed.
Include the defining header with an IWYU pragma keep, matching how the other
MOC-required includes in this area are handled.
* [Game] Give moc the complete ServerInfo_Card type
player_logic.h, game_scene.h, view_zone.h and view_zone_widget.h forward
declare ServerInfo_Card but pass QList<const ServerInfo_Card *> through signal
and slot signatures. moc instantiates the metatype for those parameters, which
Qt 6.4 expands to sizeof(ServerInfo_Card), so the connect in game_scene.cpp
fails on Debian 12 and Ubuntu 24.04 with "invalid application of 'sizeof' to
incomplete type 'const ServerInfo_Card'". Newer Qt tolerates it, so only the
6.4.2 jobs failed.
Include the generated protobuf header with an IWYU pragma keep, following the
pattern already used for ServerInfo_Player and ServerInfo_User in
game_event_handler.h.
* [Client] Complete the types Qt 6.4 needs for connect and moc
Qt 6.4 expands QMetaType::fromType on the normalised argument list of a
PMF connect and on moc-generated metatypes, which requires sizeof on every
signal and slot parameter. Newer Qt tolerates forward declarations there,
so only the Debian 12 and Ubuntu 24.04 jobs failed, with
"invalid application of 'sizeof' to incomplete type" for QMouseEvent,
QPrinter and GameReplay.
Replace the forward declarations with the real includes:
- QMouseEvent in the nine widget headers that pass it through signals
- QPrinter in abstract_tab_deck_editor.cpp, for the actPrintDeck lambda
- game_replay.pb.h in tab_replays.h. GameReplay is a protobuf message, so
the project's game/replay.h only forward declares it and cannot supply
the complete type.
Found by recompiling all 1918 project TUs against the Debian Qt 6.4.2
headers, which reproduces those two CI jobs locally in one pass.
* [Client] Complete Release and QMenu for Qt 6.4 moc and connect
Continuing the Qt 6.4 complete-type sweep:
- window_main.cpp connects ClientUpdateChecker::finishedCheck to
MainWindow::checkClientUpdatesFinished, whose Release * parameter made
moc 6.4 instantiate the metatype. GameReplay turned out to be a protobuf
message, so Release likewise needed release_channel.h in the three
headers declaring those signals: window_main.h, dlg_update.h and
client_update_checker.h.
- moc 6.4 also needs sizeof(QMenu) for the QList<QMenu *> carried by
TabSupervisor::setMenu and MainWindow::updateTabMenu.
Verified by regenerating moc output with the Debian moc 6.4.2 for all 434
Q_OBJECT/Q_GADGET classes and compiling it, plus recompiling all 1612
source TUs, against the Qt 6.4.2 headers: no remaining failures.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
TabEdhRecMain never emitted tabTextChanged, so TabSupervisor::updateTabText
was never called and the tab kept the title it was created with.
addEdhrecMainTab() creates the tab without a card, so every search left the
title reading "EDHRec: " with no card name for the rest of the tab's life.
addEdhrecTab() sets the card before myAddTab(), which is why the older
TabEdhRec tab was never affected.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
cmake/getversion.cmake reads the commit date and release tag from git, but .dockerignore excluded .git/, so docker builds had no git info and Servatrice reported its version as "3.1.0 ()". Keep .git/ in the build context; only /usr/local is copied into the final image, so its size is unaffected.
Co-authored-by: Michael Hogg <3495076+jedinerd@users.noreply.github.com>
Add a workflow_dispatch trigger that builds an existing release tag with the current workflow and publishes it to GHCR under that tag name (optionally also as latest). This allows republishing a release whose image failed to publish, since re-running the original run reuses the workflow file from the release tag.
Dispatched runs check out the given tag, publish like stable releases, and use their own concurrency group so pushes to master can't cancel them.
Co-authored-by: Michael Hogg <3495076+jedinerd@users.noreply.github.com>
With archive: false, actions/upload-artifact ignores the name input and names the artifact after the uploaded file, so the digest artifacts were named after the raw digest instead of digest-<label>. The index job's download-artifact pattern digest-* then matched nothing, leaving no digests to build the image index from. Drop archive: false so the digest-<label> names apply.
Co-authored-by: Michael Hogg <3495076+jedinerd@users.noreply.github.com>
The inline comment after type=ref,event=tag was inside the tags block scalar, so it was passed to docker/metadata-action as part of the tag spec, failing with "Invalid event". Move the comment above the block.
Co-authored-by: Michael Hogg <3495076+jedinerd@users.noreply.github.com>
* first version
* combined logic + better regex
* Simplify, we only support 64bit, installer prevents wrong installs
* lint + fix Linux versions (missing declaration)
* update regex
* improve/add comments
* comments
* ReleaseChannel: parse non-numeric OS versions on Windows Server
QSysInfo::productVersion() returns "Server 2022" on Windows Server, so
splitting on '.' and calling toInt() made the version unparseable and
disabled the in-client updater there. Extract the first numeric token
instead; Server 2022 becomes 2022 and matches the Win10 assets.
* ReleaseChannel: don't offer 64-bit assets on 32-bit Windows
The old downloadMatchesCurrentOS() had an explicit 32-bit branch that
only accepted "32bit" assets. Restore that guard: 32-bit Windows builds
return no match instead of being offered the 64-bit Win10 installer that
won't run.
* ReleaseChannel: only query the OS version on updater platforms
The in-client updater only ships assets for macOS and Windows, but the
host system version was still being resolved on every platform, including
Linux where it is never used. Compute it only under the Q_OS_MACOS /
Q_OS_WIN guards.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Translations] Load Qt's own translations and build ours with the binary
Three separate bugs kept translations from working.
installNewTranslator() loaded the application translation into
qtTranslator. QTranslator discards its previous contents on every load, so
that wiped the qt_<lang>.qm loaded one line earlier and installed an empty
translator instead. Qt's own strings were never translated on any platform.
Give each file its own translator.
QLibraryInfo::path(TranslationsPath) was the only directory searched for
Qt's translations. A relocated Qt can report a path that does not exist: the
KDE Flatpak runtime reports /usr/translations while its files live in
/usr/share/qt6/translations. Probe the usual install layouts and only hand
back the ones that exist.
The .qm files were attached to the default build target only, so building
the executable on its own left it without any translations and the language
selector empty. Attach them to the target as well.
Move the shared lookup into TranslationLoader so client and Oracle cannot
drift again, and have it report a missing translation at debug level. An
unset language is the default for English, which has no file to begin with,
and warning about it only added noise to every fresh debug log.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Tests] Define main() in translation_loader_test
Without a GTest installation the fallback in tests/CMakeLists.txt links
only gtest, not gtest_main, so the test has to bring its own main(). Every
other test already does; only this one relied on the distribution package
and failed to link on CI.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Tests] Put the translation loader fixture next to the test executable
A multi-config generator (MSBuild, Xcode) writes the executable to a
per-config subdirectory while the .qm stays in the current binary
directory, so applicationDirPath() never saw the fixture and
ABuildTreeFindsTheTranslationsNextToTheExecutable failed on Windows.
* [Translations] Depend on Qt's lrelease target and skip the loader test without lrelease
Attaching the .qm custom commands to a second custom target races with
Qt's own <Project>_lrelease target on MSBuild/Makefiles and is rejected
by Xcode, so depend on Qt's target instead. Only build the translation
loader test when Qt6::lrelease is available.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: ZeldaZach <zahalpern+github@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* [Settings] Back up the settings folder before migrating
The per-file settings migration only preserved global.ini, as
global.ini.old. Everything else the migrations write was overwritten in
place with no way back: the legacy migration and the 17 per-domain
migrateXxxSettings functions all write straight into the per-domain .ini
files, so any of them that got a key mapping wrong silently replaced a
working file. The re-migration path is the sharpest case, because
migrateSettingsFromGlobalIni() runs again whenever an older build wrote
non-sentinel keys back into global.ini, and those values then land on
top of per-domain files the user had already edited.
Snapshot the whole settings folder before either migration runs, into a
timestamped sibling settings-backup-<yyyyMMdd-HHmmss>. A sibling rather
than a child, so a backup is never copied into the next backup and the
"Open Settings Folder" action keeps showing only live files. Keep every
snapshot; settings files are kilobytes, so growth is not a concern.
SettingsMigration::isMigrationPending() gates the copy so a normal start
creates nothing. It reuses the same condition migrateSettingsFromGlobalIni()
uses to decide whether it has work, factored into a shared helper so the
two cannot drift, and it ignores the legacy migration on an empty profile
where that migration only writes its sentinel. The backup itself skips a
folder whose files are all empty, which covers a fresh profile whose
global.ini was created but never written.
A failed copy logs a warning and returns an empty path rather than
aborting the migration, since a full disk or an unwritable data folder
must not leave the client unable to start. global.ini.old still works as
the single-file escape hatch.
Document the folder layout, the automatic backups, and the fact that
migrations only run forwards, so users know to copy the settings folder
before downgrading to a version that cannot read the per-group files.
* [Client] Match the desktop file name case for the portal app ID
QGuiApplication::setDesktopFileName() was given "Cockatrice"/"Oracle"
while the installed desktop entries are cockatrice.desktop and
oracle.desktop. Linux paths are case-sensitive, so xdg-desktop-portal
failed the app-info lookup for "Cockatrice" and Qt warned on every
QDesktopServices call that it could not register with the host portal.
The same string becomes the xdg-shell appId, so the window icon lookup
failed on Wayland for the same reason.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The game log translates its sentences, zone names and counter names, but every
card name was rendered as the raw canonical name straight from the server.
The card board and the card info text already went through
CardLocalization::displayName(), so the log was the odd one out.
cardLink() used one string for both the card:// href and the visible label,
which left no room for a localized label. Split the two: the href keeps the
canonical name because ChatView::mousePressEvent feeds it straight to
showCardInfoPopup for lookup, while the label goes through the display
language. Same split for [card] chat tags in ChatView::appendCardTag.
Sanitize the name while building the link. logUndoDraw already did this by
hand and had drifted into its own non-italic copy of cardLink(); route it back
through the helper.
Add CardLocalization::displayNameFor() for callers that only hold a canonical
name and need no CardInfoPtr of their own.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Theme] Carry Default theme directory customizations over to System
The Default -> System rename rewrote the stored theme name but left the
user profile's themes/Default directory behind, so anyone who had
customized zone graphics, stylesheets or palettes there silently lost them.
Migrate the directory alongside the name. Rename it when System does not
exist yet, otherwise merge file by file without overwriting what System
already has, then drop the legacy directory. Users who already ran the
rename get their customizations back on the next start, and a hand-made
Default folder of a user on another theme is left untouched.
* [Tests] Give theme_migration_test an explicit main and gate it on WITH_CLIENT
The build machines that cannot find GTest fall back to the downloaded
googletest, which links the bare gtest library without gtest_main. Every
other test defines its own main() for that reason; this one did not, so
Debian 13, Fedora 44 and Ubuntu 26.04 failed to link it.
The test compiles client sources, so -DTEST=1 -DWITH_CLIENT=0 has no way
to build it. Gate the subdirectory on WITH_CLIENT like the other
client-source test.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Hand off single-instance payloads outside the readyRead signal
Opening a cockatrice:// link from outside a running client showed the
"Join game ...?" confirmation and then crashed on both answers. The
join reached the server first, so the other players saw the user join
and immediately disconnect.
SingleInstanceManager::handleNewConnection() emitted filesReceived from
inside the forwarding socket's readyRead lambda and only disconnected
afterwards. A joingame link makes the handler show a modal QMessageBox,
which runs a nested event loop. The sending instance exits as soon as
it has the acknowledgment, so the socket was disconnected and
deleteLater'd inside that loop: it died in the middle of its own
readyRead signal, and both this lambda and Qt's signal dispatch kept
using the freed memory.
Disconnect right after the acknowledgment and emit filesReceived
through a queued call, so no handler and no nested event loop ever runs
inside the socket's signal. Guarding the disconnect call alone is not
enough, since Qt still touches the socket after the lambda returns.
Add a regression test that reproduces the nested-event-loop hand-off.
* [Tests] Apply cmake-format to the single-instance test wiring
* [Client] Defer the single-instance socket teardown past the readyRead frame
The forwarded payload is emitted synchronously from inside the forwarding
socket's readyRead, which is what lets the modal "Join game?" confirmation
appear. A handler can spin a nested event loop, and the sending instance
exits as soon as it has the ACK, so the socket is disconnected and
deleteLater'd while that loop runs. Control then returns to this frame with
the socket already destroyed, and the inline disconnectFromServer() touched
freed memory.
Queue the teardown instead and guard it with a QPointer taken before the
handlers run -- building the guard afterwards is itself a use-after-free,
since QPointer dereferences the pointer it is constructed from. The payload
still reaches the handlers synchronously, so the confirmation is unchanged.
The regression test covers both halves: the handlers must run while the
socket is still connected, and the socket must not be torn down until the
readyRead frame has returned.
* Lint.
* [Client] Only delete the single-instance socket outside the readyRead frame
The auto-delete on `disconnected` was the remaining use-after-free. A
handler may spin a nested event loop (the modal "Join game?" box), the
sending instance exits as soon as it has the ACK, and so the peer closes
while that loop runs. deleteLater() issued from inside such a loop is not
deferred to the outer loop: Qt frees the object as soon as the nested loop
unwinds, which is still inside this readyRead emission, so Qt's own signal
dispatch and the handler lambda both keep using a freed socket. When the
notification happens to arrive late the bug hides, which is why this only
reproduced on some platforms.
Drop the auto-delete and let the queued teardown own the socket's lifetime:
it disconnects and then deletes, once the frame has unwound. The QPointer
guard was necessary but not sufficient -- it stopped us dereferencing a
dead pointer, not the socket being freed underneath the frame.
The test waits on the socket's destroyed signal instead of polling, so the
teardown assertion does not depend on platform event timing.
* [Client] Do not queue the single-instance socket teardown
The previous commit moved the deleteLater() off the disconnected signal
and into a queued invocation, believing that "queued" meant "after the
readyRead frame returns". It does not. A queued call is delivered by
whichever event loop is running, and the modal "Join game?" box runs a
nested one -- so the teardown was still executed inside the readyRead
emission, which is exactly the use-after-free it claimed to prevent.
Queueing was never necessary. Because nothing connects deleted/close to
the socket's disconnected signal any more, no nested loop can free it
while the handlers run, and emit filesReceived() is synchronous: once it
returns, the frame has unwound and the socket can be closed and deleted
inline. The QPointer stays, covering the one case that remains -- the
primary shutting down while the confirmation box is open, since
QLocalServer destroys its pending connections.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The games list in the profile hover card offered "Join game" and
"Spectate", but UserInfoPopup::joinGameRequested was only forwarded to an
identical UserListWidget::joinGameRequested signal that nothing connected
to. Clicking either entry sent no command, opened no tab and showed no
error.
Route the request to TabSupervisor, which owns the join flow. The popup's
games list spans every room the user is in, so the target room may not be
open yet; IntentJoinServerGame already joins the room first and retries,
which is why this goes through the intent rather than straight to
GameSelector.
Extract the intent construction that joinReportGame already had into a
shared joinGameByIntent helper instead of duplicating it.
Also stop disabling "Join game" for games that are full or already
running. GameSelector::joinGame() offers to join as a spectator in that
case, so a greyed-out entry read as a dead button; GameSelector's own
context menu never disables it either.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Add option to use game time in game log timestamps (#2202)
Game log timestamps showed the local clock time, which is meaningless in
a replay or in a log read after the fact — nobody can tell when a play
happened relative to the game.
Add a "Use game time instead of local time in game logs" setting. When
enabled, the message log stamps entries with the elapsed game time
(HH:MM:SS since the game started); regular chat views keep the local
clock.
* [Game] Move formatElapsedTime helper out of public slots (#2202)
The message log needs the HH:MM:SS formatter used by the game timer, but
it lives in GameState's public slots, tying a pure formatting helper to
the signal-slot machinery.
Move formatElapsedTime into a public section of GameState so the log can
call it directly, and add the missing trailing newline to game_state.cpp.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Installer] Abort instead of installing over leftover runtime files
The silent upgrade could leave a mix of old and new Qt DLLs next to the new
executables, which is what makes the next start fail with "The procedure entry
point X could not be located in the dynamic link library ...Qt6QuickWidgets.dll"
or "The code execution cannot proceed because Qt6Multimedia.dll was not found".
The check that was supposed to catch this probed only Qt6Core.dll and
Qt6Network.dll, so it was blind to the two files the reports name. It also only
warned and then installed anyway, so a caught problem was still compounded.
- Scan every file the purge targets with FindFirst, which takes the same
wildcards the Delete/RMDir calls use, instead of two hardcoded names
- Widen IsExistingCockatriceInstall from Qt6Core.dll to Qt6*.dll so a directory
left holding only some Qt modules is still recognised as an install
- Gate the scan on that, so a first-time install into a directory that happens to
hold unrelated Qt5 DLLs is not turned into an abort
- Abort with a non-zero exit code instead of warning, so the new payload is
never written next to files of the previous version
- Skip the message box on an unattended /S install, which would block on it
forever, matching the two existing abort sites
* [Installer] Cancel a silent upgrade when the running-instance guard fails
The running instance guard is what stands between a locked install directory
and the mixed Qt DLL set that breaks the next start, but it failed open in two
places and let the install carry on anyway:
- powershell.exe could not be started: the check warned and returned
- the process lookup answered "could not determine": WaitForAppToClose returned
on $R0 != 1, so "unknown" was treated exactly like "not running"
On a silent upgrade there is nobody at the keyboard to close the applications in
response to that warning, which is precisely the state in which the launcher was
still holding its Qt DLLs mapped.
- Treat "could not determine" as its own state rather than folding it into "not
running", in all three places WaitForAppToClose inspects the result
- Cancel a silent upgrade in both cases; keep an interactive install on the
existing warn-and-continue path, since refusing there would leave an installer
that can never complete and the stale-runtime check still applies
- Route every abort through one AbortInstall so the dialog-vs-silent decision
lives in a single place, and stop the two pre-existing warning dialogs that a
/S install without /R would have blocked on forever
* [Installer] Wait on the process that launched the update installer
The in-app updater starts the installer and only then quits itself, so the
installer has to decide whether the launching process is still alive while it is
running. It did that by polling powershell.exe for an image name under the
install directory, which is a proxy for the real question - is the process that
started me still holding its Qt DLLs mapped - and a proxy that can answer "not
running" or "could not determine" while the process is very much alive. Either
answer let the install carry on into the mixed Qt DLL set.
Take /PID= from the command line and wait on that process handle directly:
- OpenProcess with PROCESS_SYNCHRONIZE only, which is all that is needed to wait
and the least likely to be refused for an unrelated reason
- WaitForSingleObject for the same 60 s the name based check allows, so a client
slow to flush its card database is not squeezed
- Treat an unopenable handle as "already exited", which is what it means for a
dead process
- Cancel the upgrade if the launcher has not exited, rather than replacing files
it is still using
Run it before EnsureAppsNotRunning, not after: it needs no powershell.exe, so it
is the one check that still works on a machine where the process lookup cannot
run, and it is what lets that case keep upgrading instead of being cancelled by
what was only ever a proxy for it. Keep an older client, which passes no /PID=,
on the existing path.
An older installer ignores /PID=, and /D= stays the last parameter either way.
* [Client] Pass our process id to the update installer
The updater starts the installer and only then quits itself, so the installer
begins replacing files while this process still has every Qt runtime DLL mapped,
and a mapped DLL cannot be deleted. It has to work out that it must wait, and
until now all it had was its own image name to go on.
Pass /PID= so it can wait on this process directly instead of guessing, and
cancel the update rather than replace files this process is still using. /D=
stays the last parameter, which NSIS requires.
* [CI] Fail the build when the installer is missing a Qt runtime DLL
The Windows payload is assembled by globbing every *.dll out of the build output
directories, so nothing but luck decides whether a module the client links
statically - Qt6Multimedia.dll, Qt6QuickWidgets.dll - reaches the installer at
all. A module that is left out is invisible until a user installs an update and
the client then refuses to start, with "The code execution cannot proceed because
Qt6Multimedia.dll was not found" or a missing procedure entry point in a
Qt6QuickWidgets.dll carried over from the previous version.
Derive the required list from SERVATRICE_QT_MODULES, COCKATRICE_QT_MODULES and
ORACLE_QT_MODULES, the same module lists the targets are actually built against,
so it cannot drift from what the binaries need, and write it next to the CMake
cache as required-qt-runtime.txt. Add the platform plugin, without which the
client cannot start at all.
Assert it in the existing 7-Zip pass that already rejects build-tree artifacts,
comparing exact relative paths so a same-named file in the wrong directory does
not satisfy a requirement, and normalizing the archive's own separators so it
does not matter which one NSIS chose. Missing manifest, missing entry or missing
7-Zip-independent failures all stop the build rather than warn, which is the
whole point: the previous check already failed hard for artifacts, and a warning
here is what let a broken package ship.
* [Client] Point the Windows qt.conf translations path at the real one
The Windows qt.conf carried Translations = Resources/translations, copied from
the macOS bundle block, but the Windows install has no Resources directory: the
.qm files are installed to COCKATRICE_WIN32_QM_INSTALL_DIR, which is
translations, and main.cpp looks for them at applicationDirPath() +
"/translations". Leave the macOS value alone, which is correct for it.
* [Installer] Close the /? help string so the installer compiles again
The last line of the continued MessageBox string ended in $\n\" instead of $\n".
NSIS reads \" as an escaped quote, so the string never terminated and makensis
rejected the script, which took the CPack packaging step down with it. The
Windows build failed on "Problem running NSIS command" before ever reaching the
new required-runtime check.
* [Installer] Give FindFirst the two arguments it takes
FindFirst takes the pattern and the output variable and leaves the search
handle in $0. There is no handle argument, and FindNext taking only a variable
is the giveaway: the handle has to live somewhere implicit, and it is $0.
The call passed three arguments, so makensis rejected the script and the CPack
packaging step failed with "Problem running NSIS command" before ever reaching
the required-runtime check.
This was not only a compile error. The handle variable was never populated, so
the stale-runtime check that commit 844a1a9b added could never see a match: it
probed $StaleFindHandle, which stayed empty, and the widened wildcard scan over
the purge set was a no-op. Pass the pattern, test $0, and close $0. No FindNext
is needed, because $R0 is a yes/no flag rather than a list.
Drops the now unused StaleFindHandle Var.
* [CI] Dump the NSIS compile log when packaging fails
CPack reports a failed makensis run as "Problem running NSIS command" plus a
path to NSISOutput.log, and that log exists only on the runner, so the actual
compile error never reaches the workflow output. An installer script that does
not compile is therefore undiagnosable from CI alone, which is exactly how a
three-argument FindFirst cost two build cycles here.
* [CI] Run the NSIS log dump under bash
The dump step used the default pwsh shell, where `find` resolves to Windows'
text-search find.exe rather than a file finder, so it died with "FIND:
Parameter format not correct" instead of printing anything. Pin shell: bash.
* [Installer] Test the error flag with LogicLib's ${Errors} condition
makensis rejected the script with "macro _If requires 4 parameter(s), passed
2" at the ClearErrors/OpenProcess pair. A ${If} condition cannot be a bare
$Var: LogicLib needs either a comparison or one of its flag macros, and a lone
$Errors splits into too few arguments for _If. ${Errors} is the documented flag
condition and is the same mechanism as the ${Silent} test already used here.
The dump step earned its keep - this took one run to locate rather than another
round of guessing.
* [Installer] Give FindFirst its handle back and stop blocking silent aborts
makensis: "FindFirst expects 3 parameters, got 2. Usage: FindFirst
$(user_var: handle output) $(user_var: filename output) filespec".
The previous commit was wrong about this. FindFirst does take the handle
explicitly, and FindNext finds it in $0 as a side effect of the FindFirst that
set it, which is why FindNext takes only a variable. The three-argument form the
stale-runtime check was written with was correct all along, and the two-argument
form broke it. Restored, along with Var StaleFindHandle.
That same commit inverted the interactive check in both abort paths.
`${If} $ReinstallMode = 1` with `${OrIfNot} ${Silent}` shows the message box
exactly when the install is the silent /R upgrade - the one case the comment
says must never block, since nothing will dismiss it and the installer hangs
until it is killed. Replaced with `${IfNot} ${Silent}`, so the box appears only
for an interactive install and a silent abort goes straight to SetErrorLevel.
Audited every construct added by this branch against master, which compiles:
all 9 LogicLib condition shapes have a master precedent, FindFirst is the only
FindFirst and is back to 3 arguments, Function/Section and block depth balance,
and the 12 lines with an odd number of quotes are the same pre-existing string
continuations master has.
* [CI] Strip the CR 7-Zip leaves on its paths before comparing them
The installer check reported all 14 required files missing, including
Qt6Core-era basics an 82 MB package obviously contains, so the payload was fine
and the comparison was broken. 7-Zip writes CRLF on Windows, sed 's/^Path = //p'
kept the \r on every line, and grep -qxF matches whole lines - so every entry
carried a trailing \r and nothing matched. The build-tree artifact check next to
it uses an unanchored grep -E and was unaffected, which is what made the two
checks disagree.
Reproduced locally against a 7z archive with CRLF injected: the old pipeline
matches nothing, the new one finds the present files and reports only the
genuinely absent one.
Also match case-insensitively. That is how the installed tree behaves - the
uninstaller clears both $INSTDIR\plugins and $INSTDIR\Plugins - so a
Plugins vs plugins difference in the archive is not a missing file.
* [CI] Print both sides of the installer path comparison, and strip its prefixes
The check still reported all 14 files missing after the CR fix, so the CR was
not the cause and the real difference between 7-Zip's output and the manifest
is still unknown. Inferring it again is what produced two wrong fixes in a row,
so on failure the check now prints the required list and the first 40 paths
7-Zip lists. A bare "missing" line cannot distinguish a missing file from a
misread listing, and twice it was the latter.
Also strip $INSTDIR/, $OUTDIR/, $PLUGINSDIR/ and ./ prefixes from the listed
paths. Written as separate -e expressions rather than one alternation: with |
as the sed delimiter an escaped \| is a literal pipe, so the alternation
version silently matched nothing at all.
Verified against a 7z archive fed through seven path shapes - plain, Windows
backslashes with CRLF, each prefix, lowercase plugins, and all of them combined
- each reporting only the file genuinely absent from the archive.
* [CI] Strip CR from the manifest side of the installer path check
The dump added last run settled it: 7-Zip listed 209 paths including Qt6Gui.dll,
Qt6Multimedia.dll and Plugins/platforms/qwindows.dll verbatim, as plain relative
paths with no prefix - so the archive side was already clean and the previous
fix was right to strip CR there but not sufficient.
The trailing CR was on the other side. CMake writes required-qt-runtime.txt
with the platform line ending, so on Windows $entry arrived as
"Qt6Gui.dll\r", which cannot whole-line match a clean "Qt6Gui.dll". Every
entry then read as missing while the listing held it, which is precisely what
the two sides of the dump showed.
Reproduced locally with a CRLF manifest against a 7z archive: without the strip
every entry is reported missing, with it only the genuinely absent file is.
Checked across plain paths, Windows backslashes with CRLF, $INSTDIR, $OUTDIR,
$PLUGINSDIR, ./ and / prefixes.
The literal $INSTDIR-style prefixes are written as [$] bracket expressions so
shellcheck does not read them as unexpanded variables; that clears the SC2016
that failed the format job, without a disable directive.
* [CI] Reword the bracket-expression comment so the linter stops parsing it
A comment whose first word is shellcheck is read as a directive, so
"# shellcheck does not read these as unexpanded shell variables" was parsed as
a directive named "does" and failed with SC1073 and SC1072 - which cascaded into
the SC1009 on the else clause above it. The SC2016 it was meant to document is
already gone now that the prefixes are written as [$] bracket expressions, so
the note only had to stop looking like a directive.
* [CI] Indent the diagnostic path lists without sed, and undo comment drift
SC2001 asks for parameter expansion instead of sed 's/^/ /'. Echoing
${manifest//$'\n'/$'\n '} prepends the two spaces directly and keeps the
diagnostic block readable. Both occurrences are converted; only the one reading
from a here-string was reported, but the piped one has the same shape.
Also restore master's wording of the build-tree artifact comment. An earlier
edit inserted "in " ("not the install)" became "not in the install)"). It has no
functional effect, but it was an unintended change to a line this branch has no
reason to touch, and .ci/compile.sh now differs from master only by additions.
* [CI] Ship the Qt module runtimes the Windows package check requires
The installer check reports Qt6Concurrent.dll and Qt6ShaderTools.dll missing
from the Windows package. Both are genuinely absent, not another comparison
artifact - the manifest now matches 12 of 14 entries and the dual-list
diagnostic shows the other two are not in the archive at all.
Nothing deploys them. fixup_bundle copies only the DLLs it finds in
Cockatrice.exe's import table, and the *.dll glob only sees what was already
copied next to the executable. Qt6::Concurrent is linked but never imported,
because QtConcurrent::run and QFutureWatcher are header-only in Qt 6, so the
linker drops the import; Qt6::ShaderTools is skipped the same way. The package
has shipped without them and the client still starts, but that is luck rather
than intent, and required-qt-runtime.txt asks for both.
Install the runtime of every linked module explicitly so the installer contains
what the check demands. An unused DLL costs a few hundred kilobytes; a missing
one is what stops the client starting after an update, and stays invisible until
then.
Derive both the manifest and the install list from the imported target's TYPE,
so a statically linked Qt module - which ships no runtime at all - is skipped by
each and the two can never disagree. Scope the manifest to the client rather
than the union across server, client and Oracle: only the client is packaged on
Windows, and the other two module lists are subsets of the client's, so this
changes nothing today.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The local deck and replay browsers list every file in the folder, so a
stray file (e.g. a PNG screenshot of a deck) can be parsed as a garbage
plaintext deck, opened in the editor, and uploaded. The server also
accepts oversized deck payloads with only silent truncation.
Hide files that are not in a supported deck/replay format in the two
QFileSystemModel views (directories stay visible), skip them when opening,
reject them when uploading, and make the server reject deck uploads larger
than MAX_FILE_LENGTH instead of truncating them.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Exit deterministically when launching the update installer
* [Client] Only quit when the main window close is accepted
Wait for MainWindow::close() to be accepted before quitting after the
update installer is launched. When the close is vetoed (a running card DB
update, open games, or an unsaved deck), keep running and tell the user
the installer is already waiting, instead of exiting over their answer.
Also fix the comment so it does not claim settings are saved on the vetoed
path.
* [Client] Only quit on update when the shutdown actually ran
MainWindow::closeEvent has a static re-entrancy guard that returns early on a
second close event, leaving it in its default accepted state. DlgUpdate reached
from a nested event loop while a shutdown prompt was up could then get close()
== true with no shutdown work done (no settings flush, no tab shutdown) and tear
down the process behind an unanswered prompt.
Add MainWindow::closeForUpdate(), which reports false when a close is already in
progress or was vetoed, and gate the update-exit on it.
* [Client] Use QCoreApplication::exit() to leave on update
QCoreApplication::quit() only asks the application to quit and can be
interrupted if a top-level window refuses its close event. On the update path
the process must actually leave so its Qt DLLs are unlocked before the installer
starts replacing files - apply the exit(0) variant both on the accepted-close
path and the fallback where no MainWindow was ever closed.
* [Client] Warn that the update installer force-closes within a minute
Telling the user Cockatrice just 'stays open for now' is misleading: the
installer launched by #7308 waits only about 60 s for a graceful close before it
force-terminates the process, which would lose unsaved work. State the deadline
and tell the user to save and close before then.
* [Client] Warn that a vetoed update is cancelled, not forced
The warning on the veto path promised the opposite of what the installer
does: WaitForAppToClose in #7308 never force-closes cockatrice.exe
($AllowForceClose is 0), it aborts the update after 60 s so no unsaved data
is lost. A user who is told Cockatrice will be terminated in a minute hurries
to close it, only to find the update already given up.
State the real outcome instead: the installer waits about a minute for a
close, and the update is cancelled if that does not happen.
* [Client] Warn about a waiting installer without blocking it
CloseMatchingApps in #7308 sends the WM_CLOSE once and the wait loop only
polls IsAppRunning afterwards, but QMessageBox::warning is application modal
and QGuiApplicationPrivate::processCloseEvent drops spontaneous close events
for windows blocked by a modal widget. The veto path therefore swallowed the
installer's one and only request, so even a user who resolved the blocker and
closed the app still ended in the 60 s timeout.
Move the warning into warnInstallerIsWaiting() and show it modeless, so the
main window stays able to answer the installer while the message is up.
* [Client] Close the parent on the no-MainWindow update fallback
The fallback branch exits the process without closing anything, so it skips
the settings flush and tab shutdown this change exists to get in front of the
installer - the opposite of the fix if a call site ever stops passing a
MainWindow.
Close the parent widget first, like the code before this change did, and only
then leave unconditionally: the parent is the one that knows how to shut down
gracefully, and the deterministic exit is what the installer needs.
* Add override.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Windows] Close running instances and purge stale runtime DLLs during update
* [Windows] Scope running-instance handling to install dir and fix NSIS build
- Drop !include nsExec.nsh: nsExec is a plugin DLL, not a header, so
makensis aborts before NSIS can build the installer.
- Match processes by image name and executable path under $INSTDIR via
PowerShell, then close (WM_CLOSE) and force-stop only those PIDs, so an
unrelated oracle.exe (Oracle DB) is never killed on a silent /R update.
- Gate the stale-runtime-DLL purge on $INSTDIR\cockatrice.exe existing, so
a first-time install can't recursively delete an unrelated Plugins
directory.
* [Windows] Make process detection rely on an explicit PowerShell exit code
* [Windows] Fix exit-code detection in the uninstaller process check
* [Windows] Pass the install dir to PowerShell without quoting pitfalls
* [Windows] Fail closed when a process path is unreadable
* [Windows] Only pre-check running instances for /R updates
* [Windows] Wait on a wall-clock 60 s deadline instead of a loop counter
* [Windows] Verify the force close and abort if the process survived
* [Windows] Defer silent updates instead of force-killing the client
* [Windows] Purge OpenSSL 3 DLLs on portable upgrades and broaden the stale-DLL guard
* [Windows] Replace the GetTickCount macro with a direct System::Call
* [Windows] Only close running instances whose path is readable
CloseMatchingApps reused the fail-closed detection filter, which counts a
process whose Path cannot be read (Access denied) as a match. The installer
runs elevated, so CloseMainWindow() would then be delivered to an unrelated
oracle.exe / servatrice.exe belonging to some other program, silently closing
a third party application during a Cockatrice update.
The set of processes we act on now has to be the strict one: a readable path
under $INSTDIR. Unreadable paths are simply not ours.
* [Windows] Match running instances by a readable path under the install dir
The Path read in the Where-Object filter raises "Access is denied" for
processes the installer cannot open, and both branches of the try/catch
turned that into a match. Together with mapping every non-zero nsExec result
to "running" this made the guard a hard stop rather than a fail-closed one: a
machine with an Oracle database service (or a servatrice service) running as
SYSTEM always has a process named oracle / servatrice whose path cannot be
read, so every silent update aborted after 60 s and every interactive install
re-asked for Retry indefinitely.
A process whose path cannot be read is not one of ours - the updater is
launched by the application it updates and therefore runs in the same user
session, and an interactive install is elevated and can read the paths of the
user's own processes. Both the detection and the close path now require a
readable path under $INSTDIR, which is what the comment above the helpers
already claimed.
* [Windows] Distinguish "could not determine" from "definitely running"
Mapping every non-zero nsExec result to "running" folds nsExec's "error"
return (powershell.exe could not be started at all - removed, blocked by
policy, refused by antivirus) into "Cockatrice is running". That state can
never clear: closing applications does not help, so silent installs aborted
after the full 60 s wait and interactive installs could not get past the
Retry/Cancel prompt.
IsAppRunning now reports three states (0 not running, 1 running, 2 could not
determine) and a new IsProcessLookupAvailable preflight checks that
powershell.exe can run at all. A broken PowerShell is reported to the user
once - with a message box, since a silent install would otherwise show
nothing - and the install continues instead of being blocked forever. Only a
definite "running" is now waited for.
* [Windows] Force-stop the console applications after the grace period
Servatrice and Oracle are console applications, so CloseMainWindow()
returns false and does nothing for them. Every silent update therefore
waited the full 60 s and then aborted, and every interactive install looped
on the Retry prompt, for any user running Servatrice out of the install
directory.
They now get a second shutdown path: once the grace period is over they are
force-stopped by PID with the same strict install-dir filter, since they hold
no unsaved user data. Cockatrice keeps the graceful-only treatment - a
force-kill there could destroy an unsaved deck - and still aborts instead.
* [Windows] Tell the user when a silent install or uninstall is aborted
On the /R path the installer runs with SetSilent and SetAutoClose, so the
DetailPrint before Abort went nowhere: the user clicks Help -> Check for
Client Updates, the download finishes, Cockatrice closes its window, and
nothing else happens - no update and no error, because the client launches the
installer detached and never looks at its exit code.
Every silent abort path now shows a message box telling the user which
application is still open and that they should close it and run the
installer again. The same applies to the silent uninstall, where the only
output is a log file nobody reads.
* [Windows] Treat leftover runtime files as an install marker for the purge
The guard on the stale-DLL purge only looked for the three executables and
uninstall.exe, so it skipped precisely the case the purge exists for: a
previous failed update whose uninstaller removed what it could and silently
skipped the Qt DLLs that a running instance had locked. None of the four
markers is left in that directory, and the stale DLLs then survive into the
new install.
A leftover Qt6Core.dll (what we ship today) or the qt.conf written by a
portable install now count as an install marker as well.
* [Windows] Run the section instance check before any file is deleted
Call EnsureAppsNotRunning sat after the portable-upgrade block, which had
already deleted the old cockatrice.exe, Qt*.dll and plugins directory. An
interactive install ("Portable mode" pointing at an existing portable folder)
has $ReinstallMode = 0, so the .onInit guard is skipped and locked files were
silently skipped - exactly the mixed-Qt-DLL state this change is about.
The check now runs right after SetOutPath, before the first Delete/RMDir in
the section.
* [Windows] Guard the old install before the old uninstaller deletes files
The interactive upgrade path removes the previous installation long before the
install section: componentsPagePre runs the old UninstallString on the
components page, i.e. before the directory page and before any call of ours
can check anything. Every uninstaller shipped before this change has no
un.EnsureAppsNotRunning, so for exactly the versions affected by #1576 the old
files were deleted while cockatrice.exe was still running and the new
installer had no say in it.
componentsPagePre now runs the same check before both ExecWait calls. At that
point $INSTDIR is still only the default, not the directory the old install
lives in, so the new EnsureOldInstallNotRunning guards the location recorded
in the registry instead. The helpers now work on a $GuardDir the caller
picks; the normal entry points keep seeding it with $INSTDIR.
* [Windows] Do not abort the section check past the point of no return
"nothing has been replaced yet, so aborting just defers the update" only holds
for the guard in .onInit. On the /R path AutoUninstallIfNeeded has already run
the old uninstaller, which RMDir /r's the whole install directory, and the
purge below it deletes the runtime files - so an Abort from the section would
leave the user with the old version uninstalled and the new one not installed
at all, which is strictly worse than the bug this change fixes.
The ordering now guarantees the check already passed: .onInit guards the /R
path before the uninstall, so the section only repeats it where that is not
already true, i.e. for interactive installs, where nothing of this installer
has been deleted yet and an Abort is the correct outcome.
As a safety net for whatever the purge could not remove (a still locked file,
or a check that could not run at all), HasStaleRuntimeFiles reports surviving
runtime files with a message box - also on a silent update, where there is no
other way to let the user know the install came out incomplete.
* [Windows] Only block the in-app updater with a message box
The message box on the silent abort would also fire for a plain /S install,
which is exactly the unattended case: a package manager (or a CI job) running
the installer with a leftover Cockatrice process would now hang forever on a
dialog nobody can see instead of aborting.
The box is therefore limited to /R, the path the in-app updater takes, where
the user just clicked Help -> Check for Client Updates, the client launches
the installer detached and never inspects its exit code, and the window is
about to disappear - without a message, the update would silently not happen.
Every silent abort also sets an error level now, so an unattended caller can
see that the run failed instead of having to read the log.
The silent uninstaller reports through log and exit code for the same reason:
it is run unattended by the /R updater itself (QuietUninstallString) and by
package managers. The installer that invoked it tells the user about the
incomplete result itself.
* [Windows] Treat an unset guard directory as undeterminable
$GuardDir is set by the three entry points into the check, but it is now what
decides whether a process counts as one of ours, and an empty string would
match every process that merely shares an image name - the same false positive
the strict path filter exists to avoid. Report it as "could not determine"
instead of running the check, so a future caller that forgets to set it warns
the user rather than closing or killing a third party process.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
On a clean install, checkUnknownSets() fired before the onboarding wizard,
so the legacy 'all the sets have been enabled' welcome and the Manage Sets
dialog appeared first - with no card data there are no sets to manage yet.
Defer the set check until after the wizard closes, suppress its dialogs
while onboarding runs, and enable newly downloaded sets silently instead.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
RemoteClient's ping sweep deleted timed-out pending commands without
emitting finished, so TabServer's in-flight room-join markers were never
released: the room stayed silently unjoinable for the rest of the session
even though the connection stayed alive. Answer swept commands with
RespNotConnected instead of dropping them, matching the disconnect
cleanup, so every awaiter gets a deterministic terminal response.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The home background PNGs shipped since #7280 total ~67 MB and bloat
every clone and pull for other devs. Encode them as JPEG at quality 95
(matching resolution, visually lossless at 40-48 dB PSNR since the art
is noise-heavy renders PNG cannot compress) for ~79% size reduction.
The scheme-variant and theme asset resolution already probe .jpg, so
only the qrc entries needed updating.
Refs #7327
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Server/Client] Unify report categories into one shared source of truth
* Lont.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Ctrl+scroll over a card display now resizes the cards everywhere a card
size slider is shown, matching the standard way of resizing content.
CardSizeWidget learns to forward Ctrl+wheel events onto its slider via an
event filter that is installed on the display container and, when present,
on the scroll area's content widget so the resize intercepts the wheel
event before the view scrolls. The existing slider valueChanged wiring
then rescales the displayed cards.
Applied to the visual deck editor (per card group, covering flat and
overlapped layouts), visual database display, printing selector, sample
hand, visual and public deck storage, Archidekt previews and EDHRec card
displays.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
CardPictureLoaderLocal compared the full file name (including extension)
of the custom-folder candidate against the extension-stripped base name of
each directory entry, so a CUSTOM subfolder image like
pics/CUSTOM/poker/1 of Hearts.png never matched and the card fell through
to the network. Compare the complete base names instead.
Add matcher tests covering CUSTOM subfolder resolution.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Theme] Revert default light palette and only include AppColors
* [Style] Also remove dark mode palette
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* tests: add manual picture loader benchmark against the real card hosts
(cherry picked from commit 588d71d3b9a8278ab98b847802908f322c1035b7)
* tests: address review on picture loader benchmark
- Sandbox via unique app/org names plus Linux-only XDG redirection, derive
the warm-cache probe and data path from SettingsCache, and bail out when the
temporary sandbox cannot be created.
- Run each pass with a fresh worker and shut workers down before reading the
429 counters, so the redirect cache is persisted and no worker thread can
outlive the stack-local counters or the installed message handler.
- Make s_activeCounters atomic and always forward log output when the previous
handler is the built-in (nullptr) one.
- Validate --timeout-min, scale the cached-pass budget with --count, honour the
first --url as the stress template, and add the missing trailing newlines.
- Zero-initialise SettingsCache members so the benchmark mock cannot
dereference an indeterminate pointer.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [PictureLoader] Fix worker thread shutdown and cross-thread cache clearing
clearNetworkCache() ran directly on the UI thread while the worker thread
owned the disk cache and redirect cache, racing cache reads/writes. Make it
a worker-thread slot invoked via a blocking queued call when the thread is
running, so the 'Cached card pictures have been reset.' message is truthful.
The worker thread was also never quit()/wait()ed: both destructors only
deleteLater'd their objects, so Qt warned 'QThread: Destroyed while thread
is still running' and leaked a running loop at exit. Wire the worker's
finished() signal to its own deleteLater() (canonical worker-object
pattern), add shutdownThread() to stop the loop, and let CardPictureLoader
destroy the QThread only after wait() has returned.
* [PictureLoader] Guard cache teardown and stop blocking the UI thread
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [PictureLoader] Add user-configurable per-host request caps
Picture downloads were throttled to a uniform 10 requests/second per host
with no way to tune a specific server. A rate-limited API host (Scryfall
caps at 10 req/s) can trip 429s during bursts, and CDN hosts with no rate
limit were throttled needlessly.
Introduce developer-owned per-host caps that users can only ever lower,
never raise, exposed in the download settings page:
- DownloadSettings::DEVELOPER_HOST_CAPS sets the ceiling per host
(api.scryfall.com 9, cards.scryfall.io unlimited, others 10).
- A new hostRequestLimits setting stores user overrides in downloads.ini;
clampHostRequestLimit() bounds them to [1, devCap] so a user can reduce
api.scryfall.com to 5 but never raise it above 9.
- The picture worker seeds, halves on 429, and recovers its sustained
per-host allowance against the effective ceiling instead of the global
maximum, and skips per-host accounting entirely for unlocked hosts
(cards.scryfall.io) while global pacing and 429 backoff still apply.
- The deck editor settings page gains one spinbox per known host, each
clamped to its developer cap.
* [PictureLoader] Let unlocked hosts skip dispatch pacing; adjust limits per URL
Two refinements to the per-host request caps:
- Unlocked hosts (UNLIMITED_HOST_QUOTA, e.g. cards.scryfall.io) no longer
wait on the 100ms dispatch pacing or consume the global per-second quota.
dispatchQueuedRequest fires their queued requests back-to-back, bounded
only by their 429 backoff window and Qt's per-host connection pool, so
an unthrottled CDN is not artificially slowed.
- The deck editor download settings page replaces the static grid of one
spinbox per known host with an "Adjust Rate Limit" toolbar action on the
URL list. It picks the host out of the selected URL and clamps the entry
against the developer cap table (including for user-added URLs).
Also fixes a review finding: resetRequestQuota could write the
UNLIMITED_HOST_QUOTA sentinel (-1) into the sustained per-host quota when a
host became unlocked mid-run, permanently poisoning its allowance. Stale
entries for unlocked hosts are now dropped, and the per-second seed is
clamped against the effective ceiling so a lowered limit applies immediately.
* [PictureLoader] Cap unlocked host bursts and adapt them to 429s
* [PictureLoader] Store per-host limits readably and show them per URL
* [PictureLoader] Make dispatch and rate-limit bookkeeping key on the real host
Addresses ZeldaZach's round-4 review nits:
- Dispatch now resolves the cached-redirect chain before the in-flight gate,
so a redirect learned after a URL was queued can no longer bypass the
MAX_IN_FLIGHT_PER_HOST cap and drain the whole queue onto the redirect
target, which may carry its own developer cap. processSingleRequest does
the same so the allowance math keys on the host that is actually hit.
- The per-host in-flight slot is released when the reply is destroyed (with
the worker as the connection context) rather than on a 'finished'
connection bound to the work object, so an aborted reply or a work object
deleted while a reply is pending can never permanently shrink the fast
path's concurrency.
- storeSettings only prunes limits for hosts with neither a URL nor a
developer cap, so throttles on redirect targets (api.scryfall.com ->
cards.scryfall.io) survive URL removal.
- Unlocked hosts are offered 0..UNLOCKED_HOST_LIMIT_MAX (50) in the rate
limit dialog, matching clampHostRequestLimit() and the documented
hand-editable range, so values written into downloads.ini are no longer
silently rewritten on the next edit.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [PictureLoader] Seed per-host allowances on demand and skip hosts in 429 backoff
The quota reset re-filled every host's remaining allowance to a full
MAX_REQUESTS_PER_SEC as soon as the queue had a request for it, so a server
that was just rate limited could be hammered again at full speed immediately
after (or even during) recovery.
Only seed a host's allowance the first time it is dispatched in the current
second, seeded from its reduced sustained quota, and skip hosts still inside
their 429 backoff window entirely, handing the entry back to its worker so it
can wait the backoff out or fall through to another source instead of parking
in the queue with no reply pending. Deferrals wait on the host that is
actually blocking the request (cached-redirect targets and the reply host of
a 429) rather than the current card URL's host.
Rebased onto network-requests/request-pacing, which absorbed the earlier
pacing and dispatch-guard commits, and reuses its updateTimerState idle-429
recovery plumbing.
* [PictureLoader] Hand backed-off entries back across the whole dispatch tick
Review fixes on the 429-backoff skip:
- Hand a backed-off queue entry back to its worker via
scheduleDeferredRetry(host) instead of startNextPicDownload(), which was
looping on the pre-redirect host every 100ms tick whenever the queued URL
was a cached-redirect target whose redirect host was the one in backoff.
- Keep scanning the queue after a hand-back (--i; continue) so a backed-off
host at the head no longer consumes the entire dispatch tick, stalling
every healthy host further down.
- Emit imageRequestSucceeded(url) before makeRequest()'s cached-redirect
backoff return so the status bar reclaims the deferred URL's widget instead
of inflating the progress bar forever.
- Only spend a per-second allowance when makeRequest() actually issues a
request: it returns nullptr when the redirect target is backed off and the
work is handed back, so the slot would otherwise be wasted on a no-op.
- Gate the dispatch-time backoff skip on requestTouchesNetwork() so entries
served straight from the disk cache (e.g. with downloads disabled) are not
bounced into a 30-60s deferred retry for a host that 429'd earlier.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [PictureLoader] Pace requests and run the throttle timers on the worker thread
Previously the whole backed-up queue was drained in a burst as soon as a
request was enqueued, sending up to 10 requests back-to-back and then
immediately re-filling the quota one second later. That hard-bursts a
rate-limited API like Scryfall's (10 requests/second) into a 30 second
lockout.
Introduce a pacing timer that dispatches a single queue entry every
100 ms, so the per-second allowance is used smoothly instead of in spikes,
and keep the quota timer at 1 second. Also fix both timers' thread
affinity: they are QTimer value members and so are not QObject children,
meaning moveToThread() on the worker left them on the main thread while
the slot code started them from the picture thread, which was a no-op that
also warned. They are moved to the worker thread explicitly and started
lazily from there.
* [PictureLoader] Guard dispatch timer restarts and drop dead request quota
* [PictureLoader] Reconcile quota-timer lifecycle with idle 429 recovery
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Filter] Restore exact, case-insensitive set code search (#7332, #7333)
* [Filter] Group set query suffix modes into a subrule
Address review feedback: keep the 'e'/'set' prefix in one place and move
the three suffix modes (exact, negated, release-date comparison) into a
dedicated choice-like rule so sv.choice() still dispatches on them.
Add coverage for the release-date comparison mode.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Render room chat history usernames as live user tags (#1595)
Room chat history carries no user-level data, so history usernames were
rendered as fixed, zero-level tags: the moderation context menu was missing the
buddy/ignore and promote/demote entries and the stored name casing was never
corrected.
Resolve each history author against the online user list and, when found, build
the user tag with the real user level and name so the entry behaves exactly like
a live chat tag. Offline users keep the plain fallback.
- chat_view: look up history authors via getOnlineUser for the real level/name
* Fix offline history usernames getting a leading underscore
The offline fallback used "_" as the level placeholder, producing an
href of user://__NAME. The hover handler splits at the first underscore,
so interactions targeted a nonexistent "_NAME" user. Use level 0 so
offline history entries render as zero-level tags like before.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Card] Add a setting for the language used in card search
Localized card names and texts can now be searched too, controlled by a
'Language used in card search' toggle (English, selected card language, or
both) on the general settings page. Untranslated cards always keep matching
in English.
Removed the redundant local copy of the URL templates list in the localized
picture loader while here.
* [Card] Bind search language per FilterString instance
The peg parser rules are set up once per process, so the GenericQuery and
OracleQuery rule actions could not capture per-instance state. Instead of
storing the search language in a process-global that FilterString instance
methods mutate, hand it to the rule actions through a thread-local parse
context and copy it into the filter closures they produce. Card evaluation
in FilterString::check no longer reads any process-global state, and each
instance keeps the language it was built with; constructing one instance no
longer changes what unrelated instances (deck filter, drop-to-hand, zone
views) match against.
The card database display model stores the raw query and rebuilds the
FilterString when the search language changes, since the language is now
bound at parse time.
Add tests for the English/Selected/Both search modes, the English fallback
for untranslated cards, and per-instance language independence.
* [Card] Pass the card search language to deck and zone card searches
Wire the two remaining FilterString consumers to the configured card search
language so card-name matches respect it everywhere:
- DeckFilterString now takes the search language and mode, exposes them to its
[[card name]] rule action via a thread-local parse context (same pattern as
FilterString), and the engine's card database uses them for content search.
- ZoneViewZone reads the card language from CardsDisplaySettings when applying
its search filter, and the reveal-zone widget re-applies the active search
when the language setting changes.
- The deck-storage search re-runs its filter against the current card language
setting, including live re-application when the setting changes.
Game-action targeting (DlgMoveTopCardsUntil) intentionally keeps evaluating
against English card names.
* [Card] Rename CardSearchLanguage to SearchLanguageMode
* [Card] Restore displaced namespace doc in card_localization.h
* [Filters] Pass CardSearchLanguage as a single struct
* [CardSearchModel] Match English and localized names in Both mode
Card names are stored in both English and localized forms, so search for
matches in both during the 'Both' search mode instead of checking only
the localized name.
* [CreateTokenDialog] Fetch cardsDisplay settings inside the apply lambda
Avoid capturing the raw settings pointer in the lambda: resolve the card
language and card search language from the settings cache at call time so
the values are always current when the search language is re-applied.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Doxygen] More picture docs
Took 12 minutes
Took 8 minutes
* [Doxygen] Move custom card pictures page into card_pictures subfolder
The actual cyclic dependency fix is converting the mutual @subpage
reference from custom_card_pictures to fixing_card_pictures into a
plain @ref, so the page hierarchy no longer loops back on itself.
* [Doxygen] Correct card-picture docs per review
* fix table layout
* [Doxygen] Deduplicate placeholder table and document image overrides
- Make custom_card_pictures.md the canonical home of the URL reference-point
table; loading_card_pictures.md cross-references it through @ref instead of
maintaining a second copy (unaddressed review comment).
- Switch the remaining @subpage custom_card_pictures to @ref in
fixing_card_pictures.md so the page keeps its single parent under
user_reference.
- Document the Image Overrides feature added in #7311/#7312 on the user page,
loading_card_pictures.md and fixing_card_pictures.md: local override storage,
the downloadedPics root lookup, exact file-name matching, and the set-folder
vs flat export naming schemes.
* Update doc/doxygen/extra-pages/user_documentation/card_pictures/custom_card_pictures.md
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>
* [DeckShare] Browse and open public decks with loading, error and accessibility states
Add a public-decks tab that lists decks published by other users using the
server's deck visibility feature, previewing each deck's banner card, color
identity, tags and upload time without downloading the deck list until the
user opens it.
- Add a public-decks tab with a shared-settings widget and a remote model
that fetches the target user's decks and refreshes both automatically and
on user request, with a loading indicator and a server-error message
instead of a blank tab when the fetch fails or the connection drops
- Render each deck as a focusable preview tile whose banner, color identity,
tags and upload time follow the existing Preview settings, with the deck
name announced as the tile's accessible name and Space/Enter opening the
deck, mirroring the shared-deck preview tile
- Show a message box when opening a public deck fails or arrives corrupted
- Publish and unpublish decks from the server storage toolbar and context
menu, toggling the deck's own visibility bit (what the server persists)
rather than the inherited effective state, and batch the visibility
refresh until the last in-flight change is acknowledged
- Add the Show Upload Time setting so the tile's upload stamp can be hidden
like the other preview details
- Update the retranslateUi wiring for the new public-decks tab and rename
the share action tooltip from "Deck share" to "Share link"
* [DeckShare] Adapt deck upload to the server-derived banner and tag protocol
The server now derives the banner card and tags from the uploaded deck
list itself, so Command_DeckUpload only carries the client-computed color
identity. Drop the reserved banner/tag setters from the editor and storage
uploads, send the color identity on remote saves, and read tags from the
now-repeated ServerInfo_DeckStorage_TreeItem field.
* [DeckStorage] Refresh the visibility column with a guarded timer instead of a latch counter
A dropped visibility reply used to leave the pendingVisibilityChanges
counter permanently positive, so the Public/Private column never refreshed
again and nothing reset it on disconnect. A restartable single-shot timer
with a boolean guard re-reads the tree whenever publishes quiet down and is
stopped on disconnect, so a lost reply costs one stale refresh instead of
killing the column for the session.
* [DeckStorage] Summarize batch publish failures when the batch drains
Each rejected node stacked its own modal dialog, so publishing a ten-deck
selection against a rejecting server made the user dismiss ten dialogs one
at a time. Failures are now collected while the batch is in flight and shown
as a single summary when the visibility refresh timer fires; a reply that
lands outside an active batch still reports right away.
* [PublicDecks] Time out the loading state so a dropped reply cannot wedge the tab
loading only cleared in decksReceived, but the ping sweep can drop a pending
command without ever emitting finished, leaving the tab stuck on 'Loading
public decks...' and the refresh button permanently inert. A single-shot
timer started per refresh clears the latch and reports a timeout; the latch
also clears when the client disconnects.
* [PublicDecks] Escape remote-crafted text in tooltips and the tab title
Deck names and usernames come from other users' records and Qt renders
QLabel tooltips as AutoText, so a name like '<h1><table>...' parsed as
markup. Escape and bound the deck-name tooltip and escape the username
interpolated into the title label.
* [DeckStorage] Distinguish an inherited public state in the visibility column
The column reported the effective state while publishing toggles the node's
own bit, so a private deck inside a public folder already read 'Public' and
toggling appeared to do nothing (and toggling again silently unpublished
it). The cell now shows 'Public (inherited)' for that case and the tooltip
explains why.
* [PublicDecks] Run retranslateUi at construction and name the refresh button
retranslateUi was never called from the constructor, so the tooltips set
there were absent until a language change. Call it before the first refresh,
and give the icon-only refresh button an accessible name for screen readers.
* [PublicDecks] Keep the empty and status variants correct across language changes
retranslateUi unconditionally rewrote the empty label to the 'nothing
published' variant, stomping the 'no decks match your filters' choice
rebuildGrid had made, and a visible loading message stayed in the old
language. Let retranslateUi pick the same variant rebuildGrid does and
re-show the status so it retranslates.
* [VDS] Share one color-identity match rule between the two deck grids
The remote public decks model verbatim-copied updateColorMatches' switch,
down to the ExactMatch normalization and the fact that Includes/Excludes do
not normalize case. Extract colorIdentityMatches() next to the FilterMode
enum and call it from both so the subtle rule cannot drift.
* [DeckStorage] Drop the unused tree widget model accessor
The accessor handed the model out past the wrapper methods that exist to
keep it encapsulated, and nothing in the stack called it.
* [DeckShare] End the public-decks files with a trailing newline
keeps the final line's diff clean and stops clang-format CI from flagging
the files.
* [VDS] Reuse the shared quick settings widget for the public decks tab
PublicDecksQuickSettingsWidget was VisualDeckStorageQuickSettingsWidget
minus the folders, banner and tooltip controls, with identical wiring for
the shared keys and a version of the near-identical file to keep in sync by
hand. Fold the Show Upload Time checkbox into the shared widget, give it a
setPublicDecksMode() that hides the controls that do not apply, and delete
the duplicate.
* [PublicDecks] Drop stale deck-list replies after the loading timeout
A reply that lands after its own loading timeout (the reverse of the ping
sweep dropping the command) could stop the newer request's timeout timer
and repaint the grid with out-of-date data. Each refresh now captures a
monotonically increasing request id, and only the newest request's reply
updates the grid.
* [PublicDecks] Re-show a displayed failure message on language changes
The status label carries both the loading and the failure message, and
retranslateUi hid it whenever the model was not loading, so a language
change while a server-error or timeout message was on screen swapped it
for the (empty) grid. The tab now keeps the last failure text and
re-shows it when not loading, clearing it once a new refresh starts.
* [DeckStorage] Keep the visibility refresh armed until replies land
The single-shot drain was armed with the 500 ms delay at send time, so a
round trip slower than that drained before the server applied the change,
re-read the old state and never re-armed, leaving the column stale until
a manual refresh. The timer is now armed with the full network timeout at
send time (a lost reply still costs one stale refresh) and re-armed with
the short delay every time a reply lands.
* [DeckShare] Close public decks tabs when the client disconnects
TabSupervisor::stop() built tabsToDelete from the room and game tabs
only, so a public decks tab survived a disconnect, sitting with stale
contents and a refresh button that kept hitting the dead client. Its
values are now folded into the same cleanup.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [DeckShare] Open shared decks via links with a gated preview flow
- Serialized url-chain dispatcher in IntentUrlParser; queue-drained
urlChainFinished(bool) drives the startup auto-connect fallback
- Open-shared-deck intent with sequential download state machine,
15s per-item timeout, partial-success offer, livable Cancel via
ApplicationModal dlg_login_prompt interactive fallback
- Preview dialog: download progress label, share vocab sweep,
palette-highlight selection frame, Space/Enter keyboard toggle,
NoFocus checkbox, double-click tile opens immediately
- Confirm-before-server-migration with one-shot restore to the
previous server on failed/cancelled chains (statusChanged settle
deferral), hostname-only identity comparisons
- Skip credential link when already connected; arrow-key navigation
in FlowWidget; card glows use palette highlight
- Address code-review M1-M4 and UI/UX QA blockers 1-2
* [DeckShare] End the open-shared-deck files with a trailing newline
* [DeckShare] Forward a dependency's cancellation as the owner's own
* [DeckShare] Let intent chains opt into the link sign-in dialog
* [DeckShare] Track link-intent chains per-run so each can restore its own session
* [Settings] Match a server on the exact host and port when adding it
* [DeckShare] Confirm the share link's target server before opening a deck
* [DeckShare] Reformat the link sign-in intent constructor
* [DeckShare] Time the share-list round trip and backstop silently-destroyed intent chains
* [Client] Drain a single-instance payload before its handlers read the socket again
* [Client] Treat a busy single-instance primary as alive instead of stealing its socket
* [DeckShare] Keep arrow-key navigation between flow items inside a scroll area
* [Client] Skip the startup connection when a macOS URL launch owns the connection
* [Client] Redact share secrets from activation URL logs
* [Client] Make the link-connection gates port-aware and keyboard-safe
Second-pass review notes for the shared-deck link flow (Cockatrice#7244):
- FlowWidget arrow-key navigation is opt-in via addNavigableWidget, so
combo/spin controls on the analytics flows keep their own arrow keys
- isConnectedTo and the open-deck/join-game preconditions compare the
configured server port alongside the host, so a same-host/different-port
link cannot resolve its share token or game id on the wrong instance
- the link sign-in dialog reuses an existing server entry's saved name
instead of renaming it to the raw hostname
- skipStartupAutoConnect is cleared once the launch chain connects, so a
later mid-session declined link cannot fire the startup fallback
- the plain-launch path of SingleInstanceManager no longer blocks on the
primary's ACK
- link- and server-supplied text is html-escaped in the confirm prompts and
shared-deck preview so markup cannot spoof the shown messages
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [DeckShare] Create temporary share links for local and server decks
* [DeckShare] Address review findings and harden the share flows
Gate every share entry point on login, de-duplicate the share-link and
color-identity logic behind DeckShareUtils and an injected querier, and
replace the silent tray/status-bar notices with always-visible dialogs.
- abstract_tab_deck_editor: explain that sharing requires a connection
instead of silently doing nothing when logged out
- tab_deck_storage: disable the share action on disconnect, reject
folder/deck mixes and the root folder with clear warnings, re-enable
Create on every entry/response so a dropped connection cannot leave
the button disabled
- tab_deck_storage_visual: same login gate for the context-menu entry,
visible success/error dialogs, and a symmetric in-flight guard
- getDeckColorIdentity now takes a CardDatabaseQuerier, dropping the
CardDatabaseManager singleton access and enabling unit tests
* [DeckShare] Fix share-link expiry build on the minimum-supported Qt
QTimeZone::UTC (the Initialization enum) only exists since Qt 6.7, so
Debian 12 and Ubuntu 24.04 (Qt 6.4) fail to compile the share-link expiry
handling in the share dialog and the two deck-storage tabs. Mirror the
existing games_model guard and fall back to Qt::UTC on older Qt.
* [DeckShare] Use the stable server client for the visual deck storage tab
* [DeckShare] Extract the share-creation response handling into DeckShareUtils
* [DeckShare] Drop includes left unused by the share-response extraction
* [DeckShare] Format share expiry with the locale-aware short format
* [DeckShare] Build share links with QUrl and QUrlQuery for percent-encoding
* [DeckShare] Replace the duplicate computeColorIdentity with the shared getDeckColorIdentity
* [DeckShare] Recover the share controls when the server never answers
* [DeckShare] Provide the full share hint in each plural form
* [DeckShare] Join the selected-count label with a non-translatable separator
* [DeckShare] Retranslate the share button tooltip with the storage widget
* [DeckShare] Forward retranslateUi to the visual deck storage widget
* [DeckShare] Let the share bar owners supply the hint text
* [DeckShare] End the share-related headers and sources with a trailing newline
* [DeckShare] Keep the settings include in the project include block
* [DeckShare] Include the network settings header used by the share timeout
* [DeckShare] Resolve the share theme icon through themePixmap
QPixmap("theme:icons/share") has no file extension, so ThemeManager::assetPath()
is bypassed and the pixmap is always null. Use themePixmap(QStringLiteral("icons/share"))
like every other toolbar action, so the .svg (and dark/light variants) resolves.
* [DeckShare] Keep the share selection consistent with the visible decks
Filtered-out previews are hidden but kept alive, so selectedFilePaths() counted
them in the share and the selection highlight. Only decks the user can see are
now shared, and a deck that stops matching the filters is deselectd as the deck
pass runs, keeping the %n count and the highlight in sync with the screen.
* [DeckShare] Abandon an in-flight tree share on cancel
Leaving share mode never stopped the timeout timer, and a late response still
ran shareFromTreeFinished, copying the link and announcing success for a share
the user backed out of. Stopping the timer and tracking the outstanding request
by sequence number means a stale reply (or a timed-out one) after cancel is
ignored, and cancelling + re-entering share mode can no longer confuse the two
requests.
* [DeckShare] Abandon an in-flight tile share on cancel
exitShareMode() left shareTimeoutTimer running and did not abandon the pending
Command_DeckShareCreate, so a timer pop or a late success still reported the
share after the user cancelled. Stop the timer and ignore stale responses via a
sequence number, mirroring the tree tab.
* [DeckShare] Wire the status-changed handler after shareBar exists
handleConnectionChanged() dereferences shareBar->isVisible(), but the connection
was set up before shareBar was constructed and shareBar had no in-class
initializer. On any status change delivered before construction the slot read an
indeterminate pointer. Seed the connection (and the initial share availability)
after shareBar exists and give shareBar a = nullptr initializer.
* [DeckShare] Explain why a blank deck cannot be shared
A blank deck exited the share flow silently. The menu only disables the entry
via setSaveStatus(), a different predicate, so the path is reachable (e.g. add a
card and remove it again). Mirror the not-logged-in branch with a short
information dialog.
* [DeckShare] Restore the banner-text doc comment
Re-add the doc block above refreshBannerCardText() that was removed as part of
the share-selection work; it documents the coupling to refreshBannerCardToolTip.
* [DeckShare] Resolve the stable server client in the deck editor gate
actShareDeck went through tabSupervisor->getClient(), which hands back a
LocalClient while an offline game is running. LocalClient never sets its status,
so a logged-in user could not share from the deck editor during a local game,
and got a misleading "You must be connected" message. Expose the supervisor's
stable remote client and use it for the gate and the dialog, matching the other
share tabs.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [VDS] Decouple tag filter and fix reordered-chips crash
* [VDS] Address review: dead code, chip reparenting, filter signal and sort fast-path
* [VDS] Address second round of review nits
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Build] Keep Windows installs free of build-tree artifacts
Several Windows packaging gaps could leak Visual Studio CMake build
output into the installed application or the NSIS installer:
- The per-app DLL sweep used
${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}, which is
empty on multi-config generators, collapsing the recursive
DIRECTORY install into the whole build tree (containing *.dir,
*_autogen, .qt, .qsb, x64, ...). Point it at the real per-config
output with $<TARGET_FILE_DIR:...> and exclude build artifacts.
- install(FILES ${OPENSSL_INCLUDE_DIRS} ...) tried to install OpenSSL
include directories as files. CMake refuses this
("install FILES given directory"); it only slipped through CI
because the vcpkg OpenSSL config leaves the variable empty. Remove it;
fixup_bundle already ships the OpenSSL runtime DLLs.
- The NSIS uninstaller only deleted *.exe/*.dll and a few known files,
so build-tree leftovers survived an uninstall/reinstall cycle. Wipe
the whole directory tree instead.
- Add a Windows CI gate that lists the packaged installer with 7-Zip
and fails the build if any build-tree artifact path is found.
* Update .ci/compile.sh
Co-authored-by: tooomm <tooomm@users.noreply.github.com>
* [Build] Rework Windows installer artifact exclusions per review
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: tooomm <tooomm@users.noreply.github.com>
* Direct wildcard resolution in action + cache with version key
* add back space
* Delete .ci/resolve_latest_aqt_qt_version.sh
* Disable Qt slimming and manual caching (use build-in fat caching)
* cleanup
* Re-add resolve_latest_aqt_qt_version.sh
* [Client] Fix spurious server room join error
The server replies RespContextError when a join command is received for a
room that connection is already registered in. The client was sending such
duplicate joins in benign situations - double-clicking to join a room, or
clicking a room the selector was already auto-joining - and answered them
with a modal telling users to restart the client.
Joins for the same room are now deduplicated while one is in flight, and a
remaining RespContextError is healed by leaving and rejoining the room so
the tab appears without a client restart. Error dialogs are only shown for
user-initiated joins, so failed auto-joins no longer spam critical popups.
* [Client] Bound stale-membership room join heal to one attempt
The RespContextError heal (leave + rejoin) previously recurred
unconditionally, so a server that kept returning RespContextError for a
reason other than stale membership would loop forever. Track room ids
that already received a heal and surface the error dialog after one
attempt instead of retrying indefinitely.
* [Client] Scope room-join heal guard to one join attempt
* [Client] Hoist room-join heal guard lookup out of response switch
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Themes] Add identity default palettes for the image themes
The Fabric, Leather, Plasma and VelvetMarble themes shipped only zone art and no palette, so their chrome fell back to the bare OS palette. Each theme now ships light and dark curated defaults written in the palette editor's own conventions, with [AppColors] so the home-tab buttons keep the theme's identity over static backgrounds.
- Add palette-default-light.toml / palette-default-dark.toml for the four themes; scheme resolution follows the OS since the themes declare no color scheme.
- Tint the window/base/button chrome and bevels towards each theme's identity: navy cloth (Fabric), black-brown with brass accents (Leather), electric violet with cyan sparks (Plasma), charcoal velvet with slate marble (VelvetMarble).
- Fill [AppColors] AccentStrong / AccentSoft per theme so the home-tab button gradient matches instead of falling back to the default greens.
- Derive Disabled and Inactive groups with the same conventions as PaletteGenerator::fromAccent.
* [Themes] Add home backgrounds for the image themes
* [Themes] Ship Fusion style + scheme backgrounds for the image themes
Address PR review:
- Add theme.cfg ([Style] Name = Fusion, ColorScheme = System) to Fabric,
Leather, Plasma and VelvetMarble so their curated palettes actually apply.
Without it the native style paints button chrome from the OS (windowsvista
on Windows has no dark mode), making the dark palettes' light ButtonText
unreadable on light buttons.
- Leverage the scheme-variant asset resolution: register home-dark.png and
home-light.png in resources and add them to the qrc so the built-in home
background also flips with the palette instead of staying static.
- Fix Leather [Palette.Inactive] Accent, which copied the Active Highlight
color instead of the theme accent (dark #4a5f8f -> #c9995a, light
#34508c -> #a5712f) in both palette files.
* [Themes] Align light plasma home background with the dark variant
Recolored the Plasma light home background to mirror the dark one:
brighter violet/cyan blooms, vivid azure spark arcs, and higher accent
saturation against the same pale-lavender key. Brightness is unchanged
so it still reads as a light scheme.
* [Home] Add option to disable the home tab background dim
Adds a 'Dim the home tab background' checkbox to the Home tab settings
page (Appearance). When unchecked, HomeWidget skips the translucent
black overlay it paints over the whole background. Default is on,
preserving current behavior; the home tab repaints live on change.
* Update leather backgrounds
* Update velvet marble backgrounds
* Update light plasma background
* Update light fabric background
* WIP [UI] Theme-aware onboarding banner with frosted light mode
Banner colours now derive from palette tokens at ~60fps (tick-driven,
equality-guarded setters) so scheme switches and live accent-picker
previews apply instantly:
- dark stages: byte-for-byte the original treatment (near-black stage
from window hue, Highlight accent, white centre halo, vignette 0.62)
- light stages: pastel accent-hue wash instead of a neutral grey copy,
brightness-lifted accent for additive glow legibility, deep-Highlight
halo (uGlowColor) instead of white blowout, gentler vignette
(uVignetteMin 0.88) so corners don't go muddy
- black logo silhouette variant selected on light stages
- theme picker preseeded with brand green (brand_colors.h single source)
WIP notes for next session:
- real-pixel wizard screenshot check still pending (headless capture
exists: Xvfb :77 + isolated XDG_DATA_HOME; shader vs fallback pixel
analysis not finished)
- user plans separately: promote Fusion to default theme, Default -> system
* [Themes] Align Fusion accent tokens with the SVG brand gradient
Align AccentStrong (#139740) and AccentSoft (#c9fd62) and the linked
Link/Accent roles with cockatrice.svg's linearGradient4265-7-8 stops so the
identity gradient used by the logo emulation matches the static art the icon
shipped.
* [Onboarding] Draw the banner logo as a static gradient plate
Replace the black/white logo tint switch with a ShaderEffect plate that
repaints the SVG's brand gradient (light AccentSoft -> dark AccentStrong
along the baked-in userSpaceOnUse axis) clipped to the full-color logo's
alpha silhouette, with the white highlight path overlaid on top — matching
the home widget's QPainter composite. The plate is static: no glow or
breathing. Brand colors flow from BannerShaderConfig's new brandStrong/
brandSoft pair instead of the removed logoDark flag, and the background
motifs get a touch more accent so the mark keeps its coloured surround.
* [Home] Draw the featured logo as a theme gradient composite
Repaint cockatrice.svg in Qt instead of showing the baked-in static art:
fill the full-color logo's alpha silhouette with the same brand gradient the
banner plate uses (light AccentSoft grading to dark AccentStrong along the
SVG's userSpaceOnUse axis), then overlay the white highlight path. Renders
an explicit QPixmap so the mark stays crisp at the 200px display size, and
re-seeds it on theme/palette/appearance changes so it never goes stale.
* [Resources] Drop the unused black logo asset
No consumer remains after the banner's logoDark toggle was replaced by the
static gradient plate (unit-tested in d7529e0c6a), so remove
cockatrice-logo-black.svg and its qrc entry.
* [Onboarding] Seed the theme picker from the theme's identity accent
Replace the hardcoded brand-green preseed with the shipped theme's own
AccentStrong (Plasma seeds violet, Fusion green), resolved from the default
palette so auto/user-generated palettes can't mask it, and re-seed whenever
the theme changes so the swatch never goes stale. Also consult the shipped
palette in maybeAutoGeneratePalette so scheme flips don't regenerate a fresh
palette over curated theme colors.
* Regenerate zone assets for Fabric, Leather, VelvetMarble themes
Align zone textures with each theme's palette identity:
- Fabric: linen weave in navy/steel tones (was generic green/blue/red/gray)
- Leather: grain texture in warm brown/amber tones
- VelvetMarble: smooth charcoal marble veining (replaces .jpg with .png)
All zones now have dark + light scheme variants for OS color scheme
adaptation. Plasma zones deferred to separate iteration.
* Regenerate Plasma zone assets: horizontal-hand layout, seamless tiling
Bake each zone's phase from its true world position in the horizontal-hand
layout (player 0, stack 172, table 280, hand 172+406) so the diagonal sheen
continues continuously across zone boundaries. All zones share one
mathematically tileable (1,1) diagonal at frequency 3; playerzone gets a
distinct blue-violet identity and the light scheme gets extra contrast.
* Regenerate VelvetMarble zones: classic Perlin marble, seamless tiling
* Regenerate VelvetMarble zones: domain-warped fractal veins, stone grain
* Promote Fusion to default theme, rename Default to System
Fresh installs and new profiles now default to the Fusion (dark) theme
instead of the platform-native theme. The old "Default" theme is
renamed "System" to better describe its purpose — using the OS-native
Qt style (windowsvista, macOS, etc.).
Existing users who had "Default" selected are automatically migrated
to "System" so they keep their platform-native styling. Users with
an empty or invalid theme name now fall back to Fusion.
* Move checkbox.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Add [AppColors] application palette roles
The home-tab buttons' gradient over the static theme background was hardcoded, and accent-derived fallbacks could not be themed or edited: QPalette's role set is closed, so any application-specific color has to live in Cockatrice's own palette layer.
- Add an AppColor::Role enum (AccentStrong / AccentSoft) stored on PaletteConfig and round-tripped from palette-<scheme>.toml under a new [AppColors] section.
- Cache the applied app colors in ThemeManager and expose appColor(Role) with a palette-accent-derived fallback; emit paletteChanged() from applyStyleAndPalette so previews, scheme switches and OS dark mode repaint palette-driven widgets.
- Fill both app roles in PaletteGenerator::fromAccent and surface them as a dedicated section in the palette editor.
- Drive the home-tab buttons from appColor() whenever the background source is the theme (any theme, not just built-ins).
- Ship AccentStrong / AccentSoft values in the Fusion and Default default palettes so the static home-tab buttons keep their classic greens.
# Conflicts:
# cockatrice/src/interface/widgets/general/home_widget.cpp
* [AppColors] Address review comments
- PaletteEditorDialog::onSave(): compare whole PaletteConfig (colors and
appColors) so a change to only AccentStrong/AccentSoft writes the file;
add PaletteConfig::operator==.
- appColor(): derive both roles from QPalette::Highlight unconditionally.
The Fusion palettes pin Accent to near-Window values, and QPalette::Accent
only exists on Qt 6.6+, so keying on it made identical themes render very
differently across Qt versions.
- themeChangedSlot(): merge the theme default's [AppColors] into a custom
palette that predates the section instead of all-or-nothing per file;
hasPalette() now counts an appColors-only file as a palette.
- Add Default/palette-default-light.toml so the Default theme's Light scheme
keeps the classic greens instead of falling back to the OS accent.
- home_widget: restore the isBuiltInTheme() half of the Automatic condition;
non-built-in themes extract button colors from their own background art.
- palette_grid_widget: use appEnum.value(i) for the role cast (3 sites),
append appHeader to headerLabels, fix the 'Lighted' typo.
* [Themes] Route theme writes to the user themes directory
setColorScheme()/setStyleName() and the palette editor wrote directly to
the resolved theme directory, which for built-in themes is the read-only
system (install) location. Changes therefore landed in the install dir and
were lost on upgrade.
Add ThemeManager::writableThemeDir(), which always resolves to the user
themes directory, and route all theme writes through it. The palette editor
reuses the same helper, dropping its private writability probe.
* [Home] Replace 'Automatic' button color with explicit theme colors default
The Automatic option gated on isBuiltInTheme(): built-in themes used the
theme's accent colors, while non-built-in themes extracted colors from
their own background art. That made the result depend on the theme's
origin rather than what the user actually sees.
Remove Automatic and expose two explicit choices: 'From theme colors'
(always the theme's identity accents, now the default) and 'Extract from
background' (always sample the painted background). Drop the now-unused
isBuiltInTheme() helper.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [PrintingSelector] Add Image Overrides submenu with hover preview
* [PrintingSelector] Address review comments
- Move QAction/QMenu forward declarations after the includes
- Use the renamed installPrintingOverride API and deleteAllLocalOverrides statically
- Drop the flavorName usage; Cockatrice does not use that field anywhere yet
- Extract the Load Custom Image handler into loadCustomImage()
- Make the preview size/offset constexpr and drop the redundant pixmap copy
- Extract the preview placement into a previewPositionNear() QPoint helper
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [PictureLoader] Add local override storage and resolution with matcher tests
* [PictureLoader] Address review comments
- Make deleteAllLocalOverrides static; it does not touch instance state
- Drop the now-unused hasCustomArt dead code
- Rename the override install methods to installPrintingOverride /
installPrintingOverrideOnLoad
* [Tests] Give loader matcher tests a writable HOME in CI
Under GitHub's docker runner the process uid has no passwd entry, so HOME
resolves to '/' and the test-mode qttest data dir cannot be created.
SettingsCache's QSettings then drops every write, getPicsPath() comes back
empty, and the loader searches a blank path. Point HOME at a QTemporaryDir
for the duration of the run.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Show localized card names, texts and pictures
Localization wiring now runs end to end: the oracle importer collects
foreignData for the configured language and the client renders it.
- [Oracle] Import localized names and rules texts for the selected cardLang
- single-face cards store their foreignData name and full text
- multi-face (split/adventure/aftermath/prepare) cards collect the joined
name once and join each face's translated text with the same separator
as the English merge; an incomplete translation falls back to English;
the joined text follows the same highest-priority-set policy as the
single-face path and is only collected when localization is enabled
- the wizard switching languages re-imports the card database
- [Client] Display localized card info throughout the client
- card info text/picture widgets and the game board re-render on language
change
- pictures resolve cardLang art through Scryfall's named endpoint using the
localized name, falling back to id-based art when no match exists
- deck editor keeps canonical English names as card identity (EditRole)
while showing localized names (DisplayRole), so decks and wire names
stay stable
- [Card] Add CardLocalization-backed name/text lookup and cards.xml v4
localization elements with a bounded-size translation cache
- [Tests] Cover oracle foreignData import (incl. multi-face joins, priority
and fallback paths), XML v4 localization parsing, deck model localized
display and the language-aware settings default
Existing installations need to re-run Oracle to see translations: localized
data only lands in cards.xml when the Oracle app is started with the
preferred language selected — launch the separate "Oracle" program that
ships with Cockatrice, pick the language in the wizard and let it re-import
the card database.
The client's database cache (cards.xml.cache) is invalidated by the cache
format bump and the source-hash checks, but a cache written before the
re-import can still hold English-only entries (the hash uses file size and
mtime, so a same-size/same-timestamp rewrite may be served as-is); delete
cards.xml.cache and relaunch if no localized names/texts show up after
re-importing.
* [Card] Pass localized card names and texts into CardInfo construction
Address review: instead of constructing the card and then calling
setLocalizedName/setLocalizedText (which emit a cardInfoChanged signal per
language), both constructors, both newInstance overloads and their callers
(cards.xml v4 parser and the binary cache reader) now pass the localized maps
as constructor arguments.
* [Client] Rename LocalizedCard:: helpers namespace to CardLocalization
The namespace now matches its header file name, as the review pointed out;
LocalizedCard reads more like a class or struct. Callers (card info text
widget, board card name rendering) are updated to match.
* [Client] Drop unused info member from the card info text widget
The CardInfoPtr member was only ever initialized to nullptr and never read;
remove it together with its initializer.
* [PictureLoader] Add the localized picture URL explicitly, not implicitly
Address review: silently prepending the Scryfall named-picture URL to the
download list whenever a non-English card language was active was surprising,
consumed quota per card when it failed, and could grab the wrong (canon) art on
name collisions, with no way to turn it off.
The insert is now opt-in and user-controlled: changing the card language adds
the template to the top of the download URLs once (persisted, documented in the
re-import prompt, and editable/removable in the deck editor settings), while the
picture loader no longer injects it at request time.
* [Card] Show card languages in the same native (English) format as the UI
Address review: the card text & images language dropdown listed bare native
names, some in inconsistent lowercase (e.g. "čeština", "español de España"),
which makes the languages easy to mix up for users that do not read the script
(e.g. 日本語 vs 한국어). It now mirrors the UI language dropdown and always pairs
the native name with its English name (e.g. "Deutsch (German)",
"日本語 (Japanese)"), using the same fixed casing.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [DeckList] Extract deck metadata XML serialization
DeckList still serialized its metadata inline: a ten-branch
readElement dispatch and a static writeMetadata that duplicated the
tree plumbing. The metadata arms (name, comments, format, timestamp,
banner card, playmat, tags) move to DeckListMetadataXml free
functions over the Metadata struct, leaving DeckList::readElement a
thin dispatcher between metadata, zones and sideboard plans. The
playmat clamping helper moves along with the element that uses it.
* [DeckList] Make deck metadata XML serialization instance methods
* [DeckList] Inline deck metadata XML serialization
Fold DeckList::Metadata::readElement and write back into deck_list.cpp
alongside isEmpty(), and drop the separate deck_list_metadata_xml
translation unit. The metadata arms are instance methods of the nested
Metadata struct, so keeping them in the same file as its other method
keeps the class from being scattered across two .cpp files; the rest of
the refactor (readElement as a thin dispatcher, element-wise reads,
clamped playmat params) is unchanged.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [DeckList] Extract plain-text deck parser into own file
DeckList::loadFromStream_Plain was a 160-line god-method mixing
deck clearing, name/comment detection, sideboard heuristics, set
and multiplier extraction and normalization. The parsing logic
moves verbatim into DeckListPlainText::parse() so it lives in a
dedicated, testable unit; DeckList keeps a thin delegating wrapper
and still refreshes the deck hash exactly as before (also on the
empty-input path, to match cleanList's original behavior). The
*F* foil suffix handling is relocated unchanged.
* [DeckList] Harden plain-text parser regexes and move metadata clearing up
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
DecklistNodeTree::getZoneObjFromName creates a new zone node when
the name is unknown, so declaring it const was a lie that let a
const DecklistNodeTree mutate its tree. It is only called from
mutating paths (addCard, readZoneElement), so the const qualifier
is removed.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The playmat read path clamped margin, offset and zoom with four
nearly identical qBound + fallback blocks. A single
parseClampedParam helper now owns that logic; behavior is unchanged
(parse whose string is well-formed clamps, unparseable text uses
the documented fallback).
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
undo() and redo() were mirror images that differed only in
which stack was the source. Both now delegate to a single
restoreAndSwap(source, target, deck) helper, so the save-current-
state, apply-memento and signal-emission logic lives in one place.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [DeckList] Remove no-op card XML readElement
AbstractDecklistCardNode::readElement only advanced the XML
reader to </card> and always returned 0; a card's attributes were
already parsed by the parent InnerDecklistNode::readElement. The
containing zone loop skips the card's end tag itself, so the
method was dead weight and is dropped from the node interface
along with the pure virtual it existed to satisfy.
* [DeckList] Document writeElement as the only serialization method
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The codebase is Qt6-only since #7071 dropped Qt5, so the
#if QT_VERSION < 0x050600 branch can never compile. Removing it
deletes a dead qHash overload that only existed to support
QRegularExpression in QSet on old Qt versions.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
With picture downloads enabled, requests were issued with AlwaysNetwork
cache control, which per Qt never consults the disk cache. A picture that
had already been downloaded was therefore fetched from the network again
on every session start, with the queue bypass letting those re-fetches
skip the rate limit entirely.
Treat the network cache as the intent of the 'Network Cache' storage
method suggests: if the URL is already cached, serve it with AlwaysCache
(no network, no quota); only a genuine miss goes to the network, and only
when downloads are enabled. Cache hits skip the queue for free since they
never consume the per-second request allowance.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Replace remaining QPixmap("theme:...") call sites with themePixmap() so icons, replay controls, card backs, and other images resolve to -dark/-light variants under theme schemes. Stem-exact 1:1 migration; behavior unchanged for non-variant themes.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Add scheme-variant theme asset resolution (#7209-1)
ThemeManager::assetPath() and schemeVariantPath() resolve a theme asset to its scheme-variant file (prefix-light/dark.png) with fallback to the plain asset, and themePixmap()/loadBrush()/loadExtraBrush() use them. CSS files load style-dark.css or style-light.css when present. Home widget re-resolves its background on theme change.
Link pixel_map_generator.cpp into the oracle target, which needs Qt6::Xml for QDomDocument.
* Fix clang-format wrap of theme format probe lists
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Server] Instrument command processing, game starts, and event loops
Add a lock-free MetricsRegistry that accumulates per-command processing
times in preallocated histogram slots (one per protobuf command type,
bucketed at 1/5/10/25/50/100/250/500/1000/2500/5000 ms +Inf). The
hot-path observeCommand() uses only relaxed atomic adds — no locks,
no allocations, no cache-line ping-pong beyond the unavoidable counter
updates.
Wire the registry into AbstractServerSocketInterface::processCommandContainer()
so every processed command is attributed with its container's wall-clock
time. When a container exceeds metrics/slow_command_ms (default 500),
a warning is logged including the connected username.
Add an EventLoopWatchdog heartbeat that runs on every socket pool thread.
If a heartbeat overshoots metrics/stall_warn_ms (default 2000 ms), the
overshoot is recorded in atomic counters and a warning is logged. Both
thresholds are configurable in servatrice.ini; setting stall_warn_ms to 0
disables the watchdogs entirely.
Track game-start durations via a separate histogram in MetricsRegistry.
Server_Game::startGameNow() measures the time from zone creation through
player materialization and reports it via Server::observeGameStartDurationMs().
Add a live card-count gauge: Server_Game exposes getCardsInGame() and
Servatrice::getCardsInGamesTotal() sums across all running games under
the appropriate read locks.
Include a standalone metrics_registry_test (Google Test) that validates
empty registries, single/multi-sample histograms, kind encoding,
overflow-slot collapse, negative-duration clamping, gauge rendering,
and the game-start histogram separation.
Took 10 minutes
* [Client/Server/Protocol] Surface live metrics in the Developer tab
Extend Response_GetServerStats with live counters from the in-process
MetricsRegistry: cards in games, event loop stall totals/worst,
total commands processed, average command time, active command types,
and game-start count/duration. Add a repeated CommandStats message
carrying per-command breakdowns (kind, extension number, resolved
protobuf name, count, total ms) for every type that has seen at
least one sample.
Server-side cmdGetServerStats() populates all new fields after the
existing DB uptime snapshot query, resolving protobuf extension names
via the descriptor pool for human-readable labels like
session/Command_Ping.
Expand TabDeveloper with two tables: an overview section (existing
DB stats plus the new live metrics) and a per-command breakdown table
(Command / Count / Total ms / Avg ms) sorted by total_ms descending
so the hottest commands surface first.
Took 55 minutes
Took 47 seconds
* [Server] Drop dead Prometheus histogram, add developer command metrics, fix watchdog init order
- metrics_registry: remove toPrometheusText/appendCumulativeBuckets and the time-bucket histogram that nothing in production ever emitted (the future /metrics exporter can bring it back); keep counts/totals read by the Developer tab
- Fix +Inf bucket routing that never incremented, and its test that locked the bug in
- Instrument developer_command container (kind 6) in processCommandContainer and stats label resolution
- Read metrics/{slow_command_ms,stall_warn_ms} at the top of initServer() so stall_warn_ms=0 disables the watchdogs before pool threads start
- Shrink KindStride to 1280 (largest extension in use is 1206) with a static_assert; document scrape cost of getCardsInGamesTotal; note slow_command logging has no rate limit in servatrice.ini.example
* [Tests] Give metrics_registry_test an explicit main
* [Server] Record only the dispatched command family; drop unused totals
processCommandContainer recorded every family in a container even though
the base if/else-if dispatch processes at most one. An unauthenticated
client could batch a session command (login) with fabricated developer,
moderator, and admin entries and forge genuine-looking samples that were
never executed or authorized. Mirror the base's selection, skip when the
handler was already deleted, and skip entries whose extension number is
-1 (which would otherwise wrap into the previous kind's id range).
[Server] Drop dead process-lifetime byte/uptime counters
txBytesTotal/rxBytesTotal added an atomic RMW to every socket write and
read for counters nothing consumes (cmdGetServerStats fills tx_bytes,
rx_bytes, and uptime_secs from the DB snapshot). Remove the two atomics
and the getTxBytesTotal/getRxBytesTotal/getUptimeSeconds getters; the
incTxBytes/incRxBytes slots and mutexes remain for the ISL legacy
counters.
[Protocol] Document kind 5 as developer in CommandStats
NumKinds is 6 and the server emits kind_index = 5 for developer
commands; the comment stopped at 4.
* [Client] Togglable auto-refresh for Developer stats tab
* [Oracle] Fix clang-format alignment of card type priority list
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Server/Client/Protocol] Add developer staff role
Introduce a Developer staff level (proto flag 32, DB admin bit 8) that
sits between admin and moderator: no kick/ban/warn/report/admin powers,
but gets server log access via a new developer command container family
(GET_SERVER_STATS, VIEWLOG_HISTORY) and an idle-timeout exemption.
- Protocol: IsDeveloper flag, developer_commands.proto envelope,
Command_GetServerStats/Command_GetLogHistory, Response_GetServerStats,
Command_AdjustMod.should_be_developer
- Servatrice: fail-closed developer dispatcher, uptime snapshot handler,
shared log history handler reuse, bit-8 DB mapping
- Client: burgundy pawn/badge/labels/sort order, prepareDeveloperCommand,
minimal Developer stats tab, log tab access, promote/demote actions
Took 24 minutes
Took 18 seconds
* [Server/Client/Protocol] Address developer role review feedback
Address ZeizaZach's review of the developer staff role:
- Nudge the developer log query to exclude private chat and sender IPs
(the ModeratorCommand path still sees everything).
- Deduplicate Command_GetLogHistory into Command_ViewLogHistory, which now
extends both ModeratorCommand (ext) and DeveloperCommand (dev_ext); the
client picks the DeveloperCommand-scoped extension by extendee, and the
server reads it via the extension number.
- Pull the uptime snapshot SQL into Servatrice_DatabaseInterface as
getLatestUptimeSnapshot() and widen the reported counters to 64-bit.
- Document the admin bitfield (1 admin, 2 moderator, 4 judge, 8 developer)
and add a server-side test for the developer command path.
* Add missing trailing newline to user_context_menu.cpp
* Remove stale includes of deleted command_get_log_history proto
The Command_GetLogHistory message was folded into Command_ViewLogHistory,
which deleted command_get_log_history.proto, but serversocketinterface
still #included its generated header. Fresh CI builds fail on the missing
file; local builds masked it by reusing a previously generated header.
* [Server] Exclude chat rows when private-chat filter is bypassable
A developer who omits log_location entirely — or sends only "chat" —
leaves chatType, gameType, roomType all false, so getMessageLogHistory
skips the target_type clause and returns every row, private messages
included. When !allowPrivateChat the server now forces game+room when
no surviving location was requested, guaranteeing the query always
carries a target_type restriction.
[Client] Demote mod+dev to moderator path in log-tab dispatch
The developer command family is strictly weaker than the moderator one
(no private chat, no sender_ip, ip filter ignored), so granting the
developer bit to an existing moderator must not silently strip their
capabilities. useDeveloperCommands is now true only when the user holds
the developer bit and not the moderator bit.
[Client] Hide the IP-address filter for developer log tab users
The developer path ignores the ip_address query field server-side.
Showing the field lets a developer type an IP and get results that are
silently unfiltered by it rather than an empty result set — reads as a
broken filter. Hide labelFindIPAddress/findIPAddress alongside the
privateChat checkbox.
* Developer pawn is silver.
* [Client] Fix indentation of merged Card Art Rules / Developer tabs
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle/Client] Report card database download progress
Card database updates ran invisibly: MTGJSON parsing spun an indeterminate
bar on the UI thread and the set import blocked the window, while the
onboarding wizard spawned `oracle -b` with no progress to show at all.
- Add byte-level scan progress to `RawJson::scanSetRanges` via an optional
callback, throttled to ~100 reports per scan.
- Emit `OracleImporter::dataReadProgress` during the scan and import sets on a
worker thread, driving the wizard's progress bar per set.
- With `-b`, write machine-readable `PROGRESS <stage> <done> <total>` lines to
stdout for the download/scan/import stages; stderr keeps the log output.
- Parse the oracle stdout in `MainWindow` and forward it to the onboarding
wizard, giving the card database step a determinate bar with stage-specific
status text.
- Guard the async workers against the wizard being closed mid-run.
- Add Google Test coverage for scan progress reporting.
* [Oracle/Client] Harden oracle progress workers and quit prompt
Address review feedback on the download-progress change: decompress and read
sets files off the UI thread, cancel the load/import workers before the
wizard can tear down the importer, and show an 'Extracting file...' status
plus a clean 100% tail so the poll never looks stuck. Quitting Cockatrice
while a card database update runs now asks for confirmation.
* Show 100% for 500ms on complete.
* Disable buttons on set import until done.
* Clean up progress bar.
* Drop wrapper around lambda
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle] Prefer higher-priority maintype when merging split cards
Adventure cards (e.g. Bonecrusher Giant, Virtue of Knowledge) are stored as
split cards in MTGJSON, with a Creature/permanent face and an Instant/Sorcery
adventure face. When the two faces are merged into a single card, the code
previously discarded the second face's maintype entirely, keeping whichever
face was processed first.
If the Instant/Sorcery adventure face appeared first, the merged card got
maintype 'Instant' with tableRow 3. At runtime this made double-clicking the
card on the stack send it to the graveyard instead of the table
(PlayerActions::playCard).
Fix the merge to follow the same priority order used by getMainCardType()
(Planeswalker > Creature > Land > Sorcery > Instant > Artifact > Enchantment),
so the permanent Creature type wins for adventure cards regardless of face
order, matching the physical card and the reported expectations.
Closes#4394
* Extract helper.
* Simplify const
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Fix duplicate attach messages in the game log caused by a redundant
child arrow in CardItem::drawAttachArrow.
Unlike the sibling drawArrow, drawAttachArrow omitted the ``card ==
this`` guard when iterating selectedItems(). Because right-clicking a
card to open the attach menu selects that card, it was always present
in selectedItems(), producing a second arrow for the same source card.
On release both arrows sent an identical Command_AttachCard, so the
server broadcast two Event_AttachCard messages and the log rendered
"attaches to" twice. Mirror the drawArrow skip condition so the active
card is excluded and only one attach command is sent.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
When a player restores connection to a game, the client rebuilds each
zone from the cards the server sends in the game state. Non-coordinate
zones (hand, piles, stack) report x == 0 on every card, so inserting
each rebuilt card at that coordinate reversed the received order one
card at a time.
Append rebuilt cards in the order they arrive for zones without
coordinates; coordinate-based zones (table) keep using x/y.
Fixes#2759
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Player pawns are looked up in QPixmapCache under a key built from the
rendered size, user level, and the avatar pixmap's cacheKey(). A null
pixmap reports cacheKey() 0, so all players without a custom avatar
collided: the first pawn rendered for a given size and user level was
reused for the next one, showing the wrong player's pawn.
Extend the key with the rendered height, the lowercased privlevel
(matching UserLevelPixmapGenerator), and both pawn colors so that every
visually distinct pawn gets its own cache entry.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Nothing in the UI confirmed that a user had been added to the ignore
list, so the action felt ambiguous and could be repeated by accident.
Show an information dialog when the server acknowledges the
add-to-ignore command.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Prevent contacting users on the ignore list (#1249)
The ignore list silences incoming messages, but a user on it was still
reachable: the context menu's chat item stayed enabled, private messages
could be sent, and a chat tab could be opened for an ignored user.
Make ignored users uncontactable: disable the chat item for them, refuse
to deliver messages typed in an open PM tab with one, and refuse to open
a new private chat tab with an ignored user (with a hint on how to undo
the ignore).
* Update cockatrice/src/interface/widgets/tabs/tab_message.cpp
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
When a player left, GameScene::removePlayer iterated zoneViews while close()
synchronously removed the current view from that list. A judge with several open
views of the departing player (e.g. library and hand) only had the first one
closed; the remaining views were skipped and left pointing at a player that was
about to be deleted, crashing the client on the next access.
- GameScene::removePlayer: iterate over a copy of zoneViews
- GameScene::toggleZoneView: same fix for the identical iteration bug
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Use the landscape orientation flag to rotate sideways-layout card art
upright before cropping, so planes/sieges show their horizontal art as
the server profile banner card instead of a rotated full card.
- Curve cropCardArt around the card's landscapeOrientation flag with
landscape-specific art margins (mirrors CardInfoPictureWidget)
- Add shared CardArtUtils::rotateSidewaysLayoutArt helper and apply it
to the playmat (game render and settings preview) and card info widget,
replacing three duplicate 90-degree rotation blocks
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Prevent spectator duplication when replaying joined events
The spectator branch of eventJoin emitted spectatorJoined unconditionally
even when the spectator was already present (e.g. replayed during a rewind).
Guard it like the player branch and eventGameStateChanged, and make
PlayerListWidget::addPlayer idempotent as defense in depth.
* In resetChatAndPhase() (the rewound() handler), also clear all spectators from both PlayerManager and PlayerListWidget before the replay rebuilds from event 0. The forward replay then re-adds exactly the spectators whose join events fall within the new time range via eventGameStateChanged/eventJoin.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
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"
CACHE:${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }} # directory for caching docker image and ccache
CACHE:${{ github.workspace }}/.cache/${{ matrix.distro }}${{ matrix.version }}${{ matrix.variant }}# directory for caching docker image and ccache
CCACHE_EVICTION_AGE:7d
CCACHE_SIZE: 550M # space of all repo is 10Gi:https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 600M # space of all repo is 10Gi:https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 550M # space of all repo is 10Gi:https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
CCACHE_SIZE: 600M # space of all repo is 10Gi:https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy
<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,15 +96,17 @@ 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.
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br>**Note:**`make clean` will remove the .ts files |
| `-DTEST=1` | Enable regression tests<br>**Note:**`make test` to run tests, *googletest* will be downloaded if not available |
| `-DCMAKE_BUILD_TYPE=Debug` | Compile in debug mode<br> Enables extra logging output, debug symbols, and much more verbose compiler warnings |
| `-DWARNING_AS_ERROR=0` | Don't treat compilation warnings as errors in debug mode |
| `-DUPDATE_TRANSLATIONS=1` | Configure `make` to update the translation .ts files for new strings in the source code<br>**Note:**`make clean` will remove the .ts files |
| `-DTEST=1` | Enable regression tests<br>**Note:**`make test` to run tests, *googletest* will be downloaded if not available |
> [!TIP]
> Build code documentation ([Doxygen](https://www.doxygen.nl)) according to our [configuration](https://github.com/Cockatrice/Cockatrice/blob/master/Doxyfile) into `/docs`: