diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 74f905351..a6b9d5340 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -162,7 +162,7 @@ jobs: - name: "Restore compiler cache (ccache)" id: ccache_restore - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: @@ -215,7 +215,7 @@ jobs: - name: "Save updated compiler cache (ccache)" if: github.ref == 'refs/heads/master' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: key: ${{ steps.ccache_restore.outputs.cache-primary-key }} path: ${{ env.CACHE }} @@ -365,7 +365,7 @@ jobs: - name: "[macOS] Restore compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 id: ccache_restore - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} with: @@ -387,7 +387,7 @@ jobs: - name: "[macOS] Restore thin Qt ${{ steps.resolve_qt_version.outputs.version }} libraries" if: matrix.os == 'macOS' id: restore_qt - uses: actions/cache/restore@v5 + uses: actions/cache/restore@v6 with: key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} path: ${{ github.workspace }}/Qt @@ -410,7 +410,7 @@ jobs: - name: "[macOS] Cache thin Qt libraries" if: matrix.os == 'macOS' && steps.restore_qt.outputs.cache-hit != 'true' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: key: thin-qt-macos-${{ matrix.soc }}-${{ steps.resolve_qt_version.outputs.version }} path: ${{ github.workspace }}/Qt @@ -473,7 +473,7 @@ jobs: - name: "[macOS] Save updated compiler cache (ccache)" if: matrix.os == 'macOS' && matrix.use_ccache == 1 && github.ref == 'refs/heads/master' - uses: actions/cache/save@v5 + uses: actions/cache/save@v6 with: key: ${{ steps.ccache_restore.outputs.cache-primary-key }} path: ${{ env.CCACHE_DIR }} diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 18679664b..166b807d9 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -83,6 +83,7 @@ set(cockatrice_SOURCES src/game/game_state.cpp src/game_graphics/game_view.cpp src/game_graphics/hand_counter.cpp + src/game/selection_subtype_tally.cpp src/game_graphics/log/message_log_widget.cpp src/game/phase.cpp src/game_graphics/phases_toolbar.cpp diff --git a/cockatrice/cockatrice_en@source.ts b/cockatrice/cockatrice_en@source.ts index 810b345df..e06972ddc 100644 --- a/cockatrice/cockatrice_en@source.ts +++ b/cockatrice/cockatrice_en@source.ts @@ -4,7 +4,7 @@ AbstractCounter - + &Set counter... @@ -12,12 +12,12 @@ AbstractCounterDialog - + Set counter - + New value for counter '%1': @@ -38,60 +38,60 @@ AbstractTabDeckEditor - + Open in new tab - + Are you sure? - + The decklist has been modified. Do you want to save the changes? - - - - - - + + + + + + Error - + Could not open deck at %1 - + Could not save remote deck - - + + The deck could not be saved. Please check that the directory is writable and try again. - + Save deck - + The deck could not be saved. - + There are no cards in your deck to be exported @@ -107,12 +107,12 @@ Please check that the directory is writable and try again. AdminNotesDialog - + Update Notes - + Admin Notes for %1 @@ -129,172 +129,227 @@ Please check that the directory is writable and try again. Sideboard + + + Tokens + + AppearanceSettingsPage - + seconds - + Error - + Could not create themes directory at '%1'. - + Theme settings - + Current theme: - + Open themes folder - + Home tab background source: - + Home tab background shuffle frequency: - + Disabled - - Display card name of background in bottom right: - - - - + Menu settings - + Show keyboard shortcuts in right-click menus - + Show game filter toolbar above list in room tab - + Card rendering - + Display card names on cards having a picture - + Auto-Rotate cards with sideways layout - + Override all card art with personal set preference (Pre-ProviderID change behavior) - + + Light + + + + + Dark + + + + + System + + + + + Active theme palette: + + + + + Edit theme palette + + + + + Home tab settings + + + + + Display card name of background in bottom right + + + + + Styling settings + + + + + Style user list + + + + + Card printings + + + + Bump sets that the deck contains cards from to the top in the printing selector - + Scale cards on mouse over - + Use rounded card corners - + + Card layout + + + + Minimum overlap percentage of cards on the stack and in vertical hand - + Maximum initial height for card view window: - - + + rows - + Maximum expanded height for card view window: - + Card counters - + Counter %1 - + Hand layout - + Display hand horizontally (wastes space) - + Enable left justification - + Table grid layout - + Invert vertical coordinate - + Minimum player count for multi-column layout: - + Maximum font size for information displayed on cards: @@ -302,12 +357,12 @@ Please check that the directory is writable and try again. ArchidektApiResponseDeckDisplayWidget - + Back to results - + Open Deck in Deck Editor @@ -333,111 +388,111 @@ Please check that the directory is writable and try again. BanDialog - + ban &user name - + ban &IP address - + ban client I&D - + Ban type - + &permanent ban - + &temporary ban - + &Days: - + &Hours: - + &Minutes: - + Duration of the ban - + Please enter the reason for the ban. This is only saved for moderators and cannot be seen by the banned person. - + Please enter the reason for the ban that will be visible to the banned person. - + Redact all messages from this user in all rooms - + &OK - + &Cancel - + Ban user from server - - - - + + + + Error - + You have to select a name-based, IP-based, clientId based, or some combination of the three to place a ban. - + You must have a value in the name ban when selecting the name ban checkbox. - + You must have a value in the ip ban when selecting the ip ban checkbox. - + You must have a value in the clientid ban when selecting the clientid ban checkbox. @@ -445,59 +500,123 @@ This is only saved for moderators and cannot be seen by the banned person. BetaReleaseChannel - + Beta - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the file update server. + + CardArtPreviewWidget + + + No card selected + + + + + CardArtRulesModel + + + Card + + + + + ProviderId + + + + + Mode + + + + + Reason + + + CardDatabaseModel - + Name - + Sets - + Mana cost - + Card type - + P/T - + Color(s) + + CardDatabaseView + + + Add to Deck + + + + + Add to Sideboard + + + + + Select Printing + + + + + Show on EDHRec (Commander) + + + + + Show on EDHRec (Card) + + + + + Show Related cards + + + CardFilter @@ -626,22 +745,22 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoPictureWidget - + View related cards - + Add card to deck - + Mainboard - + Sideboard @@ -664,12 +783,12 @@ This is only saved for moderators and cannot be seen by the banned person. - + Related cards: - + Unknown card: @@ -677,128 +796,146 @@ This is only saved for moderators and cannot be seen by the banned person. CardMenu - + Re&veal to... - + &All players - + View related cards - + Token: - + All tokens - + &Select All - + S&elect Row - + S&elect Column - + &Play - + &Hide - + Play &Face Down - + &Tap / Untap Turn sideways or back again - + Skip &untapping - + T&urn Over Turn face up/face down - + &Peek at card face - + &Clone - + Attac&h to card... - + Unattac&h - + &Draw arrow... - + &Set annotation... - + + Reduce life by power + + + + Ca&rd counters - + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... + + CardPictureLoaderCacheMethod + + + Network Cache + + + + + Filesystem + + + CardSizeWidget @@ -810,133 +947,133 @@ This is only saved for moderators and cannot be seen by the banned person. CardZoneLogic - + their hand nominative - + %1's hand nominative - + their library look at zone - + %1's library look at zone - + of their library top cards of zone, - + of %1's library top cards of zone - + their library reveal zone - + %1's library reveal zone - + their library shuffle - + %1's library shuffle - + their library nominative - + %1's library nominative - + their graveyard nominative - + %1's graveyard nominative - + their exile nominative - + %1's exile nominative - + their sideboard look at zone - + %1's sideboard look at zone - + their sideboard nominative - + %1's sideboard nominative - + their custom zone '%1' nominative - + %1's custom zone '%2' nominative @@ -958,16 +1095,424 @@ This is only saved for moderators and cannot be seen by the banned person. + + ColorButton + + + Click to pick a color + + + + + Pick colour + + + + + ConnectionController + + + + The server has reached its maximum user capacity, please check back later. + + + + + There are too many concurrent connections from your address. + + + + + Banned by moderator + + + + + Expected end time: %1 + + + + + This ban lasts indefinitely. + + + + + Scheduled server shutdown. + + + + + + Invalid username. + + + + + You have been logged out due to logging in at another location. + + + + + Connection closed + + + + + The server has terminated your connection. +Reason: %1 + + + + + The server is going to be restarted in %n minute(s). +All running games will be lost. +Reason for shutdown: %1 + + + + + + + + Scheduled server shutdown + + + + + Failed Login + + + + + Your client seems to be missing features this server requires for connection. + + + + + To update your client, go to 'Help -> Check for Client Updates'. + + + + + + + + + + + + + + + + + + + + + + Error + + + + + Incorrect username or password. Please check your authentication information and try again. + + + + + There is already an active session using this user name. +Please close that session first and re-login. + + + + + + You are banned until %1. + + + + + + You are banned indefinitely. + + + + + This server requires user registration. Do you want to register now? + + + + + This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client. +Please close and reopen your client to try again. + + + + + An internal error has occurred, please close and reopen Cockatrice before trying again. +If the error persists, ensure you are running the latest version of the software and if needed contact the software developers. + + + + + Account activation + + + + + Your account has not been activated yet. +You need to provide the activation token received in the activation email. + + + + + Server Full + + + + + Unknown login error: %1 + + + + + + +This usually means that your client version is out of date, and the server sent a reply your client doesn't understand. + + + + + + + + + + Registration denied + + + + + Registration is currently disabled on this server + + + + + There is already an existing account with the same user name. + + + + + It's mandatory to specify a valid email address when registering. + + + + + It appears you are attempting to register a new account on this server yet you already have an account registered with the email provided. This server restricts the number of accounts a user can register per address. Please contact the server operator for further assistance or to obtain your credential information. + + + + + Password too short. + + + + + Registration failed for a technical problem on the server. + + + + + The connection to the server has been lost. + + + + + Unknown registration error: %1 + + + + + Account activation failed + + + + + Socket error: %1 + + + + + Server timeout + + + + + You are trying to connect to an obsolete server. Please downgrade your Cockatrice version or connect to a suitable server. +Local version is %1, remote version is %2. + + + + + Your Cockatrice client is obsolete. Please update your Cockatrice version. +Local version is %1, remote version is %2. + + + + + + Success + + + + + Registration accepted. +Will now login. + + + + + Account activation accepted. +Will now login. + + + + + Information + + + + + This server supports additional features that your client doesn't have. +This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. + +To update your client, go to Help -> Check for Updates. + + + + + + + Reset Password + + + + + Your password has been reset successfully, you can now log in using the new credentials. + + + + + Failed to reset user account password, please contact the server operator to reset your password. + + + + + Activation request received, please check your email for an activation token. + + + + + Connecting to %1... + + + + + Registering to %1 as %2... + + + + + Disconnected + + + + + Connected, logging in at %1 + + + + + Requesting forgotten password to %1 as %2... + + + + + Your username must respect these rules: + + + + + is %1 - %2 characters long + + + + + can %1 contain lowercase characters + + + + + + + + NOT + + + + + can %1 contain uppercase characters + + + + + can %1 contain numeric characters + + + + + can contain the following punctuation: %1 + + + + + first character can %1 be a punctuation mark + + + + + no unacceptable language as specified by these server rules: + note that the following lines will not be translated + + + + + can not contain any of the following words: %1 + + + + + can not match any of the following expressions: %1 + + + + + You may only use A-Z, a-z, 0-9, _, ., and - in your username. + + + CustomZoneMenu - + C&ustom Zones - - + + View custom zone '%1' @@ -975,30 +1520,35 @@ This is only saved for moderators and cannot be seen by the banned person. DeckAnalyticsWidget - + Add Panel - + Remove Panel - + Save Layout - + Load Layout + + + Include Sideboard + + DeckEditorCardDatabaseDockWidget - + Card Database @@ -1014,47 +1564,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorDatabaseDisplayWidget - + Search by card name (or search expressions) - - Add to Deck - - - - - Add to Sideboard - - - - - Select Printing - - - - - Show on EDHRec (Commander) - - - - - Show on EDHRec (Card) - - - - - Show Related cards - - - - + Add card to &maindeck - + Add card to &sideboard @@ -1087,72 +1607,72 @@ This is only saved for moderators and cannot be seen by the banned person. - + Select Printing - + Deck - + Deck &name: - + Banner Card/Tags Visibility Settings - + Show banner card selection menu - + Show tags selection menu - + &Comments: - + Group by: - + Format: - + Hash: - + &Increment number - + &Decrement number - + &Remove row - + Swap card to/from sideboard @@ -1160,17 +1680,17 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorFilterDockWidget - + Filters - + &Clear all filters - + Delete selected @@ -1301,169 +1821,113 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorSettingsPage - - + + Update Spoilers - - + Success - + Download URLs have been reset. - - Downloaded card pictures have been reset. - - - - - Error - - - - - One or more downloaded card pictures could not be cleared. - - - - + Add URL - - + + URL: - - + + Edit URL - - Network Cache Size: - - - - - Redirect Cache TTL: - - - - - How long cached redirects for urls are valid for. - - - - - Picture Cache Size: - - - - + Add New URL - + Remove URL - - Day(s) - - - - + Updating... - + Choose path - + URL Download Priority - + Spoilers - + Download Spoilers Automatically - + Spoiler Location: - + Last Change - + Spoilers download automatically on launch - + Press the button to manually update without relaunching - + Do not close settings until manual update is complete - + Download card pictures on the fly - + How to add a custom URL - - Delete Downloaded Images - - - - + Reset Download URLs - - - On-disk cache for downloaded pictures - - - - - In-memory cache for pictures not currently on screen - - DeckListHistoryManagerWidget @@ -1529,12 +1993,12 @@ This is only saved for moderators and cannot be seen by the banned person. DeckLoader - + Common deck formats (%1) - + All files (*.*) @@ -1756,27 +2220,27 @@ This is only saved for moderators and cannot be seen by the banned person. - + Moved to %1 1 × "%2" (%3) - + Removed "%1" (all copies) - + %1 1 × "%2" (%3) - + Added - + Removed @@ -1798,74 +2262,74 @@ This is only saved for moderators and cannot be seen by the banned person. DeckViewContainer - + Load deck... - + Load remote deck... - + Load from clipboard... - + Load from website... - + Unload deck - + Ready to start - + Force start - + Sideboard unlocked - + Sideboard locked - - + + Error - + The selected file could not be loaded. - + Deck is greater than maximum file size. - + Are you sure you want to force start? This will kick all non-ready players from the game. - + Cockatrice @@ -1964,7 +2428,7 @@ This will kick all non-ready players from the game. - + Webpage @@ -2004,37 +2468,37 @@ This will kick all non-ready players from the game. - + Server URL - + Communication Port - + Unique Server Name - + Connection Warning - + You need to name your new connection profile. - + Connect Warning - + The player name can't be empty. @@ -2147,17 +2611,17 @@ This will kick all non-ready players from the game. - + Game information - + Error - + Server error. @@ -2165,97 +2629,97 @@ This will kick all non-ready players from the game. DlgCreateToken - + &Name: - + Token - + C&olor: - + white - + blue - + black - + red - + green - + multicolor - + colorless - + &P/T: - + &Annotation: - + &Destroy token when it leaves the table - + Create face-down (Only hides name) - + Token data - + Show &all tokens - + Show tokens from this &deck - + Choose token from list - + Create token @@ -2289,7 +2753,7 @@ This will kick all non-ready players from the game. - + @@ -2304,12 +2768,12 @@ This will kick all non-ready players from the game. - + Duplicate Tag - + This tag already exists. @@ -2495,12 +2959,12 @@ To remove your current avatar, confirm without choosing a new image. - + Error - + The chosen name conflicts with an existing card or token. Make sure to enable the 'Token' set in the "Manage sets" dialog to display them correctly. @@ -2524,12 +2988,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia - + Real name: - + Edit user profile @@ -2567,113 +3031,113 @@ Make sure to enable the 'Token' set in the "Manage sets" dia - + Hide 'buddies only' games - + Hide full games - + Hide games that have started - + Hide password protected games - + Hide 'ignored user' games - + Hide games not created by buddies Hide games not created by buddy - + Hide games with forced open decklists - + &Newer than: - + Game &description: - + &Creator name: - + General - + &Game types - + at &least: - + at &most: - + Maximum player count - + Restrictions - + Show games only if &spectators can watch - + Show spectator password p&rotected games - + Show only if spectators can ch&at - + Show only if spectators can see &hands - + Spectators - + Filter games @@ -2976,37 +3440,37 @@ https://tappedout.net/mtg-decks/your-deck-name/ DlgMoveTopCardsUntil - + Card name (or search expressions): - + Number of hits: - + Auto play hits - + Put top cards on stack until... - + No cards matching the search expression exists in the card database. Proceed anyways? - + Cockatrice - + Invalid filter @@ -3065,40 +3529,40 @@ Your email will be used to verify your account. - + Real name: - + Register to server - - - - + + + + Registration Warning - + Your password is too short. - + Your passwords do not match, please try again. - + Your email addresses do not match, please try again. - + The player name can't be empty. @@ -3106,17 +3570,17 @@ Your email will be used to verify your account. DlgRollDice - + Number of sides: - + Number of dice: - + Roll Dice @@ -3167,12 +3631,12 @@ Your email will be used to verify your account. DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -3183,7 +3647,7 @@ Would you like to change your database location setting? - + Your card database version is too old. This can cause problems loading card information or images @@ -3194,7 +3658,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -3203,21 +3667,21 @@ Would you like to change your database location setting? - + File Error loading your card database. Would you like to change your database location setting? - + Your card database was loaded but contains no cards. Would you like to change your database location setting? - + Unknown card database load status Please file a ticket at https://github.com/Cockatrice/Cockatrice/issues @@ -3226,59 +3690,64 @@ Would you like to change your database location setting? - - - + + + Error - + The path to your deck directory is invalid. Would you like to go back and set the correct path? - + The path to your card pictures directory is invalid. Would you like to go back and set the correct path? - + Settings - + General - + Appearance - + User Interface - + + Storage + + + + Card Sources - + Chat - + Sound - + Shortcuts @@ -3370,9 +3839,9 @@ You can always change this behavior in the 'General' settings tab. - - - + + + Error @@ -3424,31 +3893,31 @@ Please visit the download page to update manually. - + Update Available - + A new version of Cockatrice is available! - + New version - + Released - + Changelog @@ -3458,50 +3927,50 @@ Please visit the download page to update manually. - + Unfortunately, the automatic updater failed to find a compatible download. You may have to manually download the new version. - + Please check the <a href="%1">releases page</a> on our Github and download the build for your system. - - - + + + Update Error - + An error occurred while checking for updates: - + An error occurred while downloading an update: - + Installing... - + Cockatrice is unable to open the installer. - + Try to update manually by closing Cockatrice and running the installer. - + Download location @@ -3591,67 +4060,67 @@ You may have to manually download the new version. DrawProbabilityWidget - + Draw Probability - + Probability of drawing - + Card Name - + Type - + Subtype - + Mana Value - + At least - + Exactly - + card(s) having drawn at least - + cards - + Category - + Qty - + Odds (%) @@ -3765,7 +4234,7 @@ You may have to manually download the new version. FilterBuilder - + Type your filter here @@ -3796,22 +4265,22 @@ You may have to manually download the new version. GameEventHandler - + kicked by game host or moderator - + player left the game - + player disconnected from server - + reason unknown @@ -3819,140 +4288,140 @@ You may have to manually download the new version. GameSelector - - - - - - - - - + + + + + + + + + Error - + Please join the appropriate room first. - + Wrong password. - + Spectators are not allowed in this game. - + The game is already full. - + The game does not exist any more. - + This game is only open to registered users. - + This game is only open to its creator's buddies. - + You are being ignored by the creator of this game. - + Join Game - + Spectate Game - + Game Information - + Join Game as Judge - + Spectate Game as Judge - + Join game - + Password: - + Please join the respective room first. - + &Filter games - + C&lear filter - + C&reate - + &Join - + Join as judge - + J&oin as spectator - + Join as judge spectator - + Games shown: %1 / %2 - + Games @@ -3960,32 +4429,32 @@ You may have to manually download the new version. GameSelectorQuickFilterToolBar - + All types - + Filter by game name... - + Filter by game type/format - + Hide games not created by buddies - + Hide full games - + Hide started games @@ -3993,12 +4462,12 @@ You may have to manually download the new version. GamesModel - + >1 day - + %1%2 hr short age in hours @@ -4007,12 +4476,12 @@ You may have to manually download the new version. - + new - + %1%2 min short age in minutes @@ -4021,83 +4490,83 @@ You may have to manually download the new version. - + password - + buddies only - + reg. users only - + open decklists - + can chat - + see hands - + can see hands - + not allowed - + Room - + Age - + Description - + Creator - + Type - + Restrictions - + Players - + Spectators @@ -4105,143 +4574,158 @@ You may have to manually download the new version. GeneralSettingsPage - + Reset all paths - + All paths have been reset - - - - - - - + + + + + + + Choose path - - Personal settings + + Language settings - + Language: - + + Version settings + + + + + Card database + + + + + Startup settings + + + + Paths (editing disabled in portable mode) - + Paths - + How to help with translations - + Decks directory: - + Filters directory: - + Replays directory: - + Pictures directory: - + Card database: - + Custom database directory: - + Token database: - + Update channel - + Check for client updates on startup - + Check for card database updates on startup - + Don't check - + Prompt for update - + Always update in the background - + Check for card database updates every - + days - + Notify if a feature supported by the server is missing in my client - + Automatically run Oracle when running a new version of Cockatrice - + Show tips on startup - + Last update check on %1 (%2 days ago) @@ -4249,47 +4733,47 @@ You may have to manually download the new version. GraveyardMenu - + &Graveyard - + &View graveyard - + &Move graveyard to... - + &Top of library - + &Bottom of library - + &All players - + &Hand - + &Exile - + Reveal random card to... @@ -4297,88 +4781,88 @@ You may have to manually download the new version. HandMenu - + &Hand - + &View hand - + Sort hand by... - + Name - + Type - + Mana Value - + Take &mulligan (Choose hand size) - + Take mulligan (Same hand size) - + Take mulligan (Hand size - 1) - + &Move hand to... - + &Top of library - + &Bottom of library - + &Graveyard - + &Exile - + &Reveal hand to... - - + + All players - + Reveal r&andom card to... @@ -4386,52 +4870,52 @@ You may have to manually download the new version. HomeWidget - + Create New Deck - + Browse Decks - + Browse Card Database - + Browse EDHRec - + Browse Archidekt - + View Replays - + Quit - + Connecting... - + Connect - + Play @@ -4439,213 +4923,213 @@ You may have to manually download the new version. LibraryMenu - + &Library - + &View library - + View &top cards of library... - + View bottom cards of library... - + Reveal &library to... - + Lend library to... - + Reveal &top cards to... - + &Top of library... - + &Bottom of library... - + &Always reveal top card - + &Always look at top card - + &Open deck in deck editor - + &Draw card - + D&raw cards... - + &Undo last draw - + Shuffle - + &Play top card - + Play top card &face down - + Put top card on &bottom - + Move top card to grave&yard - + Move top card to e&xile - + Move top cards to &graveyard... - + Move top cards to graveyard face down... - + Move top cards to &exile... - + Move top cards to exile face down... - + Put top cards on stack &until... - + Shuffle top cards... - + &Draw bottom card - + D&raw bottom cards... - + &Play bottom card - + Play bottom card &face down - + Move bottom card to grave&yard - + Move bottom card to e&xile - + Move bottom cards to &graveyard... - + Move bottom cards to graveyard face down... - + Move bottom cards to &exile... - + Move bottom cards to exile face down... - + Put bottom card on &top - + Shuffle bottom cards... - - + + &All players - + Reveal top cards of library - + Number of cards: (max. %1) @@ -4653,655 +5137,300 @@ You may have to manually download the new version. MainWindow - - - The server has reached its maximum user capacity, please check back later. - - - - - There are too many concurrent connections from your address. - - - - - Banned by moderator - - - - - Expected end time: %1 - - - - - This ban lasts indefinitely. - - - - - Scheduled server shutdown. - - - - - - Invalid username. - - - - - You have been logged out due to logging in at another location. - - - - - Connection closed - - - - - The server has terminated your connection. -Reason: %1 - - - - - The server is going to be restarted in %n minute(s). -All running games will be lost. -Reason for shutdown: %1 - - - - - - - - Scheduled server shutdown - - - - - - Success - - - - - Registration accepted. -Will now login. - - - - - Account activation accepted. -Will now login. - - - - - + + Player %1 - + Load replay - + About Cockatrice - + Version - + Cockatrice Webpage - + Project Manager: - + Past Project Managers: - + Developers: - + Our Developers - + Help Develop! - + Translators: - + Our Translators - + Help Translate! - + Support: - + Report an Issue - + Troubleshooting - + F.A.Q. - - - - - - - - - - - - - - - - - - - - + + Error - - Server timeout - - - - - Failed Login - - - - - Your client seems to be missing features this server requires for connection. - - - - - To update your client, go to 'Help -> Check for Client Updates'. - - - - - Incorrect username or password. Please check your authentication information and try again. - - - - - There is already an active session using this user name. -Please close that session first and re-login. - - - - - - You are banned until %1. - - - - - - You are banned indefinitely. - - - - - This server requires user registration. Do you want to register now? - - - - - This server requires client IDs. Your client is either failing to generate an ID or you are running a modified client. -Please close and reopen your client to try again. - - - - - An internal error has occurred, please close and reopen Cockatrice before trying again. -If the error persists, ensure you are running the latest version of the software and if needed contact the software developers. - - - - - Account activation - - - - - Your account has not been activated yet. -You need to provide the activation token received in the activation email. - - - - - Server Full - - - - - Unknown login error: %1 - - - - - - -This usually means that your client version is out of date, and the server sent a reply your client doesn't understand. - - - - - Your username must respect these rules: - - - - - is %1 - %2 characters long - - - - - can %1 contain lowercase characters - - - - - - - - NOT - - - - - can %1 contain uppercase characters - - - - - can %1 contain numeric characters - - - - - can contain the following punctuation: %1 - - - - - first character can %1 be a punctuation mark - - - - - no unacceptable language as specified by these server rules: - note that the following lines will not be translated - - - - - can not contain any of the following words: %1 - - - - - can not match any of the following expressions: %1 - - - - - You may only use A-Z, a-z, 0-9, _, ., and - in your username. - - - - - - - - - - Registration denied - - - - - Registration is currently disabled on this server - - - - - There is already an existing account with the same user name. - - - - - It's mandatory to specify a valid email address when registering. - - - - - It appears you are attempting to register a new account on this server yet you already have an account registered with the email provided. This server restricts the number of accounts a user can register per address. Please contact the server operator for further assistance or to obtain your credential information. - - - - - Password too short. - - - - - Registration failed for a technical problem on the server. - - - - - The connection to the server has been lost. - - - - - Unknown registration error: %1 - - - - - Account activation failed - - - - - Socket error: %1 - - - - - You are trying to connect to an obsolete server. Please downgrade your Cockatrice version or connect to a suitable server. -Local version is %1, remote version is %2. - - - - - Your Cockatrice client is obsolete. Please update your Cockatrice version. -Local version is %1, remote version is %2. - - - - - Connecting to %1... - - - - - Registering to %1 as %2... - - - - - Disconnected - - - - - Connected, logging in at %1 - - - - - - - Requesting forgotten password to %1 as %2... - - - - + &Connect... - + &Disconnect - + Start &local game... - + &Watch replay... - + &Full screen - + &Register to server... - + &Restore password... - + &Settings... - + &Exit - + A&ctions - + &Cockatrice - + C&ard Database - + &Manage sets... - + Edit custom &tokens... - + Open custom image folder - + Open custom sets folder - + Add custom sets/cards - + Reload card database - + Tabs - + &Help - + &About Cockatrice - + &Tip of the Day - + Check for Client Updates - + Check for Card Updates... - + Check for Card Updates (Automatic) - + Show Status Bar - + View &Debug Log - + Open Settings Folder - + Show/Hide - + New Version - + Congratulations on updating to Cockatrice %1! Oracle will now launch to update your card database. - + Cockatrice installed - + Congratulations on installing Cockatrice %1! Oracle will now launch to install the initial card database. - + Card database - + Cockatrice is unable to load the card database. Do you want to update your card database now? If unsure or first time user, choose "Yes" - - + + Yes - - + + No - + Open settings - + New sets found - + %n new set(s) found in the card database Set code(s): %1 Do you want to enable it/them? @@ -5311,179 +5440,153 @@ Do you want to enable it/them? - + + Yes, always enable + + + + View sets - + Welcome - + Hi! It seems like you're running this version of Cockatrice for the first time. All the sets in the card database have been enabled. Read more about changing the set order or disabling specific sets and consequent effects in the "Manage Sets" dialog. - - + Information - + A card database update is already running. - + Unable to run the card database updater: - + Card database update running. - + Failed to start. The file might be missing, or permissions might be incorrect. - + The process crashed some time after starting successfully. - + Timed out. The process took too long to respond. The last waitFor...() function timed out. - + An error occurred when attempting to write to the process. For example, the process may not be running, or it may have closed its input channel. - + An error occurred when attempting to read from the process. For example, the process may not be running. - + Unknown error occurred. - + The card database updater exited with an error: %1 - - This server supports additional features that your client doesn't have. -This is most likely not a problem, but this message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. - -To update your client, go to Help -> Check for Updates. - - - - - - - - + + + + + Load sets/cards - + Selected file cannot be found. - + You can only import XML databases at this time. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. - - - - - Reset Password - - - - - Your password has been reset successfully, you can now log in using the new credentials. - - - - - Failed to reset user account password, please contact the server operator to reset your password. - - - - - Activation request received, please check your email for an activation token. - - ManaBaseConfigDialog - + Mana Base Configuration - + Display type: - + pie - + bar - + combinedBar - + Filter Colors (optional): - + OK - + Cancel @@ -5555,27 +5658,27 @@ Cockatrice will now reload the card database. ManaDevotionConfigDialog - + Display type: - + pie - + bar - + combinedBar - + Filter Colors (optional): @@ -5645,297 +5748,297 @@ Cockatrice will now reload the card database. MessageLogWidget - + from play - + from their graveyard - + from exile - + from their hand - + the top card of %1's library - + the top card of their library - + from the top of %1's library - + from the top of their library - + the bottom card of %1's library - + the bottom card of their library - + from the bottom of %1's library - + from the bottom of their library - + from %1's library - + from their library - + from sideboard - + from the stack - + from custom zone '%1' - + %1 is now keeping the top card %2 revealed. - + %1 is not revealing the top card %2 any longer. - + %1 can now look at top card %2 at any time. - + %1 no longer can look at top card %2 at any time. - + %1 attaches %2 to %3's %4. - + %1 has conceded the game. - + %1 has unconceded the game. - + %1 has restored connection to the game. - + %1 has lost connection to the game. - + %1 points from their %2 to themselves. - + %1 points from their %2 to %3. - + %1 points from %2's %3 to themselves. - + %1 points from %2's %3 to %4. - + %1 points from their %2 to their %3. - + %1 points from their %2 to %3's %4. - + %1 points from %2's %3 to their own %4. - + %1 points from %2's %3 to %4's %5. - + %1 creates a face down token. - + %1 creates token: %2%3. - + %1 has loaded a deck (%2). - + %1 has loaded a deck with %2 sideboard cards (%3). - + %1 destroys %2. - + a card - + %1 gives %2 control over %3. - + %1 puts %2 into play%3 face down. - + %1 puts %2 into play%3. - + %1 puts %2%3 into their graveyard face down. - + %1 puts %2%3 into their graveyard. - + %1 exiles %2%3 face down. - + %1 exiles %2%3. - + %1 moves %2%3 to their hand. - + %1 puts %2%3 into their library. - + %1 puts %2%3 onto the bottom of their library. - + %1 puts %2%3 on top of their library. - + %1 puts %2%3 into their library %4 cards from the top. - + %1 moves %2%3 to sideboard. - + %1 plays %2%3 face down. - + %1 plays %2%3. - + %1 moves %2%3 to custom zone '%4' face down. - + %1 moves %2%3 to custom zone '%4'. - + %1 tries to draw from an empty library - + %1 draws %2 card(s). @@ -5943,12 +6046,12 @@ Cockatrice will now reload the card database. - + %1 is looking at %2. - + %1 is looking at the %4 %3 card(s) %2. top card for singular, top %3 cards for plural @@ -5957,72 +6060,72 @@ Cockatrice will now reload the card database. - + bottom - + top - + %1 turns %2 face-down. - + %1 turns %2 face-up. - + The game has been closed. - + The game has started. - + You are flooding the game. Please wait a couple of seconds. - + %1 has joined the game. - + %1 is now watching the game. - + You have been kicked out of the game. - + %1 has left the game (%2). - + %1 is not watching the game any more (%2). - + %1 is not ready to start the game any more. - + %1 shuffles their deck and draws a new hand of %2 card(s). @@ -6030,28 +6133,28 @@ Cockatrice will now reload the card database. - + %1 shuffles their deck and draws a new hand. - + You are watching a replay of game #%1. - + %1 is ready to start the game. - + cards an unknown amount of cards - + %1 card(s) a card for singular, %1 cards for plural @@ -6060,213 +6163,218 @@ Cockatrice will now reload the card database. - + %1 lends %2 to %3. - + %1 reveals %2 to %3. - + %1 reveals %2. - + %1 randomly reveals %2%3 to %4. - + %1 randomly reveals %2%3. - + %1 peeks at face down card #%2. - + %1 peeks at face down card #%2: %3. - + %1 reveals %2%3 to %4. - + %1 reveals %2%3. - + %1 reversed turn order, now it's %2. - + reversed - + normal - + Heads - + Tails - + %1 flipped a coin. It landed as %2. - + %1 rolls a %2 with a %3-sided die. - + %1 flips %2 coins. There are %3 heads and %4 tails. - + %1 rolls a %2-sided dice %3 times: %4. - + %1's turn. - + %1 sets annotation of %2 to %3. - - %1 places %2 "%3" counter(s) on %4 (now %5). + + %1 places %2 %3%4 counter(s) on %5 (now %6). - - %1 removes %2 "%3" counter(s) from %4 (now %5). + + %1 removes %2 %3%4 counter(s) from %5 (now %6). - + + %1 failed to undo their last draw. + + + + %1 sets counter %2 to %3 (%4%5). - + %1 sets %2 to not untap normally. - + %1 sets %2 to untap normally. - + %1 removes the PT of %2. - + %1 changes the PT of %2 from nothing to %4. - + %1 changes the PT of %2 from %3 to %4. - + %1 has locked their sideboard. - + %1 has unlocked their sideboard. - + %1 taps their permanents. - + %1 untaps their permanents. - + %1 taps %2. - + %1 untaps %2. - + %1 shuffles %2. - + %1 shuffles the bottom %3 cards of %2. - + %1 shuffles the top %3 cards of %2. - + %1 shuffles cards %3 - %4 of %2. - + %1 unattaches %2. - + %1 undoes their last draw. - + %1 undoes their last draw (%2). @@ -6274,110 +6382,115 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Word1 Word2 Word3 - + Add New Message - + Edit Message - + Remove Message - + Add message - - + + Message: - + Edit message - + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + How to use in-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered users - - + + Ignore private messages sent by non-buddy users + + + + + Invert text color - + Enable desktop notifications for private messages - + Enable desktop notification for mentions - + Enable room message history on join - - + + (Color is hexadecimal) - + Separate words with a space, alphanumeric characters only @@ -6385,42 +6498,42 @@ Cockatrice will now reload the card database. MoveMenu - + Move to - + &Top of library in random order - + X cards from the top of library... - + &Bottom of library in random order - + T&able - + &Hand - + &Graveyard - + &Exile @@ -6433,47 +6546,47 @@ Cockatrice will now reload the card database. - + Mana Value - + Color(s) - + Loyalty - + Main Card Type - + Mana Cost - + P/T - + Side - + Layout - + Color Identity @@ -6496,6 +6609,152 @@ Cockatrice will now reload the card database. + + PaletteEditorDialog + + + + Reset + + + + + + Apply + + + + + + Save && Apply + + + + + ▼ Edit Palette + + + + + + ▶ Edit Palette + + + + + Palette Editor — %1 + + + + + <b>Palette Editor</b> &nbsp;·&nbsp; %1 + + + + + This theme ships no default palette files + + + + + Replace current colours with the theme author's defaults + + + + + Switch between the light and dark palette files + + + + + Editing: + + + + + Show or hide the per-role colour grid for manual tweaks + + + + + ↺ Revert to theme default + + + + + Discard unsaved edits and restore the last saved palette + + + + + Preview this palette without saving to disk + + + + + Write palette-%1.toml and reload the theme + + + + + Cannot save: this theme has no directory on disk + + + + + Save failed + + + + + Could not write %1 to: +%2 + + + + + No default found + + + + + No default palette file found for the "%1" scheme. + + + + + PaletteGridWidget + + + Active + + + + + Disabled + + + + + Inactive + + + + + Normal interactive state + + + + + Widget is disabled / not interactive + + + + + Window is in background / unfocused + + + Phase @@ -6562,57 +6821,57 @@ Cockatrice will now reload the card database. PhasesToolbar - + Untap step - + Upkeep step - + Draw step - + First main phase - + Beginning of combat step - + Declare attackers step - + Declare blockers step - + Combat damage step - + End of combat step - + Second main phase - + End of turn step @@ -6620,7 +6879,7 @@ Cockatrice will now reload the card database. PictureLoader - + en code for scryfall's language property, not available for all languages @@ -6629,151 +6888,142 @@ Cockatrice will now reload the card database. PlayerActions - - View top cards of library - - - - - - - - - - - - - Number of cards: (max. %1) - - - - - View bottom cards of library - - - - - Shuffle top cards of library - - - - - Shuffle bottom cards of library - - - - - Draw hand - - - - - 0 and lower are in comparison to current hand size - - - - - Draw cards - - - - - - - + + + + grave - - - - + + + + exile + + + PlayerDialogs - + + View top cards of library + + + + + + + + + + + + + Number of cards: (max. %1) + + + + + View bottom cards of library + + + + + Shuffle top cards of library + + + + + Shuffle bottom cards of library + + + + + Draw hand + + + + + 0 and lower are in comparison to current hand size + + + + + Draw cards + + + + Move top cards to %1 - + Move bottom cards to %1 - + Draw bottom cards - - - C&reate another %1 token - - - - + Create tokens - - + Number: - + Place card X cards from top of library - + Which position should this card be placed: - + (max. %1) - + Change power/toughness - + Change stats to: - + Set annotation - + Please enter the new annotation: - - - Set counters - - PlayerMenu - + Player "%1" - + &Counters @@ -6803,7 +7053,7 @@ This setting means you'll only see the default printing for each card, inst - + Printing Selector @@ -6811,22 +7061,22 @@ This setting means you'll only see the default printing for each card, inst PrintingSelectorCardOverlayWidget - + Preference - + Pin Printing - + Unpin Printing - + Show Related cards @@ -6897,57 +7147,57 @@ This setting means you'll only see the default printing for each card, inst PtMenu - + Power / toughness - + &Increase power - + &Decrease power - + I&ncrease toughness - + D&ecrease toughness - + In&crease power and toughness - + Dec&rease power and toughness - + Increase power and decrease toughness - + Decrease power and increase toughness - + Set &power and toughness... - + Reset p&ower and toughness @@ -6955,37 +7205,37 @@ This setting means you'll only see the default printing for each card, inst QMenuBar - + Services - + Hide %1 - + Hide Others - + Show All - + Preferences... - + Quit %1 - + About %1 @@ -6993,17 +7243,17 @@ This setting means you'll only see the default printing for each card, inst QObject - + Cockatrice card database (*.xml) - + All files (*.*) - + Cockatrice replays (*.cor) @@ -7063,110 +7313,177 @@ Are you sure you would like to disable this feature? QPlatformTheme - + OK - + Save - + Save All - + Open - + &Yes - + Yes to &All - + &No - + N&o to All - + Abort - + Retry - + Ignore - + Close - + Cancel - + Discard - + Help - + Apply - + Reset - + Restore Defaults + + QuickSetupPanel + + + %1% + + + + + <b>Quick Setup</b> + + + + + Generate all palette roles automatically from a single accent colour + + + + + Accent: + + + + + Primary hue. Used directly for highlights and links. +At high intensity it also tints buttons and backgrounds. + + + + + Intensity: + + + + + Subtle + + + + + Full colour + + + + + 0–30 Subtle tint — only highlights and links change hue +30–70 Accented — buttons, tooltips, and borders join in +70–100 Full colour — backgrounds, everything + + + + + 70% + + + + + Generate ↓ + + + + + Derive all palette roles from the accent colour above. +Fine-tune individual colours in the grid afterwards. + + + RemoteDeckList_TreeModel - + Name - + ID - + Upload time @@ -7174,32 +7491,32 @@ Are you sure you would like to disable this feature? RemoteReplayList_TreeModel - + ID - + Name - + Players - + Keep - + Time started - + Duration (sec) @@ -7207,37 +7524,37 @@ Are you sure you would like to disable this feature? RfgMenu - + &Exile - + &View exile - + &Move exile to... - + &Top of library - + &Bottom of library - + &Hand - + &Graveyard @@ -7283,7 +7600,7 @@ Are you sure you would like to disable this feature? SayMenu - + S&ay @@ -7324,27 +7641,27 @@ Are you sure you would like to disable this feature? SetsModel - + Enabled - + Set type - + Set code - + Long name - + Release date @@ -7352,53 +7669,53 @@ Are you sure you would like to disable this feature? ShortcutSettingsPage - - + + Restore all default shortcuts - + Do you really want to restore all default shortcuts? - + Clear all default shortcuts - + Do you really want to clear all shortcuts? - + Section: - + Action: - + Shortcut: - + How to set custom shortcuts - + Clear all shortcuts - + Search by shortcut name @@ -7452,12 +7769,12 @@ Please check your shortcut settings! SideboardMenu - + &Sideboard - + &View sideboard @@ -7465,27 +7782,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -7557,16 +7874,146 @@ Please check your shortcut settings! - + No reply received from the tag update server. - + Invalid reply received from the tag update server. + + StorageSettingsPage + + + + + Success + + + + + Cached card pictures have been reset. + + + + + Downloaded card pictures have been reset. + + + + + Error + + + + + One or more downloaded card pictures could not be cleared. + + + + + In-memory (currently loaded) card pictures have been reset. + + + + + Card Picture Loader Caching Method: + + + + + The network cache is the preferred way of storing images. Downloaded images are stored here until the size of the cache exceeds the configured size. Cockatrice automatically monitors this cache and deletes the least recently seen card images to ensure the cache does not exceed the configured size. + + + + + Writing card images directly to a folder on your hard drive is another way of storing images. This does not change how Cockatrice accesses or downloads images. Cockatrice will NOT automatically monitor and clear this folder, so if you enable this option, it is up to you to ensure sufficient available space. It should also be noted that if a provider outage causes you to download the wrong picture (i.e. wrong printing) you will be stuck with it until you manually delete the file, as opposed to using the network cache, which automatically rotates and thus correct errors after a while. + + + + + This is the in-memory picture cache used by the application at runtime. It determines how much memory (RAM) Cockatrice can use before it has to fetch card images from the hard disk again. Increasing this will allow more card images to be displayed at once but shouldn't be necessary. Clearing this will make Cockatrice reload all images from the network cache or the disk. + + + + + Delete Cached Images + + + + + Delete Saved Images + + + + + Clear In-Memory Images + + + + + Card Picture Loader Cache Method + + + + + Network Cache + + + + + Filesystem + + + + + In-Memory Picture Cache + + + + + Network Cache Size: + + + + + On-disk cache for downloaded pictures + + + + + Redirect Cache TTL: + + + + + How long cached redirects for urls are valid for. + + + + + Picture Cache Size: + + + + + In-memory cache for pictures not currently on screen + + + + + Naming scheme: + + + + + Day(s) + + + TabAccount @@ -7702,117 +8149,117 @@ Please check your shortcut settings! TabArchidekt - - + + Desc. - - + + AND - - + + Require ALL selected colors - - + + Deck name... - - + + Owner... - - + + Packages - - + + Advanced Filters - + Bracket: - - + + Any - - + + Contains card... - - + + Commander... - - + + Tag... - - + + Deck Size - + Cards: - - + + Asc. - + Sort by: - + Filter by: - + Display Settings - - + + Search - - + + Formats @@ -7822,6 +8269,54 @@ Please check your shortcut settings! + + TabCardArtRules + + + Card: + + + + + ProviderId: + + + + + Mode: + + + + + Reason: + + + + + Type a card name... + + + + + Add rule + + + + + Remove rule + + + + + Refresh + + + + + Card Art Rules + + + TabDeckEditor @@ -7870,7 +8365,7 @@ Please check your shortcut settings! - + Deck: %1 @@ -7878,55 +8373,49 @@ Please check your shortcut settings! TabDeckEditorVisual - + Visual Deck: %1 - + &Visual Deck Editor - - + + Card Info - - + + Deck - - - Filters - - - - + &View - + Printing - + Visible - + Floating - + Reset layout @@ -7934,22 +8423,22 @@ Please check your shortcut settings! TabDeckEditorVisualTabWidget - + Visual Deck View - + Visual Database Display - + Deck Analytics - + Sample Hand @@ -7957,133 +8446,133 @@ Please check your shortcut settings! TabDeckStorage - + Local file system - + Server deck storage - - + + Open in deck editor - + Rename deck or folder - + Upload deck - + Download deck - - - + + + New folder - + Delete - + Open decks folder - + Rename local folder - + Rename local file - + New name: - - - - + + + + Error - + Rename failed - - + + Invalid deck file - + Enter deck name - + This decklist does not have a name. Please enter a name: - + Unnamed deck - + Failed to upload deck to server - + Delete local file - + Are you sure you want to delete the selected files? - + Delete remote decks - + Are you sure you want to delete the selected decks? - - + + Name of new folder: @@ -8155,191 +8644,191 @@ Please enter a name: TabGame - - - + + + Replay - - + + Game - - + + Player List - - + + Card Info - - + + Messages - - + + Replay Timeline - + &Phases - + &Game - + Next &phase - + Next phase with &action - + Next &turn - + Reverse turn order - + &Remove all local arrows - + Rotate View Cl&ockwise - + Rotate View Co&unterclockwise - + Game &information - + Un&concede - - - + + + &Concede - + &Leave game - + C&lose replay - + &Focus Chat - + &Say: - + Selected cards - + &View - + Visible - + Floating - + Reset layout - + Concede - + Are you sure you want to concede this game? - + Unconcede - + You have already conceded. Do you want to return to this game? - + Leave game - + Are you sure you want to leave this game? - + A player has joined game #%1 - + %1 has joined the game - + You have been kicked out of the game. @@ -8398,114 +8887,114 @@ Please enter a name: - + Username: - + IP Address: - + Game Name: - + GameID: - + Message: - + Main Room - + Game Room - + Private Chat - + Past X Days: - + Today - + Last Hour - + Maximum Results: - + At least one filter is required. The more information you put in, the more specific your results will be. - + Get User Logs - + Clear Filters - + Filters - + Log Locations - + Date Range - + Maximum Results - - + + Message History - + Failed to collect message history information. - + There are no messages for the selected filters. @@ -8523,27 +9012,27 @@ The more information you put in, the more specific your results will be. - + %1 - Private chat - + This user is ignoring you, they cannot see your messages in main chat and you cannot join their games. - + Private message from - + %1 has left the server. - + %1 has joined the server. @@ -8551,180 +9040,180 @@ The more information you put in, the more specific your results will be. TabReplays - + Local file system - + Server replay storage - - + + Watch replay - + Rename - - + + New folder - - + + Delete - + Open replays folder - + Download replay - + Toggle expiration lock - + Get replay share code - - + + Look up replay by share code - + Rename local folder - + Rename local file - + New name: - + Error - + Rename failed - + Name of new folder: - + Delete local file - + Are you sure you want to delete the selected files? - + Are you sure you want to delete the selected replays? - + Failed to get code - - + + Either this server does not support replay sharing, or does not permit replay sharing for you. - - - + + + Failed - + Could not get replay code - + Replay Share Code - + Others can use this code to add the replay to their list of remote replays: %1 - + Copy to clipboard - + Replay share code - + Replay code found - + Replay was added, or you already had access to it. - + Replay code not found - + Failed to submit code - + Unexpected error - + Delete remote replay @@ -8737,47 +9226,62 @@ The more information you put in, the more specific your results will be. TabRoom - + + Friends + + + + + Online + + + + + Ignored + + + + &Say: - + Chat - + &Room - + &Leave room - + &Clear chat - + Chat Settings... - + mentioned you. - + Click to view - + You are flooding the chat. Please wait a couple of seconds. @@ -8790,30 +9294,30 @@ The more information you put in, the more specific your results will be. - - - - + + + + Error - + Failed to join the server room: it doesn't exist on the server. - + The server thinks you are in the server room but your client is unable to display it. Try restarting your client. - + You do not have the required permission to join this server room. - + Failed to join the server room due to an unknown error: %1. @@ -8821,97 +9325,97 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Deck Editor - + Visual Deck Editor - + EDHRec - + Archidekt - + Home - + &Visual Deck Storage - + Visual Database Display - + Server - + Account - + Deck Storage - + Game Replays - + Administration - + Logs - + Are you sure? - + There are still open games. Are you sure you want to quit? - + Click to view - + Your buddy %1 has signed on! - + Unknown Event - + The server has sent you a message that your client does not understand. This message might mean there is a new version of Cockatrice available or this server is running a custom or pre-release version. @@ -8919,38 +9423,38 @@ To update your client, go to Help -> Check for Updates. - + Idle Timeout - + You are about to be logged out due to inactivity. - + Promotion - + You have been promoted. Please log out and back in for changes to take effect. - + Warned - + You have received a warning due to %1. Please refrain from engaging in this activity or further actions may be taken against you. If you have any questions, please private message a moderator. - + You have received the following message from the server. (custom messages like these could be untranslated) @@ -8959,12 +9463,12 @@ Please refrain from engaging in this activity or further actions may be taken ag TabVisualDatabaseDisplay - + Database Display - + Visual Database Display @@ -9001,42 +9505,42 @@ Please refrain from engaging in this activity or further actions may be taken ag TranslateCounterName - + Life - + White - + Blue - + Black - + Red - + Green - + Colorless - + Other @@ -9044,11 +9548,69 @@ Please refrain from engaging in this activity or further actions may be taken ag UpdateDownloader - + Could not open the file for reading. + + UserCardArtSettingsDialog + + + Card Art Settings + + + + + Type a card name... + + + + + Card name: + + + + + Card ProviderId: + + + + + Left margin (%): + + + + + Right margin (%): + + + + + Vertical offset: + + + + + Zoom: + + + + + Parameters + + + + + Preview + + + + + Remove Banner Card + + + UserContextMenu @@ -9166,7 +9728,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Ban History @@ -9181,77 +9743,87 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Failed to collect ban information. - - - + + + Warning History - + Warning Time;Moderator;User Name;Reason - + User has never been warned. - + Failed to collect warning information. - + Failed to get admin notes. - - + + Success - + Successfully promoted user. - + Successfully demoted user. - - - + + Kick Player + + + + + Are you sure you want to kick this player from the game? + + + + + + Failed - + Failed to promote user. - + Failed to demote user. - + Copy hash to clipboard - + Remove this user's messages @@ -9259,109 +9831,116 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInfoBox - + Location: - + Account Age: - + Edit - + Change password - + Change avatar - + Administrator - + Moderator - + Registered user - - + + Unregistered user - + Judge - + Unknown - + The entered password does not match your account. - - - + + + + Information - + User information updated. - - - - - - - - - - + + + + + + + + + + + Error - + User Information - + Real Name: - + User Level: + + + Edit Banner Card + + - + %n Year(s), amount of years (only shown if more than 0) @@ -9370,7 +9949,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + %10%n Day(s) %20 amount of years (if more than 0), amount of days, date in local short format @@ -9379,212 +9958,433 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Enter Password - + Password verification is required in order to change your email address - - - + + + An error occurred while trying to update your user information. - - This server does not permit you to update your user informations. + + The selected card is blacklisted on this server or another error occurred. - - Password changed. + + Banner card removed. - - This server does not permit you to change your password. - - - - - The new password is too short. - - - - - The old password is incorrect. - - - - - Avatar updated. - - - - - This server does not permit you to update your avatar. + + Banner card updated. - An error occured while trying to updater your avatar. + This server does not permit you to update your user informations. + + + + + Password changed. + + + + + This server does not permit you to change your password. + + + + + The new password is too short. + + + + + The old password is incorrect. + + + + + Avatar updated. + + + + + This server does not permit you to update your avatar. + + + + + An error occured while trying to update your avatar. + + + + + This server does not permit you to update your user informations. + An error occured while trying to updater your avatar. + + + + + UserInfoPopup + + + + Games + + + + + Chat + + + + + Open private chat + + + + + Profile + + + + + View user profile + + + + + Show this user's games + + + + + − Buddy + + + + + Remove from buddy list + + + + + + Buddy + + + + + Add to buddy list + + + + + − Ignore + + + + + Remove from ignore list + + + + + + Ignore + + + + + Add to ignore list + + + + + Ban + + + + + Ban from server + + + + + Warn + + + + + Warn user + + + + + Ban log + + + + + View ban history + + + + + Warn log + + + + + View warning history + + + + + Notes + + + + + View admin notes + + + + + − Mod + + + + + Demote from moderator + + + + + + Mod + + + + + Promote to moderator + + + + + − Judge + + + + + Demote from judge + + + + + + Judge + + + + + Promote to judge + + + + + Join game + + + + + Spectate + + + + + + Loading games… + + + + + Could not load games. + + + + + No active games. UserInterfaceSettingsPage - + General interface settings - + &Double-click cards to play them (instead of single-click) - + &Clicking plays all selected cards (instead of just the clicked card) - + &Play all nonlands onto the stack (not the battlefield) by default - + Do not delete &arrows inside of subphases - + Close card view window when last card is removed - + Auto focus search bar when card view window is opened - + Annotate card text on tokens - - Show selection counter during drag selection + + Show selection count during drag selection - - Show total selection counter + + Show total selection count - + + Show subtype breakdown in selection tally + + + + Use tear-off menus, allowing right click menus to persist on screen - + + Keep game chat focused when clicking in game (Note: disables card view search bar) + + + + Notifications settings - + Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating - + Notify in the taskbar when users in your buddy list connect - + Animation settings - + &Tap/untap animation - + Deck editor/storage settings - + Open deck in new tab by default - + Use visual deck storage in game lobby - + Use selection animation for Visual Deck Storage - + When adding a tag in the visual deck storage to a .txt deck: - + do nothing - + ask to convert to .cod - + always convert to .cod - + Default deck editor type - + Classic Deck Editor - + Visual Deck Editor - + Replay settings - + Buffer time for backwards skip via shortcut: @@ -9592,22 +10392,22 @@ Please refrain from engaging in this activity or further actions may be taken ag UserListWidget - + Users connected to server: %1 - + Users in this room: %1 - + Buddies online: %1 / %2 - + Ignored users online: %1 / %2 @@ -9615,35 +10415,45 @@ Please refrain from engaging in this activity or further actions may be taken ag UtilityMenu - + Increment all card counters - + &Untap all permanents - + R&oll die... - + + Flip coin + + + + &Create token... - + C&reate another token - + Cr&eate predefined token + + + C&reate another %1 token + + VisualDatabaseDisplayColorFilterWidget @@ -9695,72 +10505,72 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayFilterToolbarWidget - + Sort by - + Filter by - + Save and load filters - + Filter by exact card name - + Filter by card main-type - + Filter by card sub-type - + Filter by set - + Filter by format legality - + Save/Load - + Name - + Main Type - + Sub Type - + Sets - + Formats @@ -9768,22 +10578,22 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayFormatLegalityFilterWidget - + Show formats with at least: - + cards - + Do not display formats with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) @@ -9801,32 +10611,32 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayMainTypeFilterWidget - + Show main types with at least: - + cards - + Do not display card main-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9834,27 +10644,27 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayNameFilterWidget - + Filter by name... (Exact match) - + Load from Deck - + Apply all card names in currently loaded deck as exact match name filters - + Load from Clipboard - + Apply all card names in clipboard as exact match name filters @@ -9862,7 +10672,7 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayRecentSetFilterSettingsWidget - + Filter to most recent sets @@ -9870,19 +10680,19 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySetFilterWidget - + Search sets... - - + + Mode: Exact Match - - + + Mode: Includes @@ -9890,37 +10700,37 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplaySubTypeFilterWidget - + Search subtypes... - + Show sub types with at least: - + cards - + Do not display card sub-types with less than this amount of cards in the database - + Filter mode (AND/OR/NOT conjunctions of filters) - + Mode: Exact Match - + Mode: Includes @@ -9928,23 +10738,23 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDatabaseDisplayWidget - + Search by card name (or search expressions) - + Visual - + Loading database ... - + Clear all filters @@ -9983,17 +10793,17 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Toggle Layout: Overlap - + Change how cards are displayed within zones (i.e. overlapped or fully visible.) - + Toggle Layout: Flat @@ -10022,17 +10832,17 @@ Please refrain from engaging in this activity or further actions may be taken ag VisualDeckEditorWidget - + Type a card name here for suggestions from the database... - + Quick search and add card - + Search for closest match in the database (with auto-suggestions) and add preferred printing to the deck on pressing enter @@ -10150,43 +10960,43 @@ Please refrain from engaging in this activity or further actions may be taken ag WarningDialog - + Which warning would you like to send? - + Redact all messages from this user in all rooms - + &OK - + &Cancel - + Warn user for misconduct - - + + Error - + User name to send a warning to can not be blank, please specify a user to warn. - + Warning to use can not be blank, please select a valid warning to send. @@ -10194,133 +11004,133 @@ Please refrain from engaging in this activity or further actions may be taken ag WndSets - + Move selected set to the top - + Move selected set up - + Move selected set down - + Move selected set to the bottom - + Search by set name, code, or type - + Default order - + Restore original art priority order - + Enable all sets - + Disable all sets - + Enable selected set(s) - + Disable selected set(s) - + Deck Editor - + Use CTRL+A to select all sets in the view. - + Only cards in enabled sets will appear in the card list of the deck editor. - + Image priority is decided in the following order: - + first the CUSTOM Folder (%1), then the Enabled Sets in this dialog (Top to Bottom) %1 is a link to the wiki - + Include cards rebalanced for Alchemy [requires restart] - + Card Art - + How to use custom card art - + Hints - + Note - + Sorting by column allows you to find a set while not changing set priority. - + To enable ordering again, click the column header until this message disappears. - + Use the current sorting as the set priority instead - + Sorts the set priority using the same column - + Manage sets @@ -10328,72 +11138,72 @@ Please refrain from engaging in this activity or further actions may be taken ag ZoneViewWidget - + Search by card name (or search expressions) - + Ungrouped - + Group by Type - + Group by Mana Value - + Group by Color - + Unsorted - + Sort by Name - + Sort by Type - + Sort by Mana Cost - + Sort by Colors - + Sort by P/T - + Sort by Set - + shuffle when closing - + pile view @@ -10401,7 +11211,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English @@ -10409,12 +11219,12 @@ Please refrain from engaging in this activity or further actions may be taken ag main - + Connect on startup - + Debug to file @@ -10428,7 +11238,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Deck Editor @@ -10509,7 +11319,7 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Replays @@ -10624,129 +11434,129 @@ Please refrain from engaging in this activity or further actions may be taken ag - - + + Load Deck from Clipboard... - + Edit Deck in Clipboard, Annotated - + Edit Deck in Clipboard - + New Deck - + Open Custom Pictures Folder - + Print Deck... - + Delete Card - - + + Reset Layout - + Save Deck - + Save Deck as... - + Save Deck to Clipboard, Annotated - + Save Deck to Clipboard, Annotated (No Set Info) - + Save Deck to Clipboard - + Save Deck to Clipboard (No Set Info) - + Load Local Deck... - + Load Remote Deck... - + Set Ready to Start - + Toggle Sideboard Lock - + Add Green Counter - + Remove Green Counter - + Set Green Counters... - + Add Red Counter - + Remove Red Counter - + Set Red Counters... - + Add Life Counter @@ -10756,724 +11566,744 @@ Please refrain from engaging in this activity or further actions may be taken ag - + + Load deck from online service... + + + + + Load from website... + + + + Unload Deck - + Force Start - + Add Card Counter (F) - + Remove Card Counter (F) - + Set Card Counters (F)... - + Add Card Counter (E) - + Remove Card Counter (E) - + Set Card Counters (E)... - + Add Card Counter(D) - + Remove Card Counter (D) - + Set Card Counters (D)... - + Add Card Counter (C) - + Remove Card Counter (C) - + Set Card Counters (C)... - + Add Card Counter (B) - + Remove Card Counter (B) - + Set Card Counters (B)... - + Add Card Counter (A) - + Remove Card Counter (A) - + Set Card Counters (A)... - + Remove Life Counter - + Set Life Counters... - + Add White Counter - + Remove White Counter - + Set White Counters... - + Add Blue Counter - + Remove Blue Counter - + Set Blue Counters... - + Add Black Counter - + Remove Black Counter - + Set Black Counters... - + Add Colorless Counter - + Remove Colorless Counter - + Set Colorless Counters... - + Add Other Counter - + Remove Other Counter - + Set Other Counters... - + Increment all card counters - + Add Power (+1/+0) - + Remove Power (-1/-0) - + Move Toughness to Power (+1/-1) - + Add Toughness (+0/+1) - + Remove Toughness (-0/-1) - + Move Power to Toughness (-1/+1) - + Add Power and Toughness (+1/+1) - + Remove Power and Toughness (-1/-1) - + Set Power and Toughness... - + Reset Power and Toughness - + Untap - + Upkeep - + Draw - + First Main Phase - + Start Combat - + Attack - + Block - + Damage - + End Combat - + Second Main Phase - + End - + Next Phase - + Next Phase Action - + Next Turn - + Hide Card in Reveal Window - + Tap / Untap Card - + Untap All - + Toggle Skip Untapping Toggle Untap - + Turn Card Over - + Peek Card - + Play Card - + Play Card, Face Down - + Attach Card... - + Unattach Card - + Clone Card - + Create Token... - + Create All Related Tokens - + Create Another Token - + Set Annotation... - + + Reduce Life by Power + + + + Select All Cards in Zone - + Select All Cards in Row - + Select All Cards in Column - + Reveal Selected Cards to All Players - - + + Bottom of Library - - - - + + + + Exile - - - - + + + + Graveyard - - + + Hand - - + + Top of Library - - + + Battlefield, Face Down - + Battlefield - + Library - + Sideboard - + Top Cards of Library - + Bottom Cards of Library - + Close Recent View - - + + Stack - - + + Graveyard (Multiple) - - + + Graveyard (Multiple), Face Down - - + + Exile (Multiple) - - + + Exile (Multiple), Face Down - + Stack Until Found - + Draw Bottom Card - + Draw Multiple Cards from Bottom... - + Draw Arrow... - + Remove Local Arrows - + Leave Game - + Concede - + Roll Dice... - + + Flip Coin + + + + Shuffle Library - + Shuffle Top Cards of Library - + Shuffle Bottom Cards of Library - + Mulligan - + Mulligan (Same hand size) - + Mulligan (Hand size - 1) - + Draw a Card - + Draw Multiple Cards... - + Undo Draw - + Always Reveal Top Card - + Always Look At Top Card - + Sort Hand by Name - + Sort Hand by Type - + Sort Hand by Mana Value - + Reveal Hand to All Players - + Reveal Random Card to All Players - + Rotate View Clockwise - + Rotate View Counterclockwise - + Unfocus Text Box - + Focus Chat - + Clear Chat - + Refresh - + Skip Forward - + Skip Backward - + Skip Forward by a lot - + Skip Backward by a lot - + Play/Pause - + Toggle Fast Forward - + Home - + Visual Deck Storage - + Deck Storage - + Server - + Account - + Administration - + Logs diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 28e5eb187..b6bc8a47d 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -313,6 +313,7 @@ SettingsCache::SettingsCache() showDragSelectionCount = settings->value("interface/showlassoselectioncount", true).toBool(); showTotalSelectionCount = settings->value("interface/showpersistentselectioncount", true).toBool(); + showSubtypeSelectionTally = settings->value("interface/showsubtypeselectiontally", true).toBool(); showShortcuts = settings->value("menu/showshortcuts", true).toBool(); showGameSelectorFilterToolbar = settings->value("menu/showgameselectorfiltertoolbar", true).toBool(); @@ -1395,6 +1396,12 @@ void SettingsCache::setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSele settings->setValue("interface/showpersistentselectioncount", showTotalSelectionCount); } +void SettingsCache::setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally) +{ + showSubtypeSelectionTally = static_cast(_showSubtypeSelectionTally); + settings->setValue("interface/showsubtypeselectiontally", showSubtypeSelectionTally); +} + void SettingsCache::loadPaths() { QString dataPath = getDataPath(); diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index 5a5e0c546..29af89587 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -355,6 +355,7 @@ private: bool showStatusBar; bool showDragSelectionCount; bool showTotalSelectionCount; + bool showSubtypeSelectionTally; public: SettingsCache(); @@ -478,6 +479,10 @@ public: { return showTotalSelectionCount; } + [[nodiscard]] bool getShowSubtypeSelectionTally() const + { + return showSubtypeSelectionTally; + } [[nodiscard]] bool getNotificationsEnabled() const { return notificationsEnabled; @@ -1176,5 +1181,6 @@ public slots: void setRoundCardCorners(bool _roundCardCorners); void setShowDragSelectionCount(QT_STATE_CHANGED_T _showDragSelectionCount); void setShowTotalSelectionCount(QT_STATE_CHANGED_T _showTotalSelectionCount); + void setShowSubtypeSelectionTally(QT_STATE_CHANGED_T _showSubtypeSelectionTally); }; #endif diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index fffd23ccf..df9eb8a1d 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -27,8 +27,9 @@ #include #include #include +#include +#include #include -#include #include // milliseconds in between triggers of the move top cards until action @@ -1018,8 +1019,9 @@ void PlayerActions::actCreateAllRelatedCards() if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { dbName = cardRelationAll->getName(); bool persistent = cardRelationAll->getIsPersistent(); + bool faceDown = cardRelationAll->getIsFaceDown(); for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } ++tokensTypesCreated; if (tokensTypesCreated == 1) { @@ -1034,8 +1036,9 @@ void PlayerActions::actCreateAllRelatedCards() if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { dbName = cardRelationNotExcluded->getName(); bool persistent = cardRelationNotExcluded->getIsPersistent(); + bool faceDown = cardRelationNotExcluded->getIsFaceDown(); for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } ++tokensTypesCreated; if (tokensTypesCreated == 1) { @@ -1073,6 +1076,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, const QString dbName = cardRelation->getName(); const bool persistent = cardRelation->getIsPersistent(); + const bool faceDown = cardRelation->getIsFaceDown(); // Variable relations always use DoesNotAttach, regardless of the count the user // entered. @@ -1081,7 +1085,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, return false; } for (int i = 0; i < variableCount; ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } return true; } @@ -1090,7 +1094,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, if (count > 1) { for (int i = 0; i < count; ++i) { - createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent); + createCard(sourceCard, dbName, CardRelationType::DoesNotAttach, persistent, faceDown); } return true; } @@ -1110,7 +1114,7 @@ bool PlayerActions::createRelatedFromRelation(const CardItem *sourceCard, playCardToTable(sourceCard, false); } - createCard(sourceCard, dbName, attachType, persistent); + createCard(sourceCard, dbName, attachType, persistent, faceDown); return true; } @@ -1137,7 +1141,8 @@ void PlayerActions::onRelatedCardCreated(const CardItem *sourceCard, const CardR void PlayerActions::createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attachType, - bool persistent) + bool persistent, + bool faceDown) { CardInfoPtr cardInfo = CardDatabaseManager::query()->getCardInfo(dbCardName); @@ -1172,6 +1177,7 @@ void PlayerActions::createCard(const CardItem *sourceCard, cmd.set_destroy_on_zone_change(!persistent); cmd.set_x(gridPoint.x()); cmd.set_y(gridPoint.y()); + cmd.set_face_down(faceDown); ExactCard relatedCard = CardDatabaseManager::query()->getCardFromSameSet(cardInfo->getName(), sourceCard->getCard().getPrinting()); @@ -1525,12 +1531,15 @@ void PlayerActions::offsetCardCounter(QList selectedCards, int count QList commandList; for (auto card : selectedCards) { int oldValue = card->getCounters().value(counterId, 0); - int newValue = oldValue + offset; - // Early exit optimization: server enforces [0, MAX_COUNTERS_ON_CARD]. - // Compare clamped value to allow recovery from invalid states. - int clampedValue = qBound(0, newValue, MAX_COUNTERS_ON_CARD); - if (clampedValue != oldValue) { + // Overflow-safe clamp to the server-enforced range [0, MAX_COUNTER_VALUE]; + // a result differing from oldValue also corrects an out-of-range cached value. + // Callers only ever pass offset == ±1 (actAddCardCounter / actRemoveCardCounter). + // This client-side clamp is a defense-in-depth UX check, consistent with + // actSetCardCounter and actIncrementAllCardCounters; the server remains the + // authoritative enforcer of the bounds. + int newValue = addClamped(oldValue, offset, 0, MAX_COUNTER_VALUE); + if (newValue != oldValue) { auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); @@ -1563,7 +1572,7 @@ void PlayerActions::actSetCardCounter(QList selectedCards, int count Expression exp(oldValue); double parsed = exp.parse(counterValue); // Clamp in double precision first to avoid UB, then cast - int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTERS_ON_CARD))); + int number = static_cast(qBound(0.0, parsed, static_cast(MAX_COUNTER_VALUE))); auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); @@ -1593,7 +1602,7 @@ void PlayerActions::actIncrementAllCardCounters(QList cardsToUpdate) counterIterator.next(); int counterId = counterIterator.key(); int currentValue = counterIterator.value(); - if (currentValue >= MAX_COUNTERS_ON_CARD) { + if (currentValue >= MAX_COUNTER_VALUE) { continue; } diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 3f1960892..fa4d54110 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -240,7 +240,8 @@ private: void createCard(const CardItem *sourceCard, const QString &dbCardName, CardRelationType attach = CardRelationType::DoesNotAttach, - bool persistent = false); + bool persistent = false, + bool faceDown = false); void playSelectedCards(QList selectedCards, bool faceDown = false); diff --git a/cockatrice/src/game/selection_subtype_tally.cpp b/cockatrice/src/game/selection_subtype_tally.cpp new file mode 100644 index 000000000..e9f87fab9 --- /dev/null +++ b/cockatrice/src/game/selection_subtype_tally.cpp @@ -0,0 +1,64 @@ +#include "selection_subtype_tally.h" + +#include "../game_graphics/board/card_item.h" + +#include +#include + +namespace +{ + +/** @brief Extracts subtypes from a single card face's type line. */ +QStringList extractSubtypesFromFace(const QString &faceType) +{ + // Card type format: "Creature — Goblin Warrior" or "Legendary Enchantment — Saga" + QStringList parts = faceType.split(QStringLiteral(" — ")); + if (parts.size() > 1) { + return parts[1].split(QStringLiteral(" "), Qt::SkipEmptyParts); + } + return {}; +} + +} // anonymous namespace + +namespace SelectionSubtypeTally +{ + +QList countSubtypes(const QList &cards) +{ + QMap subtypeCounts; + + for (CardItem *card : cards) { + if (card->getFaceDown() || card->getCard().isEmpty()) { + continue; + } + + QString cardType = card->getCardInfo().getCardType(); + // Handle double-faced cards: "Creature — Human // Creature — Werewolf" + QStringList cardFaces = cardType.split(QStringLiteral(" // ")); + + for (const QString &face : cardFaces) { + QStringList subtypes = extractSubtypesFromFace(face); + for (const QString &subtype : subtypes) { + subtypeCounts[subtype]++; + } + } + } + + QList entries; + for (auto it = subtypeCounts.constBegin(); it != subtypeCounts.constEnd(); ++it) { + entries.append({it.key(), it.value()}); + } + + // Sort by count ascending, then alphabetically (lowest counts at bottom of display) + std::sort(entries.begin(), entries.end(), [](const SubtypeEntry &a, const SubtypeEntry &b) { + if (a.count != b.count) { + return a.count < b.count; + } + return a.name < b.name; + }); + + return entries; +} + +} // namespace SelectionSubtypeTally diff --git a/cockatrice/src/game/selection_subtype_tally.h b/cockatrice/src/game/selection_subtype_tally.h new file mode 100644 index 000000000..9038653f6 --- /dev/null +++ b/cockatrice/src/game/selection_subtype_tally.h @@ -0,0 +1,36 @@ +#ifndef SELECTION_SUBTYPE_TALLY_H +#define SELECTION_SUBTYPE_TALLY_H + +#include +#include + +class CardItem; + +/** @brief A single subtype (e.g., "Goblin", "Warrior") with its occurrence count. */ +struct SubtypeEntry +{ + QString name; ///< The subtype name + int count; ///< Number of selected cards with this subtype + + bool operator==(const SubtypeEntry &other) const + { + return name == other.name && count == other.count; + } +}; + +/** + * @brief Extracts and tallies subtypes from selected cards. + */ +namespace SelectionSubtypeTally +{ +/** + * @brief Parses card type lines and counts each subtype occurrence. + * + * Skips face-down cards and cards without type info. + * @param cards The list of selected card items to analyze. + * @return Entries sorted by count ascending, then alphabetically. + */ +QList countSubtypes(const QList &cards); +} // namespace SelectionSubtypeTally + +#endif diff --git a/cockatrice/src/game_graphics/board/card_item.h b/cockatrice/src/game_graphics/board/card_item.h index 8efcd085d..37f3bab50 100644 --- a/cockatrice/src/game_graphics/board/card_item.h +++ b/cockatrice/src/game_graphics/board/card_item.h @@ -12,7 +12,6 @@ #include "abstract_card_item.h" #include -#include class CardDatabase; class CardDragItem; diff --git a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp index 21284c517..d476a5012 100644 --- a/cockatrice/src/game_graphics/deckview/deck_view_container.cpp +++ b/cockatrice/src/game_graphics/deckview/deck_view_container.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false) { diff --git a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp index 11c24b72e..1a9dc59b2 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_create_token.cpp @@ -20,7 +20,7 @@ #include #include #include -#include +#include DlgCreateToken::DlgCreateToken(const QStringList &_predefinedTokens, QWidget *parent) : QDialog(parent), predefinedTokens(_predefinedTokens) diff --git a/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp index dfb3d0bc5..4f5d5b861 100644 --- a/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp +++ b/cockatrice/src/game_graphics/dialogs/dlg_roll_dice.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include DlgRollDice::DlgRollDice(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/game_graphics/game_view.cpp b/cockatrice/src/game_graphics/game_view.cpp index 41befd9a4..c2d9b2b3b 100644 --- a/cockatrice/src/game_graphics/game_view.cpp +++ b/cockatrice/src/game_graphics/game_view.cpp @@ -1,12 +1,16 @@ #include "game_view.h" #include "../client/settings/cache_settings.h" +#include "../game/selection_subtype_tally.h" #include "game_scene.h" #include +#include #include +#include #include #include +#include // QRubberBand calls raise() in showEvent() and changeEvent() to stay on top of siblings. // This subclass disables that behavior so dragCountLabel can appear above it. @@ -55,31 +59,40 @@ GameView::GameView(GameScene *scene, QWidget *parent) : QGraphicsView(scene, par refreshShortcuts(); rubberBand = new SelectionRubberBand(QRubberBand::Rectangle, this); - const QString countLabelStyle = "color: white; " - "font-size: 14px; " - "font-weight: bold; " - "background-color: rgba(0, 0, 0, 160); " - "border-radius: 3px; " - "padding: 1px 2px;"; + const QString baseProperties = "color: white; " + "font-family: monospace; " + "background-color: rgba(0, 0, 0, 160); " + "border-radius: 3px; " + "padding: 1px 2px; " + "white-space: pre;"; + + const QString dragCountLabelStyle = baseProperties + "font-size: 14px; font-weight: bold;"; + const QString totalCountLabelStyle = baseProperties + "font-size: 16px; font-weight: bold;"; + const QString subtypeTallyLabelStyle = baseProperties + "font-size: 12px;"; dragCountLabel = new QLabel(this); - dragCountLabel->setStyleSheet(countLabelStyle); + dragCountLabel->setStyleSheet(dragCountLabelStyle); dragCountLabel->hide(); dragCountLabel->raise(); totalCountLabel = new QLabel(this); - totalCountLabel->setStyleSheet(countLabelStyle); + totalCountLabel->setStyleSheet(totalCountLabelStyle); totalCountLabel->hide(); + + subtypeTallyContainer = new QWidget(this); + subtypeTallyContainer->setStyleSheet(subtypeTallyLabelStyle); + subtypeTallyLayout = new QGridLayout(subtypeTallyContainer); + subtypeTallyLayout->setContentsMargins(2, 2, 2, 2); + subtypeTallyLayout->setSpacing(2); + subtypeTallyContainer->hide(); } void GameView::resizeEvent(QResizeEvent *event) { QGraphicsView::resizeEvent(event); - GameScene *s = dynamic_cast(scene()); - if (s) { - s->processViewSizeChange(event->size()); - } + GameScene *s = static_cast(scene()); + s->processViewSizeChange(event->size()); updateSceneRect(scene()->sceneRect()); updateTotalSelectionCount(event->size()); @@ -164,29 +177,114 @@ void GameView::refreshShortcuts() SettingsCache::instance().shortcuts().getShortcut("Player/aCloseMostRecentZoneView")); } +void GameView::clearSubtypeLabels() +{ + QtUtils::clearLayoutRec(subtypeTallyLayout); +} + +QSize GameView::rebuildSubtypeLabels(const QList &entries) +{ + clearSubtypeLabels(); + + const QString nameStyle = QStringLiteral("color: white; font-size: 12px; background: transparent;"); + const QString countStyle = + QStringLiteral("color: white; font-size: 14px; font-weight: bold; background: transparent;"); + + int totalHeight = 0; + int maxNameWidth = 0; + int maxCountWidth = 0; + + int row = 0; + for (const SubtypeEntry &entry : entries) { + auto *nameLabel = new QLabel(entry.name, subtypeTallyContainer); + nameLabel->setStyleSheet(nameStyle); + nameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + subtypeTallyLayout->addWidget(nameLabel, row, 0); + + auto *countLabel = new QLabel(QString::number(entry.count), subtypeTallyContainer); + countLabel->setStyleSheet(countStyle); + countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + subtypeTallyLayout->addWidget(countLabel, row, 1); + + QSize nameSize = nameLabel->sizeHint(); + QSize countSize = countLabel->sizeHint(); + maxNameWidth = qMax(maxNameWidth, nameSize.width()); + maxCountWidth = qMax(maxCountWidth, countSize.width()); + totalHeight += qMax(nameSize.height(), countSize.height()); + + ++row; + } + + int spacing = subtypeTallyLayout->spacing(); + int margins = subtypeTallyLayout->contentsMargins().left() + subtypeTallyLayout->contentsMargins().right(); + int verticalMargins = subtypeTallyLayout->contentsMargins().top() + subtypeTallyLayout->contentsMargins().bottom(); + + int width = maxNameWidth + spacing + maxCountWidth + margins; + int height = totalHeight + (row - 1) * spacing + verticalMargins; + + return QSize(width, height); +} + void GameView::updateTotalSelectionCount(const QSize &viewSize) { - if (!SettingsCache::instance().getShowTotalSelectionCount()) { - totalCountLabel->hide(); - return; - } + constexpr int kMarginInPixels = 10; + constexpr int kSpacingBetweenLabels = 4; + + int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); + int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); int count = scene()->selectedItems().count(); - if (count > 1) { + if (!SettingsCache::instance().getShowTotalSelectionCount() || count <= 1) { + totalCountLabel->hide(); + } else { totalCountLabel->setText(QString::number(count)); totalCountLabel->adjustSize(); - constexpr int kMarginInPixels = 10; - int availableWidth = viewSize.isValid() ? viewSize.width() : viewport()->width(); - int availableHeight = viewSize.isValid() ? viewSize.height() : viewport()->height(); int x = availableWidth - totalCountLabel->width() - kMarginInPixels; int y = availableHeight - totalCountLabel->height() - kMarginInPixels; totalCountLabel->move(x, y); totalCountLabel->show(); - } else { - totalCountLabel->hide(); } + + if (!SettingsCache::instance().getShowSubtypeSelectionTally() || count <= 1) { + subtypeTallyContainer->hide(); + cachedSubtypeEntries.clear(); + return; + } + + GameScene *gameScene = static_cast(scene()); + QList entries = SelectionSubtypeTally::countSubtypes(gameScene->selectedCards()); + + if (entries.isEmpty()) { + subtypeTallyContainer->hide(); + cachedSubtypeEntries.clear(); + return; + } + + // Only rebuild labels if entries changed + QSize containerSize; + if (entries != cachedSubtypeEntries) { + cachedSubtypeEntries = entries; + containerSize = rebuildSubtypeLabels(entries); + subtypeTallyContainer->resize(containerSize); + } else { + containerSize = subtypeTallyContainer->size(); + } + + int x = availableWidth - containerSize.width() - kMarginInPixels; + int y; + + if (totalCountLabel->isVisible()) { + y = totalCountLabel->y() - containerSize.height() - kSpacingBetweenLabels; + } else { + y = availableHeight - containerSize.height() - kMarginInPixels; + } + + y = qMax(kMarginInPixels, y); + + subtypeTallyContainer->move(x, y); + subtypeTallyContainer->show(); } /** diff --git a/cockatrice/src/game_graphics/game_view.h b/cockatrice/src/game_graphics/game_view.h index 80e8e96b5..4047c87ab 100644 --- a/cockatrice/src/game_graphics/game_view.h +++ b/cockatrice/src/game_graphics/game_view.h @@ -7,9 +7,12 @@ #ifndef GAMEVIEW_H #define GAMEVIEW_H +#include "../game/selection_subtype_tally.h" + #include class GameScene; +class QGridLayout; class QLabel; class QRubberBand; @@ -21,7 +24,13 @@ private: QRubberBand *rubberBand; QLabel *dragCountLabel; QLabel *totalCountLabel; + QWidget *subtypeTallyContainer; + QGridLayout *subtypeTallyLayout; QPointF selectionOrigin; + QList cachedSubtypeEntries; ///< Cached entries to avoid redundant rebuilds + + QSize rebuildSubtypeLabels(const QList &entries); + void clearSubtypeLabels(); protected: void resizeEvent(QResizeEvent *event) override; diff --git a/cockatrice/src/game_graphics/player/player_dialogs.cpp b/cockatrice/src/game_graphics/player/player_dialogs.cpp index 3c26ae1fe..95c225812 100644 --- a/cockatrice/src/game_graphics/player/player_dialogs.cpp +++ b/cockatrice/src/game_graphics/player/player_dialogs.cpp @@ -8,6 +8,7 @@ #include #include +#include PlayerDialogs::PlayerDialogs(PlayerGraphicsItem *_player, PlayerActions *_playerActions) : QObject(_player), player(_player), playerActions(_playerActions) diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index 086845fe6..7dc757062 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -271,6 +271,9 @@ void ThemeManager::applyStyleAndPalette(const QString &themeName, const PaletteConfig &palCfg, const QString &activeScheme) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 0)) + Q_UNUSED(activeScheme) +#endif QString styleName = themeCfg.styleName; if (styleName.isEmpty() || styleName.compare("Default", Qt::CaseInsensitive) == 0) { if (themeName == FUSION_THEME_NAME) { @@ -396,6 +399,7 @@ static QString roleBgName(ThemeManager::Role role) default: Q_ASSERT(false); + return {}; } } diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index f751fa225..f61a01168 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include static int findRestoreIndex(const CardRef &wanted, const QComboBox *combo) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp index fdbc90542..2a7fc6c06 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_connect.cpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include DlgConnect::DlgConnect(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp index 30364f242..f892801f1 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_create_game.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include void DlgCreateGame::sharedCtor() { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp index db5f21701..b5710b208 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_avatar.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image() { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp index cdd4433a7..63181b44f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_password.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp index 381aa2b11..f249976c2 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_tokens.cpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp index 7015f9d47..85b95f335 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_edit_user.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include DlgEditUser::DlgEditUser(QWidget *parent, QString email, QString country, QString realName) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp index 24e9030e0..14c3cda47 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordChallenge::DlgForgotPasswordChallenge(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp index c33a41bed..fc934d082 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_request.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordRequest::DlgForgotPasswordRequest(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp index d2eb081d1..d30887988 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include DlgForgotPasswordReset::DlgForgotPasswordReset(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp index c693fb02e..b17a9306f 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_manage_sets.cpp @@ -62,7 +62,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) // search field searchField = new LineEditUnfocusable; searchField->setObjectName("searchEdit"); - searchField->setPlaceholderText(tr("Search by set name, code, or type")); + searchField->setPlaceholderText(tr("Search by set name, code, type, or release date")); searchField->addAction(QPixmap("theme:icons/search"), LineEditUnfocusable::LeadingPosition); searchField->setClearButtonEnabled(true); setFocusProxy(searchField); diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp index 0f7c17b18..942782403 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_register.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp index 70a56375e..67fb4f684 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.cpp @@ -5,9 +5,9 @@ #include #include -static QString makeKey(const QString &user, const QString &card) +static QString makeKey(const QString &user, const QString &card, const QString &providerId) { - return user + u'|' + card; + return user + u'|' + card + u'|' + providerId; } UserCardArtProvider::UserCardArtProvider(QObject *parent) : QObject(parent) @@ -31,13 +31,13 @@ const QMap &UserCardArtProvider::cache() const return cardArtCache; } -void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName) +void UserCardArtProvider::requestCardArt(const QString &userName, const QString &cardName, const QString &providerId) { if (cardName.isEmpty()) { return; } - const QString key = makeKey(userName, cardName); + const QString key = makeKey(userName, cardName, providerId); if (cardArtCache.contains(key) || pending.contains(key)) { return; @@ -83,15 +83,16 @@ void UserCardArtProvider::processQueue() const QString key = queue.dequeue(); const QStringList parts = key.split(u'|'); - if (parts.size() != 2) { + if (parts.size() != 3) { pending.remove(key); continue; } const QString userName = parts.at(0); const QString cardName = parts.at(1); + const QString providerId = parts.at(2); - ExactCard card = CardDatabaseManager::query()->getCard({cardName}); + ExactCard card = CardDatabaseManager::query()->getCard({cardName, providerId}); if (!card) { pending.remove(key); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h index a3ab874b7..fb2f37812 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_art_provider.h @@ -14,7 +14,7 @@ class UserCardArtProvider : public QObject public: explicit UserCardArtProvider(QObject *parent = nullptr); - void requestCardArt(const QString &userName, const QString &cardName); + void requestCardArt(const QString &userName, const QString &cardName, const QString &providerId); const QMap &cache() const; static QPixmap cropCardArt(const QPixmap &fullRes); diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp index 335ee097e..56c9600a0 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.cpp @@ -70,7 +70,7 @@ void CardArtPreviewWidget::paintEvent(QPaintEvent *) QString(), // userName not needed for override path nullptr, // no cache params, - &sourcePixmap // 👈 direct pixmap + &sourcePixmap // direct pixmap ); // Avatar placeholder so the left-margin interaction is visible @@ -174,6 +174,13 @@ void UserCardArtSettingsDialog::setupUi() { initializeSearchBar(); + providerComboBox = new QComboBox; + connect(providerComboBox, &QComboBox::currentIndexChanged, this, [this]() { + currentParams.cardProviderId = providerComboBox->currentData().toString(); + reloadPreview(); + onParamChanged(); + }); + marginLSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctL, 0.01); marginRSpin = makeSpinBox(0.0, 0.95, currentParams.marginPctR, 0.01); verticalOffsetSpin = makeSpinBox(0.0, 1.0, currentParams.verticalOffset, 0.01); @@ -181,6 +188,7 @@ void UserCardArtSettingsDialog::setupUi() auto *form = new QFormLayout; form->addRow(tr("Card name:"), searchBar); + form->addRow(tr("Card ProviderId:"), providerComboBox); form->addRow(tr("Left margin (%):"), marginLSpin); form->addRow(tr("Right margin (%):"), marginRSpin); form->addRow(tr("Vertical offset:"), verticalOffsetSpin); @@ -219,6 +227,32 @@ void UserCardArtSettingsDialog::setupUi() connect(zoomSpin, &QDoubleSpinBox::valueChanged, this, &UserCardArtSettingsDialog::onParamChanged); } +void UserCardArtSettingsDialog::populateProviderCombo(const QString &cardName) +{ + providerComboBox->clear(); + + auto card = CardDatabaseManager::query()->getCard({cardName}); + + const auto &sets = card.getInfo().getSets(); + + for (const auto &printings : sets) { + for (const auto &p : printings) { + + QString setName = p.getSet()->getLongName(); + QString collector = p.getProperty("num"); + QString uuid = p.getUuid(); + + QString label = setName; + + if (!collector.isEmpty()) { + label += " #" + collector; + } + + providerComboBox->addItem(label, uuid); + } + } +} + void UserCardArtSettingsDialog::onCardNameChanged(const QString &name) { if (name.isEmpty()) { @@ -231,27 +265,68 @@ void UserCardArtSettingsDialog::onCardNameChanged(const QString &name) if (!card) { currentPixmap = QPixmap(); preview->setPixmap(currentPixmap); + providerComboBox->clear(); return; } currentParams.cardName = name; + populateProviderCombo(name); + + if (providerComboBox->count() == 0) { + // No printings found for this card; nothing to preview. + currentPixmap = QPixmap(); + preview->setPixmap(currentPixmap); + currentParams.cardProviderId.clear(); + return; + } + + currentParams.cardProviderId = providerComboBox->currentData().toString(); + reloadPreview(); +} + +void UserCardArtSettingsDialog::reloadPreview() +{ + if (currentParams.cardName.isEmpty()) { + return; + } + + ExactCard card = CardDatabaseManager::query()->getCard({currentParams.cardName, currentParams.cardProviderId}); + if (!card) { + return; + } + + // CardPictureLoader::getPixmap() is async on a cache miss: it enqueues a + // background download and returns a null pixmap immediately. When that + // download finishes, CardPictureLoader::imageLoaded() caches the result + // and calls card.emitPixmapUpdated(), which emits pixmapUpdated() on the + // underlying CardInfo (see exact_card.h). Listen for that, scoped to + // whichever CardInfo we just asked for, so the preview catches up once + // the image actually arrives instead of staying on the placeholder. + // + // Disconnect any previous listener first -- otherwise switching cards + // repeatedly stacks up connections to old CardInfo objects, each of + // which would still fire reloadPreview() (harmlessly, but wastefully) + // whenever ITS art finishes loading later. + disconnect(pixmapUpdatedConnection); + QPixmap fullRes; CardPictureLoader::getPixmap(fullRes, card, QSize(745, 1040)); if (fullRes.isNull()) { - connect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, [this, card](const PrintingInfo &) { - disconnect(card.getCardPtr().data(), &CardInfo::pixmapUpdated, this, nullptr); - QPixmap loaded; - CardPictureLoader::getPixmap(loaded, card, QSize(745, 1040)); - currentPixmap = UserCardArtProvider::cropCardArt(loaded); - preview->setPixmap(currentPixmap); - }); + // Not loaded yet -- wait for the signal instead of giving up. + // card.getCardPtr() is a CardInfoPtr (QSharedPointer); + // .data() gives the raw QObject* needed for connect(). + CardInfo *cardInfo = card.getCardPtr().data(); + if (cardInfo) { + pixmapUpdatedConnection = connect(cardInfo, &CardInfo::pixmapUpdated, this, [this]() { reloadPreview(); }); + } return; } currentPixmap = UserCardArtProvider::cropCardArt(fullRes); preview->setPixmap(currentPixmap); + preview->setParams(currentParams); } void UserCardArtSettingsDialog::onParamChanged() diff --git a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h index cac26c919..018043278 100644 --- a/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h +++ b/cockatrice/src/interface/widgets/server/user/user_card_settings_dialog.h @@ -3,6 +3,7 @@ #include "user_list_painter.h" +#include #include #include @@ -43,10 +44,12 @@ public: private slots: void onCardNameChanged(const QString &name); + void reloadPreview(); void onParamChanged(); private: void setupUi(); + void populateProviderCombo(const QString &cardName); void initializeSearchBar(); QDoubleSpinBox *makeSpinBox(double min, double max, double value, double step); @@ -57,6 +60,10 @@ private: CardSearchModel *searchModel; CardCompleterProxyModel *proxyModel; + QComboBox *providerComboBox; + + QMetaObject::Connection pixmapUpdatedConnection; + QDoubleSpinBox *marginLSpin; QDoubleSpinBox *marginRSpin; QDoubleSpinBox *verticalOffsetSpin; diff --git a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp index e6cf38787..416cd42e3 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_box.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_box.cpp @@ -335,6 +335,7 @@ void UserInfoBox::actBannerCard() Command_SetCardArtParams cmd; cmd.set_card_name(p.cardName.toStdString()); if (!p.cardName.isEmpty()) { + cmd.set_card_provider_id(p.cardProviderId.toStdString()); cmd.set_margin_pct_l(p.marginPctL); cmd.set_margin_pct_r(p.marginPctR); cmd.set_vertical_offset(p.verticalOffset); diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index fd62d5ddf..2b4dcb8ed 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -542,7 +542,7 @@ void UserInfoPopup::showForUser(const QString &userName, const CardArtParams params = (m_cardArtParamsMap && m_cardArtParamsMap->contains(userName)) ? m_cardArtParamsMap->value(userName) : CardArtParams{}; - const QString artKey = userName + u'|' + params.cardName; + const QString artKey = userName + u'|' + params.cardName + u'|' + params.cardProviderId; const QPixmap cardArt = (m_cardArtCache && !params.cardName.isEmpty()) ? m_cardArtCache->value(artKey) : QPixmap{}; m_header->setUserData(userInfo, online, avatar, cardArt, params); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp index b5541b692..8891ff268 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.cpp @@ -73,9 +73,9 @@ void UserListPainter::drawBackground(QPainter *painter, painter->drawRoundedRect(QRectF(cardRect.left(), cardRect.top(), 3, cardRect.height()), 2, 2); } -static QString makeKey(const QString &user, const QString &card) +static QString makeKey(const QString &user, const QString &card, const QString &providerId) { - return user + u'|' + card; + return user + u'|' + card + u'|' + providerId; } void UserListPainter::drawCardArt(QPainter *painter, @@ -95,7 +95,7 @@ void UserListPainter::drawCardArt(QPainter *painter, return; } - const QString key = makeKey(userName, params.cardName); + const QString key = makeKey(userName, params.cardName, params.cardProviderId); if (!cardArtCache->contains(key)) { return; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_painter.h b/cockatrice/src/interface/widgets/server/user/user_list_painter.h index 95486b75e..28cab9675 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_painter.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_painter.h @@ -18,6 +18,7 @@ class ServerInfo_User; struct CardArtParams { QString cardName = ""; + QString cardProviderId = ""; double marginPctL = 0.33; double marginPctR = 0.02; double verticalOffset = 0.35; diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index ed06ea941..32f46a79f 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -30,7 +30,7 @@ #include #include #include -#include +#include BanDialog::BanDialog(const ServerInfo_User &info, QWidget *parent) : QDialog(parent) { @@ -767,13 +767,17 @@ void UserListWidget::showPopupForUser(const QString &userName) m_userInfoPopup->showForUser(userName, info, online, isBuddy, isIgn); - positionPopup(userName); - + // Realize the native window at opacity 0 before positioning so that: + // 1) move() applies to an existing native handle (not overridden by Qt's + // default centering logic on first show) + // 2) adjustSize() inside positionPopup() can measure the final laid-out + // geometry correctly + m_userInfoPopup->setWindowOpacity(0.0); m_userInfoPopup->show(); m_userInfoPopup->raise(); - // Fade in - m_userInfoPopup->setWindowOpacity(0.0); + positionPopup(userName); // geometry is now accurate; move() sticks + auto *fade = new QPropertyAnimation(m_userInfoPopup, "windowOpacity", m_userInfoPopup); fade->setDuration(120); fade->setStartValue(0.0); @@ -790,11 +794,10 @@ void UserListWidget::positionPopup(const QString &userName) QWidget *vp = userTree->viewport(); const QRect itemR = userTree->visualItemRect(item); - const QPoint itemBR = vp->mapToGlobal(itemR.bottomRight()); + const QPoint itemTL = vp->mapToGlobal(itemR.topLeft()); const QPoint vpTL = vp->mapToGlobal(vp->rect().topLeft()); const QPoint vpTR = vp->mapToGlobal(vp->rect().topRight()); - // Force a fresh size calculation so popH is accurate m_userInfoPopup->adjustSize(); const int popW = m_userInfoPopup->width(); const int popH = m_userInfoPopup->height(); @@ -802,19 +805,32 @@ void UserListWidget::positionPopup(const QString &userName) const QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); - // ── X: left of the list if there's room, otherwise right ───────────────── - int x = (vpTL.x() >= popW + margin) ? vpTL.x() - popW - margin : vpTR.x() + margin; + // ── X: prefer the side with more space ─────────────────────────────────── + const int spaceLeft = vpTL.x() - screen.left() - margin; + const int spaceRight = screen.right() - vpTR.x() - margin; + int x; + if (spaceLeft >= spaceRight) { + x = (spaceLeft >= popW) ? (vpTL.x() - margin - popW) : (vpTR.x() + margin); + } else { + x = (spaceRight >= popW) ? (vpTR.x() + margin) : (vpTL.x() - margin - popW); + } x = qBound(screen.left() + margin, x, screen.right() - popW - margin); - // ── Y: bottom of popup aligns with bottom of hovered row, grows upward ─── - int y = itemBR.y() - popH; + // ── Y: grow down if there's room, otherwise grow up ─────────────────────── + const int itemTopY = itemTL.y(); + const int spaceBelow = screen.bottom() - itemTopY - margin; + const int spaceAbove = itemTopY - screen.top() - margin; - // Clamp: never above the screen top - y = qMax(y, screen.top() + margin); - - // Clamp: never below the screen bottom (e.g. if the popup is taller - // than the space above the row, let it spill downward rather than clip) - y = qMin(y, screen.bottom() - popH - margin); + int y; + if (spaceBelow >= popH) { + y = itemTopY; // top edges align, popup grows downward + } else if (spaceAbove >= popH) { + y = itemTopY - popH; // bottom of popup meets top of item, grows upward + } else { + // Neither side fits cleanly — pick the roomier side and let clamp handle the rest + y = (spaceBelow >= spaceAbove) ? itemTopY : (itemTopY - popH); + } + y = qBound(screen.top() + margin, y, screen.bottom() - popH - margin); m_userInfoPopup->move(x, y); } @@ -904,12 +920,13 @@ void UserListWidget::processUserInfo(const ServerInfo_User &user, bool online) const auto &cap = user.card_art_params(); CardArtParams params; params.cardName = QString::fromStdString(cap.card_name()); + params.cardProviderId = QString::fromStdString(cap.card_provider_id()); params.marginPctL = cap.margin_pct_l(); params.marginPctR = cap.margin_pct_r(); params.verticalOffset = cap.vertical_offset(); params.zoom = cap.zoom(); cardArtParamsMap.insert(userName, params); - cardArtProvider->requestCardArt(userName, params.cardName); + cardArtProvider->requestCardArt(userName, params.cardName, params.cardProviderId); } else { cardArtParamsMap.remove(userName); // clear stale params on removal } diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index 1e6f99245..831cb442c 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -6,6 +6,7 @@ #include #include #include +#include MessagesSettingsPage::MessagesSettingsPage() { diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index 6039e3758..44b30d29c 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -68,6 +68,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&showTotalSelectionCountCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), &SettingsCache::setShowTotalSelectionCount); + showSubtypeSelectionTallyCheckBox.setChecked(SettingsCache::instance().getShowSubtypeSelectionTally()); + connect(&showSubtypeSelectionTallyCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), + &SettingsCache::setShowSubtypeSelectionTally); + useTearOffMenusCheckBox.setChecked(SettingsCache::instance().getUseTearOffMenus()); connect(&useTearOffMenusCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance(), [](const QT_STATE_CHANGED_T state) { SettingsCache::instance().setUseTearOffMenus(state == Qt::Checked); }); @@ -86,8 +90,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() generalGrid->addWidget(&annotateTokensCheckBox, 6, 0); generalGrid->addWidget(&showDragSelectionCountCheckBox, 7, 0); generalGrid->addWidget(&showTotalSelectionCountCheckBox, 8, 0); - generalGrid->addWidget(&useTearOffMenusCheckBox, 9, 0); - generalGrid->addWidget(&keepGameChatFocusCheckBox, 10, 0); + generalGrid->addWidget(&showSubtypeSelectionTallyCheckBox, 9, 0); + generalGrid->addWidget(&useTearOffMenusCheckBox, 10, 0); + generalGrid->addWidget(&keepGameChatFocusCheckBox, 11, 0); generalGroupBox = new QGroupBox; generalGroupBox->setLayout(generalGrid); @@ -209,8 +214,9 @@ void UserInterfaceSettingsPage::retranslateUi() closeEmptyCardViewCheckBox.setText(tr("Close card view window when last card is removed")); focusCardViewSearchBarCheckBox.setText(tr("Auto focus search bar when card view window is opened")); annotateTokensCheckBox.setText(tr("Annotate card text on tokens")); - showDragSelectionCountCheckBox.setText(tr("Show selection counter during drag selection")); - showTotalSelectionCountCheckBox.setText(tr("Show total selection counter")); + showDragSelectionCountCheckBox.setText(tr("Show selection count during drag selection")); + showTotalSelectionCountCheckBox.setText(tr("Show total selection count")); + showSubtypeSelectionTallyCheckBox.setText(tr("Show subtype breakdown in selection tally")); useTearOffMenusCheckBox.setText(tr("Use tear-off menus, allowing right click menus to persist on screen")); keepGameChatFocusCheckBox.setText( tr("Keep game chat focused when clicking in game (Note: disables card view search bar)")); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index e10ed2a06..06f0e6b83 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -29,6 +29,7 @@ private: QCheckBox annotateTokensCheckBox; QCheckBox showDragSelectionCountCheckBox; QCheckBox showTotalSelectionCountCheckBox; + QCheckBox showSubtypeSelectionTallyCheckBox; QCheckBox useTearOffMenusCheckBox; QCheckBox keepGameChatFocusCheckBox; QCheckBox tapAnimationCheckBox; diff --git a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index a8cc4cee6..4bb8ffca4 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -43,7 +43,7 @@ #include #include #include -#include +#include /** * @brief Constructs the AbstractTabDeckEditor. diff --git a/cockatrice/src/interface/widgets/tabs/tab_account.cpp b/cockatrice/src/interface/widgets/tabs/tab_account.cpp index e61732f90..2cc8165e8 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_account.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_account.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo) : Tab(_tabSupervisor), client(_client) diff --git a/cockatrice/src/interface/widgets/tabs/tab_admin.cpp b/cockatrice/src/interface/widgets/tabs/tab_admin.cpp index 533e1cc83..16f17e03e 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_admin.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_admin.cpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp index 6e8ab752a..3dc4de15f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.cpp @@ -27,7 +27,7 @@ int CardArtRulesModel::rowCount(const QModelIndex &parent) const int CardArtRulesModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); - return 3; + return 4; } QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const @@ -43,8 +43,10 @@ QVariant CardArtRulesModel::data(const QModelIndex &index, int role) const case 0: return e.cardName; case 1: - return e.mode; + return e.cardProviderId; case 2: + return e.mode; + case 3: return e.reason; } } @@ -62,8 +64,10 @@ QVariant CardArtRulesModel::headerData(int section, Qt::Orientation orientation, case 0: return tr("Card"); case 1: - return tr("Mode"); + return tr("ProviderId"); case 2: + return tr("Mode"); + case 3: return tr("Reason"); default: return {}; @@ -97,6 +101,15 @@ QString CardArtRulesModel::cardAt(int row) const return entries[row].cardName; } +const CardArtRulesModel::Entry *CardArtRulesModel::entryAt(int row) const +{ + if (row < 0 || row >= static_cast(entries.size())) { + return nullptr; + } + + return &entries[row]; +} + void CardArtRulesModel::onRefreshFinished(const Response &r) { if (r.response_code() != Response::RespOk) { @@ -109,8 +122,8 @@ void CardArtRulesModel::onRefreshFinished(const Response &r) entries.clear(); for (const auto &e : resp.entries()) { - entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.mode()), - QString::fromStdString(e.reason())}); + entries.push_back({QString::fromStdString(e.card_name()), QString::fromStdString(e.card_provider_id()), + QString::fromStdString(e.mode()), QString::fromStdString(e.reason())}); } endResetModel(); @@ -128,6 +141,7 @@ void TabCardArtRules::setupUi() initSearchBar(); + providerComboBox = new QComboBox; modeBox = new QComboBox; reasonEdit = new QLineEdit; @@ -146,6 +160,7 @@ void TabCardArtRules::setupUi() auto *form = new QFormLayout; form->addRow(tr("Card:"), searchEdit); + form->addRow(tr("ProviderId:"), providerComboBox); form->addRow(tr("Mode:"), modeBox); form->addRow(tr("Reason:"), reasonEdit); @@ -204,6 +219,34 @@ void TabCardArtRules::initSearchBar() }); connect(searchCompleter, static_cast(&QCompleter::activated), this, [this](const QString &name) { searchEdit->setText(name); }); + connect(searchEdit, &QLineEdit::editingFinished, this, + [this]() { populateProviderCombo(searchEdit->text().trimmed()); }); +} + +void TabCardArtRules::populateProviderCombo(const QString &cardName) +{ + providerComboBox->clear(); + + auto card = CardDatabaseManager::query()->getCard({cardName}); + + const auto &sets = card.getInfo().getSets(); + + for (const auto &printings : sets) { + for (const auto &p : printings) { + + QString setName = p.getSet()->getLongName(); + QString collector = p.getProperty("num"); + QString uuid = p.getUuid(); + + QString label = setName; + + if (!collector.isEmpty()) { + label += " #" + collector; + } + + providerComboBox->addItem(label, uuid); + } + } } void TabCardArtRules::retranslateUi() @@ -222,6 +265,7 @@ void TabCardArtRules::addRule() { Command_AddCardArtRule cmd; cmd.set_card_name(searchEdit->text().toStdString()); + cmd.set_card_provider_id(providerComboBox->currentData().toString().toStdString()); cmd.set_mode(modeBox->currentText().toStdString()); cmd.set_reason(reasonEdit->text().toStdString()); @@ -238,7 +282,10 @@ void TabCardArtRules::removeSelected() } Command_RemoveCardArtRule cmd; - cmd.set_card_name(tableModel->cardAt(idx.row()).toStdString()); + const auto e = tableModel->entryAt(idx.row()); + + cmd.set_card_name(e->cardName.toStdString()); + cmd.set_card_provider_id(e->cardProviderId.toStdString()); client->sendCommand(client->prepareModeratorCommand(cmd)); diff --git a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h index a47f1267d..b9ea2ca83 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h +++ b/cockatrice/src/interface/widgets/tabs/tab_card_art_rules.h @@ -20,6 +20,7 @@ public: struct Entry { QString cardName; + QString cardProviderId; QString mode; QString reason; }; @@ -35,6 +36,7 @@ public: void clear(); QString cardAt(int row) const; + const Entry *entryAt(int row) const; private slots: void onRefreshFinished(const Response &r); @@ -70,11 +72,13 @@ private: QLineEdit *searchEdit; void initSearchBar(); + void populateProviderCombo(const QString &cardName); QCompleter *searchCompleter; CardDatabaseModel *cardDbModel; CardDatabaseDisplayModel *cardDbDisplayModel; CardSearchModel *cardSearchModel; CardCompleterProxyModel *cardProxyModel; + QComboBox *providerComboBox; QComboBox *modeBox; QLineEdit *reasonEdit; diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp index 4e7cbfecf..3878e12e0 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_editor.cpp @@ -21,7 +21,6 @@ #include #include #include -#include /** * @brief Constructs a new TabDeckEditor object. diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index bdf7901f1..8e2b114d9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -28,6 +28,7 @@ #include #include #include +#include TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index a81161e83..600647171 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -44,7 +44,7 @@ #include #include #include -#include +#include TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) : Tab(_tabSupervisor), sayLabel(nullptr), sayEdit(nullptr) diff --git a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp index 9a030e7d9..e3678a903 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_logs.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_logs.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client) { diff --git a/cockatrice/src/interface/widgets/tabs/tab_message.cpp b/cockatrice/src/interface/widgets/tabs/tab_message.cpp index d77cb0391..2cacab731 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_message.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_message.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include TabMessage::TabMessage(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index c7495da5a..ec05e9ff6 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include TabRoom::TabRoom(TabSupervisor *_tabSupervisor, AbstractClient *_client, diff --git a/cockatrice/src/interface/widgets/utility/get_text_with_max.h b/cockatrice/src/interface/widgets/utility/get_text_with_max.h index 923d6f427..424bb0c6a 100644 --- a/cockatrice/src/interface/widgets/utility/get_text_with_max.h +++ b/cockatrice/src/interface/widgets/utility/get_text_with_max.h @@ -9,7 +9,7 @@ #include #include -#include +#include QString getTextWithMax(QWidget *parent, const QString &title, diff --git a/doc/carddatabase_v4/cards.xsd b/doc/carddatabase_v4/cards.xsd index 92d30b94f..59ca3e560 100644 --- a/doc/carddatabase_v4/cards.xsd +++ b/doc/carddatabase_v4/cards.xsd @@ -6,6 +6,7 @@ + diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp index edad46174..261b36690 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -85,7 +85,8 @@ void CardDatabase::refreshCachedReverseRelatedCards() for (auto *rel : card->getReverseRelatedCards()) { if (auto target = cards.value(rel->getName())) { auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(), - rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent()); + rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(), + rel->getIsFaceDown()); target->addReverseRelatedCards2Me(newRel); } } diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index 96a5ac104..c242425ab 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -329,6 +329,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) bool exclude = false; bool variable = false; bool persistent = false; + bool facedown = false; int count = 1; QXmlStreamAttributes attrs = xml.attributes(); QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements); @@ -360,7 +361,12 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) persistent = true; } - auto *relation = new CardRelation(cardName, attachType, exclude, variable, count, persistent); + if (attrs.hasAttribute("facedown")) { + facedown = true; + } + + auto *relation = + new CardRelation(cardName, attachType, exclude, variable, count, persistent, facedown); if (xmlName == "reverse-related") { reverseRelatedCards << relation; } else { @@ -510,6 +516,9 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in if (i->getIsPersistent()) { xml.writeAttribute("persistent", "persistent"); } + if (i->getIsFaceDown()) { + xml.writeAttribute("facedown", "facedown"); + } if (i->getIsVariable()) { if (1 == i->getDefaultCount()) { xml.writeAttribute("count", "x"); diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp index 90e59e439..8903c892d 100644 --- a/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.cpp @@ -7,8 +7,10 @@ CardRelation::CardRelation(const QString &_name, bool _isCreateAllExclusion, bool _isVariableCount, int _defaultCount, - bool _isPersistent) + bool _isPersistent, + bool _isFaceDown) : name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion), - isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent) + isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent), + isFaceDown(_isFaceDown) { -} \ No newline at end of file +} diff --git a/libcockatrice_card/libcockatrice/card/relation/card_relation.h b/libcockatrice_card/libcockatrice/card/relation/card_relation.h index 9ff704097..a1864f5b2 100644 --- a/libcockatrice_card/libcockatrice/card/relation/card_relation.h +++ b/libcockatrice_card/libcockatrice/card/relation/card_relation.h @@ -31,6 +31,7 @@ private: bool isVariableCount; ///< True if the number of creations is variable. int defaultCount; ///< Default number of cards created or involved. bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change. + bool isFaceDown; ///< True if this relation creates the tokens facedown public: /** @@ -42,13 +43,15 @@ public: * @param _isVariableCount Whether the count is variable. * @param _defaultCount Default number for creations or transformations. * @param _isPersistent Whether the relation persists across zone changes. + * @param _isFaceDown Whether the relation creates the token face down */ explicit CardRelation(const QString &_name = QString(), CardRelationType _attachType = CardRelationType::DoesNotAttach, bool _isCreateAllExclusion = false, bool _isVariableCount = false, int _defaultCount = 1, - bool _isPersistent = false); + bool _isPersistent = false, + bool _isFaceDown = false); /** * @brief Returns the name of the related card. @@ -151,6 +154,16 @@ public: { return isPersistent; } + + /** + * @brief Returns whether the relation creates the token facedown. + * + * @return True if facedown, false otherwise. + */ + [[nodiscard]] bool getIsFaceDown() const + { + return isFaceDown; + } }; #endif // COCKATRICE_CARD_RELATION_H diff --git a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp index 5e0cc31d8..8fd5311f2 100644 --- a/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp +++ b/libcockatrice_models/libcockatrice/models/database/card_set/card_sets_model.cpp @@ -303,12 +303,14 @@ bool SetsDisplayModel::filterAcceptsRow(int sourceRow, const QModelIndex &source auto typeIndex = sourceModel()->index(sourceRow, SetsModel::SetTypeCol, sourceParent); auto nameIndex = sourceModel()->index(sourceRow, SetsModel::LongNameCol, sourceParent); auto shortNameIndex = sourceModel()->index(sourceRow, SetsModel::ShortNameCol, sourceParent); + auto dateIndex = sourceModel()->index(sourceRow, SetsModel::ReleaseDateCol, sourceParent); const auto filter = filterRegularExpression(); return (sourceModel()->data(typeIndex).toString().contains(filter) || sourceModel()->data(nameIndex).toString().contains(filter) || - sourceModel()->data(shortNameIndex).toString().contains(filter)); + sourceModel()->data(shortNameIndex).toString().contains(filter) || + sourceModel()->data(dateIndex).toString().contains(filter)); } bool SetsDisplayModel::lessThan(const QModelIndex &left, const QModelIndex &right) const diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp index 493b8e966..5c0fdf944 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_participant.cpp @@ -48,7 +48,7 @@ #include #include #include -#include +#include Server_AbstractParticipant::Server_AbstractParticipant(Server_Game *_game, int _playerId, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp index 157fa6441..44e317bf7 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_abstract_player.cpp @@ -47,7 +47,8 @@ #include #include #include -#include +#include +#include #include #include #include diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp index b858314c0..8c7feadba 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.cpp @@ -26,7 +26,8 @@ #include #include #include -#include +#include +#include #include Server_Card::Server_Card(const CardRef &cardRef, int _id, int _coord_x, int _coord_y, Server_CardZone *_zone) @@ -114,8 +115,8 @@ QString Server_Card::setAttribute(CardAttribute attribute, const QString &avalue bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) { - // Clamp to valid card counter range [0, MAX_COUNTERS_ON_CARD] - value = qBound(0, value, MAX_COUNTERS_ON_CARD); + // Clamp to valid card counter range [0, MAX_COUNTER_VALUE] + value = qBound(0, value, MAX_COUNTER_VALUE); const int oldValue = counters.value(_id, 0); if (value == oldValue) { @@ -139,10 +140,8 @@ bool Server_Card::setCounter(int _id, int value, Event_SetCardCounter *event) bool Server_Card::incrementCounter(int counterId, int delta, Event_SetCardCounter *event) { const int oldValue = counters.value(counterId, 0); - const auto result = static_cast(oldValue) + static_cast(delta); - // Clamp to [0, MAX_COUNTERS_ON_CARD] for card counters - const int newValue = - static_cast(qBound(static_cast(0), result, static_cast(MAX_COUNTERS_ON_CARD))); + // Clamp to [0, MAX_COUNTER_VALUE] for card counters + const int newValue = addClamped(oldValue, delta, 0, MAX_COUNTER_VALUE); if (newValue == oldValue) { return false; diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h index 3d7e649b9..a2698ad61 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_card.h @@ -156,7 +156,7 @@ public: /** * @brief Sets a card counter to an exact value with clamping. * @param _id The counter ID. - * @param value The desired value (clamped to [0, MAX_COUNTERS_ON_CARD]; 0 removes the counter). + * @param value The desired value (clamped to [0, MAX_COUNTER_VALUE]; 0 removes the counter). * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. */ @@ -168,7 +168,7 @@ public: * @param event Optional event to populate with counter state. * @return true if the value changed, false otherwise. * @note If counter does not exist, starts from 0. Counter is removed if result is 0. - * @note Clamps result to [0, MAX_COUNTERS_ON_CARD]. + * @note Clamps result to [0, MAX_COUNTER_VALUE]. */ [[nodiscard]] bool incrementCounter(int counterId, int delta, Event_SetCardCounter *event = nullptr); void setTapped(bool _tapped) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp index e65205cbb..b18e11c2b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.cpp @@ -1,24 +1,12 @@ #include "server_counter.h" #include -#include Server_Counter::Server_Counter(int _id, const QString &_name, const color &_counterColor, int _radius, int _count) : id(_id), name(_name), counterColor(_counterColor), radius(_radius), count(_count) { } -//! \todo Extract overflow-safe arithmetic into shared helper. -//! Duplicated in Server_Card::incrementCounter() - keep in sync if modified. -bool Server_Counter::incrementCount(int delta) -{ - const int oldCount = count; - const auto result = static_cast(count) + static_cast(delta); - count = static_cast(qBound(static_cast(std::numeric_limits::min()), result, - static_cast(std::numeric_limits::max()))); - return count != oldCount; -} - void Server_Counter::getInfo(ServerInfo_Counter *info) { info->set_id(id); diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h index 8226e663f..ca093b7cf 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_counter.h @@ -22,6 +22,8 @@ #include #include +#include +#include class ServerInfo_Counter; @@ -92,7 +94,12 @@ public: * @return true if the value changed, false otherwise. * @note Clamps result to [INT_MIN, INT_MAX] to prevent overflow. */ - [[nodiscard]] bool incrementCount(int delta); + [[nodiscard]] bool incrementCount(int delta) + { + const int oldCount = count; + count = addClamped(count, delta, std::numeric_limits::min(), std::numeric_limits::max()); + return count != oldCount; + } /** * @brief Populates info with this counter's current state for network serialization. diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index 56e3f9f8e..d502fc7d6 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -47,7 +47,7 @@ #include #include #include -#include +#include #include Server_Player::Server_Player(Server_Game *_game, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp index 27ebaf228..c441da781 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_protocolhandler.cpp @@ -26,7 +26,7 @@ #include #include #include -#include +#include Server_ProtocolHandler::Server_ProtocolHandler(Server *_server, Server_DatabaseInterface *_databaseInterface, diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp index 1bd928e09..1f29e62fb 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_room.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include Server_Room::Server_Room(int _id, int _chatHistorySize, diff --git a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp index c419a68d4..4d4889323 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp +++ b/libcockatrice_protocol/libcockatrice/protocol/debug_pb_message.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include // FastFieldValuePrinter is added in protobuf 3.4, going out of our way to add the old FieldValuePrinter is not worth it #if GOOGLE_PROTOBUF_VERSION > 3004000 diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto index c10b9de22..ca46e4dd7 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/moderator_commands.proto @@ -116,8 +116,9 @@ message Command_AddCardArtRule { } optional string card_name = 1; - optional string mode = 2; // "ALLOW" or "DENY" - optional string reason = 3; + optional string card_provider_id = 2; + optional string mode = 3; // "ALLOW" or "DENY" + optional string reason = 4; } message Command_RemoveCardArtRule { @@ -126,6 +127,7 @@ message Command_RemoveCardArtRule { } optional string card_name = 1; + optional string card_provider_id = 2; } message Command_ListCardArtRules { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto index b13c79742..25b76e09f 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/response_card_art_rule_entry.proto @@ -3,8 +3,9 @@ import "response.proto"; message Response_CardArtRuleEntry { optional string card_name = 1; - optional string mode = 2; - optional string reason = 3; + optional string card_provider_id = 2; + optional string mode = 3; + optional string reason = 4; } message Response_ListCardArtRules { diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto index 4bbbe46bb..98cc3ce6a 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/serverinfo_user.proto @@ -15,10 +15,11 @@ message ServerInfo_User { }; message CardArtParams { optional string card_name = 1; - optional double margin_pct_l = 2 [default = 0.33]; - optional double margin_pct_r = 3 [default = 0.02]; - optional double vertical_offset = 4 [default = 0.35]; - optional double zoom = 5 [default = 1.0]; + optional string card_provider_id = 2; + optional double margin_pct_l = 3 [default = 0.33]; + optional double margin_pct_r = 4 [default = 0.02]; + optional double vertical_offset = 5 [default = 0.35]; + optional double zoom = 6 [default = 1.0]; }; optional string name = 1; diff --git a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto index ff5dbff32..9d207c711 100644 --- a/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto +++ b/libcockatrice_protocol/libcockatrice/protocol/pb/session_commands.proto @@ -212,8 +212,9 @@ message Command_SetCardArtParams { optional Command_SetCardArtParams ext = 1025; } optional string card_name = 1; - optional double margin_pct_l = 2; - optional double margin_pct_r = 3; - optional double vertical_offset = 4; - optional double zoom = 5; + optional string card_provider_id = 2; + optional double margin_pct_l = 3; + optional double margin_pct_r = 4; + optional double vertical_offset = 5; + optional double zoom = 6; } diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index df29d6c9f..2d34cad31 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -15,7 +15,10 @@ set(UTILITY_HEADERS libcockatrice/utility/levenshtein.h libcockatrice/utility/macros.h libcockatrice/utility/passwordhasher.h - libcockatrice/utility/trice_limits.h + libcockatrice/utility/string_limits.h + libcockatrice/utility/dice_limits.h + libcockatrice/utility/counter_limits.h + libcockatrice/utility/clamped_arithmetic.h libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h ) diff --git a/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h b/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h new file mode 100644 index 000000000..1afac758c --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/clamped_arithmetic.h @@ -0,0 +1,22 @@ +#ifndef CLAMPED_ARITHMETIC_H +#define CLAMPED_ARITHMETIC_H + +#include +#include + +/** + * @brief Overflow-safe clamped addition: returns value + delta bounded to [minValue, maxValue]. + * + * Uses a 64-bit intermediate so the addition cannot overflow int. Shared by the bounded + * counter arithmetic in both the client and the server. + * + * @note Requires minValue <= maxValue. Bounds come from trusted compile-time call sites; + * qBound() asserts this internally in debug builds. + */ +inline int addClamped(int value, int delta, int minValue, int maxValue) +{ + const auto result = static_cast(value) + static_cast(delta); + return static_cast(qBound(static_cast(minValue), result, static_cast(maxValue))); +} + +#endif // CLAMPED_ARITHMETIC_H diff --git a/libcockatrice_utility/libcockatrice/utility/counter_limits.h b/libcockatrice_utility/libcockatrice/utility/counter_limits.h new file mode 100644 index 000000000..1343dbb3f --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/counter_limits.h @@ -0,0 +1,17 @@ +#ifndef COUNTER_LIMITS_H +#define COUNTER_LIMITS_H + +/** + * @brief Upper bound for a bounded counter's value: [0, MAX_COUNTER_VALUE]. + * + * Caps an individual counter's VALUE (e.g. a +1/+1 counter at 999), not how many counters + * something holds. Applies to counters that are constrained to a non-negative display range, + * such as card counters and commander tax. Unbounded counters (e.g. a player's life total) + * do not use this limit and may go negative, saturating only at the int range. + * + * The max of 999 is a display constraint (3-digit rendering) and a reasonable gameplay limit. + * The server enforces these bounds; the client may also check them for UX optimization. + */ +constexpr int MAX_COUNTER_VALUE = 999; + +#endif // COUNTER_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/dice_limits.h b/libcockatrice_utility/libcockatrice/utility/dice_limits.h new file mode 100644 index 000000000..e1407c57e --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/dice_limits.h @@ -0,0 +1,15 @@ +#ifndef DICE_LIMITS_H +#define DICE_LIMITS_H + +#include // for uint + +/** @brief Fewest sides a rollable die may have. */ +constexpr uint MINIMUM_DIE_SIDES = 2; +/** @brief Most sides a rollable die may have. */ +constexpr uint MAXIMUM_DIE_SIDES = 1000000; +/** @brief Fewest dice that may be rolled at once. */ +constexpr uint MINIMUM_DICE_TO_ROLL = 1; +/** @brief Most dice that may be rolled at once. */ +constexpr uint MAXIMUM_DICE_TO_ROLL = 100; + +#endif // DICE_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/qt_utils.h b/libcockatrice_utility/libcockatrice/utility/qt_utils.h index 334e56027..8e5212031 100644 --- a/libcockatrice_utility/libcockatrice/utility/qt_utils.h +++ b/libcockatrice_utility/libcockatrice/utility/qt_utils.h @@ -1,5 +1,6 @@ #ifndef COCKATRICE_QT_UTILS_H #define COCKATRICE_QT_UTILS_H +#include #include namespace QtUtils diff --git a/libcockatrice_utility/libcockatrice/utility/string_limits.h b/libcockatrice_utility/libcockatrice/utility/string_limits.h new file mode 100644 index 000000000..cca804bf0 --- /dev/null +++ b/libcockatrice_utility/libcockatrice/utility/string_limits.h @@ -0,0 +1,31 @@ +#ifndef STRING_LIMITS_H +#define STRING_LIMITS_H + +#include +#include +#include + +/** @brief Max size for short strings, like names and things that are generally a single phrase. */ +constexpr int MAX_NAME_LENGTH = 0xff; +/** @brief Max size for chat messages and text contents. */ +constexpr int MAX_TEXT_LENGTH = 0xfff; +/** @brief Max size for deck files and pictures (about 2 megabytes). */ +constexpr int MAX_FILE_LENGTH = 0x1fffff; + +/** @brief Returns a QString from a std::string, truncated to at most MAX_NAME_LENGTH bytes. */ +inline QString nameFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH)); +} +/** @brief Returns a QString from a std::string, truncated to at most MAX_TEXT_LENGTH bytes. */ +inline QString textFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH)); +} +/** @brief Returns a QString from a std::string, truncated to at most MAX_FILE_LENGTH bytes. */ +inline QString fileFromStdString(const std::string &_string) +{ + return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH)); +} + +#endif // STRING_LIMITS_H diff --git a/libcockatrice_utility/libcockatrice/utility/trice_limits.h b/libcockatrice_utility/libcockatrice/utility/trice_limits.h deleted file mode 100644 index 833ce1b98..000000000 --- a/libcockatrice_utility/libcockatrice/utility/trice_limits.h +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef TRICE_LIMITS_H -#define TRICE_LIMITS_H - -#include - -// max size for short strings, like names and things that are generally a single phrase -constexpr int MAX_NAME_LENGTH = 0xff; -// max size for chat messages and text contents -constexpr int MAX_TEXT_LENGTH = 0xfff; -// max size for deck files and pictures -constexpr int MAX_FILE_LENGTH = 0x1fffff; // about 2 megabytes - -constexpr uint MINIMUM_DIE_SIDES = 2; -constexpr uint MAXIMUM_DIE_SIDES = 1000000; -constexpr uint MINIMUM_DICE_TO_ROLL = 1; -constexpr uint MAXIMUM_DICE_TO_ROLL = 100; - -// Card counter value bounds [0, MAX_COUNTERS_ON_CARD]. -// Counters on cards (e.g., +1/+1 counters, charge counters) are non-negative physical game objects. -// The max of 999 is a display constraint (3-digit rendering) and reasonable gameplay limit. -// Server enforces these bounds; client may also check for UX optimization. -constexpr int MAX_COUNTERS_ON_CARD = 999; - -// optimized functions to get qstrings that are at most that long -static inline QString nameFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_NAME_LENGTH)); -} -static inline QString textFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_TEXT_LENGTH)); -} -static inline QString fileFromStdString(const std::string &_string) -{ - return QString::fromUtf8(_string.data(), std::min(int(_string.size()), MAX_FILE_LENGTH)); -} - -#endif // TRICE_LIMITS_H diff --git a/oracle/oracle_en@source.ts b/oracle/oracle_en@source.ts index 943b44a97..b3263f861 100644 --- a/oracle/oracle_en@source.ts +++ b/oracle/oracle_en@source.ts @@ -278,7 +278,7 @@ OracleImporter - + Dummy set containing tokens @@ -286,7 +286,7 @@ OracleWizard - + Oracle Importer diff --git a/servatrice/migrations/servatrice_0034_to_0035.sql b/servatrice/migrations/servatrice_0034_to_0035.sql index 83502a949..acaad9c8b 100644 --- a/servatrice/migrations/servatrice_0034_to_0035.sql +++ b/servatrice/migrations/servatrice_0034_to_0035.sql @@ -3,12 +3,13 @@ ALTER TABLE `cockatrice_users` ADD COLUMN `card_art_params` TEXT DEFAULT NULL, A CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `card_name` varchar(255) NOT NULL, + `card_provider_id` varchar(255) NOT NULL, `mode` enum('ALLOW','DENY') NOT NULL, `reason` varchar(255) DEFAULT NULL, `created_by` int(7) unsigned DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `uniq_card_name` (`card_name`), + UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`), KEY `idx_mode` (`mode`), FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL diff --git a/servatrice/servatrice.sql b/servatrice/servatrice.sql index badd82d6d..7f530063c 100644 --- a/servatrice/servatrice.sql +++ b/servatrice/servatrice.sql @@ -305,12 +305,13 @@ CREATE TABLE IF NOT EXISTS `cockatrice_audit` ( CREATE TABLE IF NOT EXISTS `cockatrice_card_art_name_rules` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `card_name` varchar(255) NOT NULL, + `card_provider_id` varchar(255) NOT NULL, `mode` enum('ALLOW','DENY') NOT NULL, `reason` varchar(255) DEFAULT NULL, `created_by` int(7) unsigned DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `uniq_card_name` (`card_name`), + UNIQUE KEY `uniq_provider_card_name` (`card_provider_id`, `card_name`), KEY `idx_mode` (`mode`), FOREIGN KEY (`created_by`) REFERENCES `cockatrice_users`(`id`) ON DELETE SET NULL diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 92ff80755..d5e1f13ef 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -693,6 +693,9 @@ ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuer if (obj.contains("card_name")) { cap->set_card_name(obj["card_name"].toString().toStdString()); } + if (obj.contains("card_provider_id")) { + cap->set_card_provider_id(obj["card_provider_id"].toString().toStdString()); + } if (obj.contains("marginPctL")) { cap->set_margin_pct_l(obj["marginPctL"].toDouble(0.33)); } diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index f9d276941..6ceebfca9 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -83,7 +83,7 @@ #include #include #include -#include +#include #include #include #include @@ -1577,11 +1577,13 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Comm return Response::RespOk; } -bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName) +bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName, const QString &cardProviderId) { - QSqlQuery *q = sqlInterface->prepareQuery("SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name"); + QSqlQuery *q = sqlInterface->prepareQuery( + "SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name AND card_provider_id = :provider"); q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); if (!sqlInterface->execSqlQuery(q)) { qWarning() << "Card art rule lookup failed; failing open for" << cardName; @@ -1603,8 +1605,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const } const QString cardName = QString::fromStdString(cmd.card_name()); + const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); - if (cardName.length() > MAX_NAME_LENGTH) { + if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } @@ -1620,7 +1623,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const return Response::RespOk; } - if (!isCardNameAllowed(cardName)) { + if (!isCardNameAllowed(cardName, cardProviderId)) { return Response::RespFunctionNotAllowed; } @@ -1633,6 +1636,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const QJsonObject obj; obj["card_name"] = cardName; + obj["card_provider_id"] = cardProviderId; obj["marginPctL"] = marginPctL; obj["marginPctR"] = marginPctR; obj["verticalOffset"] = verticalOffset; @@ -1649,6 +1653,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const // Keep the in-memory userInfo in sync auto *cap = userInfo->mutable_card_art_params(); cap->set_card_name(cmd.card_name()); + cap->set_card_provider_id(cmd.card_provider_id()); cap->set_margin_pct_l(marginPctL); cap->set_margin_pct_r(marginPctR); cap->set_vertical_offset(verticalOffset); @@ -1664,21 +1669,23 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAddCardArtRule(const Co ResponseContainer &) { const QString cardName = QString::fromStdString(cmd.card_name()); + const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); const QString mode = QString::fromStdString(cmd.mode()); if (mode != "ALLOW" && mode != "DENY") { return Response::RespInvalidData; } - if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH) { + if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } QSqlQuery *q = sqlInterface->prepareQuery("INSERT INTO {prefix}_card_art_name_rules " - "(card_name, mode, reason, created_by) " - "VALUES (:name, :mode, :reason, :uid) " + "(card_name, card_provider_id, mode, reason, created_by) " + "VALUES (:name, :provider, :mode, :reason, :uid) " "ON DUPLICATE KEY UPDATE mode=:mode2, reason=:reason2"); q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); q->bindValue(":mode", mode); q->bindValue(":mode2", mode); q->bindValue(":reason", QString::fromStdString(cmd.reason())); @@ -1696,12 +1703,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const ResponseContainer &) { auto cardName = QString::fromStdString(cmd.card_name()); - if (cardName.length() > MAX_NAME_LENGTH) { + auto cardProviderId = QString::fromStdString(cmd.card_provider_id()); + if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } - QSqlQuery *q = sqlInterface->prepareQuery("DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name"); + QSqlQuery *q = sqlInterface->prepareQuery( + "DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name AND card_provider_id=:provider"); q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; @@ -1713,7 +1723,8 @@ Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const Command_ListCardArtRules &, ResponseContainer &rc) { - QSqlQuery *q = sqlInterface->prepareQuery("SELECT card_name, mode, reason FROM {prefix}_card_art_name_rules"); + QSqlQuery *q = sqlInterface->prepareQuery( + "SELECT card_name, card_provider_id, mode, reason FROM {prefix}_card_art_name_rules"); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; @@ -1724,8 +1735,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const while (q->next()) { auto *entry = re->add_entries(); entry->set_card_name(q->value(0).toString().toStdString()); - entry->set_mode(q->value(1).toString().toStdString()); - entry->set_reason(q->value(2).toString().toStdString()); + entry->set_card_provider_id(q->value(1).toString().toStdString()); + entry->set_mode(q->value(2).toString().toStdString()); + entry->set_reason(q->value(3).toString().toStdString()); } rc.setResponseExtension(re); diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index c0732ccd9..0d66ae78f 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -129,7 +129,7 @@ private: Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); - bool isCardNameAllowed(const QString &cardName); + bool isCardNameAllowed(const QString &cardName, const QString &cardProviderId); Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &); Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &); Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 04ac7fcee..a179a3603 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,7 @@ enable_testing() add_test(NAME dummy_test COMMAND dummy_test) add_test(NAME expression_test COMMAND expression_test) +add_test(NAME clamped_arithmetic_test COMMAND clamped_arithmetic_test) add_test(NAME test_age_formatting COMMAND test_age_formatting) add_test(NAME password_hash_test COMMAND password_hash_test) add_test(NAME server_card_counter_test COMMAND server_card_counter_test) @@ -16,6 +17,7 @@ set_tests_properties(deck_hash_performance_test PROPERTIES TIMEOUT 5) add_executable(dummy_test dummy_test.cpp) add_executable(expression_test expression_test.cpp) +add_executable(clamped_arithmetic_test clamped_arithmetic_test.cpp) add_executable(test_age_formatting test_age_formatting.cpp) add_executable(password_hash_test password_hash_test.cpp) add_executable(deck_hash_performance_test deck_hash_performance_test.cpp) @@ -49,6 +51,7 @@ if(NOT GTEST_FOUND) set(GTEST_BOTH_LIBRARIES gtest) add_dependencies(dummy_test gtest) add_dependencies(expression_test gtest) + add_dependencies(clamped_arithmetic_test gtest) add_dependencies(test_age_formatting gtest) add_dependencies(password_hash_test gtest) add_dependencies(deck_hash_performance_test gtest) @@ -59,6 +62,9 @@ endif() include_directories(${GTEST_INCLUDE_DIRS}) target_link_libraries(dummy_test Threads::Threads ${GTEST_BOTH_LIBRARIES}) target_link_libraries(expression_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) +target_link_libraries( + clamped_arithmetic_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} +) target_link_libraries( test_age_formatting libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) diff --git a/tests/clamped_arithmetic_test.cpp b/tests/clamped_arithmetic_test.cpp new file mode 100644 index 000000000..2471d5870 --- /dev/null +++ b/tests/clamped_arithmetic_test.cpp @@ -0,0 +1,44 @@ +/** @file clamped_arithmetic_test.cpp + * @brief Tests for shared helpers in clamped_arithmetic.h. + * @ingroup Tests + */ + +#include +#include +#include + +TEST(AddClamped, AddsWithinBounds) +{ + EXPECT_EQ(addClamped(5, 3, 0, 100), 8); + EXPECT_EQ(addClamped(10, -3, 0, 100), 7); +} + +TEST(AddClamped, ClampsToUpperAndLowerBound) +{ + EXPECT_EQ(addClamped(99, 5, 0, 100), 100); // saturates at max + EXPECT_EQ(addClamped(2, -10, 0, 100), 0); // saturates at min + EXPECT_EQ(addClamped(999, 1, 0, 999), 999); // crossing the counter cap holds at the bound +} + +TEST(AddClamped, IntOverflowDoesNotWrap) +{ + // The 64-bit intermediate must prevent signed-int overflow UB. + constexpr int intMax = std::numeric_limits::max(); + constexpr int intMin = std::numeric_limits::min(); + EXPECT_EQ(addClamped(intMax, 1, intMin, intMax), intMax); + EXPECT_EQ(addClamped(intMax, intMax, intMin, intMax), intMax); +} + +TEST(AddClamped, IntUnderflowDoesNotWrap) +{ + constexpr int intMax = std::numeric_limits::max(); + constexpr int intMin = std::numeric_limits::min(); + EXPECT_EQ(addClamped(intMin, -1, intMin, intMax), intMin); + EXPECT_EQ(addClamped(intMin, intMin, intMin, intMax), intMin); +} + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/server_card_counter_test.cpp b/tests/server_card_counter_test.cpp index ff906b906..c8bc43f8f 100644 --- a/tests/server_card_counter_test.cpp +++ b/tests/server_card_counter_test.cpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include TEST(ServerCardCounter, IncrementNewCounter) @@ -28,9 +28,9 @@ TEST(ServerCardCounter, IncrementExistingCounter) TEST(ServerCardCounter, IncrementOverflowProtection) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); EXPECT_FALSE(card.incrementCounter(1, 1)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, DecrementUnderflowProtection) @@ -113,13 +113,13 @@ TEST(ServerCardCounter, IncrementCounterPopulatesEvent) TEST(ServerCardCounter, IncrementCounterEventReflectsClampedValue) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); Event_SetCardCounter event; EXPECT_TRUE(card.incrementCounter(1, 10, &event)); EXPECT_EQ(event.counter_id(), 1); - EXPECT_EQ(event.counter_value(), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(event.counter_value(), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) @@ -133,7 +133,7 @@ TEST(ServerCardCounter, IncrementCounterNoEventWhenNullptr) TEST(ServerCardCounter, IncrementCounterEventNotPopulatedWhenUnchanged) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE)); Event_SetCardCounter event; event.set_counter_id(999); @@ -156,7 +156,7 @@ TEST(ServerCardCounter, SetCounterClampsAboveMaxToMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); EXPECT_TRUE(card.setCounter(1, 1500)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) @@ -171,9 +171,9 @@ TEST(ServerCardCounter, IncrementDoesNotGoBelowZero) TEST(ServerCardCounter, IncrementDoesNotExceedMax) { Server_Card card(CardRef{"TestCard", ""}, 1, 0, 0); - ASSERT_TRUE(card.setCounter(1, MAX_COUNTERS_ON_CARD - 5)); + ASSERT_TRUE(card.setCounter(1, MAX_COUNTER_VALUE - 5)); EXPECT_TRUE(card.incrementCounter(1, 10)); - EXPECT_EQ(card.getCounter(1), MAX_COUNTERS_ON_CARD); + EXPECT_EQ(card.getCounter(1), MAX_COUNTER_VALUE); } int main(int argc, char **argv)