Compare commits

...

84 commits

Author SHA1 Message Date
Lukas Brübach
164a67404d [Replay] Build a compact event history index
Turn replay protocol events into short localized summaries with an actor, badge, and time so the history UI can render them without retaining the replay or duplicating the chat log.
2026-11-01 22:16:51 +01:00
Lukas Brübach
77f60e0526 [Replay] Seek to the exact time of an event
Timeline seeks only expose whole timer intervals, so jumping to an event could leave that event unprocessed until the next tick. Add an event-time seek that also applies events at the target timestamp.
2026-11-01 22:04:48 +01:00
tooomm
e08402ac94
bump vcpkg submodule (#7690)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
2026-11-01 16:22:51 +01:00
BruebachL
ff393b8990
[Game] Connect zone dump response before sending the request (#7711)
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>
2026-11-01 12:49:35 +01:00
BruebachL
00151380e3
[CardDB] Break the CardInfo <-> CardSet cycle on reload (#7645)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [CardDB] Drop the CardInfo self pointer

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

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

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

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

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

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

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

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

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

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

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

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

* [CardDB] Keep the set detach in one place

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

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

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

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

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

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

* [Servatrice] Drop the settings cache from the logger

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

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

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 19:37:15 +01:00
BruebachL
344dbd2caa
[Servatrice] Fix silently broken socket error connections (#7626)
* [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>
2026-10-31 19:02:55 +01:00
BruebachL
825aba953a
[Servatrice] Fail loudly when startup initialization fails (#7602)
* [Servatrice] Exit non-zero when server initialization fails

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

* [Servatrice] Keep console logging independent of the logfile

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

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

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

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

* [Servatrice] Buffer console logs on the logger thread

Address review feedback on the startup failure change:

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

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 19:02:49 +01:00
BruebachL
ae43659b91
[Build] Use versionless Qt CMake commands (#7639)
* [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>
2026-10-31 18:53:00 +01:00
BruebachL
d4002dc52b
[Build] Reject APIs deprecated before Qt 6.4 (#7632)
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>
2026-10-31 18:52:11 +01:00
BruebachL
f2c359ba9f
[CI] Keep the vcpkg cache directory guard unconditional (#7527)
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>
2026-10-31 18:49:45 +01:00
BruebachL
52d3978f84
[Build] Let AUTOMOC process library headers (#7638)
The 13 libraries using qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) listed their\nheaders twice and depended on a generated source list. Add the headers to the\ntarget sources directly and delete the qt6_wrap_cpp call; AUTOMOC scans them\nand generates the same moc output.\n\nThe change exposed that server_card_counter_test never defined the rng global\nthat the server remote library references; every sibling server test does.\nGive the test the same definition so the target links.\n\nVerification: full client, Oracle, server and test build passed.

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

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

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

* [Game] Follow base rename to createCardRequested after rebase

* [Game] Drop unused CardItem forward declaration

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

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:15 +01:00
BruebachL
78964779c6
[Refactor] Move card item creation out of game logic (#7436)
* [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>
2026-10-31 18:44:15 +01:00
BruebachL
7ca23430cd
[Refactor] Retype PlayerActions onto CardState (#7435)
* [Refactor] Retype card containers and zone logic onto CardState

* [Refactor] Retype PlayerActions onto CardState

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:14 +01:00
BruebachL
65e9f1b370
[Refactor] Retype card containers and zone logic onto CardState (#7434)
* [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>
2026-10-31 18:44:14 +01:00
BruebachL
5d0d466930
[Refactor] Add card name, printing id, and color to CardState (#7433)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:13 +01:00
BruebachL
edb5caec8a
[Refactor] Hold zone views and card view teardown outside game graphics (#7429)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:13 +01:00
BruebachL
2335e064ac
[Refactor] Move the attachment graph and card view teardown onto CardState (#7428)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:13 +01:00
BruebachL
47c90cda10
[Refactor] Give graphics zones a card item cache and own card placement (#7427)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:12 +01:00
BruebachL
4879ddbd0f
[Refactor] Move card selection actions to GameScene and p/t parsing to CardState (#7426)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:12 +01:00
BruebachL
c9853734ad
[Refactor] Move card identity and card data into CardState (#7425)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:12 +01:00
BruebachL
6f0c5dca12
[Refactor] Move LoadedDeck and DeckFileFormat data types into libcockatrice_deck_list (#7424)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:11 +01:00
BruebachL
85fd052fdb
[Refactor] Move ArrowRegistry out of game/ into game_graphics/board (#7423)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:11 +01:00
BruebachL
3ccd44155d
[Refactor] Build arrow and attach commands in game/ instead of ArrowItem (#7422)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:11 +01:00
BruebachL
77f882ca37
[Refactor] Build deck setup commands in GameEventHandler instead of DeckViewContainer (#7421)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:10 +01:00
BruebachL
674a607960
[Refactor] Build player counter commands in game/ instead of AbstractCounter (#7420)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:10 +01:00
BruebachL
73d5a2a0ab
[Refactor] Emit phase toolbar intent instead of protobuf commands (#7419)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:10 +01:00
BruebachL
095b135640
[Refactor] Move the remaining zone commands out of game_graphics/zones/ (#7418)
* [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>
2026-10-31 18:44:09 +01:00
BruebachL
563833a88a
[Refactor] Build Command_MoveCard in game/ instead of the zone views (#7417)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:09 +01:00
BruebachL
4972ebcb97
[Refactor] Move the active card from AbstractGame into GameScene (#7416)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:08 +01:00
BruebachL
94b91be5a2
[Refactor] Pass the target card into PlayerActions instead of reading activeCard (#7415)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:08 +01:00
BruebachL
93c5d85996
[Refactor] Move attach and arrow previews from game logic into CardMenu (#7414)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:44:08 +01:00
BruebachL
52c5bd2ea3
[Refactor] Move table row mapping from TableZone into TableZoneLogic (#7413)
* [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>
2026-10-31 18:44:07 +01:00
BruebachL
d30d275296
[Refactor] Move AnnotationDialog and action data types into game/ (#7411)
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>
2026-10-31 18:44:07 +01:00
sentry[bot]
bbc7e462ff
[Installer] Graceful close and a way out of a stuck update (#7687)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] Stop calling the payload free while an executable is locked

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

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

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

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

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

* [Installer] Close running clients over a local socket

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

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

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

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 15:32:59 +01:00
tooomm
720adf5410
[Settings] Rename UI language selector & move translator hint (#7656)
* Update general_settings_page.cpp

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

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

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

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-30 13:16:50 +01:00
sentry[bot]
ded83aaab5
[Installer] Ask a hidden instance to close, then offer to end it once (#7683)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] 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>
2026-10-29 21:01:16 +01:00
sentry[bot]
9e50c5ad6e
[Installer] Recover payload files when previous uninstaller is missing or fails to launch (#7682)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-10-29 12:10:18 +01:00
sentry[bot]
06da7f815a
[Installer] End a launcher that has no window to ask instead of prompting (#7681)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] 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>
2026-10-29 04:00:42 +01:00
BruebachL
5cf3358c99
[Client] Stop reporting local login aborts as missing client features (#7678)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [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>
2026-10-28 09:48:37 +01:00
BruebachL
d89b502342
[Client] Report password-reset rate limits instead of a generic failure (#7677)
* [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>
2026-10-28 09:19:42 +01:00
sentry[bot]
a83d82b5e8
[Installer] Recover leftover payload files when previous uninstaller leaves them behind (#7676)
* [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>
2026-10-28 09:08:54 +01:00
sentry[bot]
bf3562a85a
[Installer] Offer to end running Cockatrice immediately when auto-close fails (#7675)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] 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>
2026-10-27 19:09:21 +01:00
sentry[bot]
30d3afc7b4
[Updater] Close a running Cockatrice when the installer asks (#7672)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [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>
2026-10-27 11:19:49 +01:00
sentry[bot]
92d082e735
[VisualDeckStorage] Guard against widget deletion during tag edit dialog (#7674)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-10-27 09:14:52 +01:00
sentry[bot]
aa42cfbccc
[VisualDeckEditor] Create menus before central frame to prevent Windows crash in setParent_sys (#7673)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-10-27 09:12:37 +01:00
sentry[bot]
d42b9607c5
[Installer] Check for running instances with the Restart Manager probe (#7671)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] 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>
2026-10-26 19:25:10 +01:00
sentry[bot]
4793c1fc04
[Installer] Offer to close blocking processes and retry purge on upgrade (#7664)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] 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>
2026-10-26 09:45:52 +01:00
BruebachL
675e1f7447
[Translation] Name languages without reading the translation files (#7662)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Translation] Name languages without reading the translation files

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

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

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

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

* [Translation] Name the default language selection once

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

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-26 02:39:26 +01:00
sentry[bot]
858b270fa9
[Installer] Retry locked DLL purge before aborting upgrade (#7657)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] 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>
2026-10-25 20:40:57 +01:00
ebbit1q
4d8ab54313
Revert "[Version] Bump to 3.2.0 for beta release. (#7653)" (#7660)
This reverts commit e6b4c51585.
2026-10-25 20:19:23 +01:00
BruebachL
e6b4c51585
[Version] Bump to 3.2.0 for beta release. (#7653)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 20:14:21 +01:00
sentry[bot]
4ab875be48
[VisualDeckStorage] Keep databaseLoadIndicator alive when swapping in folder widget (#7651)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-10-25 17:35:46 +01:00
sentry[bot]
f3fb3331d1
[Cards] Guard against null QScreen when positioning enlarged pixmap (#7650)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-10-25 17:35:10 +01:00
BruebachL
93309e01a2
[Server/Client/Protocol] Let moderators clear banner cards (#7546)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 13:19:20 +01:00
BruebachL
b4e24f958a
[Network] Do not format discarded debug messages, parse without memmove (#7606)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [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>
2026-10-25 12:49:24 +01:00
Neel Jogani
a4c4c67674
Fix tab retranslation (#7580)
* 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
2026-10-25 12:30:17 +01:00
BruebachL
f33cfcb288
[UI] Show 'Password saved' placeholder when verifier is stored (#7649)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 10:34:56 +01:00
BruebachL
5fbc636f93
[Models] Use QCollator for natural, cached sorting (#7633)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Models] Use QCollator for natural, cached sorting

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

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

* [Models] Sort naturally under the C locale too

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

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

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

* nit: initialize collators in member initializer lists

* nit: namespace makeNaturalSortCollator under NaturalSort

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 01:01:04 +02:00
BruebachL
5ddfcd7ead
[Pictures] Watch the custom picture folder instead of polling (#7635)
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>
2026-10-25 00:36:14 +02:00
BruebachL
518c7e6125
[Network] Add a configured QNetworkAccessManager factory (#7628)
* [Network] Add a configured QNetworkAccessManager factory

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

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

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

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

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

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 00:33:43 +02:00
BruebachL
3b07b10b19
[Avatar] Respect EXIF orientation and decode at target size (#7634)
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>
2026-10-25 00:11:09 +02:00
BruebachL
66b9338c90
[QtUtils] Use makeScrollArea() for the remaining scroll areas (#7625)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 22:50:25 +02:00
BruebachL
77a3875c25
[VisualDatabase] Debounce filter refreshes with QtUtils::Debouncer (#7624)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 22:48:11 +02:00
BruebachL
28c8474f03
[QtUtils] Add Debouncer helper (#7623)
* [QtUtils] Add Debouncer helper

* Extract long message to variable before confirm() call

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 22:43:10 +02:00
BruebachL
11e2bb0d2f
[QtUtils] Adopt window fade, path, bold font and confirmation helpers (#7622)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 21:43:50 +02:00
BruebachL
eac149656b
[QtUtils] Add window fade, path opening, bold font and confirmation helpers (#7621)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 21:38:54 +02:00
BruebachL
e806085681
[QtUtils] Use makeLayout() for zero-margin layouts (#7620)
* [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>
2026-10-24 19:06:18 +02:00
BruebachL
06b9ca368f
[QtUtils] Add layout creation and relayout helpers (#7619)
* [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>
2026-10-24 19:01:56 +02:00
BruebachL
67a212d677
[Theme] Drop stale pixmap caches on palette-only preview (#7600)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
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>
2026-10-24 10:40:20 +02:00
BruebachL
4028606bd9
[CardDB] Clear CardInfo relations before deferred deletion (#7615)
removeCard() and swapInDatabaseData() schedule CardRelation deletion with deleteLater(), but the owning CardInfo could outlive them through an external shared pointer and kept returning the freed pointers from getAllRelatedCards(). Add CardInfo::clearDirectRelations() and call it after scheduling deletion so the UI can no longer dereference dangling relations.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 10:28:24 +02:00
Neel Jogani
6e770eb7b2
Retranslate Home tab when the language changes (#7579)
* 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
2026-10-24 08:35:31 +02:00
Neel Jogani
e73dd2b520
Refactor display type toggler and add language change handling in Vis… (#7581)
* 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
2026-10-24 08:34:19 +02:00
BruebachL
ab13a6748f
[QtUtils] Adopt shared Qt helpers across the client (#7618)
* [QtUtils] Add parent lookup, layout clearing, scroll area and monospace helpers

* [QtUtils] Adopt shared Qt helpers across the client

---------

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-24 00:46:54 +02:00
BruebachL
ad4320cd7b
[CardDB] Stream card properties from blobs (#7608)
* [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>
2026-10-24 00:17:31 +02:00
BruebachL
c25a2c5c3b
[CardDB] Avoid copying printing maps and property strings (#7605)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [CardDB] 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>
2026-10-23 11:58:58 +02:00
BruebachL
b85436461e
[Filters] Precompute term data instead of redoing it per card (#7603)
* [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>
2026-10-23 11:23:17 +02:00
BruebachL
f4b4365464
[Search] Stop allocating per card in card-name completion (#7604)
* [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>
2026-10-23 11:23:04 +02:00
BruebachL
4980f88a7a
[CardDB] Sort power/toughness without splitting every comparison (#7607)
* [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>
2026-10-23 10:34:13 +02:00
BruebachL
e666a4978d
[VisualDatabase] Use scoped QSignalBlockers for filter updates (#7641)
* [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>
2026-10-23 10:33:29 +02:00
tooomm
c4c7cc0253
[README] Small tweaks (#7585)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
2026-10-22 20:13:43 +02:00
392 changed files with 10903 additions and 3540 deletions

View file

@ -459,6 +459,206 @@ if [[ $MAKE_COVERAGE ]]; then
echo "::endgroup::"
fi
if [[ $RUNNER_OS == Windows && ! $MAKE_NO_CLIENT ]]; then
echo "::group::Probe the installer's holder lookup"
# The working directory is still the build dir here: the package steps below are what
# leave and re-enter it, and that is what made a $BUILD_DIR-prefixed path name
# build/build. Both paths are relative to the dir the probe was built into.
probe="./installer-holder-probe.exe"
if [[ ! -f $probe ]]; then
echo "::error file=$0::installer-holder-probe.exe was not built"
exit 1
fi
# The probe exists to name a holder that a process-and-module scan cannot see: one
# that has a file open without having loaded it. Holding a temporary DLL with
# FileShare.None is exactly that case, so this asserts the reason the probe was
# added instead of only that the Restart Manager answers at all. It needs no NSIS
# and no packaging, so it runs on every Windows build that gets this far.
# The holder is a process of its own: the probe never names the process that started it,
# which is what keeps an in-place uninstaller from being named as a holder of the
# directory it is emptying. The same DLL held by the script that starts the probe
# therefore has to come back as nothing, and that is the second case below.
probe_check="./holder-probe-check.ps1"
cat >"$probe_check" <<'POWERSHELL'
$ErrorActionPreference = "Stop"
$dir = Join-Path ([IO.Path]::GetTempPath()) "cockatrice-holder-probe-$PID"
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force $dir | Out-Null
$probe = (Resolve-Path -LiteralPath $env:PROBE_EXE).Path
function Invoke-Probe([string] $guardDir) {
$start = New-Object System.Diagnostics.ProcessStartInfo
$start.FileName = $probe
$start.UseShellExecute = $false
$start.RedirectStandardOutput = $true
$start.EnvironmentVariables["COCKATRICE_GUARD_DIR"] = $guardDir
$process = [System.Diagnostics.Process]::Start($start)
$read = $process.StandardOutput.ReadToEndAsync()
if (-not $process.WaitForExit(20000)) {
try { $process.Kill() } catch { }
throw "the holder probe did not finish within 20 s for $guardDir"
}
$output = $read.Result
$code = $process.ExitCode
$process.Dispose()
return @{ Code = $code; Output = $output }
}
function Wait-UntilLocked([string] $path) {
$deadline = (Get-Date).AddSeconds(15)
while ((Get-Date) -lt $deadline) {
try {
$open = [IO.File]::Open($path, "Open", "ReadWrite", "None")
$open.Dispose()
} catch {
return
}
Start-Sleep -Milliseconds 100
}
throw "the holder never locked $path"
}
try {
$file = Join-Path $dir "locked.dll"
[IO.File]::WriteAllText($file, "x")
# The holder is a process of its own, not this script: the probe never names the process
# that started it (the case below asserts that), so a file held here would be left out on
# purpose and the check would pass for the wrong reason.
$holderScript = Join-Path $dir "hold.ps1"
Set-Content -LiteralPath $holderScript -Value @'
param([string] $Path)
$handle = [IO.File]::Open($Path, "Open", "ReadWrite", "None")
Start-Sleep -Seconds 120
'@
$powershell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$holder = Start-Process -FilePath $powershell -PassThru -ArgumentList @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$holderScript`"", "-Path", "`"$file`"")
try {
Wait-UntilLocked $file
$locked = Invoke-Probe $dir
if ($locked.Code -ne 1) { throw "expected exit 1 for a locked file, got $($locked.Code) : $($locked.Output)" }
if ($locked.Output -notmatch "pid\s+$($holder.Id)\b") {
throw "the lookup did not name the process holding the file: $($locked.Output)"
}
} finally {
try { $holder.Kill() } catch { }
try { $holder.WaitForExit(5000) | Out-Null } catch { }
}
# The probe is started by the installer, or by an uninstaller that runs in place from the
# directory it is emptying, so the asking process holds files there itself. Naming it would
# ask the user to close the program that is asking - a question neither the close nor the
# end offer can answer, so the guard repeated it until the update was cancelled. A holder
# that is only the asking process has to come back as nothing.
$handle = [IO.File]::Open($file, "Open", "ReadWrite", "None")
try {
$self = Invoke-Probe $dir
} finally {
$handle.Dispose()
}
if ($self.Code -ne 0) { throw "expected exit 0 when the asking process is the only holder, got $($self.Code) : $($self.Output)" }
if ($self.Output -match "pid\s+$PID\b") { throw "the lookup named the process that started it: $($self.Output)" }
# A payload executable that refuses to be replaced is running, whether or not this probe
# may name it: the Restart Manager does not report every mapping, and the image path of a
# process under another account cannot be read from here, so both process lookups can
# come back empty while the program is still there. Held here by the asking process, so
# that it cannot be named - the answer has to be "could not name it", never "nothing is
# holding it", which is what sent an update into an uninstall that could only fail
# halfway. Exit 3 is the code the guard answers by asking the user to close it and retry.
$image = Join-Path $dir "oracle.exe"
[IO.File]::WriteAllText($image, "x")
$handle = [IO.File]::Open($image, "Open", "ReadWrite", "None")
try {
$unnameable = Invoke-Probe $dir
} finally {
$handle.Dispose()
}
if ($unnameable.Code -ne 3) {
throw "expected exit 3 for a payload executable that is in use but cannot be named, got $($unnameable.Code) : $($unnameable.Output)"
}
if ($unnameable.Output -notmatch "oracle\.exe is in use") {
throw "the answer did not name the executable it could not replace: $($unnameable.Output)"
}
$empty = Join-Path $dir "empty"
New-Item -ItemType Directory -Force $empty | Out-Null
$nothing = Invoke-Probe $empty
if ($nothing.Code -ne 0) { throw "expected exit 0 for a directory with nothing open, got $($nothing.Code) : $($nothing.Output)" }
# A process running from the directory is a holder even when it has no file open: an
# instance holds its payload as mapped images, not as handles, and that is the state
# the guard used to miss - it passed, the purge failed, and the install ended in a
# warning with no offer to close and try again. The image is a copy of a native
# Windows program, so it starts without its usual directory next to it, and it carries
# one of the payload's own names because that is what the lookup names.
$copy = Join-Path $dir "cockatrice.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $copy
$running = Start-Process -FilePath $copy -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$byImage = Invoke-Probe $dir
} finally {
try { $running.Kill() } catch { }
# Wait for the image to be released, or the cleanup below leaves the copy behind.
try { $running.WaitForExit(5000) | Out-Null } catch { }
}
if ($byImage.Code -ne 1) { throw "expected exit 1 for a process running from the directory, got $($byImage.Code) : $($byImage.Output)" }
if ($byImage.Output -notmatch "cockatrice\.exe \(pid\s+$($running.Id)") {
throw "the lookup did not name the process running from the directory: $($byImage.Output)"
}
# A program in a folder below the directory is not an instance of the installation, even
# when it carries a payload program's name: the install directory can be a folder the
# user keeps other applications in, each in its own subfolder, and naming one of those
# asked for a close the probe never sends and an end it refuses to perform, so the guard
# could only abort the update. A program that really does hold a payload file is still
# named, by the Restart Manager lookup exercised above.
$nested = Join-Path $dir "other"
New-Item -ItemType Directory -Force $nested | Out-Null
$nestedImage = Join-Path $nested "cockatrice.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $nestedImage
$nestedRunning = Start-Process -FilePath $nestedImage -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$inSubfolder = Invoke-Probe $dir
} finally {
try { $nestedRunning.Kill() } catch { }
try { $nestedRunning.WaitForExit(5000) | Out-Null } catch { }
}
if ($inSubfolder.Code -ne 0) {
throw "expected exit 0 for a program in a folder below the directory, got $($inSubfolder.Code) : $($inSubfolder.Output)"
}
# A program the user keeps in the directory itself is not a payload program either: the
# guard ends only what the payload installs, and naming anything else asks for an answer
# that cannot free a single file.
$foreign = Join-Path $dir "tool.exe"
Copy-Item -LiteralPath (Join-Path $env:SystemRoot "System32\cmd.exe") -Destination $foreign
$foreignRunning = Start-Process -FilePath $foreign -ArgumentList "/c", "ping -n 20 127.0.0.1 >nul" -PassThru
try {
Start-Sleep -Milliseconds 1500
$ignored = Invoke-Probe $dir
} finally {
try { $foreignRunning.Kill() } catch { }
try { $foreignRunning.WaitForExit(5000) | Out-Null } catch { }
}
if ($ignored.Code -ne 0) {
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"
} finally {
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
}
POWERSHELL
if ! PROBE_EXE="$probe" powershell -NoProfile -File "$probe_check"; then
rm -f "$probe_check"
exit 1
fi
rm -f "$probe_check"
echo "::endgroup::"
fi
if [[ $MAKE_INSTALL ]]; then
echo "::group::Install"
cmake --build . --target install --config "$BUILDTYPE"

View file

@ -584,11 +584,17 @@ jobs:
# job. A scope nobody has written to yet therefore leaves the directory missing
# and fails the job, which is exactly the state every scope starts in. Make the
# directory up front so an empty scope is a cache miss instead of an error.
# always() covers the cache step itself failing: the action's post save still runs
# then, and it should report the real error instead of ENOENT. It cannot rescue a
# failure before the cache step, since a post step only runs for an action whose
# main step did. The fallback keeps this guard from failing when the cache step was
# skipped and left no path output.
- name: "Create vcpkg cache directory"
if: always()
shell: bash
env:
CACHE_DIR: '${{ steps.vcpkg-cache.outputs.path }}'
run: mkdir -p "$CACHE_DIR"
run: mkdir -p "${CACHE_DIR:-$GITHUB_WORKSPACE/.vcpkg-cache}"
# Uses environment variables, see compile.sh for more details
- name: "Build Cockatrice"

View file

@ -250,6 +250,15 @@ message(STATUS "Update Translations: ${UPDATE_TRANSLATIONS}")
include(FindQtRuntime)
# Reject APIs that were deprecated before the oldest Qt release we support
# (6.4). Qt 6.5 renamed the switch, so use whichever spelling the found
# version understands while still compiling everywhere.
if(Qt6_VERSION VERSION_GREATER_EQUAL 6.5)
add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400)
else()
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x060400)
endif()
set(CMAKE_AUTOMOC TRUE)
# Find other needed libraries
@ -420,6 +429,9 @@ if(WITH_ORACLE OR WITH_CLIENT)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_models ${CMAKE_BINARY_DIR}/libcockatrice_models)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_filters ${CMAKE_BINARY_DIR}/libcockatrice_filters)
endif()
if(WITH_CLIENT)
add_subdirectory(${CMAKE_SOURCE_DIR}/libcockatrice_game ${CMAKE_BINARY_DIR}/libcockatrice_game)
endif()
if(WITH_SERVER)
add_subdirectory(servatrice)
@ -431,6 +443,13 @@ if(WITH_CLIENT)
set(CPACK_INSTALL_CMAKE_PROJECTS "Cockatrice;Cockatrice;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
endif()
if(WIN32 AND WITH_CLIENT)
# Runs from the installer's $PLUGINSDIR when a purge fails, and is never installed
# (see LogProcessesUsingInstallDir in cmake/NSIS.template.in), so it has no install
# rules and is not part of the client's staged tree.
add_subdirectory(${CMAKE_SOURCE_DIR}/tools/installer-holder-probe ${CMAKE_BINARY_DIR}/tools/installer-holder-probe)
endif()
if(WITH_ORACLE)
add_subdirectory(oracle)
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
@ -443,5 +462,5 @@ endif()
if(Qt6_FOUND AND Qt6_VERSION_MINOR GREATER_EQUAL 3)
# Qt6.3+ requires project finalization to support translations
qt6_finalize_project()
qt_finalize_project()
endif()

View file

@ -15,9 +15,10 @@
---
<br><pre><p align='center'>
<b>To get started with Cockatrice &#8674; [view our webpage](https://cockatrice.github.io/)</b><br>
<b>To get support, or suggest changes to the app &#8674; [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 &#8674; learn [how to contribute](#contribute-)</b>
<b>To get started with Cockatrice &#8674; [view our webpage](https://cockatrice.github.io/)</b>
<b>To get support, or suggest changes to the app &#8674; [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 &#8674; [read our code documentation](https://cockatrice.github.io/docs/)</b>
<b>To help with development &#8674; 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:
- [![Good first issue](https://img.shields.io/github/issues/cockatrice/cockatrice/Good%20First%20Issue)](https://github.com/Cockatrice/Cockatrice/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3A%22Good%20first%20issue%22%20)<br>
Issues tagged in this way provide a simple way to get started. They don't require much experience to be worked on.
@ -95,7 +96,7 @@ We'll happily advice on how best to implement a feature, or we can show you wher
You can also have a look at our `Todo List` in our [Code Documentation](https://cockatrice.github.io/docs) or search the repo for [`\todo` comments](https://github.com/search?q=repo%3ACockatrice%2FCockatrice%20%5Ctodo&type=code).
### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Apush)
### Documentation [![CI Docs](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml/badge.svg?event=push)](https://github.com/Cockatrice/Cockatrice/actions/workflows/documentation-build.yml?query=event%3Arelease+event%3Aworkflow_dispatch)
There are various places where useful information for different needs are maintained:
- [Official Code Documentation](https://cockatrice.github.io/docs/)
@ -104,7 +105,8 @@ There are various places where useful information for different needs are mainta
- [Official README](https://github.com/Cockatrice/Cockatrice/blob/master/README.md) `This file`
- [Official CONTRIBUTING](https://github.com/Cockatrice/Cockatrice/blob/master/.github/CONTRIBUTING.md)
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.
### Translation [![Transifex Project](https://img.shields.io/badge/translate-on%20transifex-brightgreen)](https://explore.transifex.com/cockatrice/cockatrice/)

File diff suppressed because it is too large Load diff

View file

@ -57,6 +57,29 @@ try {
}
}
# --- What was holding the files, read from the log as well -------------------
# The guard and the close attempt each log one line naming every holder and what
# happened to it. Carrying the last of each as event data keeps an abort caused by
# a running instance actionable without opening the attachment. Kept out of the
# message so the issue still groups on the reason alone.
$Holders = ''
$CloseResult = ''
$EndResult = ''
if (Test-Path -LiteralPath $LogPath) {
$holderLine = Select-String -LiteralPath $LogPath -Pattern 'guard: still in use by (.*)$' | Select-Object -Last 1
if ($holderLine) {
$Holders = $holderLine.Matches[0].Groups[1].Value
}
$closeLine = Select-String -LiteralPath $LogPath -Pattern '(close-holders: .*)$' | Select-Object -Last 1
if ($closeLine) {
$CloseResult = $closeLine.Matches[0].Groups[1].Value
}
$endLine = Select-String -LiteralPath $LogPath -Pattern '(end-holders: .*)$' | Select-Object -Last 1
if ($endLine) {
$EndResult = $endLine.Matches[0].Groups[1].Value
}
}
# --- Event ---------------------------------------------------------------
$eventId = [guid]::NewGuid().ToString('N')
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
@ -75,6 +98,9 @@ try {
extra = @{
reason = $Reason
os = [System.Environment]::OSVersion.VersionString
holders = $Holders
close_result = $CloseResult
end_result = $EndResult
}
}
$eventJson = $sentryEvent | ConvertTo-Json -Compress -Depth 6

View file

@ -25,9 +25,7 @@ set(cockatrice_SOURCES
src/client/settings/shortcut_treeview.cpp
src/client/settings/shortcuts_settings.cpp
src/interface/deck_loader/card_node_function.cpp
src/interface/deck_loader/deck_file_format.cpp
src/interface/deck_loader/deck_loader.cpp
src/interface/deck_loader/loaded_deck.cpp
src/interface/widgets/dialogs/dlg_connect.cpp
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
src/interface/widgets/dialogs/dlg_create_game.cpp
@ -71,36 +69,27 @@ set(cockatrice_SOURCES
src/filters/filter_builder.cpp
src/filters/filter_tree_model.cpp
src/filters/syntax_help.cpp
src/game/abstract_game.cpp
src/game/arrow_registry.cpp
src/game_graphics/board/abstract_card_drag_item.cpp
src/game_graphics/board/abstract_card_item.cpp
src/game_graphics/board/abstract_counter.cpp
src/game/board/arrow_data.cpp
src/game_graphics/board/arrow_item.cpp
src/game_graphics/board/arrow_registry.cpp
src/game_graphics/board/arrow_target.cpp
src/game_graphics/board/card_drag_item.cpp
src/game_graphics/board/card_item.cpp
src/game/board/card_list.cpp
src/game/board/card_state.cpp
src/game_graphics/board/counter_general.cpp
src/game/board/counter_state.cpp
src/game_graphics/board/translate_counter_name.cpp
src/game_graphics/deckview/deck_view.cpp
src/game_graphics/deckview/deck_view_container.cpp
src/game_graphics/deckview/tabbed_deck_view_container.cpp
src/game_graphics/dialogs/dlg_annotation.cpp
src/game_graphics/dialogs/dlg_create_token.cpp
src/game_graphics/dialogs/dlg_move_top_cards_until.cpp
src/game_graphics/dialogs/dlg_roll_dice.cpp
src/game/game.cpp
src/game/game_event_handler.cpp
src/game/game_meta_info.cpp
src/game_graphics/game_scene.cpp
src/game/game_state.cpp
src/game_graphics/game_view.cpp
src/game_graphics/hand_counter.cpp
src/game_graphics/log/message_log_widget.cpp
src/game/phase.cpp
src/game_graphics/phases_toolbar.cpp
src/game_graphics/player/menu/card_menu.cpp
src/game_graphics/player/menu/custom_zone_menu.cpp
@ -118,23 +107,11 @@ set(cockatrice_SOURCES
src/game_graphics/tally/stats_tally.cpp
src/game_graphics/tally/subtype_tally.cpp
src/game_graphics/tally/tally.cpp
src/game/player/player_actions.cpp
src/game_graphics/player/player_area.cpp
src/game_graphics/player/player_dialogs.cpp
src/game/player/player_event_handler.cpp
src/game_graphics/player/player_graphics_item.cpp
src/game/player/player_info.cpp
src/game_graphics/player/player_list_widget.cpp
src/game/player/player_logic.cpp
src/game/player/player_manager.cpp
src/game_graphics/player/player_target.cpp
src/game/replay.cpp
src/game/zones/card_zone_logic.cpp
src/game/zones/hand_zone_logic.cpp
src/game/zones/pile_zone_logic.cpp
src/game/zones/stack_zone_logic.cpp
src/game/zones/table_zone_logic.cpp
src/game/zones/view_zone_logic.cpp
src/game_graphics/zones/card_zone.cpp
src/game_graphics/zones/hand_zone.cpp
src/game_graphics/zones/pile_zone.cpp
@ -271,6 +248,7 @@ set(cockatrice_SOURCES
src/interface/widgets/playmat/playmat_settings_dialog.cpp
src/interface/widgets/quick_settings/settings_button_widget.cpp
src/interface/widgets/quick_settings/settings_popup_widget.cpp
src/interface/widgets/replay/replay_event_index.cpp
src/interface/widgets/replay/replay_manager.cpp
src/interface/widgets/replay/replay_quick_settings_widget.cpp
src/interface/widgets/replay/replay_timeline_widget.cpp
@ -345,6 +323,7 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
src/interface/window_main.cpp
src/installer_close_server.cpp
src/main.cpp
src/single_instance_manager.cpp
src/interface/widgets/tabs/abstract_tab_deck_editor.cpp
@ -450,6 +429,7 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
src/interface/widgets/utility/compact_push_button.cpp
src/interface/widgets/utility/compact_push_button.h
src/installer_close_server.h
src/single_instance_manager.h
src/client/url_scheme_event_filter.h
src/interface/intents/intent_connect_to_server.cpp
@ -511,7 +491,7 @@ if(APPLE)
set(cockatrice_SOURCES ${cockatrice_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/resources/appicon.icns)
endif(APPLE)
qt6_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
qt_add_resources(cockatrice_RESOURCES_RCC ${cockatrice_RESOURCES})
# Declare path variables
set(ICONDIR
@ -532,7 +512,7 @@ set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translation
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations")
qt6_add_executable(
qt_add_executable(
cockatrice
WIN32
MACOSX_BUNDLE
@ -544,7 +524,7 @@ qt6_add_executable(
target_precompile_headers(cockatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtwidgets_pch.h")
qt6_add_shaders(
qt_add_shaders(
cockatrice
"onboarding_shaders"
PREFIX
@ -556,7 +536,7 @@ qt6_add_shaders(
src/interface/widgets/onboarding/shaders/brand_plate.frag
)
qt6_add_resources(
qt_add_resources(
cockatrice
"onboarding_qml"
PREFIX
@ -572,6 +552,7 @@ target_link_libraries(
PUBLIC libcockatrice_card
libcockatrice_deck_list
libcockatrice_filters
libcockatrice_game
libcockatrice_utility
libcockatrice_network
libcockatrice_models
@ -788,7 +769,7 @@ endif()
if(Qt6LinguistTools_FOUND)
#Qt6 Translations happen after the executable is built up
if(UPDATE_TRANSLATIONS)
qt6_add_translations(
qt_add_translations(
cockatrice
TS_FILES
${cockatrice_TS}
@ -798,7 +779,7 @@ if(Qt6LinguistTools_FOUND)
cockatrice_QM
)
else()
qt6_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
qt_add_translations(cockatrice TS_FILES ${cockatrice_TS} QM_FILES_OUTPUT_VARIABLE cockatrice_QM)
endif()
if(UNIX)
@ -824,4 +805,4 @@ if(Qt6LinguistTools_FOUND)
endforeach()
endif()
qt6_finalize_target(cockatrice)
qt_finalize_target(cockatrice)

View file

@ -14,6 +14,7 @@
#include <QObject>
#include <QPoint>
#include <QVBoxLayout>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
@ -24,9 +25,7 @@ LatencyStatusWidget::LatencyStatusWidget(QWidget *parent) : QWidget(parent)
latencyGraph = new LatencyGraphWidget(this);
latencyGraph->setFixedSize(90, 14);
auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(4);
auto *layout = QtUtils::makeLayout<QHBoxLayout>(this, 4);
layout->addWidget(latencyGraph);
layout->addWidget(pingLabel);

View file

@ -24,6 +24,7 @@
#include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/settings/servers_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class QWidget;
@ -243,17 +244,27 @@ void ConnectionController::onLoginError(int r,
{
switch (static_cast<Response::ResponseCode>(r)) {
case Response::RespClientUpdateRequired: {
QString formatted = "Missing Features: ";
for (int i = 0; i < missingFeatures.size(); ++i) {
formatted.append(QString("\n %1").arg(QChar(0x2022)) + " " + missingFeatures.value(i));
}
QMessageBox msgBox(dialogParent);
msgBox.setIcon(QMessageBox::Critical);
msgBox.setWindowTitle(tr("Failed Login"));
msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
"\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
msgBox.setDetailedText(formatted);
if (missingFeatures.isEmpty()) {
// Without a feature list this is either a login the client stopped itself or a
// strict server asking for a password; the reason string already says which and
// what to do. No client update can fix any of those causes, so show it instead of
// the update advice.
msgBox.setText(reasonStr.isEmpty() ? tr("The login did not complete.") : reasonStr);
} else {
QString formatted = tr("Missing Features:");
for (const QString &feature : missingFeatures) {
formatted.append(QString("\n %1 ").arg(QChar(0x2022)) + feature);
}
msgBox.setText(tr("Your client seems to be missing features this server requires for connection.") +
"\n\n" + tr("To update your client, go to 'Help -> Check for Client Updates'."));
msgBox.setDetailedText(formatted);
}
msgBox.exec();
break;
}
@ -300,9 +311,8 @@ void ConnectionController::onLoginError(int r,
}
case Response::RespRegistrationRequired: {
if (QMessageBox::question(dialogParent, tr("Error"),
tr("This server requires user registration. Do you want to register now?"),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
if (QtUtils::confirm(dialogParent, tr("Error"),
tr("This server requires user registration. Do you want to register now?"))) {
registerToServer();
}
return; // don't re-prompt connect
@ -517,14 +527,38 @@ void ConnectionController::onForgotPasswordSuccess()
SettingsCache::instance().servers().setFPPlayerName("");
}
void ConnectionController::onForgotPasswordError()
void ConnectionController::onForgotPasswordError(int responseCode)
{
QMessageBox::warning(
dialogParent, tr("Reset Password"),
tr("Failed to reset user account password, please contact the server operator to reset your password."));
SettingsCache::instance().servers().setFPHostName("");
SettingsCache::instance().servers().setFPPort("");
SettingsCache::instance().servers().setFPPlayerName("");
// Rate limiting is retryable, so it must not clear the prefill.
bool keepPrefill = false;
QString message;
switch (static_cast<Response::ResponseCode>(responseCode)) {
case Response::RespTooManyRequests:
// Retryable: keep the host/port/user prefill so the user can try again. The window
// is server-configured (an hour by default), so do not promise a shorter wait.
keepPrefill = true;
message = tr("Too many password reset attempts from your address. Please wait before "
"requesting a new reset.");
break;
case Response::RespFunctionNotAllowed:
// Also returned for an unknown user, a banned user, and an expired token or
// challenge, so the message must not claim a single cause.
message = tr("The server did not accept the reset request. Check the user name, or contact "
"the server operator to reset your password.");
break;
default:
message = tr("Failed to reset user account password, please contact the server operator to reset your "
"password.");
break;
}
QMessageBox::warning(dialogParent, tr("Reset Password"), message);
if (!keepPrefill) {
SettingsCache::instance().servers().setFPHostName("");
SettingsCache::instance().servers().setFPPort("");
SettingsCache::instance().servers().setFPPlayerName("");
}
}
void ConnectionController::onPromptForgotPasswordReset()

View file

@ -87,7 +87,7 @@ private slots:
// Forgot-password flow
void onForgotPasswordSuccess();
void onForgotPasswordError();
void onForgotPasswordError(int responseCode);
void onPromptForgotPasswordReset();
void onPromptForgotPasswordChallenge();

View file

@ -20,11 +20,12 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <libcockatrice/network/network_manager.h>
#include <version_string.h>
DeckStatsInterface::DeckStatsInterface(QObject *parent) : QObject(parent)
{
manager = new QNetworkAccessManager(this);
manager = NetworkManagerFactory::create(this);
connect(manager, &QNetworkAccessManager::finished, this, &DeckStatsInterface::queryFinished);
}

View file

@ -26,11 +26,12 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <libcockatrice/network/network_manager.h>
#include <version_string.h>
TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent)
{
manager = new QNetworkAccessManager(this);
manager = NetworkManagerFactory::create(this);
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
}

View file

@ -27,6 +27,7 @@
#include <functional>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <tuple>
@ -56,7 +57,7 @@ void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool sav
void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
{
auto *nam = new QNetworkAccessManager(this);
auto *nam = NetworkManagerFactory::create(this);
auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING));
QNetworkReply *reply = nam->get(request);

View file

@ -20,6 +20,7 @@
#include <QVariant>
#include <QVariantMap>
#include <QtGlobal>
#include <libcockatrice/network/network_manager.h>
#include <optional>
#include <qnamespace.h>
#include <qprocessordetection.h>
@ -62,7 +63,7 @@ std::optional<int> getProductVersionMajor()
} // namespace
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
ReleaseChannel::ReleaseChannel() : netMan(NetworkManagerFactory::create(this)), response(nullptr), lastRelease(nullptr)
{
}

View file

@ -9,10 +9,11 @@
#include <QNetworkRequest>
#include <QUrl>
#include <QVariant>
#include <libcockatrice/network/network_manager.h>
UpdateDownloader::UpdateDownloader(QObject *parent) : QObject(parent), response(nullptr)
{
netMan = new QNetworkAccessManager(this);
netMan = NetworkManagerFactory::create(this);
}
void UpdateDownloader::beginDownload(QUrl downloadUrl)

View file

@ -19,7 +19,6 @@ inline Q_LOGGING_CATEGORY(SettingsCacheLog, "settings_cache");
class ReleaseChannel;
#define DEFAULT_LANG_NAME "English"
#define CLIENT_INFO_NOT_SET "notset"
#define DEFAULT_FONT_SIZE 12

View file

@ -2,13 +2,18 @@
#define COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H
#include "../../client/settings/cache_settings.h"
#include <QObject>
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
class SettingsCardPreferenceProvider : public ICardPreferenceProvider
class SettingsCardPreferenceProvider : public QObject, public ICardPreferenceProvider
{
public:
explicit SettingsCardPreferenceProvider(QObject *parent = nullptr) : QObject(parent)
{
}
[[nodiscard]] QString getCardPreferenceOverride(const QString &cardName) const override
{
return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName);

View file

@ -1,6 +1,5 @@
#include "deck_filter_string.h"
#include "../interface/deck_loader/loaded_deck.h"
#include "libcockatrice/card/card_localization.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/deck_list/deck_list.h"
@ -15,6 +14,7 @@
#include <algorithm>
#include <any>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/filters/filter_string.h>
#include <libcockatrice/utility/peglib.h>
#include <mutex>

View file

@ -11,6 +11,7 @@
#include <QString>
#include <functional>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/deck_list/loaded_deck.h>
struct CardSearchLanguage;
struct LoadedDeck;

View file

@ -13,6 +13,7 @@
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/filters/filter_card.h>
#include <libcockatrice/utility/qt_utils.h>
#include <stddef.h>
FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
@ -39,9 +40,8 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent)
edit->setClearButtonEnabled(true);
edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
QGridLayout *layout = new QGridLayout;
QGridLayout *layout = QtUtils::makeLayout<QGridLayout>();
layout->setObjectName("layout");
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(typeCombo, 0, 0, 1, 2);
layout->addWidget(filterCombo, 0, 2, 1, 2);

View file

@ -1,113 +0,0 @@
#include "card_state.h"
#include <QSharedPointer>
void CardState::resetState(bool keepAnnotations)
{
attacking = false;
counters.clear();
pt.clear();
if (!keepAnnotations) {
annotation.clear();
}
attachedTo = nullptr;
}
void CardState::setZone(CardZoneLogic *_zone)
{
if (zone == _zone) {
return;
}
zone = _zone;
emit zoneChanged(this, zone);
emit stateChanged();
}
void CardState::setAttacking(bool _attacking)
{
if (attacking == _attacking) {
return;
}
attacking = _attacking;
emit attackingChanged(_attacking);
emit stateChanged();
}
void CardState::insertCounter(int id, int value)
{
counters.insert(id, value);
emit countersChanged(counters);
emit stateChanged();
}
void CardState::setCounter(int id, int value)
{
if (value) {
counters[id] = value;
} else {
counters.remove(id);
}
emit countersChanged(counters);
emit stateChanged();
}
void CardState::clearCounters()
{
counters.clear();
emit countersChanged(counters);
emit stateChanged();
}
void CardState::setAnnotation(const QString &_annotation)
{
if (annotation == _annotation) {
return;
}
annotation = _annotation;
emit annotationChanged(annotation);
emit stateChanged();
}
void CardState::setPT(const QString &_pt)
{
if (pt == _pt) {
return;
}
pt = _pt;
emit ptChanged(pt);
emit stateChanged();
}
void CardState::setDoesntUntap(bool _doesntUntap)
{
if (doesntUntap == _doesntUntap) {
return;
}
doesntUntap = _doesntUntap;
emit doesntUntapChanged(_doesntUntap);
emit stateChanged();
}
void CardState::setDestroyOnZoneChange(bool _destroyOnZoneChange)
{
if (destroyOnZoneChange == _destroyOnZoneChange) {
return;
}
destroyOnZoneChange = _destroyOnZoneChange;
emit destroyOnZoneChangeChanged(_destroyOnZoneChange);
emit stateChanged();
}
void CardState::setAttachedTo(CardItem *_attachedTo)
{
if (attachedTo == _attachedTo) {
return;
}
attachedTo = _attachedTo;
emit attachedToChanged(_attachedTo);
emit stateChanged();
}

View file

@ -1,106 +0,0 @@
#ifndef COCKATRICE_CARD_STATE_H
#define COCKATRICE_CARD_STATE_H
#include <QMap>
#include <QObject>
#include <QString>
#include <qtmetamacros.h>
class CardZoneLogic;
class CardItem;
class CardState : public QObject
{
Q_OBJECT
private:
bool attacking = false;
QMap<int, int> counters;
QString annotation;
QString pt;
bool doesntUntap = false;
bool destroyOnZoneChange = false;
CardItem *attachedTo = nullptr;
CardZoneLogic *zone = nullptr;
signals:
void stateChanged();
void attackingChanged(bool newValue);
void countersChanged(const QMap<int, int> &newCounters);
void annotationChanged(const QString &newAnnotation);
void ptChanged(const QString &newPt);
void doesntUntapChanged(bool newValue);
void destroyOnZoneChangeChanged(bool newValue);
void attachedToChanged(CardItem *newAttachedTo);
void zoneChanged(CardState *changedCard, CardZoneLogic *newZone);
public:
explicit CardState(QObject *parent, CardZoneLogic *_zone) : QObject(parent), zone(_zone)
{
}
void resetState(bool keepAnnotations);
CardZoneLogic *getZone() const
{
return zone;
}
void setZone(CardZoneLogic *_zone);
bool getAttacking() const
{
return attacking;
}
void setAttacking(bool _attacking);
const QMap<int, int> &getCounters() const
{
return counters;
}
void insertCounter(int id, int value);
void setCounter(int id, int value);
void clearCounters();
QString getAnnotation() const
{
return annotation;
}
void setAnnotation(const QString &_annotation);
QString getPT() const
{
return pt;
}
void setPT(const QString &_pt);
bool getDoesntUntap() const
{
return doesntUntap;
}
void setDoesntUntap(bool _doesntUntap);
bool getDestroyOnZoneChange() const
{
return destroyOnZoneChange;
}
void setDestroyOnZoneChange(bool _destroyOnZoneChange);
CardItem *getAttachedTo() const
{
return attachedTo;
}
void setAttachedTo(CardItem *_attachedTo);
};
#endif // COCKATRICE_CARD_STATE_H

View file

@ -1,26 +0,0 @@
/**
* @file replay.h
* @ingroup GameLogic
* @ingroup Replay
*/
//! \todo Document this file.
#ifndef COCKATRICE_REPLAY_H
#define COCKATRICE_REPLAY_H
#include "abstract_game.h"
#include <qtmetamacros.h>
class GameReplay;
class QObject;
class Replay : public AbstractGame
{
Q_OBJECT
public:
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
};
#endif // COCKATRICE_REPLAY_H

View file

@ -34,7 +34,6 @@
#include <QVariant>
#include <QtGlobal>
#include <algorithm>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/debug_settings.h>
@ -43,13 +42,23 @@
class QSizeF;
AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef, PlayerLogic *_owner, int _id)
: ArrowTarget(_owner, parent), id(_id), cardRef(cardRef), tapped(false), facedown(false), tapAngle(0),
bgColor(Qt::transparent), isHovered(false), realZValue(0), pendingSelectionCollapse(false)
: ArrowTarget(_owner, parent), state(new CardState(this, nullptr, cardRef, _id)), tapAngle(0),
bgColor(Qt::transparent), isHovered(false), realZValue(0)
{
setCursor(Qt::OpenHandCursor);
setFlag(ItemIsSelectable);
setCacheMode(DeviceCoordinateCache);
connect(state, &CardState::cardPixmapUpdated, this, &AbstractCardItem::pixmapUpdated);
connect(state, &CardState::cardInfoChanged, this, &AbstractCardItem::onCardInfoChanged);
connect(state, &CardState::cardRefChanged, this, &AbstractCardItem::onCardRefChanged);
connect(state, &CardState::tappedChanged, this, &AbstractCardItem::onTappedChanged);
connect(state, &CardState::facedownChanged, this, [this] { update(); });
connect(state, &CardState::colorChanged, this, [this] {
cacheBgColor();
update();
});
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::displayCardNamesChanged, this,
[this] { update(); });
connect(&SettingsCache::instance().cardsDisplay(), &CardsDisplaySettings::cardLangChanged, this,
@ -67,7 +76,7 @@ AbstractCardItem::AbstractCardItem(QGraphicsItem *parent, const CardRef &cardRef
AbstractCardItem::~AbstractCardItem()
{
emit deleteCardInfoPopup(cardRef.name);
emit deleteCardInfoPopup(state->getCardRef().name);
}
QRectF AbstractCardItem::boundingRect() const
@ -90,30 +99,29 @@ void AbstractCardItem::pixmapUpdated()
emit sigPixmapUpdated();
}
void AbstractCardItem::refreshCardInfo()
void AbstractCardItem::onCardInfoChanged()
{
exactCard = CardDatabaseManager::query()->getCard(cardRef);
if (!exactCard && !cardRef.name.isEmpty()) {
CardInfo::UiAttributes attributes = {.tableRow = -1};
auto info = CardInfo::newInstance(cardRef.name, "", true, {}, {}, {}, {}, attributes);
exactCard = ExactCard(info);
}
if (exactCard) {
connect(exactCard.getCardPtr().data(), &CardInfo::pixmapUpdated, this, &AbstractCardItem::pixmapUpdated);
}
cacheBgColor();
update();
}
void AbstractCardItem::onCardRefChanged(const CardRef &oldCardRef, const CardRef & /*newCardRef*/)
{
emit deleteCardInfoPopup(oldCardRef.name);
}
void AbstractCardItem::refreshCardInfo()
{
state->refreshCardInfo();
}
/**
* Convenience method to get the CardInfo of the exactCard
* @return A const reference to the CardInfo, or an empty CardInfo if card was null
*/
const CardInfo &AbstractCardItem::getCardInfo() const
{
return exactCard.getInfo();
return state->getCardInfo();
}
void AbstractCardItem::setRealZValue(qreal _zValue)
@ -158,13 +166,13 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
QPixmap translatedPixmap;
bool paintImage = true;
if (facedown || cardRef.name.isEmpty()) {
if (state->getFaceDown() || state->getCardRef().name.isEmpty()) {
// never reveal card color, always paint the card back
CardPictureLoader::getCardBackPixmap(translatedPixmap, translatedSize.toSize());
} else {
// don't even spend time trying to load the picture if our size is too small
if (translatedSize.width() > 10) {
CardPictureLoader::getPixmap(translatedPixmap, exactCard, translatedSize.toSize());
CardPictureLoader::getPixmap(translatedPixmap, state->getCard(), translatedSize.toSize());
if (translatedPixmap.isNull()) {
paintImage = false;
}
@ -185,19 +193,20 @@ void AbstractCardItem::paintPicture(QPainter *painter, const QSizeF &translatedS
painter->drawPath(shape());
}
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() || facedown) {
if (translatedPixmap.isNull() || SettingsCache::instance().cardsDisplay().getDisplayCardNames() ||
state->getFaceDown()) {
painter->save();
transformPainter(painter, translatedSize, angle);
painter->setPen(Qt::white);
painter->setBackground(Qt::black);
painter->setBackgroundMode(Qt::OpaqueMode);
QString nameStr;
if (facedown) {
nameStr = "# " + QString::number(id);
if (state->getFaceDown()) {
nameStr = "# " + QString::number(state->getId());
} else {
QString prefix = "";
if (SettingsCache::instance().debug().getShowCardId()) {
prefix = "#" + QString::number(id) + " ";
prefix = "#" + QString::number(state->getId()) + " ";
}
nameStr = prefix + CardLocalization::displayName(getCardInfo());
}
@ -235,21 +244,6 @@ void AbstractCardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
painter->restore();
}
void AbstractCardItem::setCardRef(const CardRef &_cardRef)
{
if (cardRef == _cardRef) {
return;
}
emit deleteCardInfoPopup(cardRef.name);
if (exactCard) {
disconnect(exactCard.getCardPtr().data(), nullptr, this, nullptr);
}
cardRef = _cardRef;
refreshCardInfo();
}
void AbstractCardItem::setHovered(bool _hovered)
{
if (isHovered == _hovered) {
@ -275,18 +269,17 @@ void AbstractCardItem::setHovered(bool _hovered)
void AbstractCardItem::setColor(const QString &_color)
{
color = _color;
cacheBgColor();
update();
state->setColor(_color);
}
void AbstractCardItem::cacheBgColor()
{
QChar colorChar;
if (color.isEmpty()) {
colorChar = exactCard.getInfo().getColorChar();
const QString cardColor = state->getColor();
if (cardColor.isEmpty()) {
colorChar = state->getCardInfo().getColorChar();
} else {
colorChar = color.at(0);
colorChar = cardColor.at(0);
}
switch (colorChar.toLower().toLatin1()) {
@ -314,17 +307,12 @@ void AbstractCardItem::cacheBgColor()
}
}
void AbstractCardItem::setTapped(bool _tapped, bool canAnimate)
void AbstractCardItem::onTappedChanged(bool newTapped, bool canAnimate)
{
if (tapped == _tapped) {
return;
}
tapped = _tapped;
if (SettingsCache::instance().cardsDisplay().getTapAnimation() && canAnimate) {
static_cast<GameScene *>(scene())->registerAnimationItem(this);
} else {
tapAngle = tapped ? 90 : 0;
tapAngle = newTapped ? 90 : 0;
setTransform(QTransform()
.translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F)
.rotate(tapAngle)
@ -338,12 +326,6 @@ bool AbstractCardItem::animationEvent()
return false;
}
void AbstractCardItem::setFaceDown(bool _facedown)
{
facedown = _facedown;
update();
}
void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
cancelPendingSelectionCollapse();
@ -357,7 +339,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
}
}
if ((event->modifiers() & Qt::AltModifier) && event->button() == Qt::LeftButton) {
emit cardShiftClicked(cardRef.name);
emit cardShiftClicked(state->getCardRef().name);
} else if ((event->modifiers() & Qt::ControlModifier)) {
setSelected(!isSelected());
} else if (!isSelected()) {
@ -371,7 +353,7 @@ void AbstractCardItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
if (event->button() == Qt::LeftButton) {
setCursor(Qt::ClosedHandCursor);
} else if (event->button() == Qt::MiddleButton) {
emit showCardInfoPopup(event->screenPos(), cardRef);
emit showCardInfoPopup(event->screenPos(), state->getCardRef());
}
event->accept();
}
@ -405,7 +387,7 @@ void AbstractCardItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
}
if (event->button() == Qt::MiddleButton) {
emit deleteCardInfoPopup(cardRef.name);
emit deleteCardInfoPopup(state->getCardRef().name);
}
// This function ensures the parent function doesn't mess around with our selection.

View file

@ -16,8 +16,7 @@
#include <QSizeF>
#include <QString>
#include <QtGlobal>
#include <libcockatrice/card/printing/exact_card.h>
#include <libcockatrice/utility/card_ref.h>
#include <libcockatrice/game/board/card_state.h>
#include <qtmetamacros.h>
class PlayerLogic;
@ -29,13 +28,8 @@ class AbstractCardItem : public ArrowTarget, public IAnimatedItem
{
Q_OBJECT
protected:
ExactCard exactCard;
int id;
CardRef cardRef;
bool tapped;
bool facedown;
CardState *state;
int tapAngle;
QString color;
QColor bgColor;
private:
@ -45,6 +39,9 @@ private:
int selectionCollapseGeneration = 0; ///< Guards the deferred collapse timer against stale firings
private slots:
void pixmapUpdated();
void onCardInfoChanged();
void onCardRefChanged(const CardRef &oldCardRef, const CardRef &newCardRef);
void onTappedChanged(bool newTapped, bool canAnimate);
public slots:
void refreshCardInfo();
@ -80,31 +77,38 @@ public:
QSizeF getTranslatedSize(QPainter *painter) const;
void paintPicture(QPainter *painter, const QSizeF &translatedSize, int angle);
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
[[nodiscard]] CardState *getState() const
{
return state;
}
ExactCard getCard() const
{
return exactCard;
return state->getCard();
}
const CardInfo &getCardInfo() const;
int getId() const
{
return id;
return state->getId();
}
void setId(int _id)
{
id = _id;
state->setId(_id);
}
QString getName() const
{
return cardRef.name;
return state->getCardRef().name;
}
QString getProviderId() const
{
return cardRef.providerId;
return state->getCardRef().providerId;
}
void setCardRef(const CardRef &_cardRef)
{
state->setCardRef(_cardRef);
}
void setCardRef(const CardRef &_cardRef);
CardRef getCardRef() const
{
return cardRef;
return state->getCardRef();
}
qreal getRealZValue() const
{
@ -116,27 +120,33 @@ public:
{
return isHovered;
}
QString getColor() const
[[nodiscard]] QString getColor() const
{
return color;
return state->getColor();
}
void setColor(const QString &_color);
bool getTapped() const
{
return tapped;
return state->getTapped();
}
void setTapped(bool _tapped, bool canAnimate = false)
{
state->setTapped(_tapped, canAnimate);
}
void setTapped(bool _tapped, bool canAnimate = false);
bool getFaceDown() const
{
return facedown;
return state->getFaceDown();
}
void setFaceDown(bool _facedown)
{
state->setFaceDown(_facedown);
}
void setFaceDown(bool _facedown);
void processHoverEvent();
/** @brief Drop a pending collapse and disarm any deferred collapse timer. */
void cancelPendingSelectionCollapse();
void deleteCardInfoPopup()
{
emit deleteCardInfoPopup(cardRef.name);
emit deleteCardInfoPopup(state->getCardRef().name);
}
/** @brief Default: no per-tick animation. Subclasses override to animate. */

View file

@ -2,10 +2,6 @@
#include "../../client/settings/cache_settings.h"
#include "../../client/settings/shortcuts_settings.h"
#include "../../game/board/counter_state.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game_graphics/board/translate_counter_name.h"
#include "../../interface/widgets/menus/tearoff_menu.h"
@ -23,8 +19,10 @@
#include <QString>
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/protocol/pb/command_inc_counter.pb.h>
#include <libcockatrice/protocol/pb/command_set_counter.pb.h>
#include <libcockatrice/game/board/counter_state.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/utility/expression.h>
#include <qnamespace.h>
@ -151,10 +149,7 @@ void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event)
menu->exec(event->screenPos());
}
} else {
Command_IncCounter cmd;
cmd.set_counter_id(id);
cmd.set_delta(event->button() == Qt::LeftButton ? 1 : -1);
player->getPlayerActions()->sendGameCommand(cmd);
player->getPlayerActions()->actIncrementCounter(id, event->button() == Qt::LeftButton ? 1 : -1);
}
event->accept();
}
@ -172,10 +167,7 @@ void AbstractCounter::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
void AbstractCounter::incrementCounter()
{
Command_IncCounter cmd;
cmd.set_counter_id(id);
cmd.set_delta(static_cast<QAction *>(sender())->data().toInt());
player->getPlayerActions()->sendGameCommand(cmd);
player->getPlayerActions()->actIncrementCounter(id, static_cast<QAction *>(sender())->data().toInt());
}
void AbstractCounter::setCounter()
@ -199,10 +191,7 @@ void AbstractCounter::setCounter()
}
Expression exp(value);
Command_SetCounter cmd;
cmd.set_counter_id(id);
cmd.set_value(static_cast<int>(exp.parse(dlg.textValue())));
player->getPlayerActions()->sendGameCommand(cmd);
player->getPlayerActions()->actSetCounter(id, static_cast<int>(exp.parse(dlg.textValue())));
}
AbstractCounterDialog::AbstractCounterDialog(const QString &name, const QString &value, QWidget *parent)

View file

@ -2,11 +2,6 @@
#include "arrow_item.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/board/arrow_data.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../game_scene.h"
#include "../player/player_target.h"
#include "../z_values.h"
@ -27,12 +22,12 @@
#include <QTransform>
#include <QtGlobal>
#include <QtMath>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/game/board/arrow_data.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h>
#include <math.h>
#include <qnamespace.h>
@ -426,19 +421,19 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
CardZoneLogic *startZone = startCard->getZone();
Command_CreateArrow cmd;
cmd.mutable_arrow_color()->CopyFrom(convertQColorToColor(data->color));
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_start_card_id(startCard->getId());
ArrowData arrow;
arrow.color = data->color;
arrow.startPlayerId = startZone->getPlayer()->getPlayerInfo()->getId();
arrow.startZone = startZone->getName();
arrow.startCardId = startCard->getId();
if (auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem)) {
CardZoneLogic *targetZone = targetCard->getZone();
cmd.set_target_player_id(targetZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(targetZone->getName().toStdString());
cmd.set_target_card_id(targetCard->getId());
arrow.targetPlayerId = targetZone->getPlayer()->getPlayerInfo()->getId();
arrow.targetZone = targetZone->getName();
arrow.targetCardId = targetCard->getId();
} else if (auto *targetPlayer = qgraphicsitem_cast<PlayerTarget *>(targetItem)) {
cmd.set_target_player_id(targetPlayer->getOwner()->getPlayerInfo()->getId());
arrow.targetPlayerId = targetPlayer->getOwner()->getPlayerInfo()->getId();
} else {
delArrow();
return;
@ -447,22 +442,9 @@ void ArrowDragItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
// if the card is in hand then we will move the card to stack or table as part of drawing the arrow
if (startZone->getName() == ZoneNames::HAND) {
startCard->playCard(false);
CardInfoPtr ci = startCard->getCard().getCardPtr();
bool playToStack = SettingsCache::instance().userInterface().getPlayToStack();
if (ci && ((!playToStack && ci->getUiAttributes().tableRow == 3) ||
(playToStack && ci->getUiAttributes().tableRow != 0 &&
startCard->getZone()->getName() != ZoneNames::STACK))) {
cmd.set_start_zone(ZoneNames::STACK);
} else {
cmd.set_start_zone(playToStack ? ZoneNames::STACK : ZoneNames::TABLE);
}
}
if (deleteInPhase != 0) {
cmd.set_delete_in_phase(deleteInPhase);
}
player->getPlayerActions()->sendGameCommand(cmd);
player->getPlayerActions()->createArrow(startCard->getState(), arrow, deleteInPhase);
}
delArrow();
@ -554,7 +536,7 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
auto *startCard = qgraphicsitem_cast<CardItem *>(startItem);
auto *targetCard = qgraphicsitem_cast<CardItem *>(targetItem);
if (startCard && targetCard) {
attachCards(startCard, targetCard);
player->getPlayerActions()->attachCards(startCard->getState(), targetCard->getState());
}
}
@ -563,23 +545,3 @@ void ArrowAttachItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
child->mouseReleaseEvent(event);
}
}
void ArrowAttachItem::attachCards(CardItem *startCard, const CardItem *targetCard)
{
if (targetCard->getAttachedTo() || targetCard->getZone()->getName() != ZoneNames::TABLE) {
return;
}
// move card onto table first if attaching from some other zone
if (startCard->getZone()->getName() != ZoneNames::TABLE) {
player->getPlayerActions()->playCardToTable(startCard, false);
}
Command_AttachCard cmd;
cmd.set_start_zone(ZoneNames::TABLE);
cmd.set_card_id(startCard->getId());
cmd.set_target_player_id(targetCard->getZone()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(targetCard->getZone()->getName().toStdString());
cmd.set_target_card_id(targetCard->getId());
player->getPlayerActions()->sendGameCommand(cmd);
}

View file

@ -1,7 +1,6 @@
#ifndef ARROWITEM_H
#define ARROWITEM_H
#include "../../game/board/arrow_data.h"
#include "../animated_item.h"
#include "arrow_target.h"
#include "graphics_item_type.h"
@ -18,6 +17,7 @@
#include <QRectF>
#include <QSharedPointer>
#include <QtGlobal>
#include <libcockatrice/game/board/arrow_data.h>
#include <qtmetamacros.h>
class CardItem;
@ -131,7 +131,6 @@ private:
PlayerLogic *player;
QList<ArrowAttachItem *> childArrows;
QMetaObject::Connection positionConnection;
void attachCards(CardItem *startCard, const CardItem *targetCard);
public:
explicit ArrowAttachItem(ArrowTarget *_startItem);

View file

@ -1,9 +1,9 @@
#include "arrow_registry.h"
#include "../game_graphics/board/arrow_item.h"
#include "board/arrow_data.h"
#include "arrow_item.h"
#include <QSharedPointer>
#include <libcockatrice/game/board/arrow_data.h>
void ArrowRegistry::insert(QSharedPointer<ArrowData> data, ArrowItem *arrow)
{

View file

@ -4,6 +4,7 @@
#include <QList>
#include <QMap>
#include <QSet>
#include <libcockatrice/game/board/arrow_data.h>
class ArrowItem;
struct ArrowData;

View file

@ -1,17 +1,6 @@
#include "card_item.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/abstract_game.h"
#include "../../game/board/card_state.h"
#include "../../game/game_state.h"
#include "../../game/phase.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/player/player_manager.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/table_zone_logic.h"
#include "../../game/zones/view_zone_logic.h"
#include "../card_dimensions.h"
#include "../game_scene.h"
#include "arrow_item.h"
@ -41,6 +30,17 @@
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/board/card_state.h>
#include <libcockatrice/game/game_state.h>
#include <libcockatrice/game/phase.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/table_zone_logic.h>
#include <libcockatrice/game/zones/view_zone_logic.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/card_ref.h>
@ -53,8 +53,9 @@ CardItem::CardItem(PlayerLogic *_owner,
const CardRef &cardRef,
int _cardid,
CardZoneLogic *_zone)
: AbstractCardItem(parent, cardRef, _owner, _cardid), state(new CardState(this, _zone)), dragItem(nullptr)
: AbstractCardItem(parent, cardRef, _owner, _cardid), dragItem(nullptr)
{
state->setZone(_zone);
owner->addCard(this);
connect(&SettingsCache::instance().cardCounters(), &CardCounterSettings::colorChanged, this, [this](int counterId) {
@ -62,25 +63,44 @@ CardItem::CardItem(PlayerLogic *_owner,
update();
}
});
connect(state, &CardState::stateReset, this, [this] {
if (scene()) {
static_cast<GameScene *>(scene())->unregisterAnimationItem(this);
}
update();
});
// The retyped logic layer drives CardState setters directly (bypassing the CardItem wrappers that called
// update()), so repaint on every state change: attacking, annotation, doesntUntap, P/T and counters.
connect(state, &CardState::stateChanged, this, [this] { update(); });
connect(state, &CardState::viewDeleteRequested, this, &CardItem::deleteLater);
}
void CardItem::prepareDelete()
{
if (owner != nullptr) {
if (owner->getGame()->getActiveCard() == this) {
auto *gameScene = qobject_cast<GameScene *>(scene());
if (gameScene && gameScene->getActiveCard() == this) {
emit owner->requestCardMenuUpdate(nullptr);
owner->getGame()->setActiveCard(nullptr);
gameScene->setActiveCard(nullptr);
}
owner = nullptr;
}
while (!attachedCards.isEmpty()) {
attachedCards.first()->setZone(nullptr); // so that it won't try to call reorganizeCards()
attachedCards.first()->setAttachedTo(nullptr);
const QList<CardState *> &attachedStates = state->getAttachedCards();
while (!attachedStates.isEmpty()) {
auto *attachedItem = qobject_cast<CardItem *>(attachedStates.first()->parent());
if (attachedItem == nullptr) {
break;
}
attachedItem->setZone(nullptr); // so that it won't try to call reorganizeCards()
attachedItem->setAttachedTo(nullptr);
}
if (state->getAttachedTo() != nullptr) {
state->getAttachedTo()->removeAttachedCard(this);
state->getAttachedTo()->removeAttachedCard(state);
state->setAttachedTo(nullptr);
}
}
@ -127,7 +147,7 @@ void CardItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
painter->save();
transformPainter(painter, translatedSize, tapAngle);
if (!getFaceDown() && state->getPT() == exactCard.getInfo().getPowTough()) {
if (!getFaceDown() && state->getPT() == state->getCardInfo().getPowTough()) {
painter->setPen(Qt::white);
} else {
painter->setPen(QColor(255, 150, 0)); // dark orange
@ -209,36 +229,13 @@ void CardItem::setPT(const QString &_pt)
void CardItem::setAttachedTo(CardItem *_attachedTo)
{
if (state->getAttachedTo() != nullptr) {
state->getAttachedTo()->removeAttachedCard(this);
}
CardState *newParent = _attachedTo == nullptr ? nullptr : _attachedTo->getState();
state->setAttachedTo(newParent);
gridPoint.setX(-1);
state->setAttachedTo(_attachedTo);
if (state->getAttachedTo() != nullptr) {
// If the zone is being torn down, it might already be null by the time a card tries to un-attach all its
// attached cards
if (state->getAttachedTo()->getZone() == nullptr) {
deleteLater();
} else {
emit state->getAttachedTo()->getZone()->cardAdded(this);
state->getAttachedTo()->addAttachedCard(this);
if (state->getZone() != state->getAttachedTo()->getZone()) {
state->getAttachedTo()->getZone()->reorganizeCards();
}
}
} else {
// If the zone is being torn down, it might already be null by the time a card tries to un-attach all its
// attached cards
if (state->getZone() == nullptr) {
deleteLater();
} else {
emit state->getZone()->cardAdded(this);
}
}
if (state->getZone() != nullptr) {
state->getZone()->reorganizeCards();
// If the zone is being torn down, it might already be null by the time a card tries to un-attach all its attached
// cards
if (newParent != nullptr ? newParent->getZone() == nullptr : state->getZone() == nullptr) {
deleteLater();
}
}
@ -248,13 +245,6 @@ void CardItem::setAttachedTo(CardItem *_attachedTo)
void CardItem::resetState(bool keepAnnotations)
{
state->resetState(keepAnnotations);
attachedCards.clear();
setTapped(false, false);
setDoesntUntap(false);
if (scene()) {
static_cast<GameScene *>(scene())->unregisterAnimationItem(this);
}
update();
}
void CardItem::processCardInfo(const ServerInfo_Card &_info)
@ -390,7 +380,7 @@ void CardItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
// Use the buttonDownPos to align the hot spot with the position when
// the user originally clicked
createDragItem(id, event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
createDragItem(state->getId(), event->buttonDownPos(Qt::LeftButton), event->scenePos(), forceFaceDown);
dragItem->grabMouse();
int childIndex = 0;
@ -433,42 +423,11 @@ void CardItem::playCard(bool faceDown)
emit playSelected(this);
}
} else {
state->getZone()->getPlayer()->getPlayerActions()->playCard(this, faceDown);
state->getZone()->getPlayer()->getPlayerActions()->playCard(state, faceDown);
}
}
}
QVariantList CardItem::parsePT(const QString &pt)
{
QVariantList ptList = QVariantList();
if (!pt.isEmpty()) {
int sep = pt.indexOf('/');
if (sep == 0) {
ptList.append(QVariant(pt.mid(1))); // cut off starting '/' and take full string
} else {
int start = 0;
for (;;) {
QString item = pt.mid(start, sep - start);
if (item.isEmpty()) {
ptList.append(QVariant(QString()));
} else if (item[0] == '+') {
ptList.append(QVariant(item.mid(1).toInt())); // add as int
} else if (item[0] == '-') {
ptList.append(QVariant(item.toInt())); // add as int
} else {
ptList.append(QVariant(item)); // add as qstring
}
if (sep == -1) {
break;
}
start = sep + 1;
sep = pt.indexOf('/', start);
}
}
}
return ptList;
}
/**
* @brief returns true if the zone is a unwritable reveal zone view (eg a card reveal window). Will return false if zone
* is nullptr.
@ -493,7 +452,7 @@ void CardItem::handleClickedToPlay(bool shiftHeld)
if (SettingsCache::instance().userInterface().getClickPlaysAllSelected()) {
emit hideSelected(this);
} else {
state->getZone()->removeCard(this);
state->getZone()->removeCard(state);
}
} else {
playCard(shiftHeld);
@ -537,16 +496,16 @@ bool CardItem::animationEvent()
{
int rotation = ROTATION_DEGREES_PER_FRAME;
bool animationIncomplete = true;
if (!tapped) {
if (!state->getTapped()) {
rotation *= -1;
}
tapAngle += rotation;
if (tapped && (tapAngle > 90)) {
if (state->getTapped() && (tapAngle > 90)) {
tapAngle = 90;
animationIncomplete = false;
}
if (!tapped && (tapAngle < 0)) {
if (!state->getTapped() && (tapAngle < 0)) {
tapAngle = 0;
animationIncomplete = false;
}
@ -567,7 +526,9 @@ QVariant CardItem::itemChange(GraphicsItemChange change, const QVariant &value)
bool selected = value.toBool();
if (selected) {
owner->getGame()->setActiveCard(this);
if (auto *gameScene = qobject_cast<GameScene *>(scene())) {
gameScene->setActiveCard(this);
}
}
emit selectionChanged(this, selected);

View file

@ -7,7 +7,6 @@
#ifndef CARDITEM_H
#define CARDITEM_H
#include "../../game/board/card_state.h"
#include "abstract_card_item.h"
#include "graphics_item_type.h"
@ -16,6 +15,7 @@
#include <QPointF>
#include <QString>
#include <QVariantList>
#include <libcockatrice/game/board/card_state.h>
#include <qtmetamacros.h>
class CardDragItem;
@ -33,11 +33,7 @@ class CardItem : public AbstractCardItem
{
Q_OBJECT
private:
CardState *state;
QPoint gridPoint;
CardDragItem *dragItem;
QList<CardItem *> attachedCards;
void prepareDelete();
void handleClickedToPlay(bool shiftHeld);
@ -60,10 +56,6 @@ public:
CardZoneLogic *_zone = nullptr);
void retranslateUi();
[[nodiscard]] CardState *getState() const
{
return state;
}
[[nodiscard]] CardZoneLogic *getZone() const
{
return state->getZone();
@ -72,15 +64,15 @@ public:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
[[nodiscard]] QPoint getGridPoint() const
{
return gridPoint;
return state->getGridPoint();
}
void setGridPoint(const QPoint &_gridPoint)
{
gridPoint = _gridPoint;
state->setGridPoint(_gridPoint);
}
[[nodiscard]] QPoint getGridPos() const
{
return gridPoint;
return state->getGridPoint();
}
[[nodiscard]] PlayerLogic *getOwner() const
{
@ -123,22 +115,22 @@ public:
{
state->setDestroyOnZoneChange(_destroy);
}
[[nodiscard]] CardItem *getAttachedTo() const
[[nodiscard]] CardState *getAttachedTo() const
{
return state->getAttachedTo();
}
void setAttachedTo(CardItem *_attachedTo);
void addAttachedCard(CardItem *card)
void addAttachedCard(CardState *card)
{
attachedCards.append(card);
state->addAttachedCard(card);
}
void removeAttachedCard(CardItem *card)
void removeAttachedCard(CardState *card)
{
attachedCards.removeOne(card);
state->removeAttachedCard(card);
}
[[nodiscard]] const QList<CardItem *> &getAttachedCards() const
[[nodiscard]] const QList<CardState *> &getAttachedCards() const
{
return attachedCards;
return state->getAttachedCards();
}
void resetState(bool keepAnnotations = false);
void processCardInfo(const ServerInfo_Card &_info);
@ -150,26 +142,6 @@ public:
void drawAttachArrow();
void playCard(bool faceDown);
/**
* @brief Parses a string representing a p/t in order to extract the values from it.
*
* If the string contains '/', the string will be split at the '/' and each side will be parsed separately,
* which means the result list will have two elements.
*
* If '/' is not found, then the entire string is parsed together, which means the result list will
* have a single element.
*
* If either side of the split is empty, there will also only be a single element in the result list.
*
* This function will attempt to parse each substring as an int first, handling plus and minus prefixes.
* If successful, it will put the parsed value into the QVariant as an int.
* If failed, it will just put the substring into the QVariant as a QString.
*
* @param pt The p/t string
* @return A QVariantList that can contain one or two elements, where each QVariant can be either int or QString
*/
static QVariantList parsePT(const QString &pt);
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

View file

@ -2,13 +2,8 @@
#include "../../client/settings/cache_settings.h"
#include "../../client/settings/shortcuts_settings.h"
#include "../../game/abstract_game.h"
#include "../../game/game_event_handler.h"
#include "../../game/game_state.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../../interface/deck_loader/deck_file_format.h"
#include "../../interface/deck_loader/deck_loader.h"
#include "../../interface/deck_loader/loaded_deck.h"
#include "../../interface/widgets/dialogs/dlg_load_deck.h"
#include "../../interface/widgets/dialogs/dlg_load_deck_from_clipboard.h"
#include "../../interface/widgets/dialogs/dlg_load_deck_from_website.h"
@ -33,16 +28,17 @@
#include <QRect>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_file_format.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <libcockatrice/deck_list/playmat_resolver.h>
#include <libcockatrice/protocol/pb/command_deck_select.pb.h>
#include <libcockatrice/protocol/pb/command_ready_start.pb.h>
#include <libcockatrice/protocol/pb/command_set_playmat.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_lock.pb.h>
#include <libcockatrice/protocol/pb/command_set_sideboard_plan.pb.h>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/game_event_handler.h>
#include <libcockatrice/game/game_state.h>
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <libcockatrice/utility/string_limits.h>
#include <optional>
#include <qnamespace.h>
@ -111,10 +107,9 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
connect(deckView, &DeckView::newCardAdded, this, &DeckViewContainer::newCardAdded);
connect(deckView, &DeckView::sideboardPlanChanged, this, &DeckViewContainer::sideboardPlanChanged);
deckViewLayout = new QVBoxLayout;
deckViewLayout = QtUtils::makeLayout<QVBoxLayout>();
deckViewLayout->addLayout(buttonHBox);
deckViewLayout->addWidget(deckView);
deckViewLayout->setContentsMargins(0, 0, 0, 0);
setLayout(deckViewLayout);
retranslateUi();
@ -314,9 +309,7 @@ void DeckViewContainer::loadDeckFromDeckList(const DeckList &deck)
return;
}
Command_DeckSelect cmd;
cmd.set_deck(deckString.toStdString());
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelect(deckString);
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
@ -345,15 +338,7 @@ void DeckViewContainer::resolveAndSendPlaymat()
lastResolvedPlaymat = resolved;
Command_SetPlaymat playmatCmd;
auto *pp = playmatCmd.mutable_playmat_params();
pp->set_card_name(resolved.card.name.toStdString());
pp->set_card_provider_id(resolved.card.providerId.toStdString());
pp->set_margin_pct_l(resolved.params.marginPctL);
pp->set_margin_pct_r(resolved.params.marginPctR);
pp->set_vertical_offset(resolved.params.verticalOffset);
pp->set_zoom(resolved.params.zoom);
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(playmatCmd);
PendingCommand *playmatPend = parentGame->getGame()->getGameEventHandler()->prepareSetPlaymat(resolved);
const QString sentName = resolved.card.name;
connect(playmatPend, &PendingCommand::finished, this, [this, sentName](const Response &r) {
// Only RespFunctionNotAllowed signals a card art DENY rule. Older servers that predate
@ -384,9 +369,7 @@ void DeckViewContainer::loadRemoteDeck()
{
DlgLoadRemoteDeck dlg(parentGame->getGame()->getClientForPlayer(playerId), this);
if (dlg.exec()) {
Command_DeckSelect cmd;
cmd.set_deck_id(dlg.getDeckId());
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareGameCommand(cmd);
PendingCommand *pend = parentGame->getGame()->getGameEventHandler()->prepareDeckSelectId(dlg.getDeckId());
connect(pend, &PendingCommand::finished, this, &DeckViewContainer::deckSelectFinished);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(pend, playerId);
}
@ -442,34 +425,21 @@ void DeckViewContainer::readyStart()
void DeckViewContainer::forceStart()
{
const auto msg = tr("Are you sure you want to force start?\nThis will kick all non-ready players from the game.");
const auto res =
QMessageBox::question(this, tr("Cockatrice"), msg, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (res == QMessageBox::No) {
if (!QtUtils::confirm(this, tr("Cockatrice"), msg, QMessageBox::Yes)) {
return;
}
Command_ReadyStart cmd;
cmd.set_force_start(true);
cmd.set_ready(true);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
parentGame->getGame()->getGameEventHandler()->sendForceStart(playerId);
}
void DeckViewContainer::sideboardLockButtonClicked()
{
Command_SetSideboardLock cmd;
cmd.set_locked(sideboardLockButton->getState());
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
parentGame->getGame()->getGameEventHandler()->sendSetSideboardLock(sideboardLockButton->getState(), playerId);
}
void DeckViewContainer::sideboardPlanChanged()
{
Command_SetSideboardPlan cmd;
const QList<MoveCard_ToZone> &newPlan = deckView->getSideboardPlan();
for (const auto &i : newPlan) {
cmd.add_move_list()->CopyFrom(i);
}
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
parentGame->getGame()->getGameEventHandler()->sendSetSideboardPlan(deckView->getSideboardPlan(), playerId);
}
/**
@ -481,9 +451,7 @@ void DeckViewContainer::sendReadyStartCommand(bool ready)
resolveAndSendPlaymat();
}
Command_ReadyStart cmd;
cmd.set_ready(ready);
parentGame->getGame()->getGameEventHandler()->sendGameCommand(cmd, playerId);
parentGame->getGame()->getGameEventHandler()->sendReadyStart(ready, playerId);
}
/**

View file

@ -0,0 +1,13 @@
#include "dlg_annotation.h"
#include <QKeyEvent>
void AnnotationDialog::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Return && event->modifiers() & Qt::ControlModifier) {
event->accept();
accept();
return;
}
QInputDialog::keyPressEvent(event);
}

View file

@ -0,0 +1,22 @@
/**
* @file dlg_annotation.h
* @ingroup GameDialogs
*/
#ifndef COCKATRICE_DLG_ANNOTATION_H
#define COCKATRICE_DLG_ANNOTATION_H
#include <QInputDialog>
class AnnotationDialog : public QInputDialog
{
Q_OBJECT
void keyPressEvent(QKeyEvent *e) override;
public:
explicit AnnotationDialog(QWidget *parent = nullptr) : QInputDialog(parent)
{
}
};
#endif // COCKATRICE_DLG_ANNOTATION_H

View file

@ -11,6 +11,7 @@
#include <QList>
#include <QString>
#include <QStringList>
#include <libcockatrice/game/player/token_info.h>
#include <qtmetamacros.h>
class QLabel;
@ -25,17 +26,6 @@ class CardInfoPictureWidget;
class QModelIndex;
class QWidget;
struct TokenInfo
{
QString name;
QString color;
QString pt;
QString annotation;
bool destroy = true;
bool faceDown = false;
QString providerId;
};
class DlgCreateToken : public QDialog
{
Q_OBJECT

View file

@ -11,6 +11,7 @@
#include <QList>
#include <QString>
#include <QStringList>
#include <libcockatrice/game/player/move_top_cards_until_options.h>
#include <qtmetamacros.h>
class FilterString;
@ -21,13 +22,6 @@ class QLabel;
class QSpinBox;
class QWidget;
struct MoveTopCardsUntilOptions
{
QStringList exprs = {};
int numberOfHits = 1;
bool autoPlay = false;
};
class DlgMoveTopCardsUntil : public QDialog
{
Q_OBJECT

View file

@ -1,18 +1,10 @@
#include "game_scene.h"
#include "../client/settings/cache_settings.h"
#include "../game/abstract_game.h"
#include "../game/arrow_registry.h"
#include "../game/board/arrow_data.h"
#include "../game/player/player_actions.h"
#include "../game/player/player_event_handler.h"
#include "../game/player/player_info.h"
#include "../game/player/player_logic.h"
#include "../game/zones/card_zone_logic.h"
#include "../game/zones/view_zone_logic.h"
#include "animated_item.h"
#include "board/abstract_card_item.h"
#include "board/arrow_item.h"
#include "board/arrow_registry.h"
#include "board/card_item.h"
#include "phases_toolbar.h"
#include "player/menu/player_menu.h"
@ -40,6 +32,15 @@
#include <QSharedPointer>
#include <QtMath>
#include <algorithm>
#include <functional>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/board/arrow_data.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_event_handler.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/view_zone_logic.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/zone_names.h>
#include <numeric>
@ -112,6 +113,15 @@ void GameScene::retranslateUi()
}
}
QList<CardState *> GameScene::selectedCardStates() const
{
QList<CardState *> selectedStates;
for (auto *card : selectedCards()) {
selectedStates.append(card->getState());
}
return selectedStates;
}
QList<CardItem *> GameScene::selectedCards() const
{
QList<CardItem *> selectedCards;
@ -124,6 +134,67 @@ QList<CardItem *> GameScene::selectedCards() const
return selectedCards;
}
/**
* @brief Selects every card of @p zone for which @p filter holds.
* @param filter A predicate to filter which cards are selected.
*/
static void selectCardsInZone(
const CardZoneLogic *zone,
std::function<bool(const CardItem *)> filter = [](const CardItem *) { return true; })
{
if (!zone) {
return;
}
for (auto *cardState : zone->getCards()) {
auto *cardItem = cardState == nullptr ? nullptr : qobject_cast<CardItem *>(cardState->parent());
if (cardItem && filter(cardItem)) {
cardItem->setSelected(true);
}
}
}
void GameScene::selectAllInZone(const CardItem *card)
{
if (!card) {
return;
}
selectCardsInZone(card->getZone());
}
void GameScene::selectSameRow(const CardItem *card)
{
if (!card) {
return;
}
auto isSameRow = [card](const CardItem *cardItem) {
return qAbs(card->scenePos().y() - cardItem->scenePos().y()) < 50;
};
selectCardsInZone(card->getZone(), isSameRow);
}
void GameScene::selectSameColumn(const CardItem *card)
{
if (!card) {
return;
}
auto isSameColumn = [card](const CardItem *cardItem) { return cardItem->x() == card->x(); };
selectCardsInZone(card->getZone(), isSameColumn);
}
CardItem *GameScene::getActiveCard() const
{
return activeCard;
}
void GameScene::setActiveCard(CardItem *card)
{
activeCard = card;
}
void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool selected)
{
CardItem *card = qobject_cast<CardItem *>(abstractCard);
@ -134,12 +205,12 @@ void GameScene::onCardSelectionChanged(AbstractCardItem *abstractCard, bool sele
auto *owner = card->getOwner();
if (selected) {
owner->requestCardMenuUpdate(card);
owner->requestCardMenuUpdate(card->getState());
return;
}
if (selectedItems().isEmpty()) {
owner->getGame()->setActiveCard(nullptr);
setActiveCard(nullptr);
owner->requestCardMenuUpdate(nullptr);
}
}
@ -158,9 +229,9 @@ void GameScene::onCardRightClicked(AbstractCardItem *abstractCard, QPoint screen
return;
}
card->getOwner()->getGame()->setActiveCard(card);
setActiveCard(card);
if (auto *menu = view->getPlayerMenu()->updateCardMenu(card)) {
if (auto *menu = view->getPlayerMenu()->updateCardMenu(card->getState())) {
menu->popup(screenPos);
}
}
@ -173,7 +244,7 @@ void GameScene::playSelected(AbstractCardItem *card)
if (!card->getOwner()) {
return;
}
card->getOwner()->getPlayerActions()->actPlay(selectedCards());
card->getOwner()->getPlayerActions()->actPlay(selectedCardStates());
}
void GameScene::playSelectedFaceDown(AbstractCardItem *card)
@ -184,7 +255,7 @@ void GameScene::playSelectedFaceDown(AbstractCardItem *card)
if (!card->getOwner()) {
return;
}
card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCards());
card->getOwner()->getPlayerActions()->actPlayFacedown(selectedCardStates());
}
void GameScene::hideSelected(AbstractCardItem *card)
@ -195,7 +266,7 @@ void GameScene::hideSelected(AbstractCardItem *card)
if (!card->getOwner()) {
return;
}
card->getOwner()->getPlayerActions()->actHide(selectedCards());
card->getOwner()->getPlayerActions()->actHide(selectedCardStates());
}
/**
@ -230,6 +301,8 @@ void GameScene::addPlayer(PlayerLogic *player)
[this, id = player->getPlayerInfo()->getId()]() { clearArrowsForPlayerLocally(id); });
connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardZoneChanged, this, &GameScene::onCardZoneChanged);
connect(player->getPlayerEventHandler(), &PlayerEventHandler::cardViewRefreshRequested, this,
&GameScene::onCardViewRefreshRequested);
rearrange();
}
@ -533,7 +606,11 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
return;
}
CardItem *startCard = startZone->getCard(data->startCardId);
CardState *startCardState = startZone->getCard(data->startCardId);
if (!startCardState) {
return;
}
CardItem *startCard = qobject_cast<CardItem *>(startCardState->parent());
if (!startCard) {
return;
}
@ -544,7 +621,8 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
} else {
auto *zone = targetView->getLogic()->getZones().value(data->targetZone);
if (zone) {
targetItem = zone->getCard(data->targetCardId);
CardState *targetCardState = zone->getCard(data->targetCardId);
targetItem = targetCardState == nullptr ? nullptr : qobject_cast<CardItem *>(targetCardState->parent());
}
}
if (!targetItem) {
@ -572,11 +650,15 @@ void GameScene::requestArrowDeletion(int playerId, int arrowId)
}
}
void GameScene::onCardZoneChanged(CardItem *card, bool sameZone)
void GameScene::onCardZoneChanged(CardState *card, bool sameZone)
{
auto *item = qobject_cast<CardItem *>(card->parent());
if (!item) {
return;
}
QList<ArrowItem *> toDelete;
for (auto *arrow : arrowRegistry.all()) {
if (arrow->getStartItem() == card || arrow->getTargetItem() == card) {
if (arrow->getStartItem() == item || arrow->getTargetItem() == item) {
if (sameZone) {
arrow->updatePath();
} else {
@ -589,6 +671,25 @@ void GameScene::onCardZoneChanged(CardItem *card, bool sameZone)
}
}
void GameScene::onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner)
{
auto *item = qobject_cast<CardItem *>(card->parent());
if (!item) {
return;
}
if (zoneChanged) {
item->deleteCardInfoPopup();
}
item->deleteDragItem();
if (zoneChanged) {
item->setBeingPointedAt(false);
item->setHovered(false);
}
if (newOwner) {
item->setOwner(newOwner);
}
}
void GameScene::clearArrowsForPlayer(int playerId)
{
for (int arrowId : arrowRegistry.idsForPlayer(playerId)) {

View file

@ -1,7 +1,9 @@
#ifndef GAMESCENE_H
#define GAMESCENE_H
#include "../game/arrow_registry.h"
#include "animated_item.h"
#include "board/arrow_item.h"
#include "board/arrow_registry.h"
#include <QGraphicsScene>
#include <QHash>
@ -16,6 +18,8 @@
#include <QString>
#include <QTransform>
#include <QtGlobal>
#include <libcockatrice/game/board/arrow_data.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <qtmetamacros.h>
// IWYU pragma: keep
@ -39,6 +43,7 @@ class ZoneViewWidget;
class CardZone;
class AbstractCardItem;
class CardItem;
class CardState;
class ServerInfo_Card;
class PhasesToolbar;
class QBasicTimer;
@ -69,6 +74,7 @@ private:
QList<ZoneViewWidget *> zoneViews; ///< Active zone view widgets
QSize viewSize; ///< Current view size
QPointer<CardItem> hoveredCard; ///< Currently hovered card
QPointer<CardItem> activeCard; ///< Card the currently open context menu targets
QBasicTimer *animationTimer; ///< Timer for scene animations
QHash<QObject *, IAnimatedItem *> animatedItems; ///< Items currently animating
QHash<QObject *, QMetaObject::Connection>
@ -107,6 +113,22 @@ public:
/** @brief Gets all selected CardItems. */
QList<CardItem *> selectedCards() const;
QList<CardState *> selectedCardStates() const;
/** @brief Selects every card sharing the zone of @p card. */
void selectAllInZone(const CardItem *card);
/** @brief Selects the cards laid out in the same visual row as @p card. */
void selectSameRow(const CardItem *card);
/** @brief Selects the cards laid out in the same visual column as @p card. */
void selectSameColumn(const CardItem *card);
/** @brief Card the currently open context menu targets, or null. */
CardItem *getActiveCard() const;
/** @brief Sets the card the currently open context menu targets. */
void setActiveCard(CardItem *card);
/**
* @brief Adds a player to the scene and stores their graphics item.
@ -261,7 +283,15 @@ public slots:
/// Queues up arrow deletion but doesn't directly modify the scene
void requestArrowDeletion(int playerId, int arrowId);
void onCardZoneChanged(CardItem *card, bool sameZone);
void onCardZoneChanged(CardState *card, bool sameZone);
/**
* @brief Refreshes the view of a card that the server just moved.
*
* @param card The card that moved.
* @param zoneChanged Whether the card crossed a zone boundary.
* @param newOwner The player the card now belongs to, or nullptr to leave the owner alone.
*/
void onCardViewRefreshRequested(CardState *card, bool zoneChanged, PlayerLogic *newOwner);
protected:
/** @brief Handles hover updates. */

View file

@ -1,7 +1,5 @@
#include "hand_counter.h"
#include "../game/board/card_list.h"
#include "../game/zones/card_zone_logic.h"
#include "../interface/pixel_map_generator.h"
#include <QGraphicsSceneMouseEvent>
@ -14,6 +12,8 @@
#include <QString>
#include <QStringLiteral>
#include <QTransform>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <qnamespace.h>
class QGraphicsItem;

View file

@ -3,16 +3,6 @@
#include "../../client/settings/cache_settings.h"
#include "../../client/settings/card_counter_settings.h"
#include "../../client/sound_engine.h"
#include "../../client/translation.h"
#include "../../game/abstract_game.h"
#include "../../game/board/card_list.h"
#include "../../game/game_state.h"
#include "../../game/phase.h"
#include "../../game/player/player_event_handler.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/player/player_manager.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../interface/card_localization.h"
#include "../../interface/widgets/server/chat_view/chat_view.h"
#include "../board/card_item.h"
@ -24,6 +14,16 @@
#include <QList>
#include <QSharedPointer>
#include <QStringList>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/game_state.h>
#include <libcockatrice/game/phase.h>
#include <libcockatrice/game/player/player_event_handler.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
#include <libcockatrice/game/translation.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/protocol/pb/context_move_card.pb.h>
#include <libcockatrice/protocol/pb/context_mulligan.pb.h>
#include <libcockatrice/settings/chat_settings.h>
@ -283,7 +283,7 @@ void MessageLogWidget::logDestroyCard(PlayerLogic *player, QString cardName)
}
void MessageLogWidget::logMoveCard(PlayerLogic *player,
CardItem *card,
CardState *card,
CardZoneLogic *startZone,
int oldX,
CardZoneLogic *targetZone,
@ -662,7 +662,7 @@ void MessageLogWidget::logSetActivePlayer(PlayerLogic *player)
QString(tr("%1's turn.")).arg(player->getPlayerInfo()->getName()) + "</b></font><br>");
}
void MessageLogWidget::logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation)
void MessageLogWidget::logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation)
{
appendHtmlServerMessage(
QString(tr("%1 sets annotation of %2 to %3."))
@ -706,7 +706,7 @@ void MessageLogWidget::logSetCounter(PlayerLogic *player, QString counterName, i
.arg(value - oldValue));
}
void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap)
void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap)
{
QString str;
if (doesntUntap) {
@ -717,7 +717,7 @@ void MessageLogWidget::logSetDoesntUntap(PlayerLogic *player, CardItem *card, bo
appendHtmlServerMessage(str.arg(sanitizeHtml(player->getPlayerInfo()->getName())).arg(cardLink(card->getName())));
}
void MessageLogWidget::logSetPT(PlayerLogic *player, CardItem *card, QString newPT)
void MessageLogWidget::logSetPT(PlayerLogic *player, CardState *card, QString newPT)
{
if (currentContext == MessageContext_MoveCard) {
return;
@ -755,7 +755,7 @@ void MessageLogWidget::logSetSideboardLock(PlayerLogic *player, bool locked)
}
}
void MessageLogWidget::logSetTapped(PlayerLogic *player, CardItem *card, bool tapped)
void MessageLogWidget::logSetTapped(PlayerLogic *player, CardState *card, bool tapped)
{
if (currentContext == MessageContext_MoveCard) {
return;

View file

@ -14,6 +14,7 @@
#include <qtmetamacros.h>
#include <sys/types.h>
class CardState;
class AbstractGame;
class CardItem;
class GameEventContext;
@ -76,7 +77,7 @@ public slots:
void logLeaveSpectator(QString name, QString reason);
void logNotReadyStart(PlayerLogic *player);
void logMoveCard(PlayerLogic *player,
CardItem *card,
CardState *card,
CardZoneLogic *startZone,
int oldX,
CardZoneLogic *targetZone,
@ -97,13 +98,13 @@ public slots:
void logSay(PlayerLogic *player, QString message);
void logSetActivePhase(int phase);
void logSetActivePlayer(PlayerLogic *player);
void logSetAnnotation(PlayerLogic *player, CardItem *card, QString newAnnotation);
void logSetAnnotation(PlayerLogic *player, CardState *card, QString newAnnotation);
void logSetCardCounter(PlayerLogic *player, QString cardName, int counterId, int value, int oldValue);
void logSetCounter(PlayerLogic *player, QString counterName, int value, int oldValue);
void logSetDoesntUntap(PlayerLogic *player, CardItem *card, bool doesntUntap);
void logSetPT(PlayerLogic *player, CardItem *card, QString newPT);
void logSetDoesntUntap(PlayerLogic *player, CardState *card, bool doesntUntap);
void logSetPT(PlayerLogic *player, CardState *card, QString newPT);
void logSetSideboardLock(PlayerLogic *player, bool locked);
void logSetTapped(PlayerLogic *player, CardItem *card, bool tapped);
void logSetTapped(PlayerLogic *player, CardState *card, bool tapped);
void logShuffle(PlayerLogic *player, CardZoneLogic *zone, int start, int end);
void logSpectatorSay(const ServerInfo_User &spectator, QString message);
void logUnattachCard(PlayerLogic *player, QString cardName);

View file

@ -14,11 +14,6 @@
#include <QTransform>
#include <QtGlobal>
#include <google/protobuf/stubs/port.h>
#include <libcockatrice/protocol/pb/command_draw_cards.pb.h>
#include <libcockatrice/protocol/pb/command_next_turn.pb.h>
#include <libcockatrice/protocol/pb/command_set_active_phase.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/utility/zone_names.h>
#include <qnamespace.h>
#include <qnumeric.h>
@ -120,9 +115,9 @@ PhasesToolbar::PhasesToolbar(QGraphicsItem *parent)
: QGraphicsItem(parent), width(100), height(100), ySpacing(1), symbolSize(8)
{
auto *aUntapAll = new QAction(this);
connect(aUntapAll, &QAction::triggered, this, &PhasesToolbar::actUntapAll);
connect(aUntapAll, &QAction::triggered, this, &PhasesToolbar::untapAllRequested);
auto *aDrawCard = new QAction(this);
connect(aDrawCard, &QAction::triggered, this, &PhasesToolbar::actDrawCard);
connect(aDrawCard, &QAction::triggered, this, &PhasesToolbar::drawCardRequested);
PhaseButton *untapButton = new PhaseButton("untap", this, aUntapAll);
PhaseButton *upkeepButton = new PhaseButton("upkeep", this);
@ -144,7 +139,7 @@ PhasesToolbar::PhasesToolbar(QGraphicsItem *parent)
}
nextTurnButton = new PhaseButton("nextturn", this, nullptr, false);
connect(nextTurnButton, &PhaseButton::clicked, this, &PhasesToolbar::actNextTurn);
connect(nextTurnButton, &PhaseButton::clicked, this, &PhasesToolbar::nextTurnRequested);
rearrangeButtons();
@ -265,31 +260,5 @@ void PhasesToolbar::phaseButtonClicked()
button->triggerDoubleClickAction();
}
Command_SetActivePhase cmd;
cmd.set_phase(static_cast<google::protobuf::uint32>(buttonList.indexOf(button)));
emit sendGameCommand(cmd, -1);
}
void PhasesToolbar::actNextTurn()
{
emit sendGameCommand(Command_NextTurn(), -1);
}
void PhasesToolbar::actUntapAll()
{
Command_SetCardAttr cmd;
cmd.set_zone(ZoneNames::TABLE);
cmd.set_attribute(AttrTapped);
cmd.set_attr_value("0");
emit sendGameCommand(cmd, -1);
}
void PhasesToolbar::actDrawCard()
{
Command_DrawCards cmd;
cmd.set_number(1);
emit sendGameCommand(cmd, -1);
emit setActivePhaseRequested(buttonList.indexOf(button));
}

View file

@ -17,14 +17,6 @@
class QAction;
class QTimer;
namespace google
{
namespace protobuf
{
class Message;
}
} // namespace google
class PhaseButton : public QObject, public QGraphicsItem
{
Q_OBJECT
@ -95,11 +87,16 @@ public slots:
void triggerPhaseAction(int phase);
private slots:
void phaseButtonClicked();
void actNextTurn();
void actUntapAll();
void actDrawCard();
signals:
void sendGameCommand(const ::google::protobuf::Message &command, int playerId);
/** @name Intent signals
* The view reports what the user asked for; GameEventHandler builds the commands.
* @{
*/
void setActivePhaseRequested(int phase);
void nextTurnRequested();
void untapAllRequested();
void drawCardRequested();
/** @} */
protected:
void paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) override;

View file

@ -3,16 +3,8 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/card_counter_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/abstract_game.h"
#include "../../../game/player/player_info.h"
#include "../../../game/player/player_manager.h"
#include "../../../game/zones/card_zone_logic.h"
#include "../../board/card_item.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/view_zone_logic.h"
#include "../../game_scene.h"
#include "../card_menu_action_type.h"
#include "../player_graphics_item.h"
#include "libcockatrice/card/card_info.h"
#include "libcockatrice/card/database/card_database_querier.h"
@ -35,6 +27,14 @@
#include <QtGlobal>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/player/card_menu_action_type.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/view_zone_logic.h>
#include <libcockatrice/utility/zone_names.h>
#include <qnamespace.h>
#include <utility>
@ -67,7 +67,7 @@ static QAction *makeAction(QObject *parent, Slot &&slot, bool checkable = false,
return a;
}
CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _shortcutsActive)
CardMenu::CardMenu(PlayerGraphicsItem *_player, CardItem *_card, bool _shortcutsActive)
: player(_player), card(_card), shortcutsActive(_shortcutsActive)
{
const QList<PlayerLogic *> &players = player->getLogic()->getGame()->getPlayerManager()->getPlayers().values();
@ -86,7 +86,7 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
auto *gameScene = player->getGameScene();
// Single selection resolver used by all lambdas — called at trigger time
auto sel = [gameScene]() { return gameScene->selectedCards(); };
auto sel = [gameScene]() { return gameScene->selectedCardStates(); };
// Unified dispatcher for card menu actions
auto invoke = [actions, sel](CardMenuActionType type) {
@ -108,18 +108,18 @@ CardMenu::CardMenu(PlayerGraphicsItem *_player, const CardItem *_card, bool _sho
aHide = makeAction(this, [actions, sel]() { actions->actHide(sel()); });
aReduceLifeByPower = makeAction(this, [actions, sel]() { actions->actReduceLifeByPower(sel()); });
// Actions that use activeCard, not selection — direct connection
// Actions bound to the card this menu was opened for, not to the current selection
aAttach = new QAction(this);
aDrawArrow = new QAction(this);
aSelectAll = new QAction(this);
aSelectRow = new QAction(this);
aSelectColumn = new QAction(this);
connect(aAttach, &QAction::triggered, actions, &PlayerActions::actAttach);
connect(aDrawArrow, &QAction::triggered, actions, &PlayerActions::actDrawArrow);
connect(aSelectAll, &QAction::triggered, actions, &PlayerActions::actSelectAll);
connect(aSelectRow, &QAction::triggered, actions, &PlayerActions::actSelectRow);
connect(aSelectColumn, &QAction::triggered, actions, &PlayerActions::actSelectColumn);
connect(aAttach, &QAction::triggered, this, &CardMenu::attachCard);
connect(aDrawArrow, &QAction::triggered, this, &CardMenu::drawArrow);
connect(aSelectAll, &QAction::triggered, this, [this, gameScene]() { gameScene->selectAllInZone(card); });
connect(aSelectRow, &QAction::triggered, this, [this, gameScene]() { gameScene->selectSameRow(card); });
connect(aSelectColumn, &QAction::triggered, this, [this, gameScene]() { gameScene->selectSameColumn(card); });
aRevealToAll = new QAction(this);
@ -337,7 +337,7 @@ void CardMenu::createHandOrCustomZoneMenu(bool canModifyCard)
initContextualPlayersMenu(revealMenu, aRevealToAll);
connect(revealMenu, &QMenu::triggered, this, [this](QAction *action) {
player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCards(), action);
player->getLogic()->getPlayerActions()->actReveal(player->getGameScene()->selectedCardStates(), action);
});
addSeparator();
@ -441,6 +441,7 @@ void CardMenu::addRelatedCardActions()
if (relatedCards.isEmpty()) {
return;
}
auto *playerActions = player->getLogic()->getPlayerActions();
addSeparator();
int index = 0;
@ -484,9 +485,10 @@ void CardMenu::addRelatedCardActions()
}
auto *createRelated = new QAction(text, this);
createRelated->setData(QVariant(index++));
connect(createRelated, &QAction::triggered, player->getLogic()->getPlayerActions(),
&PlayerActions::actCreateRelatedCard);
const int relatedIndex = index++;
connect(createRelated, &QAction::triggered, this, [this, playerActions, relatedIndex] {
playerActions->actCreateRelatedCard(card->getState(), relatedIndex);
});
addAction(createRelated);
}
@ -495,12 +497,26 @@ void CardMenu::addRelatedCardActions()
createRelatedCards->setShortcuts(
SettingsCache::instance().shortcuts().getShortcut("Player/aCreateRelatedTokens"));
}
connect(createRelatedCards, &QAction::triggered, player->getLogic()->getPlayerActions(),
&PlayerActions::actCreateAllRelatedCards);
connect(createRelatedCards, &QAction::triggered, this,
[this, playerActions] { playerActions->actCreateAllRelatedCards(card->getState()); });
addAction(createRelatedCards);
}
}
void CardMenu::attachCard()
{
if (card) {
card->drawAttachArrow();
}
}
void CardMenu::drawArrow()
{
if (card) {
card->drawArrow(Qt::red);
}
}
void CardMenu::retranslateUi()
{
aSelectAll->setText(tr("&Select All"));

View file

@ -27,7 +27,7 @@ signals:
void cardInfoRequested(const CardRef &cardRef);
public:
explicit CardMenu(PlayerGraphicsItem *player, const CardItem *card, bool shortcutsActive);
explicit CardMenu(PlayerGraphicsItem *player, CardItem *card, bool shortcutsActive);
void removePlayer(PlayerLogic *playerToRemove);
void createTableMenu(bool canModifyCard);
void createStackMenu(bool canModifyCard);
@ -53,11 +53,13 @@ public:
private:
PlayerGraphicsItem *player;
const CardItem *card;
CardItem *card;
QList<QPair<QString, int>> playersInfo;
bool shortcutsActive;
void addRelatedCardActions();
void attachCard();
void drawArrow();
void retranslateUi();
void initContextualPlayersMenu(QMenu *menu, QAction *allPlayersAction);
void setShortcutsActive();

View file

@ -1,13 +1,13 @@
#include "custom_zone_menu.h"
#include "../../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game_scene.h"
#include "../player_graphics_item.h"
#include <QAction>
#include <QList>
#include <QVariant>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
CustomZoneMenu::CustomZoneMenu(PlayerGraphicsItem *_player) : player(_player)
{

View file

@ -2,13 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/player/player_info.h"
#include "../../../game/player/player_manager.h"
#include "../../../game/zones/pile_zone_logic.h"
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "../../game/abstract_game.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../player_graphics_item.h"
#include <QAction>
@ -19,6 +13,12 @@
#include <QObject>
#include <QString>
#include <QVariant>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
#include <libcockatrice/game/zones/pile_zone_logic.h>
#include <libcockatrice/utility/zone_names.h>
class QWidget;

View file

@ -2,14 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/board/card_list.h"
#include "../../../game/player/player_info.h"
#include "../../../game/player/player_manager.h"
#include "../../../game/zones/hand_zone_logic.h"
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "../../game/abstract_game.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../player_graphics_item.h"
#include <QAction>
@ -20,6 +13,13 @@
#include <QObject>
#include <QString>
#include <QVariant>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
#include <libcockatrice/game/zones/hand_zone_logic.h>
#include <libcockatrice/utility/zone_names.h>
class QWidget;

View file

@ -2,14 +2,7 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/board/card_list.h"
#include "../../../game/player/player_info.h"
#include "../../../game/player/player_manager.h"
#include "../../../game/zones/pile_zone_logic.h"
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "../../game/abstract_game.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../player_graphics_item.h"
#include <QAction>
@ -23,6 +16,13 @@
#include <QObject>
#include <QString>
#include <QVariant>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
#include <libcockatrice/game/zones/pile_zone_logic.h>
class QWidget;

View file

@ -2,13 +2,13 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game_scene.h"
#include "../card_menu_action_type.h"
#include "../player_graphics_item.h"
#include <QAction>
#include <libcockatrice/game/player/card_menu_action_type.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_logic.h>
MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
{
@ -30,7 +30,7 @@ MoveMenu::MoveMenu(PlayerGraphicsItem *player) : QMenu(tr("Move to"))
auto invoke = [player](CardMenuActionType type) {
return [type, player]() {
player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCards(), type);
player->getLogic()->getPlayerActions()->cardMenuAction(player->getGameScene()->selectedCardStates(), type);
};
};

View file

@ -2,10 +2,6 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/abstract_game.h"
#include "../../../game/player/player_info.h"
#include "../../../game/player/player_logic.h"
#include "../../../game/player/player_manager.h"
#include "../../../game_graphics/zones/hand_zone.h"
#include "../../../game_graphics/zones/pile_zone.h"
#include "../../../game_graphics/zones/table_zone.h"
@ -26,6 +22,10 @@
#include <QMenu>
#include <QString>
#include <libcockatrice/game/abstract_game.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/player/player_manager.h>
PlayerMenu::PlayerMenu(PlayerGraphicsItem *_player) : QObject(_player), player(_player)
{
@ -87,22 +87,24 @@ void PlayerMenu::setMenusForGraphicItems()
}
}
QMenu *PlayerMenu::updateCardMenu(const CardItem *card)
QMenu *PlayerMenu::updateCardMenu(CardState *card)
{
if (!card) {
emit cardMenuUpdated(nullptr);
return nullptr;
}
auto *cardItem = qobject_cast<CardItem *>(card->parent());
// If is spectator (as spectators don't need card menus), return
// only update the menu if the card is actually selected
if ((player->getLogic()->getGame()->getPlayerManager()->isSpectator() &&
!player->getLogic()->getGame()->getPlayerManager()->isJudge()) ||
player->getLogic()->getGame()->getActiveCard() != card) {
cardItem == nullptr || player->getGameScene()->getActiveCard() != cardItem) {
return nullptr;
}
CardMenu *menu = new CardMenu(player, card, shortcutsActive);
CardMenu *menu = new CardMenu(player, cardItem, shortcutsActive);
connect(menu, &CardMenu::cardInfoRequested, this, &PlayerMenu::cardInfoRequested);
emit cardMenuUpdated(menu);

View file

@ -14,7 +14,7 @@
#include <qtmetamacros.h>
#include <utility>
class CardItem;
class CardState;
class CardMenu;
class PlayerGraphicsItem;
class AbstractPlayerComponent;
@ -43,7 +43,7 @@ signals:
public slots:
void setMenusForGraphicItems();
QMenu *updateCardMenu(const CardItem *card);
QMenu *updateCardMenu(CardState *card);
private slots:
void refreshShortcuts();

View file

@ -2,12 +2,12 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game_scene.h"
#include "../player_graphics_item.h"
#include <QAction>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_logic.h>
PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness"))
{
@ -15,34 +15,35 @@ PtMenu::PtMenu(PlayerGraphicsItem *player) : QMenu(tr("Power / toughness"))
aIncP = new QAction(this);
connect(aIncP, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actIncP(player->getGameScene()->selectedCardStates()); });
aDecP = new QAction(this);
connect(aDecP, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actDecP(player->getGameScene()->selectedCardStates()); });
aIncT = new QAction(this);
connect(aIncT, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actIncT(player->getGameScene()->selectedCardStates()); });
aDecT = new QAction(this);
connect(aDecT, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actDecT(player->getGameScene()->selectedCardStates()); });
aIncPT = new QAction(this);
connect(aIncPT, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actIncPT(player->getGameScene()->selectedCardStates()); });
aDecPT = new QAction(this);
connect(aDecPT, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actDecPT(player->getGameScene()->selectedCardStates()); });
aFlowP = new QAction(this);
connect(aFlowP, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actFlowP(player->getGameScene()->selectedCardStates()); });
aFlowT = new QAction(this);
connect(aFlowT, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actFlowT(player->getGameScene()->selectedCardStates()); });
aSetPT = new QAction(this);
connect(aSetPT, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCards()); });
connect(aSetPT, &QAction::triggered, playerActions, [player, playerActions] {
playerActions->actRequestSetPTDialog(player->getGameScene()->selectedCardStates());
});
aResetPT = new QAction(this);
connect(aResetPT, &QAction::triggered, playerActions,
[player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCards()); });
[player, playerActions] { playerActions->actResetPT(player->getGameScene()->selectedCardStates()); });
addAction(aIncP);
addAction(aDecP);

View file

@ -1,10 +1,6 @@
#include "rfg_menu.h"
#include "../../../game/player/player_info.h"
#include "../../../game/zones/pile_zone_logic.h"
#include "../../../interface/widgets/menus/tearoff_menu.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../player_graphics_item.h"
#include <QAction>
@ -12,6 +8,10 @@
#include <QMenu>
#include <QString>
#include <QVariant>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/pile_zone_logic.h>
#include <libcockatrice/utility/zone_names.h>
class QWidget;

View file

@ -1,14 +1,14 @@
#include "say_menu.h"
#include "../../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../player_graphics_item.h"
#include <QAction>
#include <QKeySequence>
#include <QList>
#include <QString>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/settings/message_settings.h>
SayMenu::SayMenu(PlayerGraphicsItem *_player) : player(_player)

View file

@ -2,13 +2,13 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/player/player_info.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../player_graphics_item.h"
#include <QAction>
#include <QKeySequence>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
SideboardMenu::SideboardMenu(PlayerGraphicsItem *player, QMenu *playerMenu) : QMenu(playerMenu)
{

View file

@ -2,9 +2,6 @@
#include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../../game/player/player_info.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game_scene.h"
#include "../player_graphics_item.h"
#include "libcockatrice/card/card_info.h"
@ -14,6 +11,9 @@
#include <QSet>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
#include <libcockatrice/deck_list/tree/inner_deck_list_node.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu(playerMenu), player(_player)
{
@ -44,7 +44,7 @@ UtilityMenu::UtilityMenu(PlayerGraphicsItem *_player, QMenu *playerMenu) : QMenu
aIncrementAllCardCounters = new QAction(this);
connect(aIncrementAllCardCounters, &QAction::triggered, playerActions, [this]() {
player->getLogic()->getPlayerActions()->actIncrementAllCardCounters(
player->getGameScene()->selectedCards());
player->getGameScene()->selectedCardStates());
});
createPredefinedTokenMenu = new QMenu(QString());

View file

@ -2,11 +2,10 @@
#include "../../client/settings/cache_settings.h"
#include "../../client/settings/card_counter_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../interface/widgets/utility/get_text_with_max.h"
#include "../board/abstract_counter.h"
#include "../board/card_item.h"
#include "../dialogs/dlg_annotation.h"
#include "../dialogs/dlg_create_token.h"
#include "../dialogs/dlg_move_top_cards_until.h"
#include "../dialogs/dlg_roll_dice.h"
@ -17,6 +16,8 @@
#include <QInputDialog>
#include <QLineEdit>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/utility/string_limits.h>
PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions)
@ -34,7 +35,7 @@ PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_player
&PlayerDialogs::onShuffleBottomDialogRequested);
connect(playerActions, &PlayerActions::requestMulliganDialog, this, &PlayerDialogs::onMulliganDialogRequested);
connect(playerActions, &PlayerActions::startingHandSizeChosen, this, &PlayerDialogs::onStartingHandSizeChosen);
connect(playerActions, &PlayerActions::requestDrawCardsDialog, this, &PlayerDialogs::onDrawCardsDialogRequested);
connect(playerActions, &PlayerActions::requestMoveTopCardsToDialog, this,
@ -126,6 +127,11 @@ void PlayerDialogs::onMulliganDialogRequested(int startSize, int handSize, int d
}
}
void PlayerDialogs::onStartingHandSizeChosen(int number)
{
SettingsCache::instance().userInterface().setStartingHandSize(number);
}
void PlayerDialogs::onDrawCardsDialogRequested(int defaultNumberTopCards, int deckSize)
{
bool ok;
@ -196,7 +202,7 @@ void PlayerDialogs::onRollDieDialogRequested()
playerActions->actRollDie(dlg.getDieSideCount(), dlg.getDiceToRollCount());
}
void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard,
void PlayerDialogs::onCreateRelatedFromRelationDialogRequested(const CardState *sourceCard,
const CardRelation *cardRelation)
{
if (sourceCard == nullptr || cardRelation == nullptr) {
@ -251,14 +257,14 @@ void PlayerDialogs::onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopC
number -= 1; // indexes start at 0
if (ok) {
playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCards(), number);
playerActions->actMoveCardXCardsFromTop(player->getGameScene()->selectedCardStates(), number);
}
}
void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT)
{
bool ok;
auto cards = player->getGameScene()->selectedCards();
auto cards = player->getGameScene()->selectedCardStates();
emit requestDialogSemaphore(true);
QString pt = getTextWithMax(dialogParent(), tr("Change power/toughness"), tr("Change stats to:"), QLineEdit::Normal,
oldPT, &ok);
@ -273,7 +279,7 @@ void PlayerDialogs::onSetPTDialogRequested(const QString &oldPT)
void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation)
{
auto cards = player->getGameScene()->selectedCards();
auto cards = player->getGameScene()->selectedCardStates();
emit requestDialogSemaphore(true);
AnnotationDialog *dialog = new AnnotationDialog(dialogParent());
dialog->setOptions(QInputDialog::UsePlainTextEditForTextInput);
@ -291,7 +297,7 @@ void PlayerDialogs::onSetAnnotationDialogRequested(const QString &oldAnnotation)
void PlayerDialogs::onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg)
{
auto cards = player->getGameScene()->selectedCards();
auto cards = player->getGameScene()->selectedCardStates();
emit requestDialogSemaphore(true);
auto &cardCounterSettings = SettingsCache::instance().cardCounters();

View file

@ -17,6 +17,7 @@
class CardItem;
class CardRelation;
class CardState;
class PlayerActions;
class QWidget;
struct MoveTopCardsUntilOptions;
@ -52,12 +53,13 @@ public slots:
bool faceDown);
void onDrawBottomCardsDialogRequested(int defaultNumberBottomCards, int maxCards);
void onRollDieDialogRequested();
void onCreateRelatedFromRelationDialogRequested(const CardItem *sourceCard, const CardRelation *cardRelation);
void onCreateRelatedFromRelationDialogRequested(const CardState *sourceCard, const CardRelation *cardRelation);
void onCreateTokenDialogRequested(const QStringList &predefinedTokens);
void onMoveCardXCardsFromTopDialogRequested(int defaultNumberTopCardsToPlaceBelow, int deckSize);
void onSetPTDialogRequested(const QString &oldPT);
void onSetAnnotationDialogRequested(const QString &oldAnnotation);
void onSetCardCounterDialogRequested(int counterId, const QString &oldValueForDlg);
void onStartingHandSizeChosen(int number);
private:
PlayerGraphicsItem *player;

View file

@ -1,15 +1,6 @@
#include "player_graphics_item.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/board/counter_state.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/hand_zone_logic.h"
#include "../../game/zones/pile_zone_logic.h"
#include "../../game/zones/stack_zone_logic.h"
#include "../../game/zones/table_zone_logic.h"
#include "../../interface/card_picture_loader/card_picture_loader.h"
#include "../../interface/widgets/cards/art_crop_attribution.h"
#include "../../interface/widgets/cards/card_art_utils.h"
@ -45,6 +36,15 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/playmat_resolver.h>
#include <libcockatrice/game/board/counter_state.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/hand_zone_logic.h>
#include <libcockatrice/game/zones/pile_zone_logic.h>
#include <libcockatrice/game/zones/stack_zone_logic.h>
#include <libcockatrice/game/zones/table_zone_logic.h>
#include <libcockatrice/settings/interface_settings.h>
#include <qnamespace.h>
#include <qnumeric.h>
@ -177,6 +177,22 @@ void PlayerGraphicsItem::initializeZones()
void PlayerGraphicsItem::onCustomZoneAdded(QString customZoneName)
{
// Custom zones have no board view, but their cards still need a graphics owner.
// Without one, PileZone::onCardAdded never runs and cards moved from the table
// or hand into a custom zone stay drawn at their old spot. Give them a hidden
// pile zone so added cards are reparented to (0, 0) and hidden.
CardZoneLogic *logic = player->getZone(customZoneName);
if (auto *pileLogic = qobject_cast<PileZoneLogic *>(logic)) {
auto *hiddenZone = new PileZone(pileLogic, this);
hiddenZone->setVisible(false);
hiddenZone->setPos(0, 0);
zoneGraphicsItems.insert(customZoneName, hiddenZone);
// Hide any cards that arrived before the graphics item existed.
for (CardState *state : pileLogic->getCards()) {
hiddenZone->onCardAdded(state);
}
return;
}
zoneGraphicsItems.insert(customZoneName, nullptr); // Custom zone view goes here, if we ever implement it.
}

View file

@ -1,8 +1,6 @@
#include "player_target.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../interface/pixel_map_generator.h"
#include "../board/abstract_counter.h"
#include "../board/abstract_graphics_item.h"
@ -30,6 +28,8 @@
#include <QTransform>
#include <QtGlobal>
#include <QtMath>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
#include <qnamespace.h>
#include <qnumeric.h>

View file

@ -16,7 +16,7 @@ static int sumPowers(const QList<CardItem *> &cards)
// calculate total power;
int total = 0;
for (auto card : cards) {
QVariantList parsed = CardItem::parsePT(card->getPT());
QVariantList parsed = CardState::parsePT(card->getPT());
if (!parsed.isEmpty()) {
int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
total += qMax(power, 0);
@ -44,7 +44,7 @@ static int sumToughness(const QList<CardItem *> &cards)
{
int total = 0;
for (auto card : cards) {
QVariantList parsed = CardItem::parsePT(card->getPT());
QVariantList parsed = CardState::parsePT(card->getPT());
if (parsed.size() == 2) {
int toughness = parsed.at(1).toInt(); // toInt will default to 0 if it's not an int
total += qMax(toughness, 0);

View file

@ -1,13 +1,13 @@
#include "card_zone.h"
#include "../../game/board/card_list.h"
#include "../../game/zones/card_zone_logic.h"
#include "../board/abstract_graphics_item.h"
#include "../board/card_item.h"
#include <QAction>
#include <QGraphicsSceneMouseEvent>
#include <QMenu>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <qnamespace.h>
class QGraphicsItem;
@ -18,23 +18,60 @@ CardZone::CardZone(CardZoneLogic *_logic, QGraphicsItem *parent)
: AbstractGraphicsItem(parent), menu(nullptr), doubleClickAction(0), logic(_logic)
{
connect(logic, &CardZoneLogic::retranslateUi, this, &CardZone::retranslateUi);
// Invalidation is connected before any consumer so a handler that reads cardItems()
// during the same signal emission always sees a stale flag and rebuilds.
connect(logic, &CardZoneLogic::cardAdded, this, &CardZone::invalidateItems);
connect(logic, &CardZoneLogic::cardCountChanged, this, &CardZone::invalidateItems);
connect(logic, &CardZoneLogic::reorganizeCards, this, &CardZone::invalidateItems);
connect(logic, &CardZoneLogic::cardAdded, this, &CardZone::onCardAdded);
connect(logic, &CardZoneLogic::createCardRequested, this, &CardZone::onCreateCardRequested);
connect(logic, &CardZoneLogic::setGraphicsVisibility, this, [this](bool v) { this->setVisible(v); });
connect(logic, &CardZoneLogic::updateGraphics, this, [this]() { update(); });
connect(logic, &CardZoneLogic::reorganizeCards, this, &CardZone::reorganizeCards);
}
void CardZone::onCardAdded(CardItem *addedCard)
void CardZone::invalidateItems()
{
addedCard->setParentItem(this);
addedCard->setVisible(true);
addedCard->update();
itemsDirty = true;
}
const QList<CardItem *> &CardZone::cardItems() const
{
if (itemsDirty) {
items.clear();
for (auto *cardState : getLogic()->getCards()) {
if (auto *cardItem = qobject_cast<CardItem *>(cardState->parent())) {
items.append(cardItem);
}
}
itemsDirty = false;
}
return items;
}
void CardZone::onCardAdded(CardState *addedCard)
{
auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast<CardItem *>(addedCard->parent());
if (addedItem == nullptr) {
return;
}
addedItem->setParentItem(this);
addedItem->setVisible(true);
addedItem->update();
}
void CardZone::onCreateCardRequested(const ServerInfo_Card &cardInfo, bool reorganize)
{
CardRef cardRef = {QString::fromStdString(cardInfo.name()), QString::fromStdString(cardInfo.provider_id())};
auto *card = new CardItem(getLogic()->getPlayer(), nullptr, cardRef, cardInfo.id());
card->processCardInfo(cardInfo);
getLogic()->addCard(card->getState(), reorganize, cardInfo.x(), cardInfo.y());
}
void CardZone::retranslateUi()
{
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
getLogic()->getCards()[i]->retranslateUi();
for (auto *card : cardItems()) {
card->retranslateUi();
}
}

View file

@ -16,13 +16,17 @@
class CardDragItem;
class CardItem;
class CardState;
class CardZoneLogic;
class QAction;
class QGraphicsItem;
class QMenu;
class QPoint;
class ServerInfo_Card;
template <typename T> class QList;
class CardItem;
inline Q_LOGGING_CATEGORY(CardZoneLog, "card_zone");
/**
@ -55,7 +59,9 @@ public slots:
* Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal
* connection in CardZone's constructor dispatches through the vtable.
*/
virtual void onCardAdded(CardItem *addedCard);
virtual void onCardAdded(CardState *addedCard);
/** @brief Builds the card item for @p cardInfo and hands it to this zone's logic. */
void onCreateCardRequested(const ServerInfo_Card &cardInfo, bool reorganize);
public:
enum
@ -76,6 +82,15 @@ public:
return logic;
}
/**
* @brief The CardItems backing getLogic()->getCards(), in zone order.
*
* Game logic talks about cards as states; this is the graphics side's view of the same list.
* It is rebuilt on demand after any content change, so views never reach into the logic's
* container themselves and never observe a stale one.
*/
[[nodiscard]] const QList<CardItem *> &cardItems() const;
void setMenu(QMenu *_menu, QAction *_doubleClickAction = 0)
{
menu = _menu;
@ -83,7 +98,12 @@ public:
}
private:
/** @brief Marks the cached card items stale. Connected to every content-changing signal. */
void invalidateItems();
CardZoneLogic *logic;
mutable QList<CardItem *> items;
mutable bool itemsDirty = true;
};
#endif

View file

@ -1,11 +1,6 @@
#include "hand_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/hand_zone_logic.h"
#include "../../interface/theme_manager.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
@ -18,7 +13,11 @@
#include <QPointF>
#include <QRectF>
#include <QString>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/hand_zone_logic.h>
#include <libcockatrice/settings/interface_settings.h>
#include <string>
@ -48,8 +47,8 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
QPoint point = dropPoint + scenePos().toPoint();
int x = -1;
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
for (x = 0; x < getLogic()->getCards().size(); x++) {
if (point.x() < static_cast<CardItem *>(getLogic()->getCards().at(x))->scenePos().x()) {
for (x = 0; x < cardItems().size(); x++) {
if (point.x() < cardItems().at(x)->scenePos().x()) {
break;
}
}
@ -58,19 +57,13 @@ void HandZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
x = calcDropIndexFromY(dropPoint.y(), !sameZone);
}
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
cmd.set_x(x);
cmd.set_y(-1);
for (int i = 0; i < dragItems.size(); ++i) {
cmd.mutable_cards_to_move()->add_card()->set_card_id(dragItems[i]->getId());
QList<CardMoveRequest> cards;
cards.reserve(dragItems.size());
for (const CardDragItem *item : dragItems) {
cards.append(CardMoveRequest{item->getId()});
}
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), x, -1, cards);
}
QRectF HandZone::boundingRect() const
@ -90,23 +83,23 @@ void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio
void HandZone::reorganizeCards()
{
if (!getLogic()->getCards().isEmpty()) {
const int cardCount = getLogic()->getCards().size();
if (!cardItems().isEmpty()) {
const int cardCount = cardItems().size();
if (SettingsCache::instance().userInterface().getHorizontalHand()) {
bool leftJustified = SettingsCache::instance().userInterface().getLeftJustified();
qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width();
qreal cardWidth = cardItems().at(0)->boundingRect().width();
const int xPadding = leftJustified ? cardWidth * 1.4 : 5;
qreal totalWidth =
leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding;
if (cardCount == 1) {
CardItem *c = getLogic()->getCards().at(0);
CardItem *c = cardItems().at(0);
qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2;
c->setPos(xPosition, 5);
c->setRealZValue(0);
} else {
for (int i = 0; i < cardCount; i++) {
CardItem *c = getLogic()->getCards().at(i);
CardItem *c = cardItems().at(i);
// If the total width of the cards is smaller than the available width,
// the cards do not need to overlap and are displayed in the center of the area.
if (cardWidth * cardCount > totalWidth) {
@ -131,7 +124,7 @@ void HandZone::reorganizeCards()
void HandZone::sortHand(const QList<CardList::SortOption> &options)
{
if (getLogic()->getCards().isEmpty()) {
if (cardItems().isEmpty()) {
return;
}

View file

@ -7,10 +7,10 @@
#ifndef HANDZONE_H
#define HANDZONE_H
#include "../../game/board/card_list.h"
#include "select_zone.h"
#include <QtGlobal>
#include <libcockatrice/game/board/card_list.h>
#include <qtmetamacros.h>
class HandZoneLogic;

View file

@ -1,12 +1,6 @@
#include "pile_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/board/card_list.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/pile_zone_logic.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
#include "../card_dimensions.h"
@ -24,7 +18,12 @@
#include <QString>
#include <QTransform>
#include <QtGlobal>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/pile_zone_logic.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <qnamespace.h>
#include <string>
@ -67,36 +66,35 @@ void PileZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*optio
{
painter->drawPath(shape());
if (!getLogic()->getCards().isEmpty()) {
getLogic()->getCards().at(0)->paintPicture(painter, getLogic()->getCards().at(0)->getTranslatedSize(painter),
90);
if (!cardItems().isEmpty()) {
cardItems().at(0)->paintPicture(painter, cardItems().at(0)->getTranslatedSize(painter), 90);
}
painter->translate(CardDimensions::WIDTH_HALF_F, CardDimensions::HEIGHT_HALF_F);
painter->rotate(-90);
painter->translate(-CardDimensions::WIDTH_HALF_F, -CardDimensions::HEIGHT_HALF_F);
paintNumberEllipse(getLogic()->getCards().size(), 28, Qt::white, -1, -1, painter);
paintNumberEllipse(cardItems().size(), 28, Qt::white, -1, -1, painter);
}
void PileZone::handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &)
{
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
cmd.set_x(0);
cmd.set_y(0);
for (int i = 0; i < dragItems.size(); ++i) {
auto cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(dragItems[i]->getId());
if (dragItems[i]->isForceFaceDown()) {
cardToMove->set_face_down(true);
}
QList<CardMoveRequest> cards;
cards.reserve(dragItems.size());
for (const CardDragItem *item : dragItems) {
cards.append(CardMoveRequest{item->getId(), item->isForceFaceDown()});
}
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), 0, 0, cards);
}
void PileZone::onCardAdded(CardState *addedCard)
{
CardZone::onCardAdded(addedCard);
auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast<CardItem *>(addedCard->parent());
if (addedItem) {
addedItem->setPos(0, 0);
addedItem->setVisible(false);
}
}
void PileZone::reorganizeCards()
@ -126,15 +124,14 @@ void PileZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
return;
}
if (getLogic()->getCards().isEmpty()) {
if (cardItems().isEmpty()) {
return;
}
bool forceFaceDown = event->modifiers().testFlag(Qt::ShiftModifier);
bool bottomCard = event->modifiers().testFlag(Qt::ControlModifier);
CardItem *card = bottomCard ? getLogic()->getCards().last() : getLogic()->getCards().first();
const int cardid =
getLogic()->contentsKnown() ? card->getId() : (bottomCard ? getLogic()->getCards().size() - 1 : 0);
CardItem *card = bottomCard ? cardItems().last() : cardItems().first();
const int cardid = getLogic()->contentsKnown() ? card->getId() : (bottomCard ? cardItems().size() - 1 : 0);
CardDragItem *drag = card->createDragItem(cardid, event->pos(), event->scenePos(), forceFaceDown);
drag->grabMouse();
setCursor(Qt::OpenHandCursor);
@ -147,8 +144,8 @@ void PileZone::mouseReleaseEvent(QGraphicsSceneMouseEvent * /*event*/)
void PileZone::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
if (!getLogic()->getCards().isEmpty()) {
getLogic()->getCards()[0]->processHoverEvent();
if (!cardItems().isEmpty()) {
cardItems()[0]->processHoverEvent();
}
QGraphicsItem::hoverEnterEvent(event);
}

View file

@ -33,6 +33,7 @@ public:
[[nodiscard]] QPainterPath shape() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
void reorganizeCards() override;
void onCardAdded(CardState *addedCard) override;
void
handleDropEvent(const QList<CardDragItem *> &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override;

View file

@ -1,8 +1,6 @@
#include "select_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/board/card_list.h"
#include "../../game/zones/card_zone_logic.h"
#include "../board/card_item.h"
#include "../game_scene.h"
@ -17,6 +15,8 @@
#include <QTransform>
#include <QtGlobal>
#include <QtMath>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <qnamespace.h>
#include <qnumeric.h>
@ -85,7 +85,7 @@ SelectZone *SelectZone::findOwningSelectZone(const QGraphicsItem *card)
SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const
{
const auto &cards = getLogic()->getCards();
const auto &cards = cardItems();
if (cards.isEmpty()) {
return {0, boundingRect().height(), 0.0, 0.0, minOffset};
}
@ -97,7 +97,7 @@ SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) cons
int SelectZone::calcDropIndexFromY(qreal dropY, bool allowCountExpand, qreal minOffset) const
{
const auto &cards = getLogic()->getCards();
const auto &cards = cardItems();
if (cards.isEmpty()) {
return 0;
}
@ -115,7 +115,7 @@ void SelectZone::restoreStaleEscapedCards()
if (!cardClipContainer) {
return;
}
for (auto *card : getLogic()->getCards()) {
for (auto *card : cardItems()) {
// A card parented to the zone (instead of the clip container) should
// only occur while it is actively hovered. If hover cleanup was
// missed, reparent it back so clipping resumes.
@ -127,7 +127,7 @@ void SelectZone::restoreStaleEscapedCards()
void SelectZone::layoutCardsVertically(const StackLayoutParams &params)
{
const auto &cards = getLogic()->getCards();
const auto &cards = cardItems();
if (cards.isEmpty() || params.cardCount <= 0) {
return;
}
@ -162,9 +162,9 @@ SelectZone::~SelectZone()
if (cardClipContainer) {
// Reparent any hover-escaped cards back to the clip container so Qt's
// parent-child tree is consistent for destruction. setParentItem() does
// not invalidate getLogic()->getCards() (it modifies the graphics tree,
// not the zone's logical card list).
for (auto *card : getLogic()->getCards()) {
// not invalidate cardItems() — it only moves the item in the graphics
// tree, it never changes the zone's contents.
for (auto *card : cardItems()) {
if (card && card->parentItem() == this) {
card->setParentItem(cardClipContainer);
}
@ -172,12 +172,13 @@ SelectZone::~SelectZone()
}
}
void SelectZone::onCardAdded(CardItem *addedCard)
void SelectZone::onCardAdded(CardState *addedCard)
{
if (cardClipContainer && addedCard) {
addedCard->setParentItem(cardClipContainer);
addedCard->setVisible(true);
addedCard->update();
auto *addedItem = addedCard == nullptr ? nullptr : qobject_cast<CardItem *>(addedCard->parent());
if (cardClipContainer && addedItem) {
addedItem->setParentItem(cardClipContainer);
addedItem->setVisible(true);
addedItem->update();
} else {
CardZone::onCardAdded(addedCard);
}
@ -247,7 +248,7 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
}
QRectF selectionRect = QRectF(selectionOrigin, pos).normalized();
for (auto card : getLogic()->getCards()) {
for (auto card : cardItems()) {
if (card->getAttachedTo() && card->getAttachedTo()->getZone() != getLogic()) {
continue;
}

View file

@ -35,7 +35,7 @@ public:
SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr);
~SelectZone() override;
void onCardAdded(CardItem *addedCard) override;
void onCardAdded(CardState *addedCard) override;
/**
* @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip

View file

@ -1,11 +1,5 @@
#include "stack_zone.h"
#include "../../game/board/card_list.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/stack_zone_logic.h"
#include "../../interface/theme_manager.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
@ -19,7 +13,12 @@
#include <QPoint>
#include <QRectF>
#include <QString>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/stack_zone_logic.h>
#include <qnumeric.h>
#include <string>
@ -73,31 +72,21 @@ void StackZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
int index = calcDropIndexFromY(dropPoint.y(), !sameZone, MIN_CARD_VISIBLE);
if (sameZone) {
// Same-zone no-op: don't move a card onto itself
const auto &cards = getLogic()->getCards();
const auto &cards = cardItems();
if (!cards.isEmpty() && cards.at(index)->getId() == dragItems.at(0)->getId()) {
return;
}
}
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
cmd.set_x(index);
cmd.set_y(0);
QList<CardMoveRequest> cards;
cards.reserve(dragItems.size());
for (const CardDragItem *item : dragItems) {
if (item) {
auto *cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(item->getId());
if (item->isForceFaceDown()) {
cardToMove->set_face_down(true);
}
cards.append(CardMoveRequest{item->getId(), item->isForceFaceDown()});
}
}
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), index, 0, cards);
}
void StackZone::setHeight(qreal newHeight)
@ -113,7 +102,7 @@ void StackZone::setHeight(qreal newHeight)
void StackZone::reorganizeCards()
{
if (!getLogic()->getCards().isEmpty()) {
if (!cardItems().isEmpty()) {
const auto params = buildStackParams(MIN_CARD_VISIBLE);
layoutCardsVertically(params);
}

View file

@ -1,12 +1,6 @@
#include "table_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/board/card_list.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/table_zone_logic.h"
#include "../../interface/theme_manager.h"
#include "../board/abstract_card_item.h"
#include "../board/card_drag_item.h"
@ -32,8 +26,12 @@
#include <algorithm>
#include <iterator>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/protocol/pb/command_set_card_attr.pb.h>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/table_zone_logic.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/zone_names.h>
#include <string>
@ -62,7 +60,8 @@ TableZone::TableZone(TableZoneLogic *_logic, bool _mirrored, QGraphicsItem *pare
updateBg();
height = MARGIN_TOP + MARGIN_BOTTOM + TABLEROWS * CardDimensions::HEIGHT + (TABLEROWS - 1) * PADDING_Y;
height = MARGIN_TOP + MARGIN_BOTTOM + TableZoneLogic::ROW_COUNT * CardDimensions::HEIGHT +
(TableZoneLogic::ROW_COUNT - 1) * PADDING_Y;
width = MIN_WIDTH;
currentMinimumWidth = width;
@ -208,29 +207,20 @@ void TableZone::handleDropEventByGrid(const QList<CardDragItem *> &dragItems,
CardZoneLogic *startZone,
const QPoint &gridPoint)
{
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
cmd.set_x(gridPoint.x());
cmd.set_y(gridPoint.y());
for (const auto &item : dragItems) {
CardToMove *ctm = cmd.mutable_cards_to_move()->add_card();
ctm->set_card_id(item->getId());
if (item->isForceFaceDown()) {
ctm->set_face_down(true);
}
QList<CardMoveRequest> cards;
cards.reserve(dragItems.size());
for (const CardDragItem *item : dragItems) {
CardMoveRequest card{item->getId(), item->isForceFaceDown()};
if (startZone->getName() != getLogic()->getName() && !item->isForceFaceDown()) {
const auto &card = item->getItem()->getCard();
if (card) {
ctm->set_pt(card.getInfo().getPowTough().toStdString());
const auto &cardInfo = item->getItem()->getCard();
if (cardInfo) {
card.pt = cardInfo.getInfo().getPowTough();
}
}
cards.append(card);
}
startZone->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
startZone->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), gridPoint.x(), gridPoint.y(), cards);
}
void TableZone::reorganizeCards()
@ -238,8 +228,8 @@ void TableZone::reorganizeCards()
// Calculate card stack widths so mapping functions work properly
computeCardStackWidths();
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
QPoint gridPoint = getLogic()->getCards()[i]->getGridPos();
for (int i = 0; i < cardItems().size(); ++i) {
QPoint gridPoint = cardItems()[i]->getGridPos();
if (gridPoint.x() == -1) {
continue;
}
@ -248,21 +238,24 @@ void TableZone::reorganizeCards()
qreal x = mapPoint.x();
qreal y = mapPoint.y();
int numberAttachedCards = getLogic()->getCards()[i]->getAttachedCards().size();
int numberAttachedCards = cardItems()[i]->getAttachedCards().size();
qreal actualX = x + numberAttachedCards * STACKED_CARD_OFFSET_X;
qreal actualY = y;
if (numberAttachedCards) {
actualY += 15;
}
getLogic()->getCards()[i]->setPos(actualX, actualY);
getLogic()->getCards()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY));
cardItems()[i]->setPos(actualX, actualY);
cardItems()[i]->setRealZValue(ZValues::tableCardZValue(actualX, actualY));
QListIterator<CardItem *> attachedCardIterator(getLogic()->getCards()[i]->getAttachedCards());
QListIterator<CardState *> attachedCardIterator(cardItems()[i]->getAttachedCards());
int j = 0;
while (attachedCardIterator.hasNext()) {
++j;
CardItem *attachedCard = attachedCardIterator.next();
CardItem *attachedCard = qobject_cast<CardItem *>(attachedCardIterator.next()->parent());
if (attachedCard == nullptr) {
continue;
}
qreal childX = actualX - j * STACKED_CARD_OFFSET_X;
qreal childY = y + 5;
attachedCard->setPos(childX, childY);
@ -276,35 +269,18 @@ void TableZone::reorganizeCards()
void TableZone::toggleTapped()
{
QList<QGraphicsItem *> selectedItemsRaw = scene()->selectedItems();
QList<QGraphicsItem *> selectedItems;
auto isCardOnTable = [](const QGraphicsItem *item) {
if (auto card = qgraphicsitem_cast<const CardItem *>(item)) {
return card->getZone()->getName() == ZoneNames::TABLE;
}
return false;
};
std::copy_if(selectedItemsRaw.begin(), selectedItemsRaw.end(), std::back_inserter(selectedItems), isCardOnTable);
bool tapAll = std::any_of(selectedItems.begin(), selectedItems.end(), [](const QGraphicsItem *item) {
return !qgraphicsitem_cast<const CardItem *>(item)->getTapped();
});
QList<const ::google::protobuf::Message *> cmdList;
for (const auto &selectedItem : selectedItems) {
CardItem *temp = qgraphicsitem_cast<CardItem *>(selectedItem);
if (temp->getTapped() != tapAll) {
Command_SetCardAttr *cmd = new Command_SetCardAttr;
cmd->set_zone(getLogic()->getName().toStdString());
cmd->set_card_id(temp->getId());
cmd->set_attribute(AttrTapped);
cmd->set_attr_value(tapAll ? "1" : "0");
cmdList.append(cmd);
QList<CardState *> selectedCards;
for (auto *item : scene()->selectedItems()) {
auto *card = qgraphicsitem_cast<CardItem *>(item);
if (card && card->getZone()->getName() == ZoneNames::TABLE) {
selectedCards.append(card->getState());
}
}
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(
getLogic()->getPlayer()->getPlayerActions()->prepareGameCommand(cmdList));
const bool tapAll = std::any_of(selectedCards.begin(), selectedCards.end(),
[](const CardState *card) { return !card->getTapped(); });
getLogic()->getPlayer()->getPlayerActions()->actToggleTapped(getLogic()->getName(), selectedCards, tapAll);
}
void TableZone::resizeToContents()
@ -312,9 +288,9 @@ void TableZone::resizeToContents()
int xMax = 0;
// Find rightmost card position, which includes the left margin amount.
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
if (getLogic()->getCards()[i]->pos().x() > xMax) {
xMax = (int)getLogic()->getCards()[i]->pos().x();
for (int i = 0; i < cardItems().size(); ++i) {
if (cardItems()[i]->pos().x() > xMax) {
xMax = (int)cardItems()[i]->pos().x();
}
}
@ -335,9 +311,9 @@ void TableZone::resizeToContents()
CardItem *TableZone::getCardFromGrid(const QPoint &gridPoint) const
{
for (int i = 0; i < getLogic()->getCards().size(); i++) {
if (getLogic()->getCards().at(i)->getGridPoint() == gridPoint) {
return getLogic()->getCards().at(i);
for (int i = 0; i < cardItems().size(); i++) {
if (cardItems().at(i)->getGridPoint() == gridPoint) {
return cardItems().at(i);
}
}
return 0;
@ -354,8 +330,8 @@ void TableZone::computeCardStackWidths()
// Each card stack is three grid points worth of card locations.
// First pass: compute the number of cards at each card stack.
QMap<int, int> cardStackCount;
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos();
for (int i = 0; i < cardItems().size(); ++i) {
const QPoint &gridPoint = cardItems()[i]->getGridPos();
if (gridPoint.x() == -1) {
continue;
}
@ -366,8 +342,8 @@ void TableZone::computeCardStackWidths()
// Second pass: compute the width at each card stack.
cardStackWidth.clear();
for (int i = 0; i < getLogic()->getCards().size(); ++i) {
const QPoint &gridPoint = getLogic()->getCards()[i]->getGridPos();
for (int i = 0; i < cardItems().size(); ++i) {
const QPoint &gridPoint = cardItems()[i]->getGridPos();
if (gridPoint.x() == -1) {
continue;
}
@ -375,8 +351,8 @@ void TableZone::computeCardStackWidths()
const int key = getCardStackMapKey(gridPoint.x() / 3, gridPoint.y());
const int stackCount = cardStackCount.value(key, 0);
if (stackCount == 1) {
cardStackWidth.insert(key, CardDimensions::WIDTH + getLogic()->getCards()[i]->getAttachedCards().size() *
STACKED_CARD_OFFSET_X);
cardStackWidth.insert(key, CardDimensions::WIDTH +
cardItems()[i]->getAttachedCards().size() * STACKED_CARD_OFFSET_X);
} else {
cardStackWidth.insert(key, CardDimensions::WIDTH + (stackCount - 1) * STACKED_CARD_OFFSET_X);
}
@ -397,7 +373,7 @@ QPointF TableZone::mapFromGrid(QPoint gridPoint) const
}
if (isInverted()) {
gridPoint.setY(TABLEROWS - 1 - gridPoint.y());
gridPoint.setY(TableZoneLogic::ROW_COUNT - 1 - gridPoint.y());
}
// Start with margin plus stacked card offset
@ -423,10 +399,10 @@ QPoint TableZone::mapToGrid(const QPointF &mapPoint) const
const int gridPointHeight = CardDimensions::HEIGHT + PADDING_Y;
int gridPointY = (y + PADDING_Y / 2) / gridPointHeight;
gridPointY = clampValidTableRow(gridPointY);
gridPointY = TableZoneLogic::clampValidTableRow(gridPointY);
if (isInverted()) {
gridPointY = TABLEROWS - 1 - gridPointY;
gridPointY = TableZoneLogic::ROW_COUNT - 1 - gridPointY;
}
// Calculating the x-coordinate of the grid space requires adding up the
@ -471,22 +447,3 @@ QPointF TableZone::closestGridPoint(const QPointF &point)
}
return mapFromGrid(gridPoint);
}
int TableZone::clampValidTableRow(const int row)
{
if (row < 0) {
return 0;
}
if (row >= TABLEROWS) {
return TABLEROWS - 1;
}
return row;
}
int TableZone::tableRowToGridY(int tableRow)
{
if (tableRow > 2) {
tableRow = 1;
}
return clampValidTableRow(2 - tableRow);
}

View file

@ -17,6 +17,7 @@
#include <QPoint>
#include <QPointF>
#include <QtGlobal>
#include <libcockatrice/game/zones/table_zone_logic.h>
#include <qtmetamacros.h>
class CardDragItem;
@ -25,7 +26,6 @@ class CardZoneLogic;
class QColor;
class QGraphicsItem;
class QPainter;
class TableZoneLogic;
template <typename T> class QList;
/**
@ -42,8 +42,6 @@ signals:
void sizeChanged();
private:
static const int TABLEROWS = 3;
/*
Margins between table edges and cards, paddings between cards
*/
@ -181,15 +179,6 @@ public:
QPointF closestGridPoint(const QPointF &point) override;
static int clampValidTableRow(const int row);
/**
* Converts a card's logical table row (0=creatures, 1=noncreatures, 2=lands)
* to the corresponding grid Y coordinate. Cards with tableRow > 2 (e.g.,
* instants/sorceries) default to the noncreatures row.
*/
static int tableRowToGridY(int tableRow);
/**
Resizes the TableZone in case CardItems are within or
outside of the TableZone constraints.

View file

@ -1,11 +1,6 @@
#include "view_zone.h"
#include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_info.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/view_zone_logic.h"
#include "../board/card_drag_item.h"
#include "../board/card_item.h"
#include "../card_dimensions.h"
@ -26,8 +21,11 @@
#include <QtMath>
#include <functional>
#include <libcockatrice/card/card_localization.h>
#include <libcockatrice/protocol/pb/command_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/command_move_card.pb.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/view_zone_logic.h>
#include <libcockatrice/protocol/pb/response_dump_zone.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_card.pb.h>
#include <libcockatrice/protocol/pending_command.h>
@ -42,21 +40,23 @@ class QGraphicsItem;
ZoneViewZone::ZoneViewZone(ZoneViewZoneLogic *_logic, QGraphicsItem *parent)
: SelectZone(_logic, parent), bRect(QRectF()), minRows(0), groupBy(CardList::NoSort), sortBy(CardList::NoSort)
{
if (!(qobject_cast<ZoneViewZoneLogic *>(getLogic())->getRevealZone() &&
!qobject_cast<ZoneViewZoneLogic *>(getLogic())->getWriteableRevealZone())) {
qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getViews().append(this);
auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(getLogic());
if (!(viewLogic->getRevealZone() && !viewLogic->getWriteableRevealZone())) {
viewLogic->getOriginalZone()->getViews().append(viewLogic);
}
connect(_logic, &ZoneViewZoneLogic::closeView, this, &ZoneViewZone::close);
}
void ZoneViewZone::addToViews()
{
qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getViews().append(this);
auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(getLogic());
viewLogic->getOriginalZone()->getViews().append(viewLogic);
}
void ZoneViewZone::removeFromViews()
{
qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getViews().removeOne(this);
auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(getLogic());
viewLogic->getOriginalZone()->getViews().removeOne(viewLogic);
}
/**
@ -66,9 +66,9 @@ void ZoneViewZone::removeFromViews()
void ZoneViewZone::close()
{
emit closed();
if (!(qobject_cast<ZoneViewZoneLogic *>(getLogic())->getRevealZone() &&
!qobject_cast<ZoneViewZoneLogic *>(getLogic())->getWriteableRevealZone())) {
qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getViews().removeOne(this);
auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(getLogic());
if (!(viewLogic->getRevealZone() && !viewLogic->getWriteableRevealZone())) {
viewLogic->getOriginalZone()->getViews().removeOne(viewLogic);
}
deleteLater();
}
@ -95,28 +95,27 @@ void ZoneViewZone::initializeCards(const QList<const ServerInfo_Card *> &cardLis
auto copy = new CardItem(getLogic()->getPlayer(), this, cardRef, card->id());
copy->setFaceDown(card->face_down());
getLogic()->addCard(copy, false, i);
getLogic()->addCard(copy->getState(), false, i);
}
reorganizeCards();
} else if (!qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->contentsKnown()) {
Command_DumpZone cmd;
cmd.set_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_zone_name(getLogic()->getName().toStdString());
cmd.set_number_cards(numberCards);
cmd.set_is_reversed(qobject_cast<ZoneViewZoneLogic *>(getLogic())->getIsReversed());
PendingCommand *pend = getLogic()->getPlayer()->getPlayerActions()->prepareGameCommand(cmd);
connect(pend, &PendingCommand::finished, this, &ZoneViewZone::zoneDumpReceived);
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(pend);
auto *viewLogic = qobject_cast<ZoneViewZoneLogic *>(getLogic());
PendingCommand *pend = viewLogic->prepareZoneDump();
if (pend != nullptr) {
// A local game answers synchronously, so the handler has to be connected before the
// command is sent or the response would be delivered to nobody.
connect(pend, &PendingCommand::finished, this, &ZoneViewZone::zoneDumpReceived);
viewLogic->sendZoneDump(pend);
}
} else {
const CardList &c = qobject_cast<ZoneViewZoneLogic *>(getLogic())->getOriginalZone()->getCards();
int number = numberCards == -1 ? c.size() : (numberCards < c.size() ? numberCards : c.size());
for (int i = 0; i < number; i++) {
CardItem *card = c.at(i);
CardState *card = c.at(i);
auto copy = new CardItem(getLogic()->getPlayer(), this, card->getCardRef(), card->getId());
copy->setFaceDown(card->getFaceDown());
getLogic()->addCard(copy, false, i);
getLogic()->addCard(copy->getState(), false, i);
}
reorganizeCards();
}
@ -132,7 +131,7 @@ void ZoneViewZone::zoneDumpReceived(const Response &r)
auto cardProviderId = QString::fromStdString(cardInfo.provider_id());
auto card = new CardItem(getLogic()->getPlayer(), this, {cardName, cardProviderId}, cardInfo.id(), getLogic());
card->setFaceDown(cardInfo.face_down());
getLogic()->rawInsertCard(card, i);
getLogic()->rawInsertCard(card->getState(), i);
}
qobject_cast<ZoneViewZoneLogic *>(getLogic())->updateCardIds(ZoneViewZoneLogic::INITIALIZE);
@ -146,11 +145,11 @@ void ZoneViewZone::zoneDumpReceived(const Response &r)
void ZoneViewZone::reorganizeCards()
{
// filter cards
CardList cardsToDisplay = CardList(getLogic()->getCards().getContentsKnown());
for (auto card : getLogic()->getCards()) {
CardList cardsToDisplay = CardList(getLogic()->contentsKnown());
for (auto card : cardItems()) {
if (filterString.check(card->getCard().getCardPtr())) {
card->show();
cardsToDisplay.append(card);
cardsToDisplay.append(card->getState());
} else {
card->hide();
}
@ -218,7 +217,8 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca
const auto extractor = CardList::getExtractorFor(pileOption);
for (int i = 0; i < cardCount; i++) {
CardItem *c = cards.at(i);
CardState *c = cards.at(i);
CardItem *cItem = qobject_cast<CardItem *>(c->parent());
QString columnProp = extractor(c);
if (i) { // if not the first card
@ -233,8 +233,8 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca
lastColumnProp = columnProp;
qreal x = col * CardDimensions::WIDTH_F;
qreal y = row * CardDimensions::HEIGHT_F / 3;
c->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y);
c->setRealZValue(i);
cItem->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y);
cItem->setRealZValue(i);
longestRow = qMax(row, longestRow);
}
@ -259,11 +259,11 @@ ZoneViewZone::GridSize ZoneViewZone::positionCardsForDisplay(CardList &cards, Ca
qCDebug(ViewZoneLog) << "reorganizeCards: rows=" << rows << "cols=" << cols;
for (int i = 0; i < cardCount; i++) {
CardItem *c = cards.at(i);
auto *cItem = qobject_cast<CardItem *>(cards.at(i)->parent());
qreal x = (i / rows) * CardDimensions::WIDTH_F;
qreal y = (i % rows) * CardDimensions::HEIGHT_F / 3;
c->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y);
c->setRealZValue(i);
cItem->setPos(HORIZONTAL_PADDING + x, VERTICAL_PADDING + y);
cItem->setRealZValue(i);
}
return GridSize{rows, qMax(cols, 1)};
@ -301,24 +301,13 @@ void ZoneViewZone::handleDropEvent(const QList<CardDragItem *> &dragItems,
CardZoneLogic *startZone,
const QPoint & /*dropPoint*/)
{
Command_MoveCard cmd;
cmd.set_start_player_id(startZone->getPlayer()->getPlayerInfo()->getId());
cmd.set_start_zone(startZone->getName().toStdString());
cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId());
cmd.set_target_zone(getLogic()->getName().toStdString());
cmd.set_x(0);
cmd.set_y(0);
cmd.set_is_reversed(qobject_cast<ZoneViewZoneLogic *>(getLogic())->getIsReversed());
QList<CardMoveRequest> cards;
cards.reserve(dragItems.size());
for (int i = 0; i < dragItems.size(); ++i) {
auto cardToMove = cmd.mutable_cards_to_move()->add_card();
cardToMove->set_card_id(dragItems[i]->getId());
if (dragItems[i]->isForceFaceDown()) {
cardToMove->set_face_down(true);
}
cards.append(CardMoveRequest{dragItems[i]->getId(), dragItems[i]->isForceFaceDown()});
}
getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd);
getLogic()->getPlayer()->getPlayerActions()->moveCards(startZone, getLogic(), 0, 0, cards);
}
void ZoneViewZone::setGeometry(const QRectF &rect)

View file

@ -7,7 +7,6 @@
#ifndef ZONEVIEWERZONE_H
#define ZONEVIEWERZONE_H
#include "../../game/board/card_list.h"
#include "select_zone.h"
#include <QGraphicsLayoutItem>
@ -17,6 +16,7 @@
#include <QSizeF>
#include <QString>
#include <libcockatrice/filters/filter_string.h>
#include <libcockatrice/game/board/card_list.h>
#include <qnamespace.h>
#include <qtmetamacros.h>

View file

@ -1,17 +1,12 @@
#include "view_zone_widget.h"
#include "../../client/settings/cache_settings.h"
#include "../../client/translation.h"
#include "../../filters/syntax_help.h"
#include "../../game/board/card_list.h"
#include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h"
#include "../../game/zones/card_zone_logic.h"
#include "../../game/zones/view_zone_logic.h"
#include "../../interface/pixel_map_generator.h"
#include "../card_dimensions.h"
#include "../z_values.h"
#include "libcockatrice/utility/macros.h"
#include "libcockatrice/utility/qt_utils.h"
#include "view_zone.h"
#include <QAction>
@ -44,7 +39,12 @@
#include <QVariant>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/protocol/pb/command_shuffle.pb.h>
#include <libcockatrice/game/board/card_list.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/game/translation.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/view_zone_logic.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <qnamespace.h>
@ -177,9 +177,10 @@ ZoneViewWidget::ZoneViewWidget(PlayerLogic *_player,
vbox->addItem(zoneHBox);
zone = new ZoneViewZone(new ZoneViewZoneLogic(player, _origZone, numberCards, _revealZone, _writeableRevealZone,
_isReversed, zoneContainer),
zoneContainer);
zone =
new ZoneViewZone(new ZoneViewZoneLogic(player, _origZone, numberCards, _revealZone, _writeableRevealZone,
_isReversed, &SettingsCache::instance().userInterface(), zoneContainer),
zoneContainer);
connect(zone, &ZoneViewZone::wheelEventReceived, scrollBarProxy, &ScrollableGraphicsProxyWidget::recieveWheelEvent);
retranslateUi();
@ -356,12 +357,8 @@ QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const
{
QWidget *current = eventWidget;
while (current) {
if (auto *view = qobject_cast<QGraphicsView *>(current)) {
return view;
}
current = current->parentWidget();
if (auto *view = QtUtils::findParentOfType<QGraphicsView>(eventWidget, true)) {
return view;
}
if (scene() && !scene()->views().isEmpty()) {
@ -573,7 +570,7 @@ void ZoneViewWidget::closeEvent(QCloseEvent *event)
// manually call zone->close in order to remove it from the origZones views
zone->close();
if (shuffleCheckBox.isChecked()) {
player->getPlayerActions()->sendGameCommand(Command_Shuffle());
player->getPlayerActions()->actShuffle();
}
zoneDeleted();
event->accept();

View file

@ -0,0 +1,81 @@
#include "installer_close_server.h"
#include <QCoreApplication>
#include <QLocalServer>
#include <QLocalSocket>
#include <QTimer>
#include <libcockatrice/utility/installer_close_protocol.h>
#include <libcockatrice/utility/local_server_name.h>
namespace
{
/// The one line the probe writes, from the header both ends include. A QByteArray view of
/// the shared literal, so the client cannot drift from the probe's copy.
const QByteArray CLOSE_COMMAND = QByteArray::fromRawData(INSTALLER_CLOSE_COMMAND, sizeof(INSTALLER_CLOSE_COMMAND) - 1);
/// How long a connection that never sends the command may stay open. The probe writes and
/// closes at once, so this only reaps a stray client; it must not run while a close request
/// is being handled, which is why it is a timer on the socket and not a disconnected slot.
constexpr int STRAY_CONNECTION_MILLISECONDS = 5000;
} // namespace
InstallerCloseServer::InstallerCloseServer(QObject *parent) : QObject(parent)
{
}
QString InstallerCloseServer::socketNameForUser(qint64 processId, const QString &userName)
{
return QStringLiteral("CockatriceInstallerClose-%1-%2").arg(processId).arg(userName);
}
QString InstallerCloseServer::socketName(qint64 processId)
{
// The same scoping helper the single-instance socket uses, so the probe only has to
// reproduce one rule (USER, then USERNAME, then the home directory name).
return scopedLocalServerName(QStringLiteral("CockatriceInstallerClose-%1").arg(processId));
}
bool InstallerCloseServer::listen()
{
if (server != nullptr) {
return server->isListening();
}
name = socketName(QCoreApplication::applicationPid());
server = new QLocalServer(this);
// A crashed instance can leave a Unix socket file behind; on Windows this is a no-op.
QLocalServer::removeServer(name);
if (!server->listen(name)) {
delete server;
server = nullptr;
return false;
}
connect(server, &QLocalServer::newConnection, this, [this]() {
while (QLocalSocket *socket = server->nextPendingConnection()) {
// No disconnected -> deleteLater() slot: the close request can spin a nested
// event loop, the probe disconnects while it runs, and a deferred delete would
// then free the socket from inside its own readyRead emission. The reaper covers
// a client that never sends the line; the handler below deletes after the emit.
auto *reaper = new QTimer(socket);
reaper->setSingleShot(true);
connect(reaper, &QTimer::timeout, socket, &QObject::deleteLater);
reaper->start(STRAY_CONNECTION_MILLISECONDS);
connect(socket, &QLocalSocket::readyRead, this, [this, socket, reaper]() {
if (!socket->canReadLine()) {
return;
}
if (socket->readLine() != CLOSE_COMMAND) {
return;
}
// Stop the reaper before the emit: the receiver may close the window and run
// a nested event loop for as long as the user takes, and the reaper must not
// free the socket from inside that loop while this handler still holds it.
reaper->stop();
emit closeRequested();
socket->disconnectFromServer();
socket->deleteLater();
});
}
});
return true;
}

View file

@ -0,0 +1,50 @@
#ifndef COCKATRICE_INSTALLER_CLOSE_SERVER_H
#define COCKATRICE_INSTALLER_CLOSE_SERVER_H
#include <QObject>
#include <QString>
class QLocalServer;
/**
* @brief Listens for the installer's request to close this instance gracefully.
*
* The Windows installer cannot reach an instance whose window is hidden in the notification
* area with WM_CLOSE, and it must not kill a process that may hold an unsaved deck. It
* connects to this local socket instead and writes one line; the instance then closes the
* same way its own close button does - saving settings and answering its own confirmation,
* never bypassing it.
*
* The socket name carries the process id, so every instance has its own and an installer
* that found several can ask each one. It is built by installerCloseSocketName() in
* tools/installer-holder-probe/installer_close_name.h; the two must stay identical.
*/
class InstallerCloseServer : public QObject
{
Q_OBJECT
public:
explicit InstallerCloseServer(QObject *parent = nullptr);
/**
* @brief Starts listening. A failure is not fatal: the installer falls back to
* WM_CLOSE, which is what older clients answer.
*/
bool listen();
/// The socket name this process listens on. The installer's probe builds the same
/// string from the process id and the account name.
static QString socketName(qint64 processId);
/// The name for an explicit account, exposed so the probe's name helper and this one
/// can be tested against each other.
static QString socketNameForUser(qint64 processId, const QString &userName);
signals:
void closeRequested();
private:
QLocalServer *server = nullptr;
QString name;
};
#endif // COCKATRICE_INSTALLER_CLOSE_SERVER_H

View file

@ -7,6 +7,7 @@
#include <QProgressBar>
#include <QtGlobal>
#include <climits>
#include <libcockatrice/utility/qt_utils.h>
namespace
{
@ -32,8 +33,7 @@ QString stageName(CardUpdateStage stage, const QString &fallback)
CardDatabaseUpdateStatusBar::CardDatabaseUpdateStatusBar(QWidget *parent) : QWidget(parent)
{
layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QHBoxLayout>(this);
stageLabel = new QLabel(this);
progressBar = new QProgressBar(this);

View file

@ -11,6 +11,7 @@
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QImageReader>
#include <QList>
#include <QMessageLogger>
@ -19,35 +20,62 @@
#include <QTimer>
#include <libcockatrice/settings/paths_settings.h>
static constexpr int REFRESH_INTERVAL_MS = 10 * 1000;
static constexpr int REFRESH_DEBOUNCE_MS = 500;
// Watching can fail on network and FUSE mounts; the slow poll still picks those
// changes up without the old ten-second full rescan.
static constexpr int FALLBACK_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent)
: QObject(parent), picsPath(SettingsCache::instance().paths().getPicsPath()),
customPicsPath(SettingsCache::instance().paths().getCustomPicsPath())
: CardPictureLoaderLocal(parent,
SettingsCache::instance().paths().getPicsPath(),
SettingsCache::instance().paths().getCustomPicsPath())
{
// Hook up signals to settings
connect(&SettingsCache::instance().paths(), &PathsSettings::picsPathChanged, this,
&CardPictureLoaderLocal::picsPathChanged);
}
CardPictureLoaderLocal::CardPictureLoaderLocal(QObject *parent,
const QString &_picsPath,
const QString &_customPicsPath)
: QObject(parent), picsPath(_picsPath), customPicsPath(_customPicsPath),
folderWatcher(new QFileSystemWatcher(this)), indexRefreshTimer(new QTimer(this)),
fallbackRefreshTimer(new QTimer(this))
{
indexRefreshTimer->setSingleShot(true);
indexRefreshTimer->setInterval(REFRESH_DEBOUNCE_MS);
connect(indexRefreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex);
connect(folderWatcher, &QFileSystemWatcher::directoryChanged, indexRefreshTimer, qOverload<>(&QTimer::start));
connect(folderWatcher, &QFileSystemWatcher::fileChanged, indexRefreshTimer, qOverload<>(&QTimer::start));
fallbackRefreshTimer->setInterval(FALLBACK_REFRESH_INTERVAL_MS);
connect(fallbackRefreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex);
fallbackRefreshTimer->start();
refreshIndex();
refreshTimer = new QTimer(this);
connect(refreshTimer, &QTimer::timeout, this, &CardPictureLoaderLocal::refreshIndex);
refreshTimer->start(REFRESH_INTERVAL_MS);
}
void CardPictureLoaderLocal::refreshIndex()
{
customFolderIndex.clear();
QSet<QString> directories;
QDir customDir(customPicsPath);
if (customDir.exists()) {
directories.insert(customDir.absolutePath());
}
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
const QString thisPath = it.next();
const QFileInfo thisFileInfo = it.fileInfo();
if (thisFileInfo.isFile()) {
if (thisFileInfo.isDir()) {
directories.insert(thisFileInfo.absoluteFilePath());
} else if (thisFileInfo.isFile()) {
// We don't know which name is the correctedName because there might be '.'s in the cardName.
// Just add all possibilities to be sure.
customFolderIndex.insert(thisFileInfo.baseName(), thisFileInfo.absoluteFilePath());
@ -55,8 +83,34 @@ void CardPictureLoaderLocal::refreshIndex()
}
}
updateWatchedDirectories(directories);
qCDebug(CardPictureLoaderLocalLog) << "Finished indexing local image folder CUSTOM; map now has"
<< customFolderIndex.size() << "entries.";
emit indexChanged();
}
void CardPictureLoaderLocal::updateWatchedDirectories(const QSet<QString> &directories)
{
const QSet<QString> stale = watchedDirectories - directories;
if (!stale.isEmpty()) {
folderWatcher->removePaths(QStringList(stale.begin(), stale.end()));
}
const QSet<QString> added = directories - watchedDirectories;
if (!added.isEmpty()) {
const QStringList failed = folderWatcher->addPaths(QStringList(added.begin(), added.end()));
if (!failed.isEmpty()) {
qCDebug(CardPictureLoaderLocalLog) << "Could not watch custom picture directories:" << failed;
}
}
watchedDirectories = directories;
}
QStringList CardPictureLoaderLocal::indexedPaths(const QString &baseName) const
{
return customFolderIndex.values(baseName);
}
QImage CardPictureLoaderLocal::tryLoad(const ExactCard &toLoad) const
@ -145,4 +199,5 @@ void CardPictureLoaderLocal::picsPathChanged()
{
picsPath = SettingsCache::instance().paths().getPicsPath();
customPicsPath = SettingsCache::instance().paths().getCustomPicsPath();
refreshIndex();
}

View file

@ -5,10 +5,12 @@
#include <QLoggingCategory>
#include <QMultiHash>
#include <QObject>
#include <QSet>
#include <QString>
#include <qtmetamacros.h>
class ExactCard;
class QFileSystemWatcher;
class QTimer;
inline Q_LOGGING_CATEGORY(CardPictureLoaderLocalLog, "card_picture_loader.local");
@ -40,6 +42,18 @@ public:
*/
explicit CardPictureLoaderLocal(QObject *parent);
/**
* @brief Constructs the loader against explicit folders without SettingsCache.
*
* Used by tests and by callers that already resolved the configured paths.
*/
CardPictureLoaderLocal(QObject *parent, const QString &picsPath, const QString &customPicsPath);
/**
* @brief Returns every indexed file whose base name matches @p baseName.
*/
[[nodiscard]] QStringList indexedPaths(const QString &baseName) const;
/**
* @brief Attempts to load a card image from local disk or custom folders.
* @param toLoad ExactCard object representing the card to load.
@ -54,7 +68,10 @@ private:
QString customPicsPath; ///< Path to custom card image folder
QMultiHash<QString, QString> customFolderIndex; ///< Multimap from cardName to file paths in CUSTOM folder
QTimer *refreshTimer; ///< Timer for periodic folder index refresh
QFileSystemWatcher *folderWatcher; ///< Watches the custom folder tree for changes
QTimer *indexRefreshTimer; ///< Debounces bursts of filesystem events
QTimer *fallbackRefreshTimer; ///< Slow poll for filesystems without change notification
QSet<QString> watchedDirectories; ///< Directories currently registered with folderWatcher
/**
* @brief Rebuilds the index of the custom image folder.
@ -65,6 +82,11 @@ private:
*/
void refreshIndex();
/**
* @brief Keeps the watcher registered on exactly @p directories.
*/
void updateWatchedDirectories(const QSet<QString> &directories);
/**
* @brief Attempts to load a card image from disk given its set and name info.
* @param setName Corrected short name of the card's set.
@ -82,6 +104,12 @@ private:
const QString &collectorNumber,
const QString &providerId) const;
signals:
/**
* @brief Emitted after the custom folder index has been rebuilt.
*/
void indexChanged();
private slots:
/**
* @brief Updates internal paths when the user changes picture settings.

View file

@ -28,6 +28,7 @@
#include <QVariant>
#include <QtGlobal>
#include <compare>
#include <libcockatrice/network/network_manager.h>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/paths_settings.h>
@ -46,11 +47,10 @@ CardPictureLoaderWorker::CardPictureLoaderWorker()
: QObject(nullptr), picDownload(SettingsCache::instance().downloads().getPicDownload()),
hostRequestLimits(SettingsCache::instance().downloads().getHostRequestLimits())
{
networkManager = new QNetworkAccessManager(this);
networkManager = NetworkManagerFactory::create(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
// Use Qt's default timeout (30s, as of 2023-02-22)
networkManager->setTransferTimeout();
cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L *

View file

@ -21,6 +21,7 @@
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <utility>
CardPictureToLoad::CardPictureToLoad(const ExactCard &_card)
: card(_card), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
@ -74,19 +75,20 @@ QList<CardSetPtr> CardPictureToLoad::extractSetsSorted(const ExactCard &card)
//! \todo Check if going through all sets is still necessary after the ExactCard refactor.
static PrintingInfo findPrintingForSet(const ExactCard &card, const QString &setName)
{
SetToPrintingsMap setsToPrintings = card.getInfo().getSets();
const auto &setsToPrintings = card.getInfo().getSets();
if (!setsToPrintings.contains(setName)) {
const auto setPrintings = setsToPrintings.constFind(setName);
if (setPrintings == setsToPrintings.constEnd() || setPrintings.value().isEmpty()) {
return PrintingInfo();
}
for (const auto &printing : setsToPrintings[setName]) {
for (const auto &printing : setPrintings.value()) {
if (printing.getUuid() == card.getPrinting().getUuid()) {
return printing;
}
}
return setsToPrintings[setName][0];
return setPrintings.value().first();
}
void CardPictureToLoad::populateSetUrls()
@ -183,10 +185,14 @@ static int parse(const QString &urlTemplate,
{
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
static const QRegularExpression rxCardProp("!prop:([^!]+)!");
static const QRegularExpression rxCardSet("!set:([^!]+)!");
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
// The property type only takes two values, so keep the expression that was
// compiled for it instead of rebuilding one per URL template.
const QRegularExpression &rxCardPropSelector = propType == "prop" ? rxCardProp : rxCardSet;
auto matches = rxCardProp.globalMatch(urlTemplate);
auto matches = rxCardPropSelector.globalMatch(urlTemplate);
while (matches.hasNext()) {
auto match = matches.next();
QString templatePropertyName = match.captured(1);
@ -303,10 +309,10 @@ QString CardPictureToLoad::transformUrl(const QString &urlTemplate) const
}
QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) {
for (const auto &[prop, value] : std::as_const(transformMap).asKeyValueRange()) {
if (transformedUrl.contains(prop)) {
if (!transformMap[prop].isEmpty()) {
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
if (!value.isEmpty()) {
transformedUrl.replace(prop, QUrl::toPercentEncoding(value));
} else {
/* This means the template is requesting information that is not
* populated in this card, so it should return an empty string,

View file

@ -7,13 +7,14 @@
#ifndef DECK_LOADER_H
#define DECK_LOADER_H
#include "deck_file_format.h"
#include "loaded_deck.h"
#include <QLoggingCategory>
#include <QObject>
#include <QPrinter>
#include <QString>
#include <QStringList>
#include <QTextCursor>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <optional>
#include <qtmetamacros.h>

View file

@ -1,14 +1,14 @@
#include "intent_open_local_deck.h"
#include "../deck_loader/deck_file_format.h"
#include "../deck_loader/deck_loader.h"
#include "../deck_loader/loaded_deck.h"
#include "../widgets/tabs/tab_supervisor.h"
#include "intent_wait_for_database_load.h"
#include "libcockatrice/card/database/card_database.h"
#include "libcockatrice/card/database/card_database_loader.h"
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/deck_list/deck_file_format.h>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <optional>
IntentOpenLocalDeck::IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file)

View file

@ -21,6 +21,7 @@
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_deck_share_item.pb.h>
#include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/utility/qt_utils.h>
#include <optional>
#include <qnamespace.h>
#include <string>
@ -184,13 +185,11 @@ void IntentOpenSharedDeck::onItemFailure(const QString &reason)
}
const int downloadedCount = loadedDecks.size();
const QMessageBox::StandardButton answer = QMessageBox::question(
previewDialog, tr("Open shared decks"),
tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "", downloadedCount)
.arg(itemNames.value(currentItemId, tr("Unknown deck"))),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer == QMessageBox::Yes) {
if (QtUtils::confirm(previewDialog, tr("Open shared decks"),
tr("Could not download the deck \"%1\".\n\n%n deck(s) were already downloaded. Open them?", "",
downloadedCount)
.arg(itemNames.value(currentItemId, tr("Unknown deck"))),
QMessageBox::Yes)) {
finishAll();
} else {
previewDialog->deleteLater();

View file

@ -1,7 +1,6 @@
#ifndef COCKATRICE_INTENT_OPEN_SHARED_DECK_H
#define COCKATRICE_INTENT_OPEN_SHARED_DECK_H
#include "../deck_loader/loaded_deck.h"
#include "contexts/context_open_deck.h"
#include "intent.h"
@ -9,6 +8,7 @@
#include <QMap>
#include <QScopedPointer>
#include <QString>
#include <libcockatrice/deck_list/loaded_deck.h>
#include <memory>
#include <qtmetamacros.h>

View file

@ -28,6 +28,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/settings/servers_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <memory>
#include <qlogging.h>
#include <qnamespace.h>
@ -115,9 +116,7 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingInt
const QString gameDescription = query.queryItemValue("game", QUrl::FullyDecoded);
const QString message = generateJoinGameMessage(*ctx, gameDescription);
const QMessageBox::StandardButton answer = QMessageBox::question(
mainWindow, tr("Join game"), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
if (!QtUtils::confirm(mainWindow, tr("Join game"), message, QMessageBox::Yes)) {
return nullptr;
}
@ -231,11 +230,10 @@ Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingInt
if (client->getStatus() == StatusLoggedIn) {
const QString current =
QStringLiteral("%1:%2").arg(client->serverName(), QString::number(client->serverPort()));
const QMessageBox::StandardButton answer = QMessageBox::question(
mainWindow, tr("Open shared deck"),
tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
if (!QtUtils::confirm(
mainWindow, tr("Open shared deck"),
tr("Opening this share link connects you to %1 instead of %2.\n\nContinue?").arg(target, current),
QMessageBox::Yes)) {
return nullptr;
}
chain.migrationTargetHost = ctx->serverContext.hostname;

View file

@ -25,6 +25,7 @@
#include <QVBoxLayout>
#include <QWidget>
#include <initializer_list>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QString &_themeName, QWidget *parent)
@ -57,9 +58,7 @@ PaletteEditorDialog::PaletteEditorDialog(const QString &_themeDirPath, const QSt
void PaletteEditorDialog::setupUi()
{
auto *root = new QVBoxLayout(this);
root->setSpacing(0);
root->setContentsMargins(0, 0, 0, 0);
auto *root = QtUtils::makeLayout<QVBoxLayout>(this, 0, 0);
// Header
header = new QWidget;

View file

@ -18,6 +18,7 @@
#include <QTimer>
#include <QVBoxLayout>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <utility>
@ -65,8 +66,7 @@ static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
{
scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
scroll = QtUtils::makeScrollArea(this, Qt::ScrollBarAsNeeded, Qt::ScrollBarAsNeeded);
scroll->setFrameShape(QFrame::NoFrame);
gridHost = new QWidget;
@ -74,8 +74,7 @@ PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
refreshChromePalettes();
scroll->setWidget(gridHost);
layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QVBoxLayout>(this);
layout->addWidget(scroll);
}
@ -102,9 +101,7 @@ void PaletteGridWidget::buildGrid(QWidget *host)
auto *label = new QLabel(groupHeaders[col], host);
label->setAlignment(Qt::AlignCenter);
label->setToolTip(groupTips[col]);
QFont f = label->font();
f.setBold(true);
label->setFont(f);
label->setFont(QtUtils::boldFont(label->font()));
label->setAutoFillBackground(true);
label->setContentsMargins(4, 4, 4, 4);
grid->addWidget(label, 0, col + 1);
@ -149,9 +146,7 @@ void PaletteGridWidget::buildGrid(QWidget *host)
auto *appHeader = new QLabel(tr("App colors"), host);
appHeader->setToolTip(tr("Application-specific colors layered on top of the Qt palette"));
QFont appHeaderFont = appHeader->font();
appHeaderFont.setBold(true);
appHeader->setFont(appHeaderFont);
appHeader->setFont(QtUtils::boldFont(appHeader->font()));
appHeader->setAutoFillBackground(true);
appHeader->setContentsMargins(4, 4, 4, 4);
grid->addWidget(appHeader, appHeaderRow, 0, 1, 4);

View file

@ -558,6 +558,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName,
widget->update();
}
// Palette-only previews do not run themeChangedSlot(), so drop snapshots
// generated under the previous palette here as well.
clearPixmapGeneratorCaches();
emit paletteChanged();
}

View file

@ -23,9 +23,7 @@
ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const QString &_colorIdentity)
: QWidget(parent), colorIdentity(_colorIdentity)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QHBoxLayout>(this, 5);
layout->setAlignment(Qt::AlignCenter); // Ensure icons are centered
setLayout(layout);

View file

@ -11,12 +11,11 @@
#include <QResizeEvent>
#include <QSize>
#include <QtGlobal>
#include <libcockatrice/utility/qt_utils.h>
ManaCostWidget::ManaCostWidget(QWidget *parent, CardInfoPtr _card) : QWidget(parent), card(_card)
{
layout = new QHBoxLayout(this);
layout->setSpacing(5); // Small spacing between icons
layout->setContentsMargins(0, 0, 0, 0);
layout = QtUtils::makeLayout<QHBoxLayout>(this, 5);
setLayout(layout);
setFixedHeight(50); // Fixed height

View file

@ -17,6 +17,7 @@
#include <QString>
#include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/utility/qt_utils.h>
class QWidget;
@ -30,9 +31,8 @@ CardInfoDisplayWidget::CardInfoDisplayWidget(const CardRef &cardRef, QWidget *pa
text->setObjectName("text");
connect(text, &CardInfoTextWidget::linkActivated, this, [this](const QString &card) { setCard({card}); });
auto *layout = new QVBoxLayout();
auto *layout = QtUtils::makeLayout<QVBoxLayout>();
layout->setObjectName("layout");
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(pic, 0, Qt::AlignCenter);
layout->addWidget(text, 0, Qt::AlignCenter);

View file

@ -19,6 +19,7 @@
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/card_ref.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
struct CardRef;
@ -49,15 +50,13 @@ CardInfoFrameWidget::CardInfoFrameWidget(QWidget *parent)
insertTab(ImageAndTextView, tab3, QString());
connect(this, &CardInfoFrameWidget::currentChanged, this, &CardInfoFrameWidget::setViewMode);
tab1Layout = new QVBoxLayout();
tab1Layout = QtUtils::makeLayout<QVBoxLayout>();
tab1Layout->setObjectName("tab1Layout");
tab1Layout->setContentsMargins(0, 0, 0, 0);
tab1Layout->setSpacing(0);
tab1->setLayout(tab1Layout);
tab2Layout = new QVBoxLayout();
tab2Layout = QtUtils::makeLayout<QVBoxLayout>();
tab2Layout->setObjectName("tab2Layout");
tab2Layout->setContentsMargins(0, 0, 0, 0);
tab2Layout->setSpacing(0);
tab2->setLayout(tab2Layout);
@ -65,9 +64,8 @@ CardInfoFrameWidget::CardInfoFrameWidget(QWidget *parent)
splitter->setObjectName("splitter");
splitter->setOrientation(Qt::Vertical);
tab3Layout = new QVBoxLayout();
tab3Layout = QtUtils::makeLayout<QVBoxLayout>();
tab3Layout->setObjectName("tab3Layout");
tab3Layout->setContentsMargins(0, 0, 0, 0);
tab3Layout->setSpacing(0);
tab3Layout->addWidget(splitter);
tab3->setLayout(tab3Layout);

View file

@ -491,7 +491,14 @@ void CardInfoPictureWidget::showEnlargedPixmap()
enlargedPixmapWidget->setCardPixmap(exactCard, enlargedSize);
const QPoint cursorPos = QCursor::pos();
const QRect screenGeometry = QGuiApplication::screenAt(cursorPos)->geometry();
QScreen *screen = QGuiApplication::screenAt(cursorPos);
if (!screen) {
screen = QGuiApplication::primaryScreen();
}
if (!screen) {
return;
}
const QRect screenGeometry = screen->geometry();
const QSize widgetSize = enlargedPixmapWidget->size();
int newX = cursorPos.x() + ENLARGED_PIXMAP_OFFSET;

View file

@ -19,6 +19,7 @@
#include <QWidget>
#include <libcockatrice/card/game_specific_terms.h>
#include <libcockatrice/card/relation/card_relation.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <qobjectdefs.h>
@ -33,9 +34,7 @@ CardInfoTextWidget::CardInfoTextWidget(QWidget *parent) : QFrame(parent)
textLabel->setReadOnly(true);
textLabel->setMinimumSize(35, 35);
propsScroll = new QScrollArea(this);
propsScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
propsScroll->setWidgetResizable(true);
propsScroll = QtUtils::makeScrollArea(this);
propsScroll->setAlignment(Qt::AlignLeft | Qt::AlignTop);
propsScroll->setContentsMargins(0, 0, 0, 0);
propsScroll->setFrameStyle(QFrame::NoFrame);

View file

@ -29,6 +29,7 @@
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/models/deck_list/deck_list_model.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
class CardSizeWidget;
@ -179,15 +180,7 @@ void DeckCardZoneDisplayWidget::displayCards()
void DeckCardZoneDisplayWidget::refreshDisplayType(const DisplayType &_displayType)
{
displayType = _displayType;
QLayoutItem *item;
while ((item = cardGroupLayout->takeAt(0)) != nullptr) {
if (item->widget()) {
item->widget()->deleteLater();
} else if (item->layout()) {
item->layout()->deleteLater();
}
delete item;
}
QtUtils::clearLayoutRec(cardGroupLayout);
indexToWidgetMap.clear();

View file

@ -22,6 +22,7 @@
#include <QWidget>
#include <algorithm>
#include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
#include <utility>
@ -47,12 +48,7 @@ ManaBaseWidget::ManaBaseWidget(QWidget *parent, DeckListStatisticsAnalyzer *anal
void ManaBaseWidget::updateDisplay()
{
// Clear previous widgets
while (QLayoutItem *item = barLayout->takeAt(0)) {
if (item->widget()) {
item->widget()->deleteLater();
}
delete item;
}
QtUtils::clearLayoutRec(barLayout);
auto &pipCount = analyzer->getProductionPipCount();
auto &cardCount = analyzer->getProductionCardCount();

Some files were not shown because too many files have changed in this diff Show more