- Sandbox via unique app/org names plus Linux-only XDG redirection, derive
the warm-cache probe and data path from SettingsCache, and bail out when the
temporary sandbox cannot be created.
- Run each pass with a fresh worker and shut workers down before reading the
429 counters, so the redirect cache is persisted and no worker thread can
outlive the stack-local counters or the installed message handler.
- Make s_activeCounters atomic and always forward log output when the previous
handler is the built-in (nullptr) one.
- Validate --timeout-min, scale the cached-pass budget with --count, honour the
first --url as the stress template, and add the missing trailing newlines.
- Zero-initialise SettingsCache members so the benchmark mock cannot
dereference an indeterminate pointer.
clearNetworkCache() ran directly on the UI thread while the worker thread
owned the disk cache and redirect cache, racing cache reads/writes. Make it
a worker-thread slot invoked via a blocking queued call when the thread is
running, so the 'Cached card pictures have been reset.' message is truthful.
The worker thread was also never quit()/wait()ed: both destructors only
deleteLater'd their objects, so Qt warned 'QThread: Destroyed while thread
is still running' and leaked a running loop at exit. Wire the worker's
finished() signal to its own deleteLater() (canonical worker-object
pattern), add shutdownThread() to stop the loop, and let CardPictureLoader
destroy the QThread only after wait() has returned.
Two refinements to the per-host request caps:
- Unlocked hosts (UNLIMITED_HOST_QUOTA, e.g. cards.scryfall.io) no longer
wait on the 100ms dispatch pacing or consume the global per-second quota.
dispatchQueuedRequest fires their queued requests back-to-back, bounded
only by their 429 backoff window and Qt's per-host connection pool, so
an unthrottled CDN is not artificially slowed.
- The deck editor download settings page replaces the static grid of one
spinbox per known host with an "Adjust Rate Limit" toolbar action on the
URL list. It picks the host out of the selected URL and clamps the entry
against the developer cap table (including for user-added URLs).
Also fixes a review finding: resetRequestQuota could write the
UNLIMITED_HOST_QUOTA sentinel (-1) into the sustained per-host quota when a
host became unlocked mid-run, permanently poisoning its allowance. Stale
entries for unlocked hosts are now dropped, and the per-second seed is
clamped against the effective ceiling so a lowered limit applies immediately.
Picture downloads were throttled to a uniform 10 requests/second per host
with no way to tune a specific server. A rate-limited API host (Scryfall
caps at 10 req/s) can trip 429s during bursts, and CDN hosts with no rate
limit were throttled needlessly.
Introduce developer-owned per-host caps that users can only ever lower,
never raise, exposed in the download settings page:
- DownloadSettings::DEVELOPER_HOST_CAPS sets the ceiling per host
(api.scryfall.com 9, cards.scryfall.io unlimited, others 10).
- A new hostRequestLimits setting stores user overrides in downloads.ini;
clampHostRequestLimit() bounds them to [1, devCap] so a user can reduce
api.scryfall.com to 5 but never raise it above 9.
- The picture worker seeds, halves on 429, and recovers its sustained
per-host allowance against the effective ceiling instead of the global
maximum, and skips per-host accounting entirely for unlocked hosts
(cards.scryfall.io) while global pacing and 429 backoff still apply.
- The deck editor settings page gains one spinbox per known host, each
clamped to its developer cap.
The quota reset re-filled every host's remaining allowance to a full
MAX_REQUESTS_PER_SEC as soon as the queue had a request for it. A server
that was just rate limited could therefore be hammered again at full speed
immediately after (or even during) recovery.
Only seed a host's allowance the first time it is dispatched in the
current second, seeded from its reduced sustained quota, and skip hosts
still inside their 429 backoff window entirely. This makes the pacing
commit's burst-free behavior hold per host too, instead of just smoothing
the global aggregate.
Previously the whole backed-up queue was drained in a burst as soon as a
request was enqueued, sending up to 10 requests back-to-back and then
immediately re-filling the quota one second later. That hard-bursts a
rate-limited API like Scryfall's (10 requests/second) into a 30 second
lockout.
Introduce a pacing timer that dispatches a single queue entry every
100 ms, so the per-second allowance is used smoothly instead of in spikes,
and keep the quota timer at 1 second. Also fix both timers' thread
affinity: they are QTimer value members and so are not QObject children,
meaning moveToThread() on the worker left them on the main thread while
the slot code started them from the picture thread, which was a no-op that
also warned. They are moved to the worker thread explicitly and started
lazily from there.
With picture downloads enabled, requests were issued with AlwaysNetwork
cache control, which per Qt never consults the disk cache. A picture that
had already been downloaded was therefore fetched from the network again
on every session start, with the queue bypass letting those re-fetches
skip the rate limit entirely.
Treat the network cache as the intent of the 'Network Cache' storage
method suggests: if the URL is already cached, serve it with AlwaysCache
(no network, no quota); only a genuine miss goes to the network, and only
when downloads are enabled. Cache hits skip the queue for free since they
never consume the per-second request allowance.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Replace remaining QPixmap("theme:...") call sites with themePixmap() so icons, replay controls, card backs, and other images resolve to -dark/-light variants under theme schemes. Stem-exact 1:1 migration; behavior unchanged for non-variant themes.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* Add scheme-variant theme asset resolution (#7209-1)
ThemeManager::assetPath() and schemeVariantPath() resolve a theme asset to its scheme-variant file (prefix-light/dark.png) with fallback to the plain asset, and themePixmap()/loadBrush()/loadExtraBrush() use them. CSS files load style-dark.css or style-light.css when present. Home widget re-resolves its background on theme change.
Link pixel_map_generator.cpp into the oracle target, which needs Qt6::Xml for QDomDocument.
* Fix clang-format wrap of theme format probe lists
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Server] Instrument command processing, game starts, and event loops
Add a lock-free MetricsRegistry that accumulates per-command processing
times in preallocated histogram slots (one per protobuf command type,
bucketed at 1/5/10/25/50/100/250/500/1000/2500/5000 ms +Inf). The
hot-path observeCommand() uses only relaxed atomic adds — no locks,
no allocations, no cache-line ping-pong beyond the unavoidable counter
updates.
Wire the registry into AbstractServerSocketInterface::processCommandContainer()
so every processed command is attributed with its container's wall-clock
time. When a container exceeds metrics/slow_command_ms (default 500),
a warning is logged including the connected username.
Add an EventLoopWatchdog heartbeat that runs on every socket pool thread.
If a heartbeat overshoots metrics/stall_warn_ms (default 2000 ms), the
overshoot is recorded in atomic counters and a warning is logged. Both
thresholds are configurable in servatrice.ini; setting stall_warn_ms to 0
disables the watchdogs entirely.
Track game-start durations via a separate histogram in MetricsRegistry.
Server_Game::startGameNow() measures the time from zone creation through
player materialization and reports it via Server::observeGameStartDurationMs().
Add a live card-count gauge: Server_Game exposes getCardsInGame() and
Servatrice::getCardsInGamesTotal() sums across all running games under
the appropriate read locks.
Include a standalone metrics_registry_test (Google Test) that validates
empty registries, single/multi-sample histograms, kind encoding,
overflow-slot collapse, negative-duration clamping, gauge rendering,
and the game-start histogram separation.
Took 10 minutes
* [Client/Server/Protocol] Surface live metrics in the Developer tab
Extend Response_GetServerStats with live counters from the in-process
MetricsRegistry: cards in games, event loop stall totals/worst,
total commands processed, average command time, active command types,
and game-start count/duration. Add a repeated CommandStats message
carrying per-command breakdowns (kind, extension number, resolved
protobuf name, count, total ms) for every type that has seen at
least one sample.
Server-side cmdGetServerStats() populates all new fields after the
existing DB uptime snapshot query, resolving protobuf extension names
via the descriptor pool for human-readable labels like
session/Command_Ping.
Expand TabDeveloper with two tables: an overview section (existing
DB stats plus the new live metrics) and a per-command breakdown table
(Command / Count / Total ms / Avg ms) sorted by total_ms descending
so the hottest commands surface first.
Took 55 minutes
Took 47 seconds
* [Server] Drop dead Prometheus histogram, add developer command metrics, fix watchdog init order
- metrics_registry: remove toPrometheusText/appendCumulativeBuckets and the time-bucket histogram that nothing in production ever emitted (the future /metrics exporter can bring it back); keep counts/totals read by the Developer tab
- Fix +Inf bucket routing that never incremented, and its test that locked the bug in
- Instrument developer_command container (kind 6) in processCommandContainer and stats label resolution
- Read metrics/{slow_command_ms,stall_warn_ms} at the top of initServer() so stall_warn_ms=0 disables the watchdogs before pool threads start
- Shrink KindStride to 1280 (largest extension in use is 1206) with a static_assert; document scrape cost of getCardsInGamesTotal; note slow_command logging has no rate limit in servatrice.ini.example
* [Tests] Give metrics_registry_test an explicit main
* [Server] Record only the dispatched command family; drop unused totals
processCommandContainer recorded every family in a container even though
the base if/else-if dispatch processes at most one. An unauthenticated
client could batch a session command (login) with fabricated developer,
moderator, and admin entries and forge genuine-looking samples that were
never executed or authorized. Mirror the base's selection, skip when the
handler was already deleted, and skip entries whose extension number is
-1 (which would otherwise wrap into the previous kind's id range).
[Server] Drop dead process-lifetime byte/uptime counters
txBytesTotal/rxBytesTotal added an atomic RMW to every socket write and
read for counters nothing consumes (cmdGetServerStats fills tx_bytes,
rx_bytes, and uptime_secs from the DB snapshot). Remove the two atomics
and the getTxBytesTotal/getRxBytesTotal/getUptimeSeconds getters; the
incTxBytes/incRxBytes slots and mutexes remain for the ISL legacy
counters.
[Protocol] Document kind 5 as developer in CommandStats
NumKinds is 6 and the server emits kind_index = 5 for developer
commands; the comment stopped at 4.
* [Client] Togglable auto-refresh for Developer stats tab
* [Oracle] Fix clang-format alignment of card type priority list
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Server/Client/Protocol] Add developer staff role
Introduce a Developer staff level (proto flag 32, DB admin bit 8) that
sits between admin and moderator: no kick/ban/warn/report/admin powers,
but gets server log access via a new developer command container family
(GET_SERVER_STATS, VIEWLOG_HISTORY) and an idle-timeout exemption.
- Protocol: IsDeveloper flag, developer_commands.proto envelope,
Command_GetServerStats/Command_GetLogHistory, Response_GetServerStats,
Command_AdjustMod.should_be_developer
- Servatrice: fail-closed developer dispatcher, uptime snapshot handler,
shared log history handler reuse, bit-8 DB mapping
- Client: burgundy pawn/badge/labels/sort order, prepareDeveloperCommand,
minimal Developer stats tab, log tab access, promote/demote actions
Took 24 minutes
Took 18 seconds
* [Server/Client/Protocol] Address developer role review feedback
Address ZeizaZach's review of the developer staff role:
- Nudge the developer log query to exclude private chat and sender IPs
(the ModeratorCommand path still sees everything).
- Deduplicate Command_GetLogHistory into Command_ViewLogHistory, which now
extends both ModeratorCommand (ext) and DeveloperCommand (dev_ext); the
client picks the DeveloperCommand-scoped extension by extendee, and the
server reads it via the extension number.
- Pull the uptime snapshot SQL into Servatrice_DatabaseInterface as
getLatestUptimeSnapshot() and widen the reported counters to 64-bit.
- Document the admin bitfield (1 admin, 2 moderator, 4 judge, 8 developer)
and add a server-side test for the developer command path.
* Add missing trailing newline to user_context_menu.cpp
* Remove stale includes of deleted command_get_log_history proto
The Command_GetLogHistory message was folded into Command_ViewLogHistory,
which deleted command_get_log_history.proto, but serversocketinterface
still #included its generated header. Fresh CI builds fail on the missing
file; local builds masked it by reusing a previously generated header.
* [Server] Exclude chat rows when private-chat filter is bypassable
A developer who omits log_location entirely — or sends only "chat" —
leaves chatType, gameType, roomType all false, so getMessageLogHistory
skips the target_type clause and returns every row, private messages
included. When !allowPrivateChat the server now forces game+room when
no surviving location was requested, guaranteeing the query always
carries a target_type restriction.
[Client] Demote mod+dev to moderator path in log-tab dispatch
The developer command family is strictly weaker than the moderator one
(no private chat, no sender_ip, ip filter ignored), so granting the
developer bit to an existing moderator must not silently strip their
capabilities. useDeveloperCommands is now true only when the user holds
the developer bit and not the moderator bit.
[Client] Hide the IP-address filter for developer log tab users
The developer path ignores the ip_address query field server-side.
Showing the field lets a developer type an IP and get results that are
silently unfiltered by it rather than an empty result set — reads as a
broken filter. Hide labelFindIPAddress/findIPAddress alongside the
privateChat checkbox.
* Developer pawn is silver.
* [Client] Fix indentation of merged Card Art Rules / Developer tabs
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle/Client] Report card database download progress
Card database updates ran invisibly: MTGJSON parsing spun an indeterminate
bar on the UI thread and the set import blocked the window, while the
onboarding wizard spawned `oracle -b` with no progress to show at all.
- Add byte-level scan progress to `RawJson::scanSetRanges` via an optional
callback, throttled to ~100 reports per scan.
- Emit `OracleImporter::dataReadProgress` during the scan and import sets on a
worker thread, driving the wizard's progress bar per set.
- With `-b`, write machine-readable `PROGRESS <stage> <done> <total>` lines to
stdout for the download/scan/import stages; stderr keeps the log output.
- Parse the oracle stdout in `MainWindow` and forward it to the onboarding
wizard, giving the card database step a determinate bar with stage-specific
status text.
- Guard the async workers against the wizard being closed mid-run.
- Add Google Test coverage for scan progress reporting.
* [Oracle/Client] Harden oracle progress workers and quit prompt
Address review feedback on the download-progress change: decompress and read
sets files off the UI thread, cancel the load/import workers before the
wizard can tear down the importer, and show an 'Extracting file...' status
plus a clean 100% tail so the poll never looks stuck. Quitting Cockatrice
while a card database update runs now asks for confirmation.
* Show 100% for 500ms on complete.
* Disable buttons on set import until done.
* Clean up progress bar.
* Drop wrapper around lambda
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle] Prefer higher-priority maintype when merging split cards
Adventure cards (e.g. Bonecrusher Giant, Virtue of Knowledge) are stored as
split cards in MTGJSON, with a Creature/permanent face and an Instant/Sorcery
adventure face. When the two faces are merged into a single card, the code
previously discarded the second face's maintype entirely, keeping whichever
face was processed first.
If the Instant/Sorcery adventure face appeared first, the merged card got
maintype 'Instant' with tableRow 3. At runtime this made double-clicking the
card on the stack send it to the graveyard instead of the table
(PlayerActions::playCard).
Fix the merge to follow the same priority order used by getMainCardType()
(Planeswalker > Creature > Land > Sorcery > Instant > Artifact > Enchantment),
so the permanent Creature type wins for adventure cards regardless of face
order, matching the physical card and the reported expectations.
Closes#4394
* Extract helper.
* Simplify const
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Fix duplicate attach messages in the game log caused by a redundant
child arrow in CardItem::drawAttachArrow.
Unlike the sibling drawArrow, drawAttachArrow omitted the ``card ==
this`` guard when iterating selectedItems(). Because right-clicking a
card to open the attach menu selects that card, it was always present
in selectedItems(), producing a second arrow for the same source card.
On release both arrows sent an identical Command_AttachCard, so the
server broadcast two Event_AttachCard messages and the log rendered
"attaches to" twice. Mirror the drawArrow skip condition so the active
card is excluded and only one attach command is sent.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
When a player restores connection to a game, the client rebuilds each
zone from the cards the server sends in the game state. Non-coordinate
zones (hand, piles, stack) report x == 0 on every card, so inserting
each rebuilt card at that coordinate reversed the received order one
card at a time.
Append rebuilt cards in the order they arrive for zones without
coordinates; coordinate-based zones (table) keep using x/y.
Fixes#2759
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Player pawns are looked up in QPixmapCache under a key built from the
rendered size, user level, and the avatar pixmap's cacheKey(). A null
pixmap reports cacheKey() 0, so all players without a custom avatar
collided: the first pawn rendered for a given size and user level was
reused for the next one, showing the wrong player's pawn.
Extend the key with the rendered height, the lowercased privlevel
(matching UserLevelPixmapGenerator), and both pawn colors so that every
visually distinct pawn gets its own cache entry.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Nothing in the UI confirmed that a user had been added to the ignore
list, so the action felt ambiguous and could be repeated by accident.
Show an information dialog when the server acknowledges the
add-to-ignore command.
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Prevent contacting users on the ignore list (#1249)
The ignore list silences incoming messages, but a user on it was still
reachable: the context menu's chat item stayed enabled, private messages
could be sent, and a chat tab could be opened for an ignored user.
Make ignored users uncontactable: disable the chat item for them, refuse
to deliver messages typed in an open PM tab with one, and refuse to open
a new private chat tab with an ignored user (with a hint on how to undo
the ignore).
* Update cockatrice/src/interface/widgets/tabs/tab_message.cpp
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
When a player left, GameScene::removePlayer iterated zoneViews while close()
synchronously removed the current view from that list. A judge with several open
views of the departing player (e.g. library and hand) only had the first one
closed; the remaining views were skipped and left pointing at a player that was
about to be deleted, crashing the client on the next access.
- GameScene::removePlayer: iterate over a copy of zoneViews
- GameScene::toggleZoneView: same fix for the identical iteration bug
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Use the landscape orientation flag to rotate sideways-layout card art
upright before cropping, so planes/sieges show their horizontal art as
the server profile banner card instead of a rotated full card.
- Curve cropCardArt around the card's landscapeOrientation flag with
landscape-specific art margins (mirrors CardInfoPictureWidget)
- Add shared CardArtUtils::rotateSidewaysLayoutArt helper and apply it
to the playmat (game render and settings preview) and card info widget,
replacing three duplicate 90-degree rotation blocks
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Prevent spectator duplication when replaying joined events
The spectator branch of eventJoin emitted spectatorJoined unconditionally
even when the spectator was already present (e.g. replayed during a rewind).
Guard it like the player branch and eventGameStateChanged, and make
PlayerListWidget::addPlayer idempotent as defense in depth.
* In resetChatAndPhase() (the rewound() handler), also clear all spectators from both PlayerManager and PlayerListWidget before the replay rebuilds from event 0. The forward replay then re-adds exactly the spectators whose join events fall within the new time range via eventGameStateChanged/eventJoin.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Render custom deck zones in the deck view
The in-game deck view now walks custom zones like the standard
boards, so cards filed under a user-created zone show up in their
zone's card pile instead of disappearing from the view.
* [Game] Collect deck-view cards via DeckList::getCardNodes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Show custom zones in the card display widgets
Card group displays and deck zone displays learn to render custom
zones alongside the standard boards.
- Group display widgets treat custom-zone nodes like other group
headers, keeping counts and layout consistent.
- Zone display widgets resolve their title through visibleNameFromName
so custom zones show their user-chosen names localized like the
standard zones.
* [DeckEditor] Apply sort criteria inside custom zones and align display order with the model
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Expose custom zone management in the deck editor
Wires the state layer into every editor surface that shows deck zones.
- Deck dock: context menu on zones gains New/Rename/Delete/Change
board actions, with per-zone submenus for adding cards.
- Card database dock and visual database display gain an add-to-zone
submenu listing custom zones per board plus a create-zone entry.
- All prompt call sites pass validateNewZoneName so duplicates and
reserved names are rejected inline before Ok unlocks.
- Rename reuses the same dialog in name-only mode, keeping one
validation contract for every zone-name entry point.
- Change board marks the current board instead of offering a no-op,
and the state layer refuses moves onto boards holding a same-named
zone from imported decks.
* [DeckEditor] Address custom-zone menu and export review feedback
* [DeckLoader] Keep the sideboard marker and block ordering when exporting nested zones
- saveToStream_DeckZone threads the owning board zone name down to the card
writer, so cards in a custom zone under the sideboard keep their SB:
prefix instead of being re-imported into the maindeck
- nested sub-zones are collected during the loop and written after the
parent zone's own header and cards, so they no longer read as part of the
zone printed before them
* [DeckEditor] Fix move-to-zone menu use-after-free and per-zone enabled state
- resolve the card name/provider/collector number before createNewCustomZone
rebuilds the model tree, then re-find the refreshed index via findCard and
move it (mirrors the decrementCard re-find pattern)
- the enabled test now compares the card's own zone (nearest custom-zone
ancestor, else its board), matching moveCardToZone's lookup, so moving a
card out of a custom zone back to the board root is offered and the card's
own zone is disabled
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Add zone management to the deck state manager
State-layer operations for custom deck zones, plus the shared prompt
dialog that later editor menus will call into.
- moveCardToZone relocates every copy of a card row into any zone,
refusing non-card rows and tokens so miswired selections can never
shred a group or turn tokens into deck cards. The current zone is
found by walking ancestors, which also handles legacy top-level
zones.
- createCustomZone, renameCustomZone, moveCustomZone and
removeCustomZone wrap the tree API with memento history, model
rebuilds and deck hash refreshes via modifyTree.
- Same-board zone moves return success without minting a history
entry, keeping the undo log honest.
- promptForNewZone asks for a name and the parent zone, keeps Ok
disabled until the trimmed name passes a caller-supplied validator
(shown inline as an error), and reports its own translation context.
Took 14 minutes
# Commit time for manual adjustment:
# Took 6 minutes
# Commit time for manual adjustment:
# Took 33 seconds
* [DeckEditor] Address zone-management review feedback
- Expose DecklistNodeTree::hasZoneName and use it in validateNewZoneName
so the uniqueness scan covers custom zones on every board, not just the
standard ones.
- Hide the board selector in the rename dialog path where it is not used.
- Emit deckHashChanged after refreshDeckHash so the deck hash label stays
current after zone create/rename/move/remove.
* [DeckEditor] Notify card set changes after zone edits and drop the board scan
- modifyTree emits cardNodesChanged alongside deckHashChanged so the
banner-card combo and printing in-deck counts refresh after removing a
zone that still holds cards
- DecklistNodeTree::findCustomZoneByName is public and moveCustomZone uses
it, locating zones under non-standard boards (e.g. tokens) instead of
scanning only main/side/maybeboard
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Models] Mirror custom deck zones in the deck list model
DeckListModel now surfaces the custom zones from the deck tree so
views can render and edit them alongside criteria groups.
The custom-zone bookkeeping that made the model unwieldy is extracted
into DeckListModelCustomZones (deck_list_model_custom_zones.h/.cpp), a
single self-contained unit owning every "what is / where is a custom
zone" decision for the model's shadow tree:
- rebuildTree mirrors each custom zone as a DecklistModelSubZoneNode
under its board zone, cards flat inside (no further grouping).
- The freshly built shadow tree is sorted while the model reset is
still open, so views never observe unsorted intermediate order and
proxies cannot desync.
- Custom zones always sort after criteria groups within a board,
regardless of their names. One shared sortWithCustomZonesLast backs
both the live sortHelper (which remaps persistent indexes from the
movement mapping) and the silent reset-time sortShadowTree.
- addCard inserts flat into a custom zone by name and keeps grouping
by active criteria for board zones. findCardNode resolves cards in
both layouts, legacy top-level zones unchanged.
- New IsCustomZoneRole lets views tell zones apart from groups.
- Empty custom zones survive row removal. Zone rows themselves are
only mutable through the deck tree API.
A new deck_list_model_custom_zones_test suite locks the extracted
shadow-tree logic (type testing, mirroring, name lookup, and the
sort-with-custom-zones-last mapping).
No behavior change.
* [Models] Route group lookups around mirrored custom zones
Group lookups (createNodeIfNeeded, findCardNode) must not resolve a
mirrored custom zone that shares the group name. Introduce
findGroupChild to search only non-custom children, and make addCard
consult the deck tree before falling back to creating a top-level zone
so cards added to an un-mirrored custom zone land inside it.
mirrorCustomZones now flattens cards nested at any depth into the
mirrored zone so no card is left without a model row.
Add model behaviour tests (addCard routing, same-name group/zone
collision, removeRows guard, empty-zone survival, findCard inside a
custom zone) and fix the missing main() in the unit test binaries.
* [Models] Fix addCard routing for card-named zones and nested custom zones
- hasDeckZone no longer matches board cards that merely share the zone
name, which previously caused infinite addCard/rebuildTree recursion
- Adding to a custom zone whose deck side holds nested sub-zones appends
to the deck tree instead of writing past its direct children
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This compares the release dates of sets, which enables users to filter
for sets in a certain range, for example to filter for all commanders
with an old card frame, `t:legendary set<8ED` can be used, which will
only include cards appearing before 8th edition.
This acts as a more powerful superset of the "Filter to X most recent
sets" feature.
Fixes#7238
* [Oracle] Parse sets lazily to slash importer peak memory
- Add a raw JSON scanner that splits the document into per-set byte ranges
without materializing the JSON tree
- Keep only the raw document bytes and parse one set at a time in startImport
- Take readSetsFromByteArray by value so the wizard's buffer is moved, not copied
- Clear the retained raw data in releaseSetData()/clear()
- Cover the scanner and lazy parsing with tests
Took 2 minutes
* [Oracle] Fix nesting-depth cap, tolerate unescaped control chars, lazy-parse review fixes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle] Add RAM usage benchmarks for the oracle importer
- Measure process peak/current RSS via procfs (Linux) or getrusage (macOS)
- Add a synthetic-scale RAM benchmark and an opt-in real AllPrintings
run gated by COCKATRICE_ORACLE_RAM_BENCHMARK=1
- Mirror the wizard's magic-byte handling to decompress .xz/.zip payloads
- Wire optional ZLIB/LibLZMA into the benchmark target and raise its timeout
Took 2 minutes
* [Oracle/Tests] Measure parse against post-fixture baseline; assert release empties sets
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Build] Enable ccache by default when it is installed
ccache is a near free win for both clean and incremental rebuilds and
has no effect on systems where it is not installed (find_program
guards the whole block). Aligns the CMake default with the documented
behavior; users can still arch with -DUSE_CCACHE=OFF.
* [Build] Disable ccache auto-engage on Windows (MSVC)
* [Build] Report ccache skip on Windows explicitly
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Build] Add precompiled headers for Qt-backed executables
Reparsing QtCore/QtGui/QtWidgets/QtNetwork in ~460 client translation
units is the dominant compilation cost. Precompile the two common layers:
- qtcore_pch.h (Qt Core only; safe even for headless Servatrice)
- qtwidgets_pch.h (adds Gui/Widgets/Network; used by Cockatrice and Oracle)
target_precompile_headers() requires CMake 3.16, now the project minimum.
Estimated 30-50% faster client rebuilds.
* [Build] Format qtwidgets precompiled header
clang-format include regrouping and a missing trailing newline.
* [Build] Add PCH-aware ccache sloppiness config; format cmake/pch headers
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle] Add oracle importer tests and fix set parsing details
- Add oracle_importer_test and oracle_importer_benchmark_test targets
- Preserve the first printing's legalities when an existing card is reused
- Concatenate split-card coloridentity and sort/dedupe card colors
- Use a raw string for the Basic Land format regex
- Pre-allocate the card hash and micro-optimize string handling
Took 2 minutes
* [Oracle/Tests] Pin cmc coercion in CI run; scope the reserve pass
The #7214 coercion assertion lived only in oracle_importer_benchmark_test,
which gets no add_test and so never runs under ctest. Add NumericManaValueCoercedToCmc
and LegacyConvertedManaCostCoercedToCmc to oracle_importer_test (a CI-ran
binary): manaValue/convertedManaCost are JSON numbers in AllPrintings, and
QJsonValue::toString() would drop them to an empty cmc without the
#7214 coercion fix.
Wrap the distinct-name reserve pass in a bare block so the ~35k name
QStrings are handed back before the memory-heavy import loop starts.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Oracle] Replace vendored QtJson with native QJson for set import
- Drop the vendored oracle/src/qt-json/json.{h,cpp} implementation
- Switch SetToDownload and importCardsFromSet from QList<QVariant> to
native QJsonArray/QJsonObject
- Release set JSON data after import in the save sets page
Took 20 minutes
* [Oracle] Restore property coercion and legality merge in native JSON import
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>