From 7e6c4d8a6c86faa2a71333a2f35805e9bc2af2de Mon Sep 17 00:00:00 2001 From: tooomm Date: Sun, 28 Oct 2018 18:11:51 +0100 Subject: [PATCH 01/62] fix appveyor dependencies (#3420) ty @ctrlaltca --- .appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 6f1ca0f99..1e143afa7 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -33,8 +33,8 @@ cache: environment: - openssl_ver: 1.0.2o - protobuf_ver: 3.6.0 + openssl_ver: 1.0.2p + protobuf_ver: 3.6.1 zlib_ver: 1.2.11 matrix: From 4e3d4991620a55bd501fd7be43e47a58d7e2fb59 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 30 Oct 2018 17:45:04 -0400 Subject: [PATCH 02/62] improve translating section of CONTRIBUTING.md (#3424) Remove the weird out of order numbering and replace it with just chapters for specific users. Add a bit of explanation to adding translations as a developer. This is just a recommendation for an improvement, jumplist could be added and/or #3423 merged with this. (it fixes the typos) --- .github/CONTRIBUTING.md | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5a7f7bec7..20affe746 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -177,33 +177,30 @@ You can find more information on how we use Protobuf on [our wiki!](https://gith # Translations # -**Basic workflow for translations:** +Basic workflow for translations: 1. Developer adds a `tr("foo")` string in the code; 2. Every few days, a maintainer updates the `*_en.ts files` with the new strings; 3. Transifex picks up the new files from github every 24 hours; 4. Translators translate the new untraslated strings on Transifex; 5. Before a release, a maintainer fetches the updated translations from Transifex. -### Translations (for developers) ### +### Using Translations (for developers) ### -**Step 1: Adding translatable strings to the code (`tr("foo")`)** - -All the user-interface strings inside Cockatrice's source code must be written in -english language.
+All the user-interface strings inside Cockatrice's source code must be written in english. Translations to other languages are managed using [Transifex](https://www.transifex.com/projects/p/cockatrice/). -If you're about to propose a change that adds or modifies any translatable string -in the code, you don't need to take care of adding the new strings to the -translation files. Every few days, or when a lot of new strings have been added, -someone from the development team will take care of extracing all the new strings, -adding them to the english translation files and making them available to -translators on Transifex. +Adding a new string to translate is as easy as adding the string in the 'tr("")' function, the string will be picked up as translatable automatically and translated as needed. +For example setting the text of this label in a way that the string "My name is:" can be translated: +```c++ +nameLabel.setText(tr("My name is:")); +``` -### Translations (for maintainers) ### +If you're about to propose a change that adds or modifies any translatable string in the code, you don't need to take care of adding the new strings to the translation files. +Every few days, or when a lot of new strings have been added, someone from the development team will take care of extracing all the new strings and adding them to the english translation files and making them available to translators on Transifex. -**Step 2: Updating `*_en.ts` files with new strings** +### Maintaining Translations (for maintainers) ### -When new translatable strings have been added to the code, it would be nice to +When new translatable strings have been added to the code, a maintainer should make them available to translators on Transifex. Every few days, or when a lot of new strings have been added, a maintainer should take care of extracing all the new strings and add them to the english translation files. @@ -233,14 +230,12 @@ cmake .. -DUPDATE_TRANSLATIONS=OFF ``` Now you are ready to propose your change. -**Step 3: Automatic pushing to Transifex** - Once your change gets merged, Transifex will pick up the modified files automatically (checks every 24 hours) and update the interface where translators will be able to translate the new strings. -**Step 5: Fetching new translations from Transifex** +### Releasing Translations (for maintainers) ### -Before rushing out a new release, it would be nice to fetch the most up to date +Before rushing out a new release, a maintainer should fetch the most up to date translations from Transifex and commit them into the Cockatrice source code. This can be done manually from the Transifex web interface, but it's quite time consuming. @@ -252,10 +247,9 @@ As an alternative, you can install the Transifex CLI: You'll then be able to use a git-like cli command to push and pull translations from Transifex to the source code and vice versa. -### Translations (for translators) ### - -**Step 4: Editing translations at Transifex** +### Adding Translations (for translators) ### +As a translator you can help translate the new strings on [Transifex](https://www.transifex.com/projects/p/cockatrice/). Please have a look at the specific [FAQ for translators](https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ). From 051fcff284090385ac6e2a33c92bf186e8d66964 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 31 Oct 2018 21:14:05 -0400 Subject: [PATCH 03/62] add more precise rules to CONTRIBUTING.md (#3423) Add extra rules that clang-format enforces, like the order of includes and the way single line comments should be formatted. Correct some grammar ( what is up with the translation section ordering steps out of order?? ). This is a web edit. --- .github/CONTRIBUTING.md | 44 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 20affe746..bcf29001f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -38,25 +38,25 @@ Use header guards in the form of `FILE_NAME_H`. Simple functions, such as getters, may be written inline in the header file, but other functions should be written in the source file. -Keep library includes and project includes grouped together. So this is okay: +Group library includes after project includes, and in alphabetic order. Like this: ```c++ -// Good -#include -#include -#include "card.h" -#include "deck.h" - // Good #include "card.h" #include "deck.h" #include #include -// Bad: +// Bad #include #include "card.h" #include #include "deck.h" + +// Bad +#include "card.h" +#include "deck.h" +#include +#include ``` ### Naming ### @@ -95,10 +95,10 @@ Braces should go on their own line except for control statements, the use of bra See the following example: ```c++ int main() -{ // function or class: own line - if (someCondition) { // control statement: same line - doSomething(); // single line statement, braces preferred - } else if (someOtherCondition1) { // else goes after closing brace +{ // function or class: own line + if (someCondition) { // control statement: same line + doSomething(); // single line statement, braces preferred + } else if (someOtherCondition1) { // else goes on the same line as a closing brace for (int i = 0; i < 100; i++) { doSomethingElse(); } @@ -110,9 +110,11 @@ int main() } ``` -### Indentation ### +### Indentation and Spacing ### Always indent using 4 spaces, do not use tabs. Opening and closing braces should be on the same indentation layer, member access specifiers in classes or structs should not be indented. +All operators and braces should be separated by spaces, do not add a space next to the inside of a brace. +If multiple lines of code that follow eachother have single line comments behind them, place all of them on the same indentation level. This indentation level should be equal to the longest line of code for each of these comments, without added spacing. ### Lines ### @@ -181,7 +183,7 @@ Basic workflow for translations: 1. Developer adds a `tr("foo")` string in the code; 2. Every few days, a maintainer updates the `*_en.ts files` with the new strings; 3. Transifex picks up the new files from github every 24 hours; - 4. Translators translate the new untraslated strings on Transifex; + 4. Translators translate the new untranslated strings on Transifex; 5. Before a release, a maintainer fetches the updated translations from Transifex. ### Using Translations (for developers) ### @@ -196,13 +198,13 @@ nameLabel.setText(tr("My name is:")); ``` If you're about to propose a change that adds or modifies any translatable string in the code, you don't need to take care of adding the new strings to the translation files. -Every few days, or when a lot of new strings have been added, someone from the development team will take care of extracing all the new strings and adding them to the english translation files and making them available to translators on Transifex. +Every few days, or when a lot of new strings have been added, someone from the development team will take care of extracting all the new strings and adding them to the english translation files and making them available to translators on Transifex. ### Maintaining Translations (for maintainers) ### When new translatable strings have been added to the code, a maintainer should make them available to translators on Transifex. Every few days, or when a lot -of new strings have been added, a maintainer should take care of extracing all +of new strings have been added, a maintainer should take care of extracting all the new strings and add them to the english translation files. To update the english translation files, re-run cmake enabling the appropriate @@ -224,13 +226,13 @@ You should then notice that the following files have uncommitted changes: cockatrice/translations/cockatrice_en.ts oracle/translations/oracle_en.ts -It's now suggested to disable the parameter using: +It is recommended to disable the parameter afterwards using: ```sh cmake .. -DUPDATE_TRANSLATIONS=OFF ``` Now you are ready to propose your change. -Once your change gets merged, Transifex will pick up the modified files automatically (checks every 24 hours) +Once your change gets merged, Transifex will pick up the modified files automatically (checked every 24 hours) and update the interface where translators will be able to translate the new strings. ### Releasing Translations (for maintainers) ### @@ -289,11 +291,11 @@ git tag -d $TAG_NAME **NOTE:** Unfortunately, due to the method of how Travis and AppVeyor work, to publish a stable release you will need to make a copy of the release notes locally and then paste them into the GitHub GUI once the binaries have been uploaded by them. These CI services will automatically overwrite the name of the release (to "Cockatrice $TAG_NAME"), the status of the release (to "Pre-release"), and the release body (to "Beta build of Cockatrice"). -**NOTE 2:** In the first lines of https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt there's an hardcoded version number used when compiling custom (not tagged) versions. While on tagged versions these numbers are overriden by the version numbers coming from the tag title, it's a good practice to keep them aligned with the real ones. -The preferred flow of operations is: +**NOTE 2:** In the first lines of https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt there's an hardcoded version number used when compiling custom (not tagged) versions. While on tagged versions these numbers are overridden by the version numbers coming from the tag title, it's good practice to keep them aligned with the real ones. +The preferred flow of operation is: * just before a release, update the version number in CMakeLists.txt to "next release version"; * tag the release following the previously described syntax in order to get it built by CI; * wait for CI to upload the binaries, double check if everything is in order * after the release is complete, update the version number again to "next targeted beta version", typically increasing `PROJECT_VERSION_PATCH` by one. -**NOTE 3:** When releasing a new stable version, all the previous beta versions should be deleted. This is needed for Cockatrice to pick up the stable release also for users that chose the "beta" release channel. +**NOTE 3:** When releasing a new stable version, all the previous beta versions should be deleted. This is needed for Cockatrice to update users of the "beta" release channel to the latest version like other users. From 0cd671c0225c6b418b5df0a159685d78d5c2b35c Mon Sep 17 00:00:00 2001 From: tooomm Date: Thu, 1 Nov 2018 02:14:47 +0100 Subject: [PATCH 04/62] Improve hints for manage sets dialog (#3419) --- cockatrice/src/window_sets.cpp | 14 +++++++++----- cockatrice/src/window_sets.h | 12 +++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/cockatrice/src/window_sets.cpp b/cockatrice/src/window_sets.cpp index 374b79b09..a637f0243 100644 --- a/cockatrice/src/window_sets.cpp +++ b/cockatrice/src/window_sets.cpp @@ -127,14 +127,18 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) labNotes->setOpenExternalLinks(true); labNotes->setText( "" + tr("Deck Editor") + ": " + - tr("Only cards in enabled sets will appear in the deck editor card list") + "
" + tr("Card Art") + - ": " + tr("Image priority is decided in the following order") + "
  1. " + tr("The") + - " " + tr("CUSTOM Folder") + "
  2. " + tr("Enabled Sets (Top to Bottom)") + "
  3. " + tr("Disabled Sets (Top to Bottom)") + "
"); + QGridLayout *hintsGrid = new QGridLayout; + hintsGrid->addWidget(labNotes, 0, 0); + hintsGroupBox = new QGroupBox(tr("Hints")); + hintsGroupBox->setLayout(hintsGrid); + sortWarning = new QLabel; sortWarning->setWordWrap(true); sortWarning->setText( @@ -157,7 +161,7 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) mainLayout->addWidget(enableSomeButton, 2, 1); mainLayout->addWidget(disableSomeButton, 2, 2); mainLayout->addWidget(sortWarning, 3, 1, 1, 2); - mainLayout->addWidget(labNotes, 4, 1, 1, 2); + mainLayout->addWidget(hintsGroupBox, 4, 1, 1, 2); mainLayout->addWidget(buttonBox, 5, 1, 1, 2); mainLayout->setColumnStretch(1, 1); mainLayout->setColumnStretch(2, 1); diff --git a/cockatrice/src/window_sets.h b/cockatrice/src/window_sets.h index 86632d7d0..d8ce32915 100644 --- a/cockatrice/src/window_sets.h +++ b/cockatrice/src/window_sets.h @@ -8,13 +8,14 @@ #include #include +class CardDatabase; +class QGroupBox; +class QItemSelection; +class QPushButton; +class QTreeView; +class SetsDisplayModel; class SetsModel; class SetsProxyModel; -class SetsDisplayModel; -class QPushButton; -class CardDatabase; -class QItemSelection; -class QTreeView; class WndSets : public QMainWindow { @@ -22,6 +23,7 @@ class WndSets : public QMainWindow private: SetsModel *model; SetsDisplayModel *displayModel; + QGroupBox *hintsGroupBox; QTreeView *view; QPushButton *toggleAllButton, *toggleSelectedButton; QPushButton *enableAllButton, *disableAllButton, *enableSomeButton, *disableSomeButton; From 8b7a287b4417cf6ab45c77bd764caf84da221912 Mon Sep 17 00:00:00 2001 From: tooomm Date: Wed, 7 Nov 2018 08:32:20 +0100 Subject: [PATCH 05/62] clarify min req (#3426) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0b8e8d11..cfcf41fdb 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Check out our [Translator FAQ](https://github.com/Cockatrice/Cockatrice/wiki/Tra **Detailed compiling instructions are on the Cockatrice wiki under [Compiling Cockatrice](https://github.com/Cockatrice/Cockatrice/wiki/Compiling-Cockatrice)** -Dependencies: +Dependencies: *(for minimum requirements search our [CMake file](https://github.com/Cockatrice/Cockatrice/blob/master/CMakeLists.txt))* - [Qt](https://www.qt.io/developers/) - [protobuf](https://github.com/google/protobuf) - [CMake](https://www.cmake.org/) From e1e9caf0ef7f027394529ca378313dd3ab5a1745 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 7 Nov 2018 12:05:25 +0100 Subject: [PATCH 06/62] Load plain improvements (#3422) * improve loading from plain text Fixes the loadFromStream_Plain function that is used to load plain text decklists. The rewritten function uses more regexes and is a bit cleaner. This fixes multiple bugs with loading the various sources of decklists. Note that the new function still has a few issues that are shared with the original version like creating duplicate cards. * clang format comments apparently clang-format even complains about the spacing in your comments * refactor loading_from_clipboard tests Remove all heap allocation and use references. Use std::pair and std::string so gtest will show the cardnames in error messages. (note that using QPair or QString does not work with gtest) Improve the last two testcases to include weird names; and name and comments testing. Remove empty header file. * fix compatibility with more formats skip "sideboard" line include everything in mainboard when there are multiple empty lines add removal of the mwdeck cardversion selector in round braces add replacal of lowercase ae combination that should never occur Set cardname to lowercase as apparently our checks are hardcoded to only accept lowercase. * remove bugged test The current load from plain is simply broken, removed checking the comments for correct contents. * rework load_from_clipboard tests again rework the test to have less code duplication add more tests and more special cases note that text is still all lowercase * improve loading from plain text Fixes the loadFromStream_Plain function that is used to load plain text decklists. The rewritten function uses more regexes and is a bit cleaner. This fixes multiple bugs with loading the various sources of decklists. Note that the new function still has a few issues that are shared with the original version like creating duplicate cards. * clang format comments apparently clang-format even complains about the spacing in your comments * refactor loading_from_clipboard tests Remove all heap allocation and use references. Use std::pair and std::string so gtest will show the cardnames in error messages. (note that using QPair or QString does not work with gtest) Improve the last two testcases to include weird names; and name and comments testing. Remove empty header file. * fix compatibility with more formats skip "sideboard" line include everything in mainboard when there are multiple empty lines add removal of the mwdeck cardversion selector in round braces add replacal of lowercase ae combination that should never occur Set cardname to lowercase as apparently our checks are hardcoded to only accept lowercase. * remove bugged test The current load from plain is simply broken, removed checking the comments for correct contents. * rework load_from_clipboard tests again rework the test to have less code duplication add more tests and more special cases note that text is still all lowercase * remove forcing of lowercase cardnames Cardnames in DeckList::loadFromStream_Plain will no longer be forced lowercase if they aren't found in the database. Empty lines in the comments of plaintext decklists will not be skipped. The loading_from_clipboard_test gets its functions declared in a separate header "clipboard_testing.h". Add more edgecase tests. Refactor code. * add old QHash version support QT 5.5 does not support using initializer lists for QHash. Implement a preprocessor version check for conditionally using inserts instead of a const with initializer list. * add old QHash version support QT 5.5 does not support using initializer lists for QHash. Implement a preprocessor version check for conditionally using [] access instead of a const with initializer list. * add qHash on QRegularExpression below QT 5.6 Apparently QRegularExpression can't be hashed in lower QT versions, so we add our own hash function, and everyone lived happily ever after, and none the wiser. * add header guards to clipboard_testing.h --- common/decklist.cpp | 241 +++++++------- tests/loading_from_clipboard/CMakeLists.txt | 3 +- .../clipboard_testing.cpp | 40 +++ .../clipboard_testing.h | 32 ++ .../loading_from_clipboard_test.cpp | 304 +++++++----------- .../loading_from_clipboard_test.h | 0 6 files changed, 296 insertions(+), 324 deletions(-) create mode 100644 tests/loading_from_clipboard/clipboard_testing.cpp create mode 100644 tests/loading_from_clipboard/clipboard_testing.h delete mode 100644 tests/loading_from_clipboard/loading_from_clipboard_test.h diff --git a/common/decklist.cpp b/common/decklist.cpp index 0993d71f1..be96cfa2d 100644 --- a/common/decklist.cpp +++ b/common/decklist.cpp @@ -2,8 +2,17 @@ #include #include #include +#include #include +#if QT_VERSION < 0x050600 +// qHash on QRegularExpression was added in 5.6, FIX IT +uint qHash(const QRegularExpression &key, uint seed) noexcept +{ + return qHash(key.pattern(), seed); // call qHash on pattern QString instead +} +#endif + SideboardPlan::SideboardPlan(const QString &_name, const QList &_moveList) : name(_name), moveList(_moveList) { @@ -477,161 +486,131 @@ bool DeckList::saveToFile_Native(QIODevice *device) bool DeckList::loadFromStream_Plain(QTextStream &in) { + const QRegularExpression reCardLine("^\\s*[\\w\\[\\(\\{].*$", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reEmpty("^\\s*$"); + const QRegularExpression reComment("[\\w\\[\\(\\{].*$", QRegularExpression::UseUnicodePropertiesOption); + const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption); + const QRegularExpression reSBComment("sideboard", QRegularExpression::CaseInsensitiveOption); + + // simplified matches + const QRegularExpression reMultiplier("^[xX\\(\\[]*(\\d+)[xX\\*\\)\\]]* ?(.+)"); + const QRegularExpression reBrace(" ?[\\[\\{][^\\]\\}]*[\\]\\}] ?"); // not nested + const QRegularExpression reRoundBrace("^\\([^\\)]*\\) ?"); // () are only matched at start of string + const QRegularExpression reDigitBrace(" ?\\(\\d*\\) ?"); // () are matched if containing digits + const QHash differences{{QRegularExpression("’"), QString("'")}, + {QRegularExpression("Æ"), QString("Ae")}, + {QRegularExpression("æ"), QString("ae")}, + {QRegularExpression(" ?[|/]+ ?"), QString(" // ")}, + {QRegularExpression("(? inputs; // QTextStream -> QVector - bool priorEntryIsBlank = true, isAtBeginning = true; - int blankLines = 0; - while (!in.atEnd()) { - QString line = in.readLine().simplified().toLower(); + QStringList inputs = in.readAll().trimmed().split('\n'); + int max_line = inputs.size(); - /* - * Removes all blank lines at start of inputs - * Ex: ("", "", "", "Card1", "Card2") => ("Card1", "Card2") - * - * This will also concise multiple blank lines in a row to just one blank - * Ex: ("Card1", "Card2", "", "", "", "Card3") => ("Card1", "Card2", "", "Card3") - */ - if (line.isEmpty()) { - if (priorEntryIsBlank || isAtBeginning) { - continue; - } - - priorEntryIsBlank = true; - blankLines++; - } else { - isAtBeginning = false; - priorEntryIsBlank = false; + // start at the first empty line before the first cardline + int deckStart = inputs.indexOf(reCardLine); + if (deckStart == -1) { // there are no cards? + if (inputs.indexOf(reComment) == -1) + return false; // input is empty + deckStart = max_line; + } else { + deckStart = inputs.lastIndexOf(reEmpty, deckStart); + if (deckStart == -1) { + deckStart = 0; } - - inputs.push_back(line); } - /* - * Removes blank line at end of inputs (if applicable) - * Ex: ("Card1", "Card2", "") => ("Card1", "Card2") - * NOTE: Any duplicates were taken care of above, so there can be - * at most one blank line at the very end - */ - if (!inputs.empty() && inputs.last().isEmpty()) { - blankLines--; - inputs.erase(inputs.end() - 1); - } - - // If "Sideboard" line appears in inputs, then blank lines mean nothing - if (inputs.contains("sideboard")) { - blankLines = 2; - } - - bool inSideboard = false, titleFound = false, isSideboard; - int okRows = 0; - - foreach (QString line, inputs) { - // This is a comment line, ignore it - if (line.startsWith("//")) { - if (!titleFound) // Set the title to the first comment - { - name = line.mid(2).trimmed(); - titleFound = true; - } else if (okRows == 0) // We haven't processed any cards yet - { - comments += line.mid(2).trimmed() + "\n"; + // find sideboard position, if marks are used this won't be needed + int sBStart = -1; + if (inputs.indexOf(reSBMark, deckStart) == -1) { + sBStart = inputs.indexOf(reSBComment, deckStart); + if (sBStart == -1) { + sBStart = inputs.indexOf(reEmpty, deckStart + 1); + if (sBStart == -1) { + sBStart = max_line; } + int nextCard = inputs.indexOf(reCardLine, sBStart + 1); + if (inputs.indexOf(reEmpty, nextCard + 1) != -1) { + sBStart = max_line; // if there is another empty line all cards are mainboard + } + } + } + int index = 0; + QRegularExpressionMatch match; + + // parse name and comments + while (index < deckStart) { + const QString current = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + name = match.captured(); + break; + } + } + while (index < deckStart) { + const QString current = inputs.at(index++); + if (!current.contains(reEmpty)) { + match = reComment.match(current); + comments += match.captured() + '\n'; + } + } + comments.chop(1); // remove last newline + + // parse decklist + for (; index < max_line; ++index) { + + // check if line is a card + match = reCardLine.match(inputs.at(index)); + if (!match.hasMatch()) continue; - } + QString cardName = match.captured().simplified(); - // If we have a blank line and it's the _ONLY_ blank line in the paste - // and it follows at least one valid card - // Then we assume it means to start the sideboard section of the paste. - // If we have the word "Sideboard" appear on any line, then that will - // also indicate the start of the sideboard. - if ((line.isEmpty() && blankLines == 1 && okRows > 0) || line.startsWith("sideboard")) { - inSideboard = true; - continue; // The line isn't actually a card - } - - isSideboard = inSideboard; - - if (line.startsWith("sb:")) { - line = line.mid(3).trimmed(); - isSideboard = true; - } - - if (line.trimmed().isEmpty()) { - continue; // The line was " " instead of "\n" - } - - // Filter out MWS edition symbols and basic land extras - QRegExp rx("\\[.*\\]\\s?"); - line.remove(rx); - rx.setPattern("\\s?\\(.*\\)"); - line.remove(rx); - - // Filter out post card name editions - rx.setPattern("\\|.*$"); - line.remove(rx); - - // If the user inputs "Quicksilver Elemental" then it will cut it off - // 1x Squishy Treaker - int i = line.indexOf(' '); - int cardNameStart = i + 1; - - if (i > 0) { - // If the count ends with an 'x', ignore it. For example, - // "4x Storm Crow" will count 4 correctly. - if (line.at(i - 1) == 'x') { - i--; - } else if (!line.at(i - 1).isDigit()) { - // If the user inputs "Quicksilver Elemental" then it will work as 1x of that card - cardNameStart = 0; + // check if card should be sideboard + bool sideboard = false; + if (sBStart < 0) { + match = reSBMark.match(cardName); + if (match.hasMatch()) { + sideboard = true; + cardName = match.captured(1); } + } else { + if (index == sBStart) // skip sideboard line itself + continue; + sideboard = index > sBStart; } - bool ok; - int number = line.left(i).toInt(&ok); - - if (!ok) { - number = 1; // If input is "cardName" assume it's "1x cardName" + // check if a specific amount is mentioned + int amount = 1; + match = reMultiplier.match(cardName); + if (match.hasMatch()) { + amount = match.capturedRef(1).toInt(); + cardName = match.captured(2); } - QString cardName = line.mid(cardNameStart); + // remove stuff inbetween braces + cardName.remove(reBrace); + cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards + cardName.remove(reDigitBrace); // all cards from un-sets that have a word in between round braces at the end - // Common differences between Cockatrice's card names - // and what's commonly used in decklists - rx.setPattern("’"); - cardName.replace(rx, "'"); - rx.setPattern("Æ"); - cardName.replace(rx, "Ae"); - rx.setPattern("\\s*[|/]{1,2}\\s*"); - cardName.replace(rx, " // "); - - // Replace only if the ampersand is preceded by a non-capital letter, - // as would happen with acronyms. So 'Fire & Ice' is replaced but not - // 'R&D' or 'R & D'. - // Qt regexes don't support lookbehind so we capture and replace instead. - rx.setPattern("([^A-Z])\\s*&\\s*"); - if (rx.indexIn(cardName) != -1) { - cardName.replace(rx, QString("%1 // ").arg(rx.cap(1))); + // replace common differences in cardnames + for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) { + cardName.replace(diff.key(), diff.value()); } - // We need to get the name of the card from the database, - // but we can't do that until we get the "real" name - // (name stored in database for the card) - // and establish a card info that is of the card, then it's - // a simple getting the _real_ name of the card - // (i.e. "STOrm, CrOW" => "Storm Crow") + // get cardname, this function does nothing if the name is not found cardName = getCompleteCardName(cardName); - // Look for the correct card zone of where to place the new card - QString zoneName = getCardZoneFromName(cardName, isSideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN); + // get zone name based on if it's in sideboard + QString zoneName = getCardZoneFromName(cardName, sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN); - okRows++; - new DecklistCardNode(cardName, number, getZoneObjFromName(zoneName)); + // make new entry in decklist + new DecklistCardNode(cardName, amount, getZoneObjFromName(zoneName)); } updateDeckHash(); - return (okRows > 0); + return true; } InnerDecklistNode *DeckList::getZoneObjFromName(const QString zoneName) diff --git a/tests/loading_from_clipboard/CMakeLists.txt b/tests/loading_from_clipboard/CMakeLists.txt index bdb36e497..ddd497569 100644 --- a/tests/loading_from_clipboard/CMakeLists.txt +++ b/tests/loading_from_clipboard/CMakeLists.txt @@ -1,6 +1,7 @@ ADD_DEFINITIONS("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") add_executable(loading_from_clipboard_test loading_from_clipboard_test.cpp + clipboard_testing.cpp ../../common/decklist.cpp ) @@ -12,4 +13,4 @@ find_package(Qt5 COMPONENTS Concurrent Network Widgets REQUIRED) set(TEST_QT_MODULES Qt5::Concurrent Qt5::Network Qt5::Widgets) target_link_libraries(loading_from_clipboard_test cockatrice_common ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES}) -add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test) \ No newline at end of file +add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test) diff --git a/tests/loading_from_clipboard/clipboard_testing.cpp b/tests/loading_from_clipboard/clipboard_testing.cpp new file mode 100644 index 000000000..5b6be5931 --- /dev/null +++ b/tests/loading_from_clipboard/clipboard_testing.cpp @@ -0,0 +1,40 @@ +#include "clipboard_testing.h" +#include + +void Result::operator()(const InnerDecklistNode *innerDecklistNode, const DecklistCardNode *card) +{ + if (innerDecklistNode->getName() == DECK_ZONE_MAIN) { + mainboard.append({card->getName().toStdString(), card->getNumber()}); + } else if (innerDecklistNode->getName() == DECK_ZONE_SIDE) { + sideboard.append({card->getName().toStdString(), card->getNumber()}); + } else { + FAIL(); + } +} + +void testEmpty(const QString &clipboard) +{ + QString cp(clipboard); + DeckList deckList; + QTextStream stream(&cp); // text stream requires local copy + deckList.loadFromStream_Plain(stream); + + ASSERT_TRUE(deckList.getCardList().isEmpty()); +} + +void testDeck(const QString &clipboard, const Result &result) +{ + QString cp(clipboard); + DeckList deckList; + QTextStream stream(&cp); // text stream requires local copy + deckList.loadFromStream_Plain(stream); + + ASSERT_EQ(result.name, deckList.getName().toStdString()); + ASSERT_EQ(result.comments, deckList.getComments().toStdString()); + + Result decklistBuilder; + deckList.forEachCard(decklistBuilder); + + ASSERT_EQ(result.mainboard, decklistBuilder.mainboard); + ASSERT_EQ(result.sideboard, decklistBuilder.sideboard); +} diff --git a/tests/loading_from_clipboard/clipboard_testing.h b/tests/loading_from_clipboard/clipboard_testing.h new file mode 100644 index 000000000..03bdf5e97 --- /dev/null +++ b/tests/loading_from_clipboard/clipboard_testing.h @@ -0,0 +1,32 @@ +#ifndef CLIPBOARD_TESTING_H +#define CLIPBOARD_TESTING_H + +#include "../../common/decklist.h" +#include "gtest/gtest.h" + +struct Result +{ + // using std types because qt types aren't understood by gtest (without this you'll get less nice errors) + using CardRows = QVector>; + std::string name; + std::string comments; + CardRows mainboard; + CardRows sideboard; + + Result() + { + } + + Result(std::string _name, std::string _comments, CardRows _mainboard, CardRows _sideboard) + : name(_name), comments(_comments), mainboard(_mainboard), sideboard(_sideboard) + { + } + + void operator()(const InnerDecklistNode *innerDecklistNode, const DecklistCardNode *card); +}; + +void testEmpty(const QString &clipboard); + +void testDeck(const QString &clipboard, const Result &result); + +#endif // CLIPBOARD_TESTING_H diff --git a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp index f21f610a7..11b7a7f8f 100644 --- a/tests/loading_from_clipboard/loading_from_clipboard_test.cpp +++ b/tests/loading_from_clipboard/loading_from_clipboard_test.cpp @@ -1,249 +1,169 @@ -#include "loading_from_clipboard_test.h" -#include "../../common/decklist.h" -#include "gtest/gtest.h" -#include +#include "clipboard_testing.h" -DeckList *fromClipboard(QString *clipboard); -DeckList *fromClipboard(QString *clipboard) -{ - DeckList *deckList = new DeckList; - QTextStream *stream = new QTextStream(clipboard); - deckList->loadFromStream_Plain(*stream); - return deckList; -} +// Testing is done by using the DeckList::loadFromString_Plain function in common/decklist.h +// It does not check if cards are in the database at all, so no comparisons to the database will be made. -using CardRows = QMap; - -struct DecklistBuilder -{ - CardRows actualMainboard; - CardRows actualSideboard; - - explicit DecklistBuilder() : actualMainboard({}), actualSideboard({}) - { - } - - void operator()(const InnerDecklistNode *innerDecklistNode, const DecklistCardNode *card) - { - if (innerDecklistNode->getName() == DECK_ZONE_MAIN) { - actualMainboard[card->getName()] += card->getNumber(); - } else if (innerDecklistNode->getName() == DECK_ZONE_SIDE) { - actualSideboard[card->getName()] += card->getNumber(); - } else { - FAIL(); - } - } - - CardRows mainboard() - { - return actualMainboard; - } - - CardRows sideboard() - { - return actualSideboard; - } -}; - -namespace -{ TEST(LoadingFromClipboardTest, EmptyDeck) { - DeckList *deckList = fromClipboard(new QString("")); - ASSERT_TRUE(deckList->getCardList().isEmpty()); + testEmpty(""); } TEST(LoadingFromClipboardTest, EmptySideboard) { - DeckList *deckList = fromClipboard(new QString("Sideboard")); - ASSERT_TRUE(deckList->getCardList().isEmpty()); + testEmpty("Sideboard"); } TEST(LoadingFromClipboardTest, QuantityPrefixed) { - QString *clipboard = new QString("1 Mountain\n" - "2x Island\n" - "3X FOREST\n"); - DeckList *deckList = fromClipboard(clipboard); - - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); - - CardRows expectedMainboard = CardRows({{"mountain", 1}, {"island", 2}, {"forest", 3}}); - CardRows expectedSideboard = CardRows({}); - - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + QString clipboard("1 Mountain\n" + "2x Island\n" + "3x Forest\n"); + Result result("", "", {{"Mountain", 1}, {"Island", 2}, {"Forest", 3}}, {}); + testDeck(clipboard, result); } TEST(LoadingFromClipboardTest, CommentsAreIgnored) { - QString *clipboard = new QString("//1 Mountain\n" - "//2x Island\n" - "//SB:2x Island\n"); - - DeckList *deckList = fromClipboard(clipboard); - - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); - - CardRows expectedMainboard = CardRows({}); - CardRows expectedSideboard = CardRows({}); - - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + QString clipboard("//1 Mountain\n" + "//2x Island\n" + "//SB:2x Island\n"); + testEmpty(clipboard); } TEST(LoadingFromClipboardTest, SideboardPrefix) { - QString *clipboard = new QString("1 Mountain\n" - "SB: 1 Mountain\n" - "SB: 2x Island\n"); - DeckList *deckList = fromClipboard(clipboard); + QString clipboard("1 Mountain\n" + "SB: 1 Mountain\n" + "sb: 2x Island\n" + "2 Swamp\n" + "\n" + "3 Plains\n"); + Result result("", "", {{"Mountain", 1}, {"Swamp", 2}, {"Plains", 3}}, {{"Mountain", 1}, {"Island", 2}}); + testDeck(clipboard, result); +} - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); - - CardRows expectedMainboard = CardRows({{"mountain", 1}}); - CardRows expectedSideboard = CardRows({{"mountain", 1}, {"island", 2}}); - - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); +TEST(LoadingFromClipboardTest, SideboardLine) +{ + QString clipboard("1 Mountain\n" + "2 Swamp\n" + "\n" + "3 Plains\n" + "sideboard\n" + "1 Mountain\n" + "2x Island\n"); + Result result("", "", {{"Mountain", 1}, {"Swamp", 2}, {"Plains", 3}}, {{"Mountain", 1}, {"Island", 2}}); + testDeck(clipboard, result); } TEST(LoadingFromClipboardTest, UnknownCardsAreNotDiscarded) { - QString *clipboard = new QString("1 CardThatDoesNotExistInCardsXml\n"); - DeckList *deckList = fromClipboard(clipboard); + QString clipboard("1 CardThatDoesNotExistInCardsXml\n"); + Result result("", "", {{"CardThatDoesNotExistInCardsXml", 1}}, {}); + testDeck(clipboard, result); +} - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); - - CardRows expectedMainboard = CardRows({{"cardthatdoesnotexistincardsxml", 1}}); - CardRows expectedSideboard = CardRows({}); - - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); +TEST(LoadingFromClipboardTest, WeirdWhitespaceIsIgnored) +{ + QString clipboard( + "\t\tSb:\t1\tOur Market Research Shows That Players Like Really Long Card Names So We Made " + " This Card to Have\tthe Absolute \t Longest Card Name \tEver Elemental\t\n\t"); + Result result("", "", {}, + {{"Our Market Research Shows That Players Like Really Long Card Names So We Made This Card to Have " + "the Absolute Longest Card Name Ever Elemental", + 1}}); + testDeck(clipboard, result); } TEST(LoadingFromClipboardTest, RemoveBlankEntriesFromBeginningAndEnd) { - QString *clipboard = new QString("\n" - "\n" - "\n" - "1x Algae Gharial\n" - "3x CardThatDoesNotExistInCardsXml\n" - "2x Phelddagrif\n" - "\n" - "\n"); + QString clipboard("\n" + "\n" + "\n" + "1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "2x Phelddagrif\n" + "\n" + "\n"); - DeckList *deckList = fromClipboard(clipboard); - - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); - - CardRows expectedMainboard = - CardRows({{"algae gharial", 1}, {"cardthatdoesnotexistincardsxml", 3}, {"phelddagrif", 2}}); - CardRows expectedSideboard = CardRows({}); - - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + Result result("", "", {{"Algae Gharial", 1}, {"CardThatDoesNotExistInCardsXml", 3}, {"Phelddagrif", 2}}, {}); + testDeck(clipboard, result); } TEST(LoadingFromClipboardTest, UseFirstBlankIfOnlyOneBlankToSplitSideboard) { - QString *clipboard = new QString("1x Algae Gharial\n" - "3x CardThatDoesNotExistInCardsXml\n" - "\n" - "2x Phelddagrif\n"); + QString clipboard("1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "\n" + "2x Phelddagrif\n"); - DeckList *deckList = fromClipboard(clipboard); - - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); - - CardRows expectedMainboard = CardRows({{"algae gharial", 1}, {"cardthatdoesnotexistincardsxml", 3}}); - CardRows expectedSideboard = CardRows({{"phelddagrif", 2}}); - - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + Result result("", "", {{"Algae Gharial", 1}, {"CardThatDoesNotExistInCardsXml", 3}}, {{"Phelddagrif", 2}}); + testDeck(clipboard, result); } TEST(LoadingFromClipboardTest, IfMultipleScatteredBlanksAllMainBoard) { - QString *clipboard = new QString("1x Algae Gharial\n" - "3x CardThatDoesNotExistInCardsXml\n" - "\n" - "2x Phelddagrif\n" - "\n" - "3 Giant Growth\n"); + QString clipboard("1x Algae Gharial\n" + "3x CardThatDoesNotExistInCardsXml\n" + "\n" + "2x Phelddagrif\n" + "\n" + "3 Giant Growth\n"); - DeckList *deckList = fromClipboard(clipboard); - - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); - - CardRows expectedMainboard = CardRows( - {{"algae gharial", 1}, {"cardthatdoesnotexistincardsxml", 3}, {"phelddagrif", 2}, {"giant growth", 3}}); - CardRows expectedSideboard = CardRows({}); - - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + Result result( + "", "", {{"Algae Gharial", 1}, {"CardThatDoesNotExistInCardsXml", 3}, {"Phelddagrif", 2}, {"Giant Growth", 3}}, + {}); + testDeck(clipboard, result); } -TEST(LoadingFromClipboardTest, LotsOfStuffInBulkTesting) +TEST(LoadingFromClipboardTest, EdgeCaseTesting) { - QString *clipboard = new QString("\n" - "\n" - "\n" - "1x test1\n" - "testNoValueMB\n" - "2x test2\n" - "SB: 10 testSB\n" - "3 test3\n" - "4X test4\n" - "\n" - "\n" - "\n" - "\n" - "5x test5\n" - "6X test6\n" - "testNoValueSB\n" - "\n" - "\n" - "\n" - "\n"); + QString clipboard(R"( +// DeckName - DeckList *deckList = fromClipboard(clipboard); + // Comment 1 - DecklistBuilder decklistBuilder = DecklistBuilder(); - deckList->forEachCard(decklistBuilder); +// +//Comment [two] +//(test) Æ ’ | / (3) - CardRows expectedMainboard = CardRows({{"test1", 1}, {"test2", 2}, {"test3", 3}, {"test4", 4}, {"testnovaluemb", 1} - }); - CardRows expectedSideboard = CardRows({{"testsb", 10}, {"test5", 5}, {"test6", 6}, {"testnovaluesb", 1} +// Mainboard (10 cards) +Æther Adept +2x Fire & Ice +3 Pain/Suffering +4X [B] Forest (3) - }); - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + +// Sideboard (11 cards) + +5x [WTH] Nature’s Resurgence +6X Gaea's Skyfolk +7 B.F.M. (Big Furry Monster) + + + +)"); + + Result result("DeckName", "Comment 1\n\nComment [two]\n(test) Æ ’ | / (3)", + {{"Aether Adept", 1}, {"Fire // Ice", 2}, {"Pain // Suffering", 3}, {"Forest", 4}}, + {{"Nature's Resurgence", 5}, {"Gaea's Skyfolk", 6}, {"B.F.M. (Big Furry Monster)", 7}}); + testDeck(clipboard, result); } TEST(LoadingFromClipboardTest, CommentsBeforeCardsTesting) { - QString *clipboard = new QString("//NAME: Title from Website.com\n" - "\n" - "//Main\n" - "1 test1\n"); - DeckList *decklist = fromClipboard(clipboard); - DecklistBuilder decklistBuilder = DecklistBuilder(); - decklist->forEachCard(decklistBuilder); - CardRows expectedMainboard = CardRows({{"test1", 1}}); - CardRows expectedSideboard = CardRows({}); - ASSERT_EQ(expectedMainboard, decklistBuilder.mainboard()); - ASSERT_EQ(expectedSideboard, decklistBuilder.sideboard()); + QString clipboard("// Title from website.com\n" + "// A nice deck\n" + "// With nice cards\n" + "\n" + "// Mainboard\n" + "1 test1\n" + "Sideboard\n" + "2 test2\n"); + + Result result("Title from website.com", "A nice deck\nWith nice cards", {{"test1", 1}}, {{"test2", 2}}); + testDeck(clipboard, result); } -} // namespace int main(int argc, char **argv) { diff --git a/tests/loading_from_clipboard/loading_from_clipboard_test.h b/tests/loading_from_clipboard/loading_from_clipboard_test.h deleted file mode 100644 index e69de29bb..000000000 From 8028bad7b1f13dde30231a932d459bb0cda17b0b Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 9 Nov 2018 11:05:11 +0100 Subject: [PATCH 07/62] set common protobuf files to compile with other tags in debug (#3432) * set common protobuf files to compile with other tags in debug force the same flags as release on debug builds fixes #3431 * check for version first * use Protobuf_VERSION instead of calling protoc --version * lower verion requirement * set tags to disable certain warnings instead of removing all --- common/pb/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/common/pb/CMakeLists.txt b/common/pb/CMakeLists.txt index 889bc7692..7be8edba5 100644 --- a/common/pb/CMakeLists.txt +++ b/common/pb/CMakeLists.txt @@ -168,3 +168,12 @@ if (UNIX) set(cockatrice_protocol_LIBS ${cockatrice_protocol_LIBS} -lpthread) endif (UNIX) target_link_libraries(cockatrice_protocol ${cockatrice_protocol_LIBS}) + +# ubuntu uses an outdated package for protobuf, 3.1.0 is required +if(${Protobuf_VERSION} VERSION_LESS "3.1.0") + # remove unused parameter and misleading indentation warnings when compiling to avoid errors + set(CMAKE_CXX_FLAGS_DEBUG + "${CMAKE_CXX_FLAGS_DEBUG} -Wno-unused-parameter -Wno-misleading-indentation") + message(WARNING "Outdated protobuf version found (${Protobuf_VERSION} < 3.1.0), " + "disabled warnings to avoid compilation errors.") +endif() From 638ee1af4a954e8055ac2fcd3166596d680506fe Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Mon, 12 Nov 2018 18:50:38 +0100 Subject: [PATCH 08/62] This should fix appveyor compilation (#3434) --- cmake/FindVCredistRuntime.cmake | 2 +- cmake/NSIS.template.in | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmake/FindVCredistRuntime.cmake b/cmake/FindVCredistRuntime.cmake index 2786315f2..9c3d335b8 100644 --- a/cmake/FindVCredistRuntime.cmake +++ b/cmake/FindVCredistRuntime.cmake @@ -9,7 +9,7 @@ if (WIN32) set(REDIST_ARCH x86) endif() - set(REDIST_FILE vc_redist.${REDIST_ARCH}.exe) + set(REDIST_FILE vcredist_${REDIST_ARCH}.exe) set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) include(InstallRequiredSystemLibraries) diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 9373c6cd6..fdc015200 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -247,20 +247,20 @@ ${If} $PortableMode = 0 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMajor" "@CPACK_PACKAGE_VERSION_MAJOR@" WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "VersionMinor" "@CPACK_PACKAGE_VERSION_MINOR@" - IfFileExists "$INSTDIR\vc_redist.x86.exe" VcRedist86Exists PastVcRedist86Check + IfFileExists "$INSTDIR\vcredist_x86.exe" VcRedist86Exists PastVcRedist86Check VcRedist86Exists: - ExecWait '"$INSTDIR\vc_redist.x86.exe" /passive /norestart' + ExecWait '"$INSTDIR\vcredist_x86.exe" /passive /norestart' DetailPrint "Sleep to ensure unlock of vc_redist file after installation..." Sleep 3000 - Delete "$INSTDIR\vc_redist.x86.exe" + Delete "$INSTDIR\vcredist_x86.exe" PastVcRedist86Check: - IfFileExists "$INSTDIR\vc_redist.x64.exe" VcRedist64Exists PastVcRedist64Check + IfFileExists "$INSTDIR\vcredist_x64.exe" VcRedist64Exists PastVcRedist64Check VcRedist64Exists: - ExecWait '"$INSTDIR\vc_redist.x64.exe" /passive /norestart' + ExecWait '"$INSTDIR\vcredist_x64.exe" /passive /norestart' DetailPrint "Sleep to ensure unlock of vc_redist file after installation..." Sleep 3000 - Delete "$INSTDIR\vc_redist.x64.exe" + Delete "$INSTDIR\vcredist_x64.exe" PastVcRedist64Check: ${Else} From f70699d3deb651611989f47b9eb97d59986d8cd4 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 13 Nov 2018 09:21:08 +0100 Subject: [PATCH 09/62] Improve clangify.sh (#3435) * Improve clangify.sh Separated from #3433 merge this branch first! * fix error codes on -n and -t fix output on -n format -h message * separate color diff from diff --- clangify.sh | 192 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 177 insertions(+), 15 deletions(-) diff --git a/clangify.sh b/clangify.sh index bad86ca42..1f58e93b8 100755 --- a/clangify.sh +++ b/clangify.sh @@ -1,22 +1,184 @@ #!/bin/bash # This script will run clang-format on all modified, non-3rd-party C++/Header files. +# Never, ever, should this recieve a path with a newline in it. Don't bother proofing it for that. -set -e -if hash clang-format 2>/dev/null && hash git 2>/dev/null; then - files_to_clean=($(git diff --name-only $(git merge-base origin/master HEAD))) +# use script dir and return on exit +current=$PWD +function cleanup { + cd $current +} +trap cleanup EXIT +cd "${BASH_SOURCE%/*}/" - printf "%s\n" ${files_to_clean[@]} | \ - xargs -I{} find '{}' \( -name "*.cpp" -o -name "*.h" \) \ - -not -path "./cockatrice/src/qt-json/*" \ - -not -path "./servatrice/src/smtp/*" \ - -not -path "./common/sfmt/*" \ - -not -path "./oracle/src/zip/*" \ - -not -path "./build*/*" \ - -exec clang-format -style=file -i {} \; - echo "Successfully formatted following files:" - printf "%s\n" ${files_to_clean[@]} -else - echo "Please install clang-format and git to use this program" +# defaults +include=("common" \ +"cockatrice/src" \ +"oracle/src" \ +"servatrice/src") +exclude=("cockatrice/src/qt-json" \ +"servatrice/src/smtp" \ +"common/sfmt" \ +"oracle/src/zip") +exts=("cpp" "h") +cf_cmd="clang-format" +branch="origin/master" + +# parse options +while [[ $@ ]]; do + case "$1" in + '-b'|'--branch') + branch=$2 + set_branch=1 + shift 2 + ;; + '-c'|'--color-diff') + color=" --color=always" + mode=diff + shift + ;; + '-d'|'--diff') + mode=diff + shift + ;; + '-h'|'--help') + cat <s are given, all source files in those directories are checked, recursively. + +USAGE: $0 [option] [-b[ranch] ] [ ...] + +DEFAULTS: +Default includes are: + ${include[@]/%/ + } +Default excludes are: + ${exclude[@]/%/ + } +OPTIONS: + -b, --branch + Compare to this git branch and format only files that differ. + If unspecified it defaults to origin/master. + To not compare to a branch this has to be explicitly set to "". + When not comparing to a branch git will not be used at all and all valid files will be parsed. + + -c, --color-diff + Display a colored diff. Implies --diff. + Only available on systems which support 'diff --color'. + + -d, --diff + Display a diff. Implies --test. + + -h, --help + Display this message and exit. + + -n, --names + Display a list of filenames that require formatting. Implies --test. + + -t, --test + Do not edit files in place. Set exit code to 1 if changes are required. + +EXIT CODES: + 0 on a successful format or if no files require formatting. + 1 if a file requires formatting. + 2 if given incorrect arguments. + 3 if clang-format could not be found. +EOM + exit 0 + ;; + '-n'|'--names') + mode=name + shift + ;; + '-t'|'--test') + mode=code + shift + ;; + '--') + shift + ;; + *) + include=() # empty includes + while [[ $@ ]]; do + if [[ -d $1 ]]; then + include+=("$1") + shift + else + echo "error in parsing arguments of $0: $1 is not a directory" >&2 + exit 2 # input error + fi + done + if ! [[ $set_branch ]]; then + unset branch # unset branch if not set explicitly + fi + break + ;; + esac +done + +# check availability of clang-format +if ! hash $cf_cmd 2>/dev/null; then + echo "could not find $cf_cmd" >&2 + # find any clang-format-x.x in /usr/bin + cf_cmd=$(find /usr/bin -regex '.*/clang-format-[0-9]+\.[0-9]+' -print -quit) + if [[ $cf_cmd ]]; then + echo "found $cf_cmd instead" >&2 + else + exit 3 # special exit code for missing dependency + fi fi + +if [[ $branch ]]; then + # get all dirty files through git + if ! base=$(git merge-base ${branch} HEAD); then + echo "could not find git merge base" >&2 + exit 2 # input error + fi + declare -a reg + for ex in ${exts[@]}; do + reg+=(${include[@]/%/.*\\.$ex\$}) + done + names=$(git diff --name-only $base | grep ${reg[@]/#/-e ^}) +else + names=$(find ${include[@]} -type f -false ${exts[@]/#/-o -name *\\.}) +fi + +# filter excludes +names=$(<<<"$names" grep -v ${exclude[@]/#/-e ^}) + +if ! [[ $names ]]; then + exit 0 # nothing to format means format is successful! +fi + +# format +case $mode in + diff) + declare -i code=0 + for name in ${names[@]}; do + if ! $cf_cmd "$name" | diff "$name" - -p $color; then + code=1 + fi + done + exit $code + ;; + name) + declare -i code=0 + for name in ${names[@]}; do + if ! $cf_cmd "$name" | diff "$name" - -q >/dev/null; then + echo "$name" + code=1 + fi + done + exit $code + ;; + code) + for name in ${names[@]}; do + $cf_cmd "$name" | diff "$name" - -q >/dev/null || exit 1 + done + ;; + *) + $cf_cmd -i $names + ;; +esac From 72ed98e4042a09bcb4f7374a1f0ce33de2d68672 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 16 Nov 2018 15:44:22 +0100 Subject: [PATCH 10/62] add docker compilation to travis (#3433) * add docker compilation to travis add new matrix entry in .travis.yml for compiling on 18.04 add Dockerfile in .ci to build ubuntu 18.04 inside docker remove release entry for uvuntu 16.04 to not conflict refactor .travis.yml refactor travis-comile.sh merge travis-dependencies.sh into the travis.yml remove travis-dependencies.sh * enable debugging on travis-compile.sh * set ubuntu16 buildtype to "Debug" set buildtype Debug for as requirement for "test" add --debug and --release flags to travis-compile.sh * make output prettier edit the format warning message and clangify.sh output * fix clangify.sh fix --cf-version flag fix directory argument parsing add directory parsing details to --help add examples to --help --- .ci/Dockerfile | 20 ++++++ .ci/travis-compile.sh | 136 +++++++++++++++++++++++++------------ .ci/travis-dependencies.sh | 9 --- .travis.yml | 79 ++++++++++++--------- clangify.sh | 67 ++++++++++++------ 5 files changed, 206 insertions(+), 105 deletions(-) create mode 100644 .ci/Dockerfile delete mode 100644 .ci/travis-dependencies.sh diff --git a/.ci/Dockerfile b/.ci/Dockerfile new file mode 100644 index 000000000..5fe8b4c17 --- /dev/null +++ b/.ci/Dockerfile @@ -0,0 +1,20 @@ +FROM ubuntu:bionic + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bc \ + build-essential \ + clang-format \ + g++ \ + git \ + cmake \ + libprotobuf-dev \ + libqt5multimedia5-plugins \ + libqt5svg5-dev \ + libqt5sql5-mysql \ + libqt5websockets5-dev \ + protobuf-compiler \ + qt5-default \ + qttools5-dev \ + qttools5-dev-tools \ + qtmultimedia5-dev \ + && rm -rf /var/lib/apt/lists/* diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index 8dbde5c28..7adc99c34 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -1,59 +1,111 @@ #!/bin/bash +# This script is to be used in .travis.yaml from the project root directory, do not use it from somewhere else. + set -e -./servatrice/check_schema_version.sh +# Read arguments +while [[ "$@" ]]; do + case "$1" in + '--format') + CHECK_FORMAT=1 + shift + ;; + '--install') + MAKE_INSTALL=1 + shift + ;; + '--package') + MAKE_PACKAGE=1 + shift + ;; + '--server') + MAKE_SERVER=1 + shift + ;; + '--test') + MAKE_TEST=1 + shift + ;; + '--debug') + BUILDTYPE="Debug" + shift + ;; + '--release') + BUILDTYPE="Release" + shift + ;; + *) + BUILDTYPE="$1" + shift + ;; + esac +done +# Check formatting using clang-format +if [[ $CHECK_FORMAT ]]; then + echo "Checking your code using clang-format..." + if ! diff="$(./clangify.sh --color-diff --cf-version)"; then + cat <s are given, all source files in those directories are checked, recursively. +A bash script to automatically format your code using clang-format. -USAGE: $0 [option] [-b[ranch] ] [ ...] +If no options are given, all dirty source files are edited in place. +If s are given, all source files in those directories of the project root +path are formatted. To only format changed files in these directories use the +--branch option in combination. has to be a path relative to the project +root path or a full path inside $PWD. +. can not be specified as a dir, if you really want to format everything use */. + +USAGE: $0 [option] [--branch ] [ ...] DEFAULTS: -Default includes are: +Current includes are: ${include[@]/%/ } Default excludes are: @@ -62,7 +62,8 @@ OPTIONS: Compare to this git branch and format only files that differ. If unspecified it defaults to origin/master. To not compare to a branch this has to be explicitly set to "". - When not comparing to a branch git will not be used at all and all valid files will be parsed. + When not comparing to a branch, git will not be used at all and every + source file in the entire project will be parsed. -c, --color-diff Display a colored diff. Implies --diff. @@ -80,11 +81,23 @@ OPTIONS: -t, --test Do not edit files in place. Set exit code to 1 if changes are required. + --cf-version + Print the version of clang-format being used before continuing. + EXIT CODES: 0 on a successful format or if no files require formatting. 1 if a file requires formatting. 2 if given incorrect arguments. 3 if clang-format could not be found. + +EXAMPLES: + $0 --test \$PWD || echo "code requires formatting" + Tests if the source files in the current directory are correctly + formatted and prints an error message if formatting is required. + + $0 --branch $USER/patch-2 ${include[0]} + Formats all changed files compared to the git branch "$USER/patch-2" + in the directory ${include[0]}. EOM exit 0 ;; @@ -96,24 +109,31 @@ EOM mode=code shift ;; + '--cf-version') + print_version=1 + shift + ;; '--') shift ;; *) - include=() # empty includes - while [[ $@ ]]; do - if [[ -d $1 ]]; then - include+=("$1") - shift - else - echo "error in parsing arguments of $0: $1 is not a directory" >&2 + if next_dir=$(cd "$1" && pwd); then + if [[ ${next_dir#$PWD/} == /* ]]; then + echo "error in parsing arguments of $0: $next_dir is not in $PWD" >&2 exit 2 # input error + elif ! [[ $set_include ]]; then + include=() # remove default includes + set_include=1 fi - done + include+=("${next_dir#$PWD/}") + else + echo "error in parsing arguments of $0: $PWD/$1 is not a directory" >&2 + exit 2 # input error + fi if ! [[ $set_branch ]]; then unset branch # unset branch if not set explicitly fi - break + shift ;; esac done @@ -152,6 +172,9 @@ if ! [[ $names ]]; then exit 0 # nothing to format means format is successful! fi +# optionally print version +[[ $print_version ]] && $cf_cmd -version + # format case $mode in diff) From 57f15a9e984674777e7848fa09d101511428380f Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 16 Nov 2018 18:32:00 +0100 Subject: [PATCH 11/62] fix building packages on travis-docker (#3441) * add docker compilation to travis add new matrix entry in .travis.yml for compiling on 18.04 add Dockerfile in .ci to build ubuntu 18.04 inside docker remove release entry for uvuntu 16.04 to not conflict refactor .travis.yml refactor travis-comile.sh merge travis-dependencies.sh into the travis.yml remove travis-dependencies.sh * enable debugging on travis-compile.sh * set ubuntu16 buildtype to "Debug" set buildtype Debug for as requirement for "test" add --debug and --release flags to travis-compile.sh * make output prettier edit the format warning message and clangify.sh output * fix clangify.sh fix --cf-version flag fix directory argument parsing add directory parsing details to --help add examples to --help * test making packages move dockerfile for bionic to make room for possibly other files add missing file dependency set macos brew to use protobuf --without-python@2 * remove test * rm old Dockerfile --- .ci/{ => UbuntuBionic}/Dockerfile | 1 + .travis.yml | 14 +++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) rename .ci/{ => UbuntuBionic}/Dockerfile (97%) diff --git a/.ci/Dockerfile b/.ci/UbuntuBionic/Dockerfile similarity index 97% rename from .ci/Dockerfile rename to .ci/UbuntuBionic/Dockerfile index 5fe8b4c17..17b8e1cbb 100644 --- a/.ci/Dockerfile +++ b/.ci/UbuntuBionic/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bc \ build-essential \ clang-format \ + file \ g++ \ git \ cmake \ diff --git a/.travis.yml b/.travis.yml index f447968aa..0f7b87bd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,14 +8,14 @@ matrix: - name: Ubuntu Bionic (Debug) if: tag IS NOT present services: docker - before_install: docker build -t img .ci + before_install: docker build -t img .ci/UbuntuBionic script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" img bash .ci/travis-compile.sh --server --debug - name: Ubuntu Bionic (Release) if: (branch = master AND NOT type = pull_request) OR tag IS present services: docker - script: docker build -t img .ci && - docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" img + before_install: docker build -t img .ci/UbuntuBionic + script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" img bash .ci/travis-compile.sh --server --package --release #Ubuntu Xenial (Debug only) @@ -44,11 +44,9 @@ matrix: os: osx osx_image: xcode8 before_install: - - brew update - brew update - brew install ccache - - brew unlink python # protobuf python2 install requires this link to be removed - - brew install protobuf + - brew install protobuf --without-python@2 - brew install qt script: bash ./.ci/travis-compile.sh --server --install --debug - name: macOS (Release) @@ -56,11 +54,9 @@ matrix: os: osx osx_image: xcode8 before_install: - - brew update - brew update - brew install ccache - - brew unlink python # protobuf python2 install requires this link to be removed - - brew install protobuf + - brew install protobuf --without-python@2 - brew install qt script: bash ./.ci/travis-compile.sh --server --package --release From 6b6c6c71ca97fdd5c7436e47e15626f0dddabca8 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Tue, 20 Nov 2018 14:55:20 +0100 Subject: [PATCH 12/62] add ccache to docker compilation (#3446) * add ccache to docker image * test commit please ignore --- .ci/UbuntuBionic/Dockerfile | 1 + .travis.yml | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.ci/UbuntuBionic/Dockerfile b/.ci/UbuntuBionic/Dockerfile index 17b8e1cbb..65ef1f6d1 100644 --- a/.ci/UbuntuBionic/Dockerfile +++ b/.ci/UbuntuBionic/Dockerfile @@ -7,6 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ file \ g++ \ git \ + ccache \ cmake \ libprotobuf-dev \ libqt5multimedia5-plugins \ diff --git a/.travis.yml b/.travis.yml index 0f7b87bd4..d17557738 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,13 +9,17 @@ matrix: if: tag IS NOT present services: docker before_install: docker build -t img .ci/UbuntuBionic - script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" img + script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" + --mount "type=bind,source=$HOME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" + img bash .ci/travis-compile.sh --server --debug - name: Ubuntu Bionic (Release) if: (branch = master AND NOT type = pull_request) OR tag IS present services: docker before_install: docker build -t img .ci/UbuntuBionic - script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" img + script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" + --mount "type=bind,source=$HOME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" + img bash .ci/travis-compile.sh --server --package --release #Ubuntu Xenial (Debug only) From af2bce714165d88e80374a797a9dfd9f533e748f Mon Sep 17 00:00:00 2001 From: Zach H Date: Wed, 21 Nov 2018 03:53:35 -0500 Subject: [PATCH 13/62] add token creations from graveyard/exile (#3445) --- cockatrice/src/player.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index aabb4cdb5..0d2759ce4 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -2716,6 +2716,9 @@ void Player::updateCardMenu(const CardItem *card) cardMenu->addSeparator(); cardMenu->addAction(aClone); cardMenu->addMenu(moveMenu); + + addRelatedCardView(card, cardMenu); + addRelatedCardActions(card, cardMenu); } else { // Card is in hand or a custom zone specified by server cardMenu->addAction(aPlay); From ec61d1f0122ae71e60290762bab690f7b5733310 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 21 Nov 2018 21:18:01 +0100 Subject: [PATCH 14/62] travis ci format check hotfix (#3449) * set error message to be conditional #3443 fails on the git merge base for "some reason" but at least this error message should be clearer. * fix issues created earlier in #3433 split version from actual diff remove --color-diff argument as apparently it isn't supported --- .ci/travis-compile.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index 7adc99c34..4aa1c1aa9 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -36,6 +36,10 @@ while [[ "$@" ]]; do shift ;; *) + if [[ $1 == -* ]]; then + echo "unrecognized option: $1" + exit 3 + fi BUILDTYPE="$1" shift ;; @@ -45,8 +49,8 @@ done # Check formatting using clang-format if [[ $CHECK_FORMAT ]]; then echo "Checking your code using clang-format..." - if ! diff="$(./clangify.sh --color-diff --cf-version)"; then - cat < Date: Wed, 21 Nov 2018 22:57:16 +0100 Subject: [PATCH 15/62] exclude mac debug on tagged release (#3443) leftover from #3433 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d17557738..54762df4e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,6 +45,7 @@ matrix: #macOS - name: macOS (Debug) + if: tag IS NOT present os: osx osx_image: xcode8 before_install: From 3f40a51cdb25b23d6219249fa738d8accd07a212 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 21 Nov 2018 22:59:46 +0100 Subject: [PATCH 16/62] move package files to unique name (#3438) --- .ci/travis-compile.sh | 11 ++++++++++- .travis.yml | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index 4aa1c1aa9..faaa5f604 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -17,7 +17,8 @@ while [[ "$@" ]]; do ;; '--package') MAKE_PACKAGE=1 - shift + PACKAGE_NAME="$2" + shift 2 ;; '--server') MAKE_SERVER=1 @@ -117,4 +118,12 @@ fi if [[ $MAKE_PACKAGE ]]; then make package + if [[ $PACKAGE_NAME ]]; then + if file=$(find . -maxdepth 1 -type f -name Cockatrice-*.* -print -quit); then + mv "$file" "${file%%.*}-$PACKAGE_NAME.${file#*.}" + else + echo "could not find package" >&2 + exit 1 + fi + fi fi diff --git a/.travis.yml b/.travis.yml index 54762df4e..21a7a55d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ matrix: script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" --mount "type=bind,source=$HOME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" img - bash .ci/travis-compile.sh --server --package --release + bash .ci/travis-compile.sh --server --package "Ubuntu18.04" --release #Ubuntu Xenial (Debug only) - name: Ubuntu Xenial (Debug) @@ -63,7 +63,7 @@ matrix: - brew install ccache - brew install protobuf --without-python@2 - brew install qt - script: bash ./.ci/travis-compile.sh --server --package --release + script: bash ./.ci/travis-compile.sh --server --package "" --release # Builds for pull requests skip the deployment step altogether From 964bb0974c7dfdb2d6c0fc1bf8afbf2425610cdc Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 21 Nov 2018 23:00:35 +0100 Subject: [PATCH 17/62] add more clang-format focus to CONTRIBUTING.md (#3442) * add more clang-format focus to CONTRIBUTING.md Add more clang-format instructions and instructions on clangify.sh. Move clang-format instructions to the section Formatting and make other format topics a subheader of it to articulate focus on it. Add section about our ci formatting for people that look at this file after their test failed. fixes #3065 also fixes #3340 * grammar * grammar --- .github/CONTRIBUTING.md | 45 ++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index bcf29001f..41defe1ea 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,6 +21,20 @@ If you have any questions on IDEs, feel free to chat with us on [Gitter](https:/ # Code Style Guide # +### Formatting and continuous integration (ci) ### + +We currently use Travis CI to check your code for formatting issues, if your pull request was rejected because of this it would show a message in the logs. Click on "Details" next to the failed Travis CI build and then click on the failed build (most likely the fastest one) to see the log. + +The message will look somewhat similar to this: +``` +************************************************************ +*** Your code does not meet our formatting guidelines. *** +*** Please correct it then commit and push your changes. *** +*** See our CONTRIBUTING.md file for more information. *** +************************************************************ +``` +The CONTRIBUTING.md file mentioned is this file. Please read [this section](#Formatting) for full information on our formatting guidelines. + ### Compatibility ### Cockatrice is currently compiled on all platforms using C++11. You'll notice C++03 code throughout the codebase. Please feel free to help convert it over! @@ -28,7 +42,17 @@ Cockatrice is currently compiled on all platforms using C++11. You'll For consistency, we use Qt data structures where possible. For example, `QString` over `std::string` and `QList` over `std::vector`. -### Header files ### +### Formatting ### + +The handy tool `clang-format` can format your code for you, it is available for almost any environment. A special `.clang-format` configuration file is included in the project and is used to format your code. + +We've also included a bash script, `clangify.sh`, that will use clang-format to format all files in one go. Use `./clangify.sh --help` to show a full help page. + +To run clang-format on a single source file simply use the command `clang-format -i ` to format it in place. (some systems install clang-format with a specific version number appended, `find /usr/bin -name clang-format*` should find it for you) + +See [the clang-format documentation](https://clang.llvm.org/docs/ClangFormat.html) for more information about the tool. + +#### Header files #### Use header files with the extension `.h` and source files with the extension `.cpp`. @@ -59,7 +83,7 @@ Group library includes after project includes, and in alphabetic order. Like thi #include ``` -### Naming ### +#### Naming #### Use `UpperCamelCase` for classes, structs, enums, etc. and `lowerCamelCase` for function and variable names. @@ -89,7 +113,7 @@ Bar& bar2 = *bar1; Use `nullptr` instead of `NULL` (or `0`) for null pointers. If you find any usage of the old keywords, we encourage you to fix it. -### Braces ### +#### Braces #### Braces should go on their own line except for control statements, the use of braces around single line statements is preferred. See the following example: @@ -110,22 +134,19 @@ int main() } ``` -### Indentation and Spacing ### +#### Indentation and Spacing #### Always indent using 4 spaces, do not use tabs. Opening and closing braces should be on the same indentation layer, member access specifiers in classes or structs should not be indented. + All operators and braces should be separated by spaces, do not add a space next to the inside of a brace. + If multiple lines of code that follow eachother have single line comments behind them, place all of them on the same indentation level. This indentation level should be equal to the longest line of code for each of these comments, without added spacing. -### Lines ### +#### Lines #### -Do not have trailing whitespace in your lines, if possible. Most IDEs check for this nowadays and clean it up for you. +Do not have trailing whitespace in your lines. Most IDEs check for this nowadays and clean it up for you. -Lines should be 120 characters or less, but you can exceed this if you find it necessary. - -### Automatic Formatting ### - -The handy tool `clang-format` can format your code for you, a special `.clang-format` configuration file is included [here](https://github.com/Cockatrice/Cockatrice/blob/master/.clang-format). -See [the clang-format documentation](https://clang.llvm.org/docs/ClangFormat.html) for more information. +Lines should be 120 characters or less. Please break up lines that are too long into smaller parts, for example at spaces or after opening a brace. ### Memory Management ### From c7b8f3e923a9c7a1e65a8d70a55fde3d94e2d5fa Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Thu, 22 Nov 2018 23:32:18 +0100 Subject: [PATCH 18/62] Corrections to 3438 (#3452) * correct previous misstake in #3438 wrong, this fixes that. * add checking for second argument to --package nice to have * split find result into path and file --- .ci/travis-compile.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index faaa5f604..8a5293453 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -17,8 +17,11 @@ while [[ "$@" ]]; do ;; '--package') MAKE_PACKAGE=1 - PACKAGE_NAME="$2" - shift 2 + shift + if [[ $1 != -* ]]; then + PACKAGE_NAME="$1" + shift + fi ;; '--server') MAKE_SERVER=1 @@ -119,11 +122,13 @@ fi if [[ $MAKE_PACKAGE ]]; then make package if [[ $PACKAGE_NAME ]]; then - if file=$(find . -maxdepth 1 -type f -name Cockatrice-*.* -print -quit); then - mv "$file" "${file%%.*}-$PACKAGE_NAME.${file#*.}" - else + found=$(find . -maxdepth 1 -type f -name "Cockatrice-*.*" -print -quit) + path=${found%/*} + file=${found##*/} + if [[ $file != *.* ]]; then echo "could not find package" >&2 exit 1 fi + mv "$path/$file" "$path/${file%%.*}-$PACKAGE_NAME.${file#*.}" fi fi From 843b9df9391f365996b3438a8bce0443c93dd58c Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sat, 24 Nov 2018 04:19:06 +0100 Subject: [PATCH 19/62] fix a bug from #3412 (#3454) check if the card returned any info at all --- cockatrice/src/player.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index 0d2759ce4..782bc3def 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -2359,6 +2359,9 @@ void Player::actResetPT() while (selected.hasNext()) { CardItem *card = static_cast(selected.next()); CardInfoPtr info = card->getInfo(); + if (!info) { + continue; + } Command_SetCardAttr *cmd = new Command_SetCardAttr; QString zoneName = card->getZone()->getName(); cmd->set_zone(zoneName.toStdString()); From 06081bd9409f4cee3e31368e26d2ffbee756458f Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sat, 24 Nov 2018 04:21:20 +0100 Subject: [PATCH 20/62] add keybinds to mill cards (#3439) * add keybinds to mill cards Add functions to move single card from top of deck to the graveyard and exile. Add keybinds to move single or multiple cards from top of deck to exile or graveyard. Add new keybinds to settings menu. Move settings menu items around for a better fit. Rename a few of the items in the settings menu. Add default keybinds: ctrl alt d/e for single/multiple cards from top of deck to the graveyard. No defaults are set for moving to exile. * fix shortcut menu * fix missing tag * rename mismatched functions * fixed your typos * refactor shortcutsettings correct a lot of typos optimize a lot of functions this could merit a pr on its own * set mill keybinds * refactor add related card actions I found a function that was completely unintelligible so I made it remotely legible and removed the duplication. * shorten line by 17 characters replace a lot of function calls with just a single reference * add brackets add brackets to all single line if statements etc. readability improvements --- cockatrice/src/abstractcounter.cpp | 2 +- .../src/dlg_load_deck_from_clipboard.cpp | 2 +- cockatrice/src/gameview.cpp | 2 +- cockatrice/src/player.cpp | 912 +++++++++++------- cockatrice/src/player.h | 10 +- cockatrice/src/sequenceEdit/sequenceedit.cpp | 36 +- cockatrice/src/sequenceEdit/sequenceedit.h | 25 +- cockatrice/src/sequenceEdit/ui_shortcutstab.h | 626 ++++++------ cockatrice/src/shortcutssettings.cpp | 278 ++---- cockatrice/src/shortcutssettings.h | 184 +++- cockatrice/src/tab_deck_editor.cpp | 2 +- cockatrice/src/tab_game.cpp | 6 +- cockatrice/src/window_main.cpp | 2 +- 13 files changed, 1154 insertions(+), 933 deletions(-) diff --git a/cockatrice/src/abstractcounter.cpp b/cockatrice/src/abstractcounter.cpp index 64c19253a..42cf6ad83 100644 --- a/cockatrice/src/abstractcounter.cpp +++ b/cockatrice/src/abstractcounter.cpp @@ -46,7 +46,7 @@ AbstractCounter::AbstractCounter(Player *_player, } else menu = nullptr; - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); retranslateUi(); } diff --git a/cockatrice/src/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/dlg_load_deck_from_clipboard.cpp index 8d39ea41d..32d1dec46 100644 --- a/cockatrice/src/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/dlg_load_deck_from_clipboard.cpp @@ -32,7 +32,7 @@ DlgLoadDeckFromClipboard::DlgLoadDeckFromClipboard(QWidget *parent) : QDialog(pa resize(500, 500); actRefresh(); - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); } diff --git a/cockatrice/src/gameview.cpp b/cockatrice/src/gameview.cpp index b96e5f5ba..26f975504 100644 --- a/cockatrice/src/gameview.cpp +++ b/cockatrice/src/gameview.cpp @@ -22,7 +22,7 @@ GameView::GameView(QGraphicsScene *scene, QWidget *parent) : QGraphicsView(scene connect(aCloseMostRecentZoneView, SIGNAL(triggered()), scene, SLOT(closeMostRecentZoneView())); addAction(aCloseMostRecentZoneView); - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); rubberBand = new QRubberBand(QRubberBand::Rectangle, this); } diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index 782bc3def..95a55152d 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -24,7 +24,8 @@ #include #include #include -#include +#include +#include #include #include "pb/command_attach_card.pb.h" @@ -219,8 +220,12 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare connect(aMulligan, SIGNAL(triggered()), this, SLOT(actMulligan())); aMoveTopToPlayFaceDown = new QAction(this); connect(aMoveTopToPlayFaceDown, SIGNAL(triggered()), this, SLOT(actMoveTopCardToPlayFaceDown())); - aMoveTopCardsToGrave = new QAction(this); - connect(aMoveTopCardsToGrave, SIGNAL(triggered()), this, SLOT(actMoveTopCardsToGrave())); + aMoveTopCardToGraveyard = new QAction(this); + connect(aMoveTopCardToGraveyard, SIGNAL(triggered()), this, SLOT(actMoveTopCardToGrave())); + aMoveTopCardToExile = new QAction(this); + connect(aMoveTopCardToExile, SIGNAL(triggered()), this, SLOT(actMoveTopCardToExile())); + aMoveTopCardsToGraveyard = new QAction(this); + connect(aMoveTopCardsToGraveyard, SIGNAL(triggered()), this, SLOT(actMoveTopCardsToGrave())); aMoveTopCardsToExile = new QAction(this); connect(aMoveTopCardsToExile, SIGNAL(triggered()), this, SLOT(actMoveTopCardsToExile())); aMoveTopCardToBottom = new QAction(this); @@ -266,7 +271,9 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare libraryMenu->addAction(aMoveTopCardToBottom); libraryMenu->addAction(aMoveBottomCardToGrave); libraryMenu->addSeparator(); - libraryMenu->addAction(aMoveTopCardsToGrave); + libraryMenu->addAction(aMoveTopCardToGraveyard); + libraryMenu->addAction(aMoveTopCardToExile); + libraryMenu->addAction(aMoveTopCardsToGraveyard); libraryMenu->addAction(aMoveTopCardsToExile); libraryMenu->addSeparator(); libraryMenu->addAction(aOpenDeckInDeckEditor); @@ -449,7 +456,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare rearrangeZones(); retranslateUi(); - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); } @@ -483,8 +490,9 @@ void Player::clear() void Player::addPlayer(Player *player) { - if (player == nullptr || player == this) + if (player == nullptr || player == this) { return; + } for (int i = 0; i < playerLists.size(); ++i) { QAction *newAction = playerLists[i]->addAction(player->getName()); @@ -495,8 +503,9 @@ void Player::addPlayer(Player *player) void Player::removePlayer(Player *player) { - if (player == nullptr) + if (player == nullptr) { return; + } for (int i = 0; i < playerLists.size(); ++i) { QList actionList = playerLists[i]->actions(); @@ -515,8 +524,9 @@ void Player::playerListActionTriggered() Command_RevealCards cmd; const int otherPlayerId = action->data().toInt(); - if (otherPlayerId != -1) + if (otherPlayerId != -1) { cmd.set_player_id(otherPlayerId); + } if (menu == mRevealLibrary) { cmd.set_zone_name("deck"); @@ -533,13 +543,14 @@ void Player::playerListActionTriggered() cmd.set_card_id(0); } - } else if (menu == mRevealHand) + } else if (menu == mRevealHand) { cmd.set_zone_name("hand"); - else if (menu == mRevealRandomHandCard) { + } else if (menu == mRevealRandomHandCard) { cmd.set_zone_name("hand"); cmd.set_card_id(RANDOM_CARD_FROM_ZONE); - } else + } else { return; + } sendGameCommand(cmd); } @@ -603,9 +614,10 @@ void Player::updateBoundingRect() if (settingsCache->getHorizontalHand()) { qreal handHeight = handVisible ? hand->boundingRect().height() : 0; bRect = QRectF(0, 0, width + table->boundingRect().width(), table->boundingRect().height() + handHeight); - } else + } else { bRect = QRectF(0, 0, width + hand->boundingRect().width() + table->boundingRect().width(), table->boundingRect().height()); + } playerArea->setSize(CARD_HEIGHT + counterAreaWidth + 15, bRect.height()); emit sizeChanged(); @@ -652,7 +664,9 @@ void Player::retranslateUi() aMulligan->setText(tr("Take &mulligan")); aShuffle->setText(tr("&Shuffle")); aMoveTopToPlayFaceDown->setText(tr("Play top card &face down")); - aMoveTopCardsToGrave->setText(tr("Move top cards to &graveyard...")); + aMoveTopCardToGraveyard->setText(tr("Move top card to grave&yard")); + aMoveTopCardToExile->setText(tr("Move top card to e&xile")); + aMoveTopCardsToGraveyard->setText(tr("Move top cards to &graveyard...")); aMoveTopCardsToExile->setText(tr("Move top cards to &exile...")); aMoveTopCardToBottom->setText(tr("Put top card on &bottom")); aMoveBottomCardToGrave->setText(tr("Put bottom card &in graveyard")); @@ -730,52 +744,54 @@ void Player::retranslateUi() aMoveToExile->setText(tr("&Exile")); QMapIterator zoneIterator(zones); - while (zoneIterator.hasNext()) + while (zoneIterator.hasNext()) { zoneIterator.next().value()->retranslateUi(); + } } void Player::setShortcutsActive() { shortcutsActive = true; + ShortcutsSettings &shortcuts = settingsCache->shortcuts(); - aPlay->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aPlay")); - aTap->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aTap")); - aDoesntUntap->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aDoesntUntap")); - aFlip->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aFlip")); - aPeek->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aPeek")); - aClone->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aClone")); - aAttach->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aAttach")); - aUnattach->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aUnattach")); - aDrawArrow->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aDrawArrow")); - aIncP->setShortcuts(settingsCache->shortcuts().getShortcut("Player/IncP")); - aDecP->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aDecP")); - aIncT->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aIncT")); - aDecT->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aDecT")); - aIncPT->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aIncPT")); - aDecPT->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aDecPT")); - aSetPT->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aSetPT")); - aResetPT->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aResetPT")); - aSetAnnotation->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aSetAnnotation")); - aMoveToTopLibrary->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aMoveToTopLibrary")); - aMoveToBottomLibrary->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aMoveToBottomLibrary")); - aMoveToHand->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aMoveToHand")); - aMoveToGraveyard->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aMoveToGraveyard")); - aMoveToExile->setShortcuts(settingsCache->shortcuts().getShortcut("Player/aMoveToExile")); + aPlay->setShortcuts(shortcuts.getShortcut("Player/aPlay")); + aTap->setShortcuts(shortcuts.getShortcut("Player/aTap")); + aDoesntUntap->setShortcuts(shortcuts.getShortcut("Player/aDoesntUntap")); + aFlip->setShortcuts(shortcuts.getShortcut("Player/aFlip")); + aPeek->setShortcuts(shortcuts.getShortcut("Player/aPeek")); + aClone->setShortcuts(shortcuts.getShortcut("Player/aClone")); + aAttach->setShortcuts(shortcuts.getShortcut("Player/aAttach")); + aUnattach->setShortcuts(shortcuts.getShortcut("Player/aUnattach")); + aDrawArrow->setShortcuts(shortcuts.getShortcut("Player/aDrawArrow")); + aIncP->setShortcuts(shortcuts.getShortcut("Player/IncP")); + aDecP->setShortcuts(shortcuts.getShortcut("Player/aDecP")); + aIncT->setShortcuts(shortcuts.getShortcut("Player/aIncT")); + aDecT->setShortcuts(shortcuts.getShortcut("Player/aDecT")); + aIncPT->setShortcuts(shortcuts.getShortcut("Player/aIncPT")); + aDecPT->setShortcuts(shortcuts.getShortcut("Player/aDecPT")); + aSetPT->setShortcuts(shortcuts.getShortcut("Player/aSetPT")); + aResetPT->setShortcuts(shortcuts.getShortcut("Player/aResetPT")); + aSetAnnotation->setShortcuts(shortcuts.getShortcut("Player/aSetAnnotation")); + aMoveToTopLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToTopLibrary")); + aMoveToBottomLibrary->setShortcuts(shortcuts.getShortcut("Player/aMoveToBottomLibrary")); + aMoveToHand->setShortcuts(shortcuts.getShortcut("Player/aMoveToHand")); + aMoveToGraveyard->setShortcuts(shortcuts.getShortcut("Player/aMoveToGraveyard")); + aMoveToExile->setShortcuts(shortcuts.getShortcut("Player/aMoveToExile")); QList addCCShortCuts; - addCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aCCRed")); - addCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aCCYellow")); - addCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aCCGreen")); + addCCShortCuts.append(shortcuts.getSingleShortcut("Player/aCCRed")); + addCCShortCuts.append(shortcuts.getSingleShortcut("Player/aCCYellow")); + addCCShortCuts.append(shortcuts.getSingleShortcut("Player/aCCGreen")); QList removeCCShortCuts; - removeCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aRCRed")); - removeCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aRCYellow")); - removeCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aRCGreen")); + removeCCShortCuts.append(shortcuts.getSingleShortcut("Player/aRCRed")); + removeCCShortCuts.append(shortcuts.getSingleShortcut("Player/aRCYellow")); + removeCCShortCuts.append(shortcuts.getSingleShortcut("Player/aRCGreen")); QList setCCShortCuts; - setCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aSCRed")); - setCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aSCYellow")); - setCCShortCuts.append(settingsCache->shortcuts().getSingleShortcut("Player/aSCGreen")); + setCCShortCuts.append(shortcuts.getSingleShortcut("Player/aSCRed")); + setCCShortCuts.append(shortcuts.getSingleShortcut("Player/aSCYellow")); + setCCShortCuts.append(shortcuts.getSingleShortcut("Player/aSCGreen")); for (int i = 0; i < aAddCounter.size(); ++i) { aAddCounter[i]->setShortcut(addCCShortCuts.at(i)); @@ -788,24 +804,29 @@ void Player::setShortcutsActive() } QMapIterator counterIterator(counters); - while (counterIterator.hasNext()) + while (counterIterator.hasNext()) { counterIterator.next().value()->setShortcutsActive(); + } - aViewSideboard->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aViewSideboard")); - aViewLibrary->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aViewLibrary")); - aViewTopCards->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aViewTopCards")); - aViewGraveyard->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aViewGraveyard")); - aDrawCard->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aDrawCard")); - aDrawCards->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aDrawCards")); - aUndoDraw->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aUndoDraw")); - aMulligan->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aMulligan")); - aShuffle->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aShuffle")); - aUntapAll->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aUntapAll")); - aRollDie->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aRollDie")); - aCreateToken->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aCreateToken")); - aCreateAnotherToken->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aCreateAnotherToken")); - aAlwaysRevealTopCard->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aAlwaysRevealTopCard")); - aMoveTopToPlayFaceDown->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aMoveTopToPlayFaceDown")); + aViewSideboard->setShortcut(shortcuts.getSingleShortcut("Player/aViewSideboard")); + aViewLibrary->setShortcut(shortcuts.getSingleShortcut("Player/aViewLibrary")); + aViewTopCards->setShortcut(shortcuts.getSingleShortcut("Player/aViewTopCards")); + aViewGraveyard->setShortcut(shortcuts.getSingleShortcut("Player/aViewGraveyard")); + aDrawCard->setShortcut(shortcuts.getSingleShortcut("Player/aDrawCard")); + aDrawCards->setShortcut(shortcuts.getSingleShortcut("Player/aDrawCards")); + aUndoDraw->setShortcut(shortcuts.getSingleShortcut("Player/aUndoDraw")); + aMulligan->setShortcut(shortcuts.getSingleShortcut("Player/aMulligan")); + aShuffle->setShortcut(shortcuts.getSingleShortcut("Player/aShuffle")); + aUntapAll->setShortcut(shortcuts.getSingleShortcut("Player/aUntapAll")); + aRollDie->setShortcut(shortcuts.getSingleShortcut("Player/aRollDie")); + aCreateToken->setShortcut(shortcuts.getSingleShortcut("Player/aCreateToken")); + aCreateAnotherToken->setShortcut(shortcuts.getSingleShortcut("Player/aCreateAnotherToken")); + aAlwaysRevealTopCard->setShortcut(shortcuts.getSingleShortcut("Player/aAlwaysRevealTopCard")); + aMoveTopToPlayFaceDown->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopToPlayFaceDown")); + aMoveTopCardToGraveyard->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardToGraveyard")); + aMoveTopCardsToGraveyard->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardsToGraveyard")); + aMoveTopCardToExile->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardToExile")); + aMoveTopCardsToExile->setShortcut(shortcuts.getSingleShortcut("Player/aMoveTopCardsToExile")); } void Player::setShortcutsInactive() @@ -826,10 +847,16 @@ void Player::setShortcutsInactive() aCreateToken->setShortcut(QKeySequence()); aCreateAnotherToken->setShortcut(QKeySequence()); aAlwaysRevealTopCard->setShortcut(QKeySequence()); + aMoveTopToPlayFaceDown->setShortcut(QKeySequence()); + aMoveTopCardToGraveyard->setShortcut(QKeySequence()); + aMoveTopCardsToGraveyard->setShortcut(QKeySequence()); + aMoveTopCardToExile->setShortcut(QKeySequence()); + aMoveTopCardsToExile->setShortcut(QKeySequence()); QMapIterator counterIterator(counters); - while (counterIterator.hasNext()) + while (counterIterator.hasNext()) { counterIterator.next().value()->setShortcutsInactive(); + } } void Player::initSayMenu() @@ -838,7 +865,7 @@ void Player::initSayMenu() int count = settingsCache->messages().getCount(); - for (int i = 0; i < count; i++) { + for (int i = 0; i < count; ++i) { QAction *newAction = new QAction(settingsCache->messages().getMessageAt(i), this); if (i <= 10) { newAction->setShortcut(QKeySequence("Ctrl+" + QString::number((i + 1) % 10))); @@ -909,8 +936,9 @@ void Player::actRevealRandomGraveyardCard() Command_RevealCards cmd; QAction *action = dynamic_cast(sender()); const int otherPlayerId = action->data().toInt(); - if (otherPlayerId != -1) + if (otherPlayerId != -1) { cmd.set_player_id(otherPlayerId); + } cmd.set_zone_name("grave"); cmd.set_card_id(RANDOM_CARD_FROM_ZONE); sendGameCommand(cmd); @@ -958,15 +986,51 @@ void Player::actUndoDraw() sendGameCommand(Command_UndoDraw()); } +void Player::actMoveTopCardToGrave() +{ + if (zones.value("deck")->getCards().size() == 0) { + return; + } + + Command_MoveCard cmd; + cmd.set_start_zone("deck"); + cmd.mutable_cards_to_move()->add_card()->set_card_id(0); + cmd.set_target_player_id(getId()); + cmd.set_target_zone("grave"); + cmd.set_x(0); + cmd.set_y(0); + + sendGameCommand(cmd); +} + +void Player::actMoveTopCardToExile() +{ + if (zones.value("deck")->getCards().size() == 0) { + return; + } + + Command_MoveCard cmd; + cmd.set_start_zone("deck"); + cmd.mutable_cards_to_move()->add_card()->set_card_id(0); + cmd.set_target_player_id(getId()); + cmd.set_target_zone("rfg"); + cmd.set_x(0); + cmd.set_y(0); + + sendGameCommand(cmd); +} + void Player::actMoveTopCardsToGrave() { int number = QInputDialog::getInt(0, tr("Move top cards to grave"), tr("Number:")); - if (!number) + if (!number) { return; + } const int maxCards = zones.value("deck")->getCards().size(); - if (number > maxCards) + if (number > maxCards) { number = maxCards; + } Command_MoveCard cmd; cmd.set_start_zone("deck"); @@ -975,8 +1039,9 @@ void Player::actMoveTopCardsToGrave() cmd.set_x(0); cmd.set_y(0); - for (int i = 0; i < number; ++i) + for (int i = 0; i < number; ++i) { cmd.mutable_cards_to_move()->add_card()->set_card_id(i); + } sendGameCommand(cmd); } @@ -984,12 +1049,14 @@ void Player::actMoveTopCardsToGrave() void Player::actMoveTopCardsToExile() { int number = QInputDialog::getInt(0, tr("Move top cards to exile"), tr("Number:")); - if (!number) + if (!number) { return; + } const int maxCards = zones.value("deck")->getCards().size(); - if (number > maxCards) + if (number > maxCards) { number = maxCards; + } Command_MoveCard cmd; cmd.set_start_zone("deck"); @@ -998,8 +1065,9 @@ void Player::actMoveTopCardsToExile() cmd.set_x(0); cmd.set_y(0); - for (int i = 0; i < number; ++i) + for (int i = 0; i < number; ++i) { cmd.mutable_cards_to_move()->add_card()->set_card_id(i); + } sendGameCommand(cmd); } @@ -1071,8 +1139,9 @@ void Player::actRollDie() void Player::actCreateToken() { DlgCreateToken dlg(predefinedTokens); - if (!dlg.exec()) + if (!dlg.exec()) { return; + } lastTokenName = dlg.getName(); lastTokenPT = dlg.getPT(); @@ -1080,8 +1149,9 @@ void Player::actCreateToken() if (correctedCard) { lastTokenName = correctedCard->getName(); lastTokenTableRow = table->clampValidTableRow(2 - correctedCard->getTableRow()); - if (lastTokenPT.isEmpty()) + if (lastTokenPT.isEmpty()) { lastTokenPT = correctedCard->getPowTough(); + } } lastTokenColor = dlg.getColor(); lastTokenAnnotation = dlg.getAnnotation(); @@ -1093,8 +1163,9 @@ void Player::actCreateToken() void Player::actCreateAnotherToken() { - if (lastTokenName.isEmpty()) + if (lastTokenName.isEmpty()) { return; + } Command_CreateToken cmd; cmd.set_zone("table"); @@ -1113,8 +1184,9 @@ void Player::actCreatePredefinedToken() { QAction *action = static_cast(sender()); CardInfoPtr cardInfo = db->getCard(action->text()); - if (!cardInfo) + if (!cardInfo) { return; + } setLastToken(cardInfo); @@ -1124,8 +1196,9 @@ void Player::actCreatePredefinedToken() void Player::actCreateRelatedCard() { CardItem *sourceCard = game->getActiveCard(); - if (!sourceCard) + if (!sourceCard) { return; + } QAction *action = static_cast(sender()); // If there is a better way of passing a CardRelation through a QAction, please add it here. QList relatedCards = QList(); @@ -1146,70 +1219,70 @@ void Player::actCreateRelatedCard() void Player::actCreateAllRelatedCards() { CardItem *sourceCard = game->getActiveCard(); - if (!sourceCard) + if (!sourceCard) { return; + } - QList relatedCards = QList(); - relatedCards.append(sourceCard->getInfo()->getRelatedCards()); + QList relatedCards = sourceCard->getInfo()->getRelatedCards(); relatedCards.append(sourceCard->getInfo()->getReverseRelatedCards2Me()); + if (relatedCards.empty()) { + return; + } - QList nonExcludedRelatedCards = QList(); - QString dbName; CardRelation *cardRelation = nullptr; int tokensTypesCreated = 0; - switch (relatedCards.length()) { // Is an if/elseif/else pattern better? - case 0: // if (relatedCards.length() == 0) - return; - case 1: // else if (relatedCards.length() == 1) - cardRelation = relatedCards.at(0); - if (createRelatedFromRelation(sourceCard, cardRelation)) - tokensTypesCreated++; - break; - default: // else - foreach (CardRelation *cardRelationTemp, relatedCards) { - if (!cardRelationTemp->getIsCreateAllExclusion() && !cardRelationTemp->getDoesAttach()) { - nonExcludedRelatedCards.append(cardRelationTemp); + if (relatedCards.length() == 1) { + cardRelation = relatedCards.at(0); + if (createRelatedFromRelation(sourceCard, cardRelation)) { + ++tokensTypesCreated; + } + } else { + QList nonExcludedRelatedCards; + QString dbName; + for (CardRelation *cardRelationTemp : relatedCards) { + if (!cardRelationTemp->getIsCreateAllExclusion() && !cardRelationTemp->getDoesAttach()) { + nonExcludedRelatedCards.append(cardRelationTemp); + } + } + switch (nonExcludedRelatedCards.length()) { + case 1: // if nonExcludedRelatedCards == 1 + cardRelation = nonExcludedRelatedCards.at(0); + if (createRelatedFromRelation(sourceCard, cardRelation)) { + ++tokensTypesCreated; } - } - switch (nonExcludedRelatedCards.length()) { - case 1: // if nonExcludedRelatedCards == 1 - cardRelation = nonExcludedRelatedCards.at(0); - if (createRelatedFromRelation(sourceCard, cardRelation)) - tokensTypesCreated++; - break; - // If all are marked "Exclude", then treat the situation as if none of them are. - // We won't accept "garbage in, garbage out", here. - case 0: // else if nonExcludedRelatedCards == 0 - foreach (CardRelation *cardRelationAll, relatedCards) { - if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { - dbName = dbNameFromTokenDisplayName(cardRelationAll->getName()); - for (int i = 0; i < cardRelationAll->getDefaultCount(); i++) { - createCard(sourceCard, dbName); - } - tokensTypesCreated++; - if (tokensTypesCreated == 1) { - cardRelation = cardRelationAll; - } + break; + // If all are marked "Exclude", then treat the situation as if none of them are. + // We won't accept "garbage in, garbage out", here. + case 0: // else if nonExcludedRelatedCards == 0 + for (CardRelation *cardRelationAll : relatedCards) { + if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { + dbName = dbNameFromTokenDisplayName(cardRelationAll->getName()); + for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) { + createCard(sourceCard, dbName); + } + ++tokensTypesCreated; + if (tokensTypesCreated == 1) { + cardRelation = cardRelationAll; } } - break; - default: // else - foreach (CardRelation *cardRelationNotExcluded, nonExcludedRelatedCards) { - if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { - dbName = dbNameFromTokenDisplayName(cardRelationNotExcluded->getName()); - for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); i++) { - createCard(sourceCard, dbName); - } - tokensTypesCreated++; - if (tokensTypesCreated == 1) { - cardRelation = cardRelationNotExcluded; - } + } + break; + default: // else + for (CardRelation *cardRelationNotExcluded : nonExcludedRelatedCards) { + if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { + dbName = dbNameFromTokenDisplayName(cardRelationNotExcluded->getName()); + for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) { + createCard(sourceCard, dbName); + } + ++tokensTypesCreated; + if (tokensTypesCreated == 1) { + cardRelation = cardRelationNotExcluded; } } - break; - } - break; + } + break; + } } /* @@ -1224,8 +1297,9 @@ void Player::actCreateAllRelatedCards() bool Player::createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation) { - if (sourceCard == nullptr || cardRelation == nullptr) + if (sourceCard == nullptr || cardRelation == nullptr) { return false; + } QString dbName = dbNameFromTokenDisplayName(cardRelation->getName()); if (cardRelation->getIsVariable()) { bool ok; @@ -1233,13 +1307,14 @@ bool Player::createRelatedFromRelation(const CardItem *sourceCard, const CardRel int count = QInputDialog::getInt(0, tr("Create tokens"), tr("Number:"), cardRelation->getDefaultCount(), 1, MAX_TOKENS_PER_DIALOG, 1, &ok); dialogSemaphore = false; - if (!ok) + if (!ok) { return false; - for (int i = 0; i < count; i++) { + } + for (int i = 0; i < count; ++i) { createCard(sourceCard, dbName); } } else if (cardRelation->getDefaultCount() > 1) { - for (int i = 0; i < cardRelation->getDefaultCount(); i++) { + for (int i = 0; i < cardRelation->getDefaultCount(); ++i) { createCard(sourceCard, dbName); } } else { @@ -1256,8 +1331,9 @@ void Player::createCard(const CardItem *sourceCard, const QString &dbCardName, b { CardInfoPtr cardInfo = db->getCard(dbCardName); - if (cardInfo == nullptr || sourceCard == nullptr) + if (cardInfo == nullptr || sourceCard == nullptr) { return; + } // get the target token's location // TODO: Define this QPoint into its own function along with the one below @@ -1268,20 +1344,28 @@ void Player::createCard(const CardItem *sourceCard, const QString &dbCardName, b cmd.set_zone("table"); cmd.set_card_name(cardInfo->getName().toStdString()); if (cardInfo->getColors().length() > 1) // Multicoloured + { cmd.set_color("m"); - else - cmd.set_color(cardInfo->getColors().isEmpty() ? QString().toStdString() - : cardInfo->getColors().first().toLower().toStdString()); + } else if (cardInfo->getColors().isEmpty()) { + cmd.set_color(""); + } else { + cmd.set_color(cardInfo->getColors().first().toLower().toStdString()); + } + cmd.set_pt(cardInfo->getPowTough().toStdString()); - cmd.set_annotation(settingsCache->getAnnotateTokens() ? cardInfo->getText().toStdString() - : QString().toStdString()); + if (settingsCache->getAnnotateTokens()) { + cmd.set_annotation(cardInfo->getText().toStdString()); + } else { + cmd.set_annotation(""); + } cmd.set_destroy_on_zone_change(true); cmd.set_target_zone(sourceCard->getZone()->getName().toStdString()); cmd.set_x(gridPoint.x()); cmd.set_y(gridPoint.y()); - if (attach) + if (attach) { cmd.set_target_card_id(sourceCard->getId()); + } sendGameCommand(cmd); } @@ -1305,29 +1389,34 @@ void Player::setCardAttrHelper(const GameEventContext &context, const QString &avalue, bool allCards) { - if (card == nullptr) + if (card == nullptr) { return; + } bool moveCardContext = context.HasExtension(Context_MoveCard::ext); switch (attribute) { case AttrTapped: { bool tapped = avalue == "1"; if (!(!tapped && card->getDoesntUntap() && allCards)) { - if (!allCards) + if (!allCards) { emit logSetTapped(this, card, tapped); + } card->setTapped(tapped, !moveCardContext); } break; } - case AttrAttacking: + case AttrAttacking: { card->setAttacking(avalue == "1"); break; - case AttrFaceDown: + } + case AttrFaceDown: { card->setFaceDown(avalue == "1"); break; - case AttrColor: + } + case AttrColor: { card->setColor(avalue); break; + } case AttrAnnotation: { emit logSetAnnotation(this, card, avalue); card->setAnnotation(avalue); @@ -1352,11 +1441,10 @@ void Player::setCardAttrHelper(const GameEventContext &context, // trailing whitespace is significant; it is hacked on at the end as an additional identifier in our single key database QString Player::dbNameFromTokenDisplayName(const QString &tokenName) { - QRegExp tokenNamePattern(".*/\\S+\\s+(.*)"); - - int index = tokenNamePattern.indexIn(tokenName); - if (index != -1) { - return tokenNamePattern.capturedTexts()[1]; + QRegularExpression tokenNamePattern(".*/\\S+\\s+(.*)"); + QRegularExpressionMatch match = tokenNamePattern.match(tokenName); + if (match.hasMatch()) { + return match.captured(1); } else if (tokenName.indexOf(tr("Token: ")) != -1) { return tokenName.mid(tr("Token: ").length()); } else { @@ -1372,11 +1460,12 @@ void Player::eventGameSay(const Event_GameSay &event) void Player::eventShuffle(const Event_Shuffle &event) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name())); - if (!zone) + if (!zone) { return; - if (zone->getView()) - if (zone->getView()->getRevealZone()) - zone->getView()->setWriteableRevealZone(false); + } + if (zone->getView() && zone->getView()->getRevealZone()) { + zone->getView()->setWriteableRevealZone(false); + } emit logShuffle(this, zone); } @@ -1388,17 +1477,19 @@ void Player::eventRollDie(const Event_RollDie &event) void Player::eventCreateArrow(const Event_CreateArrow &event) { ArrowItem *arrow = addArrow(event.arrow_info()); - if (!arrow) + if (!arrow) { return; + } CardItem *startCard = static_cast(arrow->getStartItem()); CardItem *targetCard = qgraphicsitem_cast(arrow->getTargetItem()); - if (targetCard) + if (targetCard) { emit logCreateArrow(this, startCard->getOwner(), startCard->getName(), targetCard->getOwner(), targetCard->getName(), false); - else + } else { emit logCreateArrow(this, startCard->getOwner(), startCard->getName(), arrow->getTargetItem()->getOwner(), QString(), true); + } } void Player::eventDeleteArrow(const Event_DeleteArrow &event) @@ -1409,8 +1500,9 @@ void Player::eventDeleteArrow(const Event_DeleteArrow &event) void Player::eventCreateToken(const Event_CreateToken &event) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name()), 0); - if (!zone) + if (!zone) { return; + } CardItem *card = new CardItem(this, QString::fromStdString(event.card_name()), event.card_id()); // use db PT if not provided in event @@ -1418,8 +1510,9 @@ void Player::eventCreateToken(const Event_CreateToken &event) card->setPT(QString::fromStdString(event.pt())); } else { CardInfoPtr dbCard = db->getCard(QString::fromStdString(event.card_name())); - if (dbCard) + if (dbCard) { card->setPT(dbCard->getPowTough()); + } } card->setColor(QString::fromStdString(event.color())); card->setAnnotation(QString::fromStdString(event.annotation())); @@ -1432,16 +1525,19 @@ void Player::eventCreateToken(const Event_CreateToken &event) void Player::eventSetCardAttr(const Event_SetCardAttr &event, const GameEventContext &context) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name()), 0); - if (!zone) + if (!zone) { return; + } if (!event.has_card_id()) { const CardList &cards = zone->getCards(); - for (int i = 0; i < cards.size(); i++) + for (int i = 0; i < cards.size(); ++i) { setCardAttrHelper(context, cards.at(i), event.attribute(), QString::fromStdString(event.attr_value()), true); - if (event.attribute() == AttrTapped) + } + if (event.attribute() == AttrTapped) { emit logSetTapped(this, 0, event.attr_value() == "1"); + } } else { CardItem *card = zone->getCard(event.card_id(), QString()); if (!card) { @@ -1455,12 +1551,14 @@ void Player::eventSetCardAttr(const Event_SetCardAttr &event, const GameEventCon void Player::eventSetCardCounter(const Event_SetCardCounter &event) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name()), 0); - if (!zone) + if (!zone) { return; + } CardItem *card = zone->getCard(event.card_id(), QString()); - if (!card) + if (!card) { return; + } int oldValue = card->getCounters().value(event.counter_id(), 0); card->setCounter(event.counter_id(), event.counter_value()); @@ -1474,12 +1572,12 @@ void Player::eventCreateCounter(const Event_CreateCounter &event) void Player::eventSetCounter(const Event_SetCounter &event) { - AbstractCounter *c = counters.value(event.counter_id(), 0); - if (!c) + AbstractCounter *ctr = counters.value(event.counter_id(), 0); + if (!ctr) return; - int oldValue = c->getValue(); - c->setValue(event.value()); - emit logSetCounter(this, c->getName(), event.value(), oldValue); + int oldValue = ctr->getValue(); + ctr->setValue(event.value()); + emit logSetCounter(this, ctr->getName(), event.value(), oldValue); } void Player::eventDelCounter(const Event_DelCounter &event) @@ -1490,41 +1588,49 @@ void Player::eventDelCounter(const Event_DelCounter &event) void Player::eventDumpZone(const Event_DumpZone &event) { Player *zoneOwner = game->getPlayers().value(event.zone_owner_id(), 0); - if (!zoneOwner) + if (!zoneOwner) { return; + } CardZone *zone = zoneOwner->getZones().value(QString::fromStdString(event.zone_name()), 0); - if (!zone) + if (!zone) { return; + } emit logDumpZone(this, zone, event.number_cards()); } void Player::eventStopDumpZone(const Event_StopDumpZone &event) { Player *zoneOwner = game->getPlayers().value(event.zone_owner_id(), 0); - if (!zoneOwner) + if (!zoneOwner) { return; + } CardZone *zone = zoneOwner->getZones().value(QString::fromStdString(event.zone_name()), 0); - if (!zone) + if (!zone) { return; + } emit logStopDumpZone(this, zone); } void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext &context) { Player *startPlayer = game->getPlayers().value(event.start_player_id()); - if (!startPlayer) + if (!startPlayer) { return; + } CardZone *startZone = startPlayer->getZones().value(QString::fromStdString(event.start_zone()), 0); Player *targetPlayer = game->getPlayers().value(event.target_player_id()); - if (!targetPlayer) + if (!targetPlayer) { return; + } CardZone *targetZone; - if (event.has_target_zone()) + if (event.has_target_zone()) { targetZone = targetPlayer->getZones().value(QString::fromStdString(event.target_zone()), 0); - else + } else { targetZone = startZone; - if (!startZone || !targetZone) + } + if (!startZone || !targetZone) { return; + } int position = event.position(); int x = event.x(); @@ -1532,15 +1638,19 @@ void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext & int logPosition = position; int logX = x; - if (x == -1) + if (x == -1) { x = 0; + } CardItem *card = startZone->takeCard(position, event.card_id(), startZone != targetZone); - if (!card) + if (!card) { return; - if (startZone != targetZone) + } + if (startZone != targetZone) { card->deleteCardInfoPopup(); - if (event.has_card_name()) + } + if (event.has_card_name()) { card->setName(QString::fromStdString(event.card_name())); + } if (card->getAttachedTo() && (startZone != targetZone)) { CardItem *parentCard = card->getAttachedTo(); @@ -1557,19 +1667,22 @@ void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext & card->setHovered(false); const QList &attachedCards = card->getAttachedCards(); - for (int i = 0; i < attachedCards.size(); ++i) + for (int i = 0; i < attachedCards.size(); ++i) { attachedCards[i]->setParentItem(targetZone); + } - if (startZone->getPlayer() != targetZone->getPlayer()) + if (startZone->getPlayer() != targetZone->getPlayer()) { card->setOwner(targetZone->getPlayer()); + } } // The log event has to be sent before the card is added to the target zone // because the addCard function can modify the card object. - if (context.HasExtension(Context_UndoDraw::ext)) + if (context.HasExtension(Context_UndoDraw::ext)) { emit logUndoDraw(this, card->getName()); - else + } else { emit logMoveCard(this, card, startZone, logPosition, targetZone, logX); + } targetZone->addCard(card, true, x, y); @@ -1584,25 +1697,29 @@ void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext & while (arrowIterator.hasNext()) { ArrowItem *arrow = arrowIterator.next().value(); if ((arrow->getStartItem() == card) || (arrow->getTargetItem() == card)) { - if (startZone == targetZone) + if (startZone == targetZone) { arrow->updatePath(); - else + } else { arrowsToDelete.append(arrow); + } } } - for (int i = 0; i < arrowsToDelete.size(); ++i) + for (int i = 0; i < arrowsToDelete.size(); ++i) { arrowsToDelete[i]->delArrow(); + } } } void Player::eventFlipCard(const Event_FlipCard &event) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name()), 0); - if (!zone) + if (!zone) { return; + } CardItem *card = zone->getCard(event.card_id(), QString::fromStdString(event.card_name())); - if (!card) + if (!card) { return; + } emit logFlipCard(this, card->getName(), event.face_down()); card->setFaceDown(event.face_down()); } @@ -1610,17 +1727,20 @@ void Player::eventFlipCard(const Event_FlipCard &event) void Player::eventDestroyCard(const Event_DestroyCard &event) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name()), 0); - if (!zone) + if (!zone) { return; + } CardItem *card = zone->getCard(event.card_id(), QString()); - if (!card) + if (!card) { return; + } QList attachedCards = card->getAttachedCards(); // This list is always empty except for buggy server implementations. - for (int i = 0; i < attachedCards.size(); ++i) + for (int i = 0; i < attachedCards.size(); ++i) { attachedCards[i]->setAttachedTo(0); + } emit logDestroyCard(this, card->getName()); zone->takeCard(-1, event.card_id(), true); @@ -1637,33 +1757,39 @@ void Player::eventAttachCard(const Event_AttachCard &event) targetPlayer = playerList.value(event.target_player_id(), 0); if (targetPlayer) { targetZone = targetPlayer->getZones().value(QString::fromStdString(event.target_zone()), 0); - if (targetZone) + if (targetZone) { targetCard = targetZone->getCard(event.target_card_id(), QString()); + } } } CardZone *startZone = getZones().value(QString::fromStdString(event.start_zone()), 0); - if (!startZone) + if (!startZone) { return; + } CardItem *startCard = startZone->getCard(event.card_id(), QString()); - if (!startCard) + if (!startCard) { return; + } CardItem *oldParent = startCard->getAttachedTo(); startCard->setAttachedTo(targetCard); startZone->reorganizeCards(); - if ((startZone != targetZone) && targetZone) + if ((startZone != targetZone) && targetZone) { targetZone->reorganizeCards(); - if (oldParent) + } + if (oldParent) { oldParent->getZone()->reorganizeCards(); + } - if (targetCard) + if (targetCard) { emit logAttachCard(this, startCard->getName(), targetPlayer, targetCard->getName()); - else + } else { emit logUnattachCard(this, startCard->getName()); + } } void Player::eventDrawCards(const Event_DrawCards &event) @@ -1681,8 +1807,9 @@ void Player::eventDrawCards(const Event_DrawCards &event) } } else { const int number = event.number(); - for (int i = 0; i < number; ++i) + for (int i = 0; i < number; ++i) { hand->addCard(deck->takeCard(0, -1), false, -1); + } } hand->reorganizeCards(); @@ -1693,13 +1820,15 @@ void Player::eventDrawCards(const Event_DrawCards &event) void Player::eventRevealCards(const Event_RevealCards &event) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name())); - if (!zone) + if (!zone) { return; + } Player *otherPlayer = 0; if (event.has_other_player_id()) { otherPlayer = game->getPlayers().value(event.other_player_id()); - if (!otherPlayer) + if (!otherPlayer) { return; + } } bool peeking = false; @@ -1707,8 +1836,9 @@ void Player::eventRevealCards(const Event_RevealCards &event) const int cardListSize = event.cards_size(); for (int i = 0; i < cardListSize; ++i) { const ServerInfo_Card *temp = &event.cards(i); - if (temp->face_down()) + if (temp->face_down()) { peeking = true; + } cardList.append(temp); } @@ -1716,8 +1846,9 @@ void Player::eventRevealCards(const Event_RevealCards &event) for (int i = 0; i < cardList.size(); ++i) { QString cardName = QString::fromStdString(cardList.at(i)->name()); CardItem *card = zone->getCard(cardList.at(i)->id(), QString()); - if (!card) + if (!card) { continue; + } card->setName(cardName); emit logRevealCards(this, zone, cardList.at(i)->id(), cardName, this, true); } @@ -1732,8 +1863,9 @@ void Player::eventRevealCards(const Event_RevealCards &event) showZoneView = false; } } - if (showZoneView && !cardList.isEmpty()) + if (showZoneView && !cardList.isEmpty()) { static_cast(scene())->addRevealedZoneView(this, zone, cardList, event.grant_write_access()); + } emit logRevealCards(this, zone, event.card_id(), cardName, otherPlayer, false); } @@ -1742,8 +1874,9 @@ void Player::eventRevealCards(const Event_RevealCards &event) void Player::eventChangeZoneProperties(const Event_ChangeZoneProperties &event) { CardZone *zone = zones.value(QString::fromStdString(event.zone_name())); - if (!zone) + if (!zone) { return; + } if (event.has_always_reveal_top_card()) { zone->setAlwaysRevealTopCard(event.always_reveal_top_card()); @@ -1842,20 +1975,23 @@ void Player::processPlayerInfo(const ServerInfo_Player &info) clearArrows(); QMapIterator zoneIt(zones); - while (zoneIt.hasNext()) + while (zoneIt.hasNext()) { zoneIt.next().value()->clearContents(); + } const int zoneListSize = info.zone_list_size(); for (int i = 0; i < zoneListSize; ++i) { const ServerInfo_Zone &zoneInfo = info.zone_list(i); CardZone *zone = zones.value(QString::fromStdString(zoneInfo.name()), 0); - if (!zone) + if (!zone) { continue; + } const int cardListSize = zoneInfo.card_list_size(); if (!cardListSize) { - for (int j = 0; j < zoneInfo.card_count(); ++j) + for (int j = 0; j < zoneInfo.card_count(); ++j) { zone->addCard(new CardItem(this), false, -1); + } } else { for (int j = 0; j < cardListSize; ++j) { const ServerInfo_Card &cardInfo = zoneInfo.card_list(j); @@ -1864,15 +2000,17 @@ void Player::processPlayerInfo(const ServerInfo_Player &info) zone->addCard(card, false, cardInfo.x(), cardInfo.y()); } } - if (zoneInfo.has_always_reveal_top_card()) + if (zoneInfo.has_always_reveal_top_card()) { zone->setAlwaysRevealTopCard(zoneInfo.always_reveal_top_card()); + } zone->reorganizeCards(); } const int counterListSize = info.counter_list_size(); - for (int i = 0; i < counterListSize; ++i) + for (int i = 0; i < counterListSize; ++i) { addCounter(info.counter_list(i)); + } setConceded(info.properties().conceded()); } @@ -1883,8 +2021,9 @@ void Player::processCardAttachment(const ServerInfo_Player &info) for (int i = 0; i < zoneListSize; ++i) { const ServerInfo_Zone &zoneInfo = info.zone_list(i); CardZone *zone = zones.value(QString::fromStdString(zoneInfo.name()), 0); - if (!zone) + if (!zone) { continue; + } const int cardListSize = zoneInfo.card_list_size(); for (int j = 0; j < cardListSize; ++j) { @@ -1894,8 +2033,9 @@ void Player::processCardAttachment(const ServerInfo_Player &info) CardItem *targetCard = game->getCard(cardInfo.attach_player_id(), QString::fromStdString(cardInfo.attach_zone()), cardInfo.attach_card_id()); - if (!targetCard) + if (!targetCard) { continue; + } startCard->setAttachedTo(targetCard); } @@ -1903,36 +2043,39 @@ void Player::processCardAttachment(const ServerInfo_Player &info) } const int arrowListSize = info.arrow_list_size(); - for (int i = 0; i < arrowListSize; ++i) + for (int i = 0; i < arrowListSize; ++i) { addArrow(info.arrow_list(i)); + } } -void Player::playCard(CardItem *c, bool faceDown, bool tapped) +void Player::playCard(CardItem *card, bool faceDown, bool tapped) { - if (c == nullptr) + if (card == nullptr) { return; + } Command_MoveCard cmd; - cmd.set_start_player_id(c->getZone()->getPlayer()->getId()); - cmd.set_start_zone(c->getZone()->getName().toStdString()); + cmd.set_start_player_id(card->getZone()->getPlayer()->getId()); + cmd.set_start_zone(card->getZone()->getName().toStdString()); cmd.set_target_player_id(getId()); CardToMove *cardToMove = cmd.mutable_cards_to_move()->add_card(); - cardToMove->set_card_id(c->getId()); + cardToMove->set_card_id(card->getId()); - CardInfoPtr ci = c->getInfo(); - if (!ci) + CardInfoPtr info = card->getInfo(); + if (!info) { return; - if (!faceDown && ((!settingsCache->getPlayToStack() && ci->getTableRow() == 3) || - ((settingsCache->getPlayToStack() && ci->getTableRow() != 0) && - c->getZone()->getName().toStdString() != "stack"))) { + } + if (!faceDown && ((!settingsCache->getPlayToStack() && info->getTableRow() == 3) || + ((settingsCache->getPlayToStack() && info->getTableRow() != 0) && + card->getZone()->getName().toStdString() != "stack"))) { cmd.set_target_zone("stack"); cmd.set_x(0); cmd.set_y(0); } else { - int tableRow = faceDown ? 2 : ci->getTableRow(); + int tableRow = faceDown ? 2 : info->getTableRow(); QPoint gridPoint = QPoint(-1, table->clampValidTableRow(2 - tableRow)); cardToMove->set_face_down(faceDown); - cardToMove->set_pt(ci->getPowTough().toStdString()); + cardToMove->set_pt(info->getPowTough().toStdString()); cardToMove->set_tapped(faceDown ? false : tapped); if (tableRow != 3) cmd.set_target_zone("table"); @@ -1942,25 +2085,25 @@ void Player::playCard(CardItem *c, bool faceDown, bool tapped) sendGameCommand(cmd); } -void Player::addCard(CardItem *c) +void Player::addCard(CardItem *card) { - emit newCardAdded(c); + emit newCardAdded(card); } -void Player::deleteCard(CardItem *c) +void Player::deleteCard(CardItem *card) { - if (c == nullptr) { + if (card == nullptr) { return; } else if (dialogSemaphore) { - cardsToDelete.append(c); + cardsToDelete.append(card); } else { - c->deleteLater(); + card->deleteLater(); } } -void Player::addZone(CardZone *z) +void Player::addZone(CardZone *zone) { - zones.insert(z->getName(), z); + zones.insert(zone->getName(), zone); } AbstractCounter *Player::addCounter(const ServerInfo_Counter &counter) @@ -1972,30 +2115,35 @@ AbstractCounter *Player::addCounter(const ServerInfo_Counter &counter) AbstractCounter *Player::addCounter(int counterId, const QString &name, QColor color, int radius, int value) { qDebug() << "addCounter:" << getName() << counterId << name; - if (counters.contains(counterId)) + if (counters.contains(counterId)) { return 0; + } - AbstractCounter *c; - if (name == "life") - c = playerTarget->addCounter(counterId, name, value); - else - c = new GeneralCounter(this, counterId, name, color, radius, value, true, this); - counters.insert(counterId, c); - if (countersMenu) - countersMenu->addMenu(c->getMenu()); - if (shortcutsActive) - c->setShortcutsActive(); + AbstractCounter *ctr; + if (name == "life") { + ctr = playerTarget->addCounter(counterId, name, value); + } else { + ctr = new GeneralCounter(this, counterId, name, color, radius, value, true, this); + } + counters.insert(counterId, ctr); + if (countersMenu) { + countersMenu->addMenu(ctr->getMenu()); + } + if (shortcutsActive) { + ctr->setShortcutsActive(); + } rearrangeCounters(); - return c; + return ctr; } void Player::delCounter(int counterId) { - AbstractCounter *c = counters.value(counterId, 0); - if (!c) + AbstractCounter *ctr = counters.value(counterId, 0); + if (!ctr) { return; + } - c->delCounter(); + ctr->delCounter(); counters.remove(counterId); rearrangeCounters(); } @@ -2003,8 +2151,9 @@ void Player::delCounter(int counterId) void Player::clearCounters() { QMapIterator counterIterator(counters); - while (counterIterator.hasNext()) + while (counterIterator.hasNext()) { counterIterator.next().value()->delCounter(); + } counters.clear(); } @@ -2013,28 +2162,34 @@ ArrowItem *Player::addArrow(const ServerInfo_Arrow &arrow) const QMap &playerList = game->getPlayers(); Player *startPlayer = playerList.value(arrow.start_player_id(), 0); Player *targetPlayer = playerList.value(arrow.target_player_id(), 0); - if (!startPlayer || !targetPlayer) + if (!startPlayer || !targetPlayer) { return 0; + } CardZone *startZone = startPlayer->getZones().value(QString::fromStdString(arrow.start_zone()), 0); CardZone *targetZone = 0; - if (arrow.has_target_zone()) + if (arrow.has_target_zone()) { targetZone = targetPlayer->getZones().value(QString::fromStdString(arrow.target_zone()), 0); - if (!startZone || (!targetZone && arrow.has_target_zone())) + } + if (!startZone || (!targetZone && arrow.has_target_zone())) { return 0; + } CardItem *startCard = startZone->getCard(arrow.start_card_id(), QString()); CardItem *targetCard = 0; - if (targetZone) + if (targetZone) { targetCard = targetZone->getCard(arrow.target_card_id(), QString()); - if (!startCard || (!targetCard && arrow.has_target_card_id())) + } + if (!startCard || (!targetCard && arrow.has_target_card_id())) { return 0; + } - if (targetCard) + if (targetCard) { return addArrow(arrow.id(), startCard, targetCard, convertColorToQColor(arrow.arrow_color())); - else + } else { return addArrow(arrow.id(), startCard, targetPlayer->getPlayerTarget(), convertColorToQColor(arrow.arrow_color())); + } } ArrowItem *Player::addArrow(int arrowId, CardItem *startCard, ArrowTarget *targetItem, const QColor &color) @@ -2047,23 +2202,26 @@ ArrowItem *Player::addArrow(int arrowId, CardItem *startCard, ArrowTarget *targe void Player::delArrow(int arrowId) { - ArrowItem *a = arrows.value(arrowId, 0); - if (!a) + ArrowItem *arr = arrows.value(arrowId, 0); + if (!arr) { return; - a->delArrow(); + } + arr->delArrow(); } void Player::removeArrow(ArrowItem *arrow) { - if (arrow->getId() != -1) + if (arrow->getId() != -1) { arrows.remove(arrow->getId()); + } } void Player::clearArrows() { QMapIterator arrowIterator(arrows); - while (arrowIterator.hasNext()) + while (arrowIterator.hasNext()) { arrowIterator.next().value()->delArrow(); + } arrows.clear(); } @@ -2076,23 +2234,25 @@ void Player::rearrangeCounters() QMapIterator counterIterator(counters); while (counterIterator.hasNext()) { counterIterator.next(); - if (counterIterator.value()->getShownInCounterArea()) + if (counterIterator.value()->getShownInCounterArea()) { totalHeight += counterIterator.value()->boundingRect().height(); + } } const qreal padding = 5; - qreal y = boundingRect().y() + marginTop; + qreal ySize = boundingRect().y() + marginTop; // Place objects for (counterIterator.toFront(); counterIterator.hasNext();) { - AbstractCounter *c = counterIterator.next().value(); + AbstractCounter *ctr = counterIterator.next().value(); - if (!c->getShownInCounterArea()) + if (!ctr->getShownInCounterArea()) { continue; + } - QRectF br = c->boundingRect(); - c->setPos((counterAreaWidth - br.width()) / 2, y); - y += br.height() + padding; + QRectF br = ctr->boundingRect(); + ctr->setPos((counterAreaWidth - br.width()) / 2, ySize); + ySize += br.height() + padding; } } @@ -2118,8 +2278,9 @@ void Player::sendGameCommand(PendingCommand *pend) bool Player::clearCardsToDelete() { - if (cardsToDelete.isEmpty()) + if (cardsToDelete.isEmpty()) { return false; + } for (auto &i : cardsToDelete) { if (i != nullptr) { @@ -2139,23 +2300,27 @@ void Player::actMoveCardXCardsFromTop() defaultNumberTopCardsToPlaceBelow, 1, 2000000000, 1, &ok); number--; - if (!ok) + if (!ok) { return; + } defaultNumberTopCardsToPlaceBelow = number; QList sel = scene()->selectedItems(); QList cardList; - while (!sel.isEmpty()) + while (!sel.isEmpty()) { cardList.append(qgraphicsitem_cast(sel.takeFirst())); + } QList commandList; ListOfCardsToMove idList; - for (auto &i : cardList) + for (auto &i : cardList) { idList.add_card()->set_card_id(i->getId()); + } - if (cardList.isEmpty()) + if (cardList.isEmpty()) { return; + } int startPlayerId = cardList[0]->getZone()->getPlayer()->getId(); QString startZone = cardList[0]->getZone()->getName(); @@ -2170,10 +2335,11 @@ void Player::actMoveCardXCardsFromTop() cmd->set_y(0); commandList.append(cmd); - if (local) + if (local) { sendGameCommand(prepareGameCommand(commandList)); - else + } else { game->sendGameCommand(prepareGameCommand(commandList)); + } } void Player::cardMenuAction() @@ -2181,8 +2347,9 @@ void Player::cardMenuAction() QAction *a = dynamic_cast(sender()); QList sel = scene()->selectedItems(); QList cardList; - while (!sel.isEmpty()) + while (!sel.isEmpty()) { cardList.append(qgraphicsitem_cast(sel.takeFirst())); + } QList commandList; if (a->data().toInt() <= (int)cmClone) { @@ -2191,6 +2358,7 @@ void Player::cardMenuAction() switch (static_cast(a->data().toInt())) { // Leaving both for compatibility with server case cmUntap: + // fallthrough case cmTap: { Command_SetCardAttr *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); @@ -2216,8 +2384,9 @@ void Player::cardMenuAction() cmd->set_face_down(!card->getFaceDown()); if (card->getFaceDown()) { CardInfoPtr ci = card->getInfo(); - if (ci) + if (ci) { cmd->set_pt(ci->getPowTough().toStdString()); + } } commandList.append(cmd); break; @@ -2249,7 +2418,7 @@ void Player::cardMenuAction() } } else { ListOfCardsToMove idList; - for (int i = 0; i < cardList.size(); i++) { + for (int i = 0; i < cardList.size(); ++i) { idList.add_card()->set_card_id(cardList[i]->getId()); } int startPlayerId = cardList[0]->getZone()->getPlayer()->getId(); @@ -2344,8 +2513,9 @@ void Player::actIncPT(int deltaP, int deltaT) cmd->set_attr_value(ptString.toStdString()); commandList.append(cmd); - if (local) + if (local) { playerid = card->getZone()->getPlayer()->getId(); + } } game->sendGameCommand(prepareGameCommand(commandList), playerid); @@ -2387,18 +2557,21 @@ void Player::actSetPT() QListIterator i(scene()->selectedItems()); while (i.hasNext()) { CardItem *card = static_cast(i.next()); - if (!card->getPT().isEmpty()) + if (!card->getPT().isEmpty()) { oldPT = card->getPT(); + } } bool ok; dialogSemaphore = true; QString pt = QInputDialog::getText(0, tr("Set power/toughness"), tr("Please enter the new PT:"), QLineEdit::Normal, oldPT, &ok); dialogSemaphore = false; - if (clearCardsToDelete()) + if (clearCardsToDelete()) { return; - if (!ok) + } + if (!ok) { return; + } QList commandList; QListIterator j(scene()->selectedItems()); @@ -2411,8 +2584,9 @@ void Player::actSetPT() cmd->set_attr_value(pt.toStdString()); commandList.append(cmd); - if (local) + if (local) { playerid = card->getZone()->getPlayer()->getId(); + } } game->sendGameCommand(prepareGameCommand(commandList), playerid); @@ -2420,8 +2594,9 @@ void Player::actSetPT() void Player::actDrawArrow() { - if (!game->getActiveCard()) + if (!game->getActiveCard()) { return; + } game->getActiveCard()->drawArrow(Qt::red); } @@ -2462,8 +2637,9 @@ void Player::actSetAnnotation() QListIterator i(scene()->selectedItems()); while (i.hasNext()) { CardItem *card = static_cast(i.next()); - if (!card->getAnnotation().isEmpty()) + if (!card->getAnnotation().isEmpty()) { oldAnnotation = card->getAnnotation(); + } } bool ok; @@ -2471,10 +2647,12 @@ void Player::actSetAnnotation() QString annotation = QInputDialog::getText(0, tr("Set annotation"), tr("Please enter the new annotation:"), QLineEdit::Normal, oldAnnotation, &ok); dialogSemaphore = false; - if (clearCardsToDelete()) + if (clearCardsToDelete()) { return; - if (!ok) + } + if (!ok) { return; + } QList commandList; i.toFront(); @@ -2492,8 +2670,9 @@ void Player::actSetAnnotation() void Player::actAttach() { - if (!game->getActiveCard()) + if (!game->getActiveCard()) { return; + } ArrowAttachItem *arrow = new ArrowAttachItem(game->getActiveCard()); scene()->addItem(arrow); @@ -2502,8 +2681,9 @@ void Player::actAttach() void Player::actUnattach() { - if (!game->getActiveCard()) + if (!game->getActiveCard()) { return; + } Command_AttachCard cmd; cmd.set_start_zone(game->getActiveCard()->getZone()->getName().toStdString()); @@ -2513,11 +2693,10 @@ void Player::actUnattach() void Player::actCardCounterTrigger() { - QAction *a = static_cast(sender()); - int counterId = a->data().toInt() / 1000; - int action = a->data().toInt() % 1000; + QAction *action = static_cast(sender()); + int counterId = action->data().toInt() / 1000; QList commandList; - switch (action) { + switch (action->data().toInt() % 1000) { // TODO: define case numbers case 9: { QListIterator i(scene()->selectedItems()); while (i.hasNext()) { @@ -2553,10 +2732,9 @@ void Player::actCardCounterTrigger() dialogSemaphore = true; int number = QInputDialog::getInt(0, tr("Set counters"), tr("Number:"), 0, 0, MAX_COUNTERS_ON_CARD, 1, &ok); dialogSemaphore = false; - if (clearCardsToDelete()) - return; - if (!ok) + if (clearCardsToDelete() || !ok) { return; + } QListIterator i(scene()->selectedItems()); while (i.hasNext()) { @@ -2577,8 +2755,9 @@ void Player::actCardCounterTrigger() void Player::actPlay() { - if (!game->getActiveCard()) + if (!game->getActiveCard()) { return; + } bool cipt = game->getActiveCard()->getInfo() ? game->getActiveCard()->getInfo()->getCipt() : false; playCard(game->getActiveCard(), false, cipt); @@ -2586,16 +2765,18 @@ void Player::actPlay() void Player::actHide() { - if (!game->getActiveCard()) + if (!game->getActiveCard()) { return; + } game->getActiveCard()->getZone()->removeCard(game->getActiveCard()); } void Player::actPlayFacedown() { - if (!game->getActiveCard()) + if (!game->getActiveCard()) { return; + } playCard(game->getActiveCard(), true, false); } @@ -2605,7 +2786,7 @@ void Player::refreshShortcuts() if (shortcutsActive) { setShortcutsActive(); - foreach (const CardItem *cardItem, table->getCards()) { + for (const CardItem *cardItem : table->getCards()) { updateCardMenu(cardItem); } } @@ -2769,85 +2950,66 @@ void Player::addRelatedCardView(const CardItem *card, QMenu *cardMenu) void Player::addRelatedCardActions(const CardItem *card, QMenu *cardMenu) { - if (card == nullptr || cardMenu == nullptr || card->getInfo() == nullptr) + if (card == nullptr || cardMenu == nullptr || card->getInfo() == nullptr) { return; - - QList relatedCards = QList(); - relatedCards.append(card->getInfo()->getRelatedCards()); - relatedCards.append(card->getInfo()->getReverseRelatedCards2Me()); - - switch (relatedCards.length()) { - case 0: - break; - case 1: { - cardMenu->addSeparator(); - QAction *createRelatedCards; - if (relatedCards.at(0)->getDoesAttach()) { - createRelatedCards = - new QAction(tr("Token: ") + tr("Attach to ") + "\"" + relatedCards.at(0)->getName() + "\"", this); - } else - createRelatedCards = new QAction( - tr("Token: ") + - (relatedCards.at(0)->getIsVariable() - ? "X " - : QString(relatedCards.at(0)->getDefaultCount() == 1 - ? QString() - : QString::number(relatedCards.at(0)->getDefaultCount()) + "x ")) + - relatedCards.at(0)->getName(), - this); - connect(createRelatedCards, SIGNAL(triggered()), this, SLOT(actCreateAllRelatedCards())); - if (shortcutsActive) { - createRelatedCards->setShortcut( - settingsCache->shortcuts().getSingleShortcut("Player/aCreateRelatedTokens")); - } - cardMenu->addAction(createRelatedCards); - break; - } - default: { - cardMenu->addSeparator(); - int i = 0; - foreach (CardRelation *cardRelation, relatedCards) { - QString cardName = cardRelation->getName(); - QAction *createRelated; - if (cardRelation->getDoesAttach()) - createRelated = new QAction(tr("Token: ") + tr("Attach to ") + "\"" + cardName + "\"", this); - else - createRelated = - new QAction(tr("Token: ") + - (cardRelation->getIsVariable() - ? "X " - : QString(cardRelation->getDefaultCount() == 1 - ? QString() - : QString::number(cardRelation->getDefaultCount()) + "x ")) + - cardName, - this); - createRelated->setData(QVariant(i)); - connect(createRelated, SIGNAL(triggered()), this, SLOT(actCreateRelatedCard())); - cardMenu->addAction(createRelated); - i++; - } - QAction *createRelatedCards = new QAction(tr("All tokens"), this); - connect(createRelatedCards, SIGNAL(triggered()), this, SLOT(actCreateAllRelatedCards())); - if (shortcutsActive) { - createRelatedCards->setShortcut( - settingsCache->shortcuts().getSingleShortcut("Player/aCreateRelatedTokens")); - } - cardMenu->addAction(createRelatedCards); - break; - } } + + QList relatedCards(card->getInfo()->getRelatedCards()); + relatedCards.append(card->getInfo()->getReverseRelatedCards2Me()); + if (relatedCards.empty()) { + return; + } + + cardMenu->addSeparator(); + int index = 0; + QAction *createRelatedCards = nullptr; + for (const CardRelation *cardRelation : relatedCards) { + QString cardName = cardRelation->getName(); + QString text = tr("Token: "); + if (cardRelation->getDoesAttach()) { + text += tr("Attach to ") + "\"" + cardName + "\""; + } else if (cardRelation->getIsVariable()) { + text += "X " + cardName; + } else if (cardRelation->getDefaultCount() != 1) { + text += QString(cardRelation->getDefaultCount()) + "x " + cardName; + } else { + text += cardName; + } + + if (createRelatedCards == nullptr) { + if (relatedCards.length() == 1) { + createRelatedCards = new QAction(text, this); // set actCreateAllRelatedCards with this text + break; // do not set an individual entry as there is only one entry + } else { + createRelatedCards = new QAction(tr("All tokens"), this); + } + } + + QAction *createRelated = new QAction(text, this); + createRelated->setData(QVariant(index++)); + connect(createRelated, SIGNAL(triggered()), this, SLOT(actCreateRelatedCard())); + cardMenu->addAction(createRelated); + } + + if (shortcutsActive) { + createRelatedCards->setShortcut(settingsCache->shortcuts().getSingleShortcut("Player/aCreateRelatedTokens")); + } + connect(createRelatedCards, SIGNAL(triggered()), this, SLOT(actCreateAllRelatedCards())); + cardMenu->addAction(createRelatedCards); } void Player::setCardMenu(QMenu *menu) { - if (aCardMenu) + if (aCardMenu) { aCardMenu->setMenu(menu); + } } QMenu *Player::getCardMenu() const { - if (aCardMenu) + if (aCardMenu) { return aCardMenu->menu(); + } return 0; } @@ -2859,15 +3021,17 @@ QString Player::getName() const qreal Player::getMinimumWidth() const { qreal result = table->getMinimumWidth() + CARD_HEIGHT + 15 + counterAreaWidth + stack->boundingRect().width(); - if (!settingsCache->getHorizontalHand()) + if (!settingsCache->getHorizontalHand()) { result += hand->boundingRect().width(); + } return result; } void Player::setGameStarted() { - if (local) + if (local) { aAlwaysRevealTopCard->setChecked(false); + } setConceded(false); } @@ -2893,8 +3057,9 @@ void Player::processSceneSizeChange(int newPlayerWidth) { // Extend table (and hand, if horizontal) to accomodate the new player width. qreal tableWidth = newPlayerWidth - CARD_HEIGHT - 15 - counterAreaWidth - stack->boundingRect().width(); - if (!settingsCache->getHorizontalHand()) + if (!settingsCache->getHorizontalHand()) { tableWidth -= hand->boundingRect().width(); + } table->setWidth(tableWidth); hand->setWidth(tableWidth + stack->boundingRect().width()); @@ -2902,8 +3067,9 @@ void Player::processSceneSizeChange(int newPlayerWidth) void Player::setLastToken(CardInfoPtr cardInfo) { - if (cardInfo == nullptr || aCreateAnotherToken == nullptr) + if (cardInfo == nullptr || aCreateAnotherToken == nullptr) { return; + } lastTokenName = cardInfo->getName(); lastTokenColor = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().first().toLower(); diff --git a/cockatrice/src/player.h b/cockatrice/src/player.h index e459d3d24..c4f6ed008 100644 --- a/cockatrice/src/player.h +++ b/cockatrice/src/player.h @@ -144,6 +144,8 @@ public slots: void actUndoDraw(); void actMulligan(); void actMoveTopCardToPlayFaceDown(); + void actMoveTopCardToGrave(); + void actMoveTopCardToExile(); void actMoveTopCardsToGrave(); void actMoveTopCardsToExile(); void actMoveTopCardToBottom(); @@ -201,10 +203,10 @@ private: QAction *aMoveHandToTopLibrary, *aMoveHandToBottomLibrary, *aMoveHandToGrave, *aMoveHandToRfg, *aMoveGraveToTopLibrary, *aMoveGraveToBottomLibrary, *aMoveGraveToHand, *aMoveGraveToRfg, *aMoveRfgToTopLibrary, *aMoveRfgToBottomLibrary, *aMoveRfgToHand, *aMoveRfgToGrave, *aViewLibrary, *aViewTopCards, - *aAlwaysRevealTopCard, *aOpenDeckInDeckEditor, *aMoveTopCardsToGrave, *aMoveTopCardsToExile, - *aMoveTopCardToBottom, *aViewGraveyard, *aViewRfg, *aViewSideboard, *aDrawCard, *aDrawCards, *aUndoDraw, - *aMulligan, *aShuffle, *aMoveTopToPlayFaceDown, *aUntapAll, *aRollDie, *aCreateToken, *aCreateAnotherToken, - *aCardMenu, *aMoveBottomCardToGrave; + *aAlwaysRevealTopCard, *aOpenDeckInDeckEditor, *aMoveTopCardToGraveyard, *aMoveTopCardToExile, + *aMoveTopCardsToGraveyard, *aMoveTopCardsToExile, *aMoveTopCardToBottom, *aViewGraveyard, *aViewRfg, + *aViewSideboard, *aDrawCard, *aDrawCards, *aUndoDraw, *aMulligan, *aShuffle, *aMoveTopToPlayFaceDown, + *aUntapAll, *aRollDie, *aCreateToken, *aCreateAnotherToken, *aCardMenu, *aMoveBottomCardToGrave; QList aAddCounter, aSetCounter, aRemoveCounter; QAction *aPlay, *aPlayFacedown, *aHide, *aTap, *aDoesntUntap, *aAttach, *aUnattach, *aDrawArrow, *aSetPT, *aResetPT, diff --git a/cockatrice/src/sequenceEdit/sequenceedit.cpp b/cockatrice/src/sequenceEdit/sequenceedit.cpp index 3553870d3..c652a5252 100644 --- a/cockatrice/src/sequenceEdit/sequenceedit.cpp +++ b/cockatrice/src/sequenceEdit/sequenceedit.cpp @@ -1,20 +1,12 @@ #include "sequenceedit.h" #include "../settingscache.h" -#include #include #include -#include -#include #include #include -SequenceEdit::SequenceEdit(QString _shorcutName, QWidget *parent) : QWidget(parent) +SequenceEdit::SequenceEdit(const QString &_shortcutName, QWidget *parent) : QWidget(parent), shortcutName(_shortcutName) { - shorcutName = std::move(_shorcutName); - currentKey = 0; - keys = 0; - valid = false; - lineEdit = new QLineEdit(this); clearButton = new QPushButton("", this); defaultButton = new QPushButton("", this); @@ -42,20 +34,20 @@ SequenceEdit::SequenceEdit(QString _shorcutName, QWidget *parent) : QWidget(pare connect(defaultButton, SIGNAL(clicked()), this, SLOT(restoreDefault())); lineEdit->installEventFilter(this); - lineEdit->setText(settingsCache->shortcuts().getShortcutString(shorcutName)); + lineEdit->setText(settingsCache->shortcuts().getShortcutString(shortcutName)); } -QString SequenceEdit::getSecuence() +QString SequenceEdit::getSequence() { return lineEdit->text(); } void SequenceEdit::removeLastShortcut() { - QString secuences = lineEdit->text(); - if (!secuences.isEmpty()) { - if (secuences.lastIndexOf(";") > 0) { - QString valid = secuences.left(secuences.lastIndexOf(";")); + QString sequences = lineEdit->text(); + if (!sequences.isEmpty()) { + if (sequences.lastIndexOf(";") > 0) { + QString valid = sequences.left(sequences.lastIndexOf(";")); lineEdit->setText(valid); } else { lineEdit->clear(); @@ -67,18 +59,18 @@ void SequenceEdit::removeLastShortcut() void SequenceEdit::restoreDefault() { - lineEdit->setText(settingsCache->shortcuts().getDefaultShortcutString(shorcutName)); + lineEdit->setText(settingsCache->shortcuts().getDefaultShortcutString(shortcutName)); updateSettings(); } void SequenceEdit::refreshShortcut() { - lineEdit->setText(settingsCache->shortcuts().getShortcutString(shorcutName)); + lineEdit->setText(settingsCache->shortcuts().getShortcutString(shortcutName)); } void SequenceEdit::clear() { - this->lineEdit->setText(""); + lineEdit->setText(""); } bool SequenceEdit::eventFilter(QObject *, QEvent *event) @@ -142,8 +134,8 @@ void SequenceEdit::finishShortcut() QKeySequence sequence(keys); if (!sequence.isEmpty() && valid) { QString sequenceString = sequence.toString(); - if (settingsCache->shortcuts().isKeyAllowed(shorcutName, sequenceString)) { - if (settingsCache->shortcuts().isValid(shorcutName, sequenceString)) { + if (settingsCache->shortcuts().isKeyAllowed(shortcutName, sequenceString)) { + if (settingsCache->shortcuts().isValid(shortcutName, sequenceString)) { if (!lineEdit->text().isEmpty()) { if (lineEdit->text().contains(sequenceString)) { return; @@ -167,5 +159,5 @@ void SequenceEdit::finishShortcut() void SequenceEdit::updateSettings() { - settingsCache->shortcuts().setShortcuts(shorcutName, lineEdit->text()); -} \ No newline at end of file + settingsCache->shortcuts().setShortcuts(shortcutName, lineEdit->text()); +} diff --git a/cockatrice/src/sequenceEdit/sequenceedit.h b/cockatrice/src/sequenceEdit/sequenceedit.h index 2dffc101e..5f61127a4 100644 --- a/cockatrice/src/sequenceEdit/sequenceedit.h +++ b/cockatrice/src/sequenceEdit/sequenceedit.h @@ -1,19 +1,18 @@ -#ifndef SECUENCEEDIT_H -#define SECUENCEEDIT_H +#ifndef SEQUENCEEDIT_H +#define SEQUENCEEDIT_H +#include #include +#include +#include #include -class QLineEdit; -class QPushButton; -class QEvent; - class SequenceEdit : public QWidget { Q_OBJECT public: - SequenceEdit(QString _shorcutName, QWidget *parent = nullptr); - QString getSecuence(); + SequenceEdit(const QString &_shortcutName, QWidget *parent = nullptr); + QString getSequence(); void refreshShortcut(); void clear(); @@ -25,13 +24,13 @@ protected: bool eventFilter(QObject *, QEvent *event); private: - QString shorcutName; + QString shortcutName; QLineEdit *lineEdit; QPushButton *clearButton; QPushButton *defaultButton; - int keys; - int currentKey; - bool valid; + int keys = 0; + int currentKey = 0; + bool valid = false; void processKey(QKeyEvent *e); int translateModifiers(Qt::KeyboardModifiers state, const QString &text); @@ -39,4 +38,4 @@ private: void updateSettings(); }; -#endif // SECUENCEEDIT_H +#endif // SEQUENCEEDIT_H diff --git a/cockatrice/src/sequenceEdit/ui_shortcutstab.h b/cockatrice/src/sequenceEdit/ui_shortcutstab.h index 1a6714df3..17c786a8d 100644 --- a/cockatrice/src/sequenceEdit/ui_shortcutstab.h +++ b/cockatrice/src/sequenceEdit/ui_shortcutstab.h @@ -279,42 +279,8 @@ public: QSpacerItem *verticalSpacer_2; QWidget *tab_3; QGridLayout *gridLayout_20; - QGroupBox *groupBox_15; - QGridLayout *gridLayout_15; - QLabel *lbl_Player_aMoveToBottomLibrary; - SequenceEdit *Player_aMoveToBottomLibrary; - QLabel *lbl_Player_aMoveToTopLibrary; - SequenceEdit *Player_aMoveToTopLibrary; - QLabel *lbl_Player_aMoveToGraveyard; - SequenceEdit *Player_aMoveToGraveyard; - QLabel *lbl_Player_aMoveToExile; - SequenceEdit *Player_aMoveToExile; - QLabel *lbl_Player_aMoveToHand; - SequenceEdit *Player_aMoveToHand; - QLabel *lbl_Player_aMoveTopToPlayFaceDown; - SequenceEdit *Player_aMoveTopToPlayFaceDown; - QGroupBox *groupBox_16; - QGridLayout *gridLayout_16; - QLabel *lbl_Player_aViewGraveyard; - SequenceEdit *Player_aViewGraveyard; - QLabel *lbl_Player_aViewLibrary; - SequenceEdit *Player_aViewLibrary; - QLabel *lbl_Player_aViewTopCards; - SequenceEdit *Player_aViewTopCards; - QLabel *lbl_Player_aViewSideboard; - SequenceEdit *Player_aViewSideboard; - QLabel *lbl_Player_aViewRfg; - SequenceEdit *Player_aViewRfg; - QLabel *lbl_GameView_aCloseMostRecentZoneView; - SequenceEdit *GameView_aCloseMostRecentZoneView; - QGroupBox *groupBox_17; - QGridLayout *gridLayout_18; - SequenceEdit *DeckViewContainer_loadRemoteButton; - SequenceEdit *DeckViewContainer_loadLocalButton; - QLabel *lbl_DeckViewContainer_loadRemoteButton; - QLabel *lbl_DeckViewContainer_loadLocalButton; - QGroupBox *groupBox_18; - QGridLayout *gridLayout_19; + QGroupBox *groupBox_gameplay; + QGridLayout *gridLayout_gameplay; QLabel *lbl_Player_aDrawArrow; SequenceEdit *Player_aDrawArrow; QLabel *lbl_TabGame_aLeaveGame; @@ -331,8 +297,36 @@ public: SequenceEdit *Player_aShuffle; QLabel *lbl_TabGame_aRotateViewCCW; SequenceEdit *TabGame_aRotateViewCCW; - QGroupBox *groupBox_14; - QGridLayout *gridLayout_14; + QGroupBox *groupBox_moveCard; + QGridLayout *gridLayout_moveCard; + QLabel *lbl_Player_aMoveToBottomLibrary; + SequenceEdit *Player_aMoveToBottomLibrary; + QLabel *lbl_Player_aMoveToTopLibrary; + SequenceEdit *Player_aMoveToTopLibrary; + QLabel *lbl_Player_aMoveToGraveyard; + SequenceEdit *Player_aMoveToGraveyard; + QLabel *lbl_Player_aMoveToExile; + SequenceEdit *Player_aMoveToExile; + QLabel *lbl_Player_aMoveToHand; + SequenceEdit *Player_aMoveToHand; + QLabel *lbl_Player_aMoveTopToPlayFaceDown; + SequenceEdit *Player_aMoveTopToPlayFaceDown; + QGroupBox *groupBox_view; + QGridLayout *gridLayout_view; + QLabel *lbl_Player_aViewGraveyard; + SequenceEdit *Player_aViewGraveyard; + QLabel *lbl_Player_aViewLibrary; + SequenceEdit *Player_aViewLibrary; + QLabel *lbl_Player_aViewTopCards; + SequenceEdit *Player_aViewTopCards; + QLabel *lbl_Player_aViewSideboard; + SequenceEdit *Player_aViewSideboard; + QLabel *lbl_Player_aViewRfg; + SequenceEdit *Player_aViewRfg; + QLabel *lbl_GameView_aCloseMostRecentZoneView; + SequenceEdit *GameView_aCloseMostRecentZoneView; + QGroupBox *groupBox_draw; + QGridLayout *gridLayout_draw; QLabel *lbl_Player_aMulligan; SequenceEdit *Player_aMulligan; QLabel *lbl_Player_aDrawCard; @@ -343,6 +337,22 @@ public: SequenceEdit *Player_aUndoDraw; QLabel *lbl_Player_aAlwaysRevealTopCard; SequenceEdit *Player_aAlwaysRevealTopCard; + QGroupBox *groupBox_moveDeck; + QGridLayout *gridLayout_moveDeck; + QLabel *lbl_Player_aMoveTopCardToGraveyard; + SequenceEdit *Player_aMoveTopCardToGraveyard; + QLabel *lbl_Player_aMoveTopCardToExile; + SequenceEdit *Player_aMoveTopCardToExile; + QLabel *lbl_Player_aMoveTopCardsToGraveyard; + SequenceEdit *Player_aMoveTopCardsToGraveyard; + QLabel *lbl_Player_aMoveTopCardsToExile; + SequenceEdit *Player_aMoveTopCardsToExile; + QGroupBox *groupBox_gameLobby; + QGridLayout *gridLayout_gameLobby; + SequenceEdit *DeckViewContainer_loadRemoteButton; + QLabel *lbl_DeckViewContainer_loadRemoteButton; + SequenceEdit *DeckViewContainer_loadLocalButton; + QLabel *lbl_DeckViewContainer_loadLocalButton; QSpacerItem *verticalSpacer_3; QWidget *tab_4; QLabel *faqLabel; @@ -1406,304 +1416,352 @@ public: tab_3->setObjectName("tab_3"); gridLayout_20 = new QGridLayout(tab_3); gridLayout_20->setObjectName("gridLayout_20"); - groupBox_15 = new QGroupBox(tab_3); - groupBox_15->setObjectName("groupBox_15"); - gridLayout_15 = new QGridLayout(groupBox_15); - gridLayout_15->setObjectName("gridLayout_15"); - lbl_Player_aMoveToBottomLibrary = new QLabel(groupBox_15); - lbl_Player_aMoveToBottomLibrary->setObjectName("lbl_Player_aMoveToBottomLibrary"); - gridLayout_15->addWidget(lbl_Player_aMoveToBottomLibrary, 0, 0, 1, 1); - - Player_aMoveToBottomLibrary = new SequenceEdit("Player/aMoveToBottomLibrary", groupBox_15); - Player_aMoveToBottomLibrary->setObjectName("Player_aMoveToBottomLibrary"); - - gridLayout_15->addWidget(Player_aMoveToBottomLibrary, 0, 1, 1, 1); - - lbl_Player_aMoveToTopLibrary = new QLabel(groupBox_15); - lbl_Player_aMoveToTopLibrary->setObjectName("lbl_Player_aMoveToTopLibrary"); - - gridLayout_15->addWidget(lbl_Player_aMoveToTopLibrary, 1, 0, 1, 1); - - Player_aMoveToTopLibrary = new SequenceEdit("Player/aMoveToTopLibrary", groupBox_15); - Player_aMoveToTopLibrary->setObjectName("Player_aMoveToTopLibrary"); - - gridLayout_15->addWidget(Player_aMoveToTopLibrary, 1, 1, 1, 1); - - lbl_Player_aMoveToGraveyard = new QLabel(groupBox_15); - lbl_Player_aMoveToGraveyard->setObjectName("lbl_Player_aMoveToGraveyard"); - - gridLayout_15->addWidget(lbl_Player_aMoveToGraveyard, 2, 0, 1, 1); - - Player_aMoveToGraveyard = new SequenceEdit("Player/aMoveToGraveyard", groupBox_15); - Player_aMoveToGraveyard->setObjectName("Player_aMoveToGraveyard"); - - gridLayout_15->addWidget(Player_aMoveToGraveyard, 2, 1, 1, 1); - - lbl_Player_aMoveToExile = new QLabel(groupBox_15); - lbl_Player_aMoveToExile->setObjectName("lbl_Player_aMoveToExile"); - - gridLayout_15->addWidget(lbl_Player_aMoveToExile, 3, 0, 1, 1); - - Player_aMoveToExile = new SequenceEdit("Player/aMoveToExile", groupBox_15); - Player_aMoveToExile->setObjectName("Player_aMoveToExile"); - - gridLayout_15->addWidget(Player_aMoveToExile, 3, 1, 1, 1); - - lbl_Player_aMoveToHand = new QLabel(groupBox_15); - lbl_Player_aMoveToHand->setObjectName("lbl_Player_aMoveToHand"); - - gridLayout_15->addWidget(lbl_Player_aMoveToHand, 4, 0, 1, 1); - - Player_aMoveToHand = new SequenceEdit("Player/aMoveToHand", groupBox_15); - Player_aMoveToHand->setObjectName("Player_aMoveToHand"); - - gridLayout_15->addWidget(Player_aMoveToHand, 4, 1, 1, 1); - - lbl_Player_aMoveTopToPlayFaceDown = new QLabel(groupBox_15); - lbl_Player_aMoveTopToPlayFaceDown->setObjectName("lbl_Player_aMoveTopToPlayFaceDown"); - - gridLayout_15->addWidget(lbl_Player_aMoveTopToPlayFaceDown, 5, 0, 1, 1); - - Player_aMoveTopToPlayFaceDown = new SequenceEdit("Player/aMoveTopToPlayFaceDown", groupBox_15); - Player_aMoveTopToPlayFaceDown->setObjectName("Player_aMoveTopToPlayFaceDown"); - - gridLayout_15->addWidget(Player_aMoveTopToPlayFaceDown, 5, 1, 1, 1); - - gridLayout_20->addWidget(groupBox_15, 0, 1, 1, 1); - - groupBox_16 = new QGroupBox(tab_3); - groupBox_16->setObjectName("groupBox_16"); - gridLayout_16 = new QGridLayout(groupBox_16); - gridLayout_16->setObjectName("gridLayout_16"); - lbl_Player_aViewGraveyard = new QLabel(groupBox_16); - lbl_Player_aViewGraveyard->setObjectName("lbl_Player_aViewGraveyard"); - - gridLayout_16->addWidget(lbl_Player_aViewGraveyard, 0, 0, 1, 1); - - Player_aViewGraveyard = new SequenceEdit("Player/aViewGraveyard", groupBox_16); - Player_aViewGraveyard->setObjectName("Player_aViewGraveyard"); - - gridLayout_16->addWidget(Player_aViewGraveyard, 0, 1, 1, 1); - - lbl_Player_aViewLibrary = new QLabel(groupBox_16); - lbl_Player_aViewLibrary->setObjectName("lbl_Player_aViewLibrary"); - - gridLayout_16->addWidget(lbl_Player_aViewLibrary, 1, 0, 1, 1); - - Player_aViewLibrary = new SequenceEdit("Player/aViewLibrary", groupBox_16); - Player_aViewLibrary->setObjectName("Player_aViewLibrary"); - - gridLayout_16->addWidget(Player_aViewLibrary, 1, 1, 1, 1); - - lbl_Player_aViewTopCards = new QLabel(groupBox_16); - lbl_Player_aViewTopCards->setObjectName("lbl_Player_aViewTopCards"); - - gridLayout_16->addWidget(lbl_Player_aViewTopCards, 2, 0, 1, 1); - - Player_aViewTopCards = new SequenceEdit("Player/aViewTopCards", groupBox_16); - Player_aViewTopCards->setObjectName("Player_aViewTopCards"); - - gridLayout_16->addWidget(Player_aViewTopCards, 2, 1, 1, 1); - - lbl_Player_aViewSideboard = new QLabel(groupBox_16); - lbl_Player_aViewSideboard->setObjectName("lbl_Player_aViewSideboard"); - - gridLayout_16->addWidget(lbl_Player_aViewSideboard, 3, 0, 1, 1); - - Player_aViewSideboard = new SequenceEdit("Player/aViewSideboard", groupBox_16); - Player_aViewSideboard->setObjectName("Player_aViewSideboard"); - - gridLayout_16->addWidget(Player_aViewSideboard, 3, 1, 1, 1); - - lbl_Player_aViewRfg = new QLabel(groupBox_16); - lbl_Player_aViewRfg->setObjectName("lbl_Player_aViewRfg"); - - gridLayout_16->addWidget(lbl_Player_aViewRfg, 4, 0, 1, 1); - - Player_aViewRfg = new SequenceEdit("Player/aViewRfg", groupBox_16); - Player_aViewRfg->setObjectName("Player_aViewRfg"); - - gridLayout_16->addWidget(Player_aViewRfg, 4, 1, 1, 1); - - lbl_GameView_aCloseMostRecentZoneView = new QLabel(groupBox_16); - lbl_GameView_aCloseMostRecentZoneView->setObjectName("lbl_GameView_aCloseMostRecentZoneView"); - - gridLayout_16->addWidget(lbl_GameView_aCloseMostRecentZoneView, 5, 0, 1, 1); - - GameView_aCloseMostRecentZoneView = new SequenceEdit("Player/aCloseMostRecentZoneView", groupBox_16); - GameView_aCloseMostRecentZoneView->setObjectName("GameView_aCloseMostRecentZoneView"); - - gridLayout_16->addWidget(GameView_aCloseMostRecentZoneView, 5, 1, 1, 1); - - gridLayout_20->addWidget(groupBox_16, 0, 2, 1, 1); - - groupBox_17 = new QGroupBox(tab_3); - groupBox_17->setObjectName("groupBox_17"); - gridLayout_18 = new QGridLayout(groupBox_17); - gridLayout_18->setObjectName("gridLayout_18"); - DeckViewContainer_loadRemoteButton = new SequenceEdit("DeckViewContainer/loadRemoteButton", groupBox_17); - DeckViewContainer_loadRemoteButton->setObjectName("DeckViewContainer_loadRemoteButton"); - - gridLayout_18->addWidget(DeckViewContainer_loadRemoteButton, 2, 1, 1, 1); - - DeckViewContainer_loadLocalButton = new SequenceEdit("DeckViewContainer/loadLocalButton", groupBox_17); - DeckViewContainer_loadLocalButton->setObjectName("DeckViewContainer_loadLocalButton"); - - gridLayout_18->addWidget(DeckViewContainer_loadLocalButton, 0, 1, 1, 1); - - lbl_DeckViewContainer_loadRemoteButton = new QLabel(groupBox_17); - lbl_DeckViewContainer_loadRemoteButton->setObjectName("lbl_DeckViewContainer_loadRemoteButton"); - - gridLayout_18->addWidget(lbl_DeckViewContainer_loadRemoteButton, 2, 0, 1, 1); - - lbl_DeckViewContainer_loadLocalButton = new QLabel(groupBox_17); - lbl_DeckViewContainer_loadLocalButton->setObjectName("lbl_DeckViewContainer_loadLocalButton"); - - gridLayout_18->addWidget(lbl_DeckViewContainer_loadLocalButton, 0, 0, 1, 1); - - gridLayout_20->addWidget(groupBox_17, 1, 0, 1, 1); - - groupBox_18 = new QGroupBox(tab_3); - groupBox_18->setObjectName("groupBox_18"); - gridLayout_19 = new QGridLayout(groupBox_18); - gridLayout_19->setObjectName("gridLayout_19"); - lbl_Player_aDrawArrow = new QLabel(groupBox_18); + groupBox_gameplay = new QGroupBox(tab_3); + groupBox_gameplay->setObjectName("groupBox_gameplay"); + gridLayout_gameplay = new QGridLayout(groupBox_gameplay); + gridLayout_gameplay->setObjectName("gridLayout_gameplay"); + lbl_Player_aDrawArrow = new QLabel(groupBox_gameplay); lbl_Player_aDrawArrow->setObjectName("lbl_Player_aDrawArrow"); - gridLayout_19->addWidget(lbl_Player_aDrawArrow, 0, 0, 1, 1); + gridLayout_gameplay->addWidget(lbl_Player_aDrawArrow, 0, 0, 1, 1); - Player_aDrawArrow = new SequenceEdit("Player/aDrawArrow", groupBox_18); + Player_aDrawArrow = new SequenceEdit("Player/aDrawArrow", groupBox_gameplay); Player_aDrawArrow->setObjectName("Player_aDrawArrow"); - gridLayout_19->addWidget(Player_aDrawArrow, 0, 1, 1, 1); + gridLayout_gameplay->addWidget(Player_aDrawArrow, 0, 1, 1, 1); - lbl_TabGame_aLeaveGame = new QLabel(groupBox_18); - lbl_TabGame_aLeaveGame->setObjectName("lbl_TabGame_aLeaveGame"); - - gridLayout_19->addWidget(lbl_TabGame_aLeaveGame, 0, 2, 1, 1); - - TabGame_aLeaveGame = new SequenceEdit("Player/aLeaveGame", groupBox_18); - TabGame_aLeaveGame->setObjectName("TabGame_aLeaveGame"); - - gridLayout_19->addWidget(TabGame_aLeaveGame, 0, 3, 1, 1); - - lbl_TabGame_aRemoveLocalArrows = new QLabel(groupBox_18); + lbl_TabGame_aRemoveLocalArrows = new QLabel(groupBox_gameplay); lbl_TabGame_aRemoveLocalArrows->setObjectName("lbl_TabGame_aRemoveLocalArrows"); - gridLayout_19->addWidget(lbl_TabGame_aRemoveLocalArrows, 1, 0, 1, 1); + gridLayout_gameplay->addWidget(lbl_TabGame_aRemoveLocalArrows, 1, 0, 1, 1); - TabGame_aRemoveLocalArrows = new SequenceEdit("Player/aRemoveLocalArrows", groupBox_18); + TabGame_aRemoveLocalArrows = new SequenceEdit("Player/aRemoveLocalArrows", groupBox_gameplay); TabGame_aRemoveLocalArrows->setObjectName("TabGame_aRemoveLocalArrows"); - gridLayout_19->addWidget(TabGame_aRemoveLocalArrows, 1, 1, 1, 1); + gridLayout_gameplay->addWidget(TabGame_aRemoveLocalArrows, 1, 1, 1, 1); - lbl_TabGame_aConcede = new QLabel(groupBox_18); + lbl_TabGame_aConcede = new QLabel(groupBox_gameplay); lbl_TabGame_aConcede->setObjectName("lbl_TabGame_aConcede"); - gridLayout_19->addWidget(lbl_TabGame_aConcede, 1, 2, 1, 1); + gridLayout_gameplay->addWidget(lbl_TabGame_aConcede, 2, 0, 1, 1); - TabGame_aConcede = new SequenceEdit("Player/aConcede", groupBox_18); + TabGame_aConcede = new SequenceEdit("Player/aConcede", groupBox_gameplay); TabGame_aConcede->setObjectName("TabGame_aConcede"); - gridLayout_19->addWidget(TabGame_aConcede, 1, 3, 1, 1); + gridLayout_gameplay->addWidget(TabGame_aConcede, 2, 1, 1, 1); - lbl_Player_aRollDie = new QLabel(groupBox_18); + lbl_TabGame_aLeaveGame = new QLabel(groupBox_gameplay); + lbl_TabGame_aLeaveGame->setObjectName("lbl_TabGame_aLeaveGame"); + + gridLayout_gameplay->addWidget(lbl_TabGame_aLeaveGame, 3, 0, 1, 1); + + TabGame_aLeaveGame = new SequenceEdit("Player/aLeaveGame", groupBox_gameplay); + TabGame_aLeaveGame->setObjectName("TabGame_aLeaveGame"); + + gridLayout_gameplay->addWidget(TabGame_aLeaveGame, 3, 1, 1, 1); + + lbl_Player_aRollDie = new QLabel(groupBox_gameplay); lbl_Player_aRollDie->setObjectName("lbl_Player_aRollDie"); - gridLayout_19->addWidget(lbl_Player_aRollDie, 2, 0, 1, 1); + gridLayout_gameplay->addWidget(lbl_Player_aRollDie, 4, 0, 1, 1); - Player_aRollDie = new SequenceEdit("Player/aRollDie", groupBox_18); + Player_aRollDie = new SequenceEdit("Player/aRollDie", groupBox_gameplay); Player_aRollDie->setObjectName("Player_aRollDie"); - gridLayout_19->addWidget(Player_aRollDie, 2, 1, 1, 1); + gridLayout_gameplay->addWidget(Player_aRollDie, 4, 1, 1, 1); - lbl_TabGame_aRotateViewCW = new QLabel(groupBox_18); - lbl_TabGame_aRotateViewCW->setObjectName("lbl_TabGame_aRotateViewCW"); - - gridLayout_19->addWidget(lbl_TabGame_aRotateViewCW, 2, 2, 1, 1); - - TabGame_aRotateViewCW = new SequenceEdit("Player/aRotateViewCW", groupBox_18); - TabGame_aRotateViewCW->setObjectName("TabGame_aRotateViewCW"); - - gridLayout_19->addWidget(TabGame_aRotateViewCW, 2, 3, 1, 1); - - lbl_Player_aShuffle = new QLabel(groupBox_18); + lbl_Player_aShuffle = new QLabel(groupBox_gameplay); lbl_Player_aShuffle->setObjectName("lbl_Player_aShuffle"); - gridLayout_19->addWidget(lbl_Player_aShuffle, 3, 0, 1, 1); + gridLayout_gameplay->addWidget(lbl_Player_aShuffle, 5, 0, 1, 1); - Player_aShuffle = new SequenceEdit("Player/aShuffle", groupBox_18); + Player_aShuffle = new SequenceEdit("Player/aShuffle", groupBox_gameplay); Player_aShuffle->setObjectName("Player_aShuffle"); - gridLayout_19->addWidget(Player_aShuffle, 3, 1, 1, 1); + gridLayout_gameplay->addWidget(Player_aShuffle, 5, 1, 1, 1); - lbl_TabGame_aRotateViewCCW = new QLabel(groupBox_18); + lbl_TabGame_aRotateViewCW = new QLabel(groupBox_gameplay); + lbl_TabGame_aRotateViewCW->setObjectName("lbl_TabGame_aRotateViewCW"); + + gridLayout_gameplay->addWidget(lbl_TabGame_aRotateViewCW, 6, 0, 1, 1); + + TabGame_aRotateViewCW = new SequenceEdit("Player/aRotateViewCW", groupBox_gameplay); + TabGame_aRotateViewCW->setObjectName("TabGame_aRotateViewCW"); + + gridLayout_gameplay->addWidget(TabGame_aRotateViewCW, 6, 1, 1, 1); + + lbl_TabGame_aRotateViewCCW = new QLabel(groupBox_gameplay); lbl_TabGame_aRotateViewCCW->setObjectName("lbl_TabGame_aRotateViewCCW"); - gridLayout_19->addWidget(lbl_TabGame_aRotateViewCCW, 3, 2, 1, 1); + gridLayout_gameplay->addWidget(lbl_TabGame_aRotateViewCCW, 7, 0, 1, 1); - TabGame_aRotateViewCCW = new SequenceEdit("Player/aRotateViewCCW", groupBox_18); + TabGame_aRotateViewCCW = new SequenceEdit("Player/aRotateViewCCW", groupBox_gameplay); TabGame_aRotateViewCCW->setObjectName("TabGame_aRotateViewCCW"); - gridLayout_19->addWidget(TabGame_aRotateViewCCW, 3, 3, 1, 1); + gridLayout_gameplay->addWidget(TabGame_aRotateViewCCW, 7, 1, 1, 1); - gridLayout_20->addWidget(groupBox_18, 1, 1, 1, 2); + gridLayout_20->addWidget(groupBox_gameplay, 0, 0, 1, 1); - groupBox_14 = new QGroupBox(tab_3); - groupBox_14->setObjectName("groupBox_14"); - gridLayout_14 = new QGridLayout(groupBox_14); - gridLayout_14->setObjectName("gridLayout_14"); - lbl_Player_aMulligan = new QLabel(groupBox_14); + groupBox_moveCard = new QGroupBox(tab_3); + groupBox_moveCard->setObjectName("groupBox_moveCard"); + gridLayout_moveCard = new QGridLayout(groupBox_moveCard); + gridLayout_moveCard->setObjectName("gridLayout_moveCard"); + lbl_Player_aMoveToBottomLibrary = new QLabel(groupBox_moveCard); + lbl_Player_aMoveToBottomLibrary->setObjectName("lbl_Player_aMoveToBottomLibrary"); + + gridLayout_moveCard->addWidget(lbl_Player_aMoveToBottomLibrary, 0, 0, 1, 1); + + Player_aMoveToBottomLibrary = new SequenceEdit("Player/aMoveToBottomLibrary", groupBox_moveCard); + Player_aMoveToBottomLibrary->setObjectName("Player_aMoveToBottomLibrary"); + + gridLayout_moveCard->addWidget(Player_aMoveToBottomLibrary, 0, 1, 1, 1); + + lbl_Player_aMoveToTopLibrary = new QLabel(groupBox_moveCard); + lbl_Player_aMoveToTopLibrary->setObjectName("lbl_Player_aMoveToTopLibrary"); + + gridLayout_moveCard->addWidget(lbl_Player_aMoveToTopLibrary, 1, 0, 1, 1); + + Player_aMoveToTopLibrary = new SequenceEdit("Player/aMoveToTopLibrary", groupBox_moveCard); + Player_aMoveToTopLibrary->setObjectName("Player_aMoveToTopLibrary"); + + gridLayout_moveCard->addWidget(Player_aMoveToTopLibrary, 1, 1, 1, 1); + + lbl_Player_aMoveToGraveyard = new QLabel(groupBox_moveCard); + lbl_Player_aMoveToGraveyard->setObjectName("lbl_Player_aMoveToGraveyard"); + + gridLayout_moveCard->addWidget(lbl_Player_aMoveToGraveyard, 2, 0, 1, 1); + + Player_aMoveToGraveyard = new SequenceEdit("Player/aMoveToGraveyard", groupBox_moveCard); + Player_aMoveToGraveyard->setObjectName("Player_aMoveToGraveyard"); + + gridLayout_moveCard->addWidget(Player_aMoveToGraveyard, 2, 1, 1, 1); + + lbl_Player_aMoveToExile = new QLabel(groupBox_moveCard); + lbl_Player_aMoveToExile->setObjectName("lbl_Player_aMoveToExile"); + + gridLayout_moveCard->addWidget(lbl_Player_aMoveToExile, 3, 0, 1, 1); + + Player_aMoveToExile = new SequenceEdit("Player/aMoveToExile", groupBox_moveCard); + Player_aMoveToExile->setObjectName("Player_aMoveToExile"); + + gridLayout_moveCard->addWidget(Player_aMoveToExile, 3, 1, 1, 1); + + lbl_Player_aMoveToHand = new QLabel(groupBox_moveCard); + lbl_Player_aMoveToHand->setObjectName("lbl_Player_aMoveToHand"); + + gridLayout_moveCard->addWidget(lbl_Player_aMoveToHand, 4, 0, 1, 1); + + Player_aMoveToHand = new SequenceEdit("Player/aMoveToHand", groupBox_moveCard); + Player_aMoveToHand->setObjectName("Player_aMoveToHand"); + + gridLayout_moveCard->addWidget(Player_aMoveToHand, 4, 1, 1, 1); + + lbl_Player_aMoveTopToPlayFaceDown = new QLabel(groupBox_moveCard); + lbl_Player_aMoveTopToPlayFaceDown->setObjectName("lbl_Player_aMoveTopToPlayFaceDown"); + + gridLayout_moveCard->addWidget(lbl_Player_aMoveTopToPlayFaceDown, 5, 0, 1, 1); + + Player_aMoveTopToPlayFaceDown = new SequenceEdit("Player/aMoveTopToPlayFaceDown", groupBox_moveCard); + Player_aMoveTopToPlayFaceDown->setObjectName("Player_aMoveTopToPlayFaceDown"); + + gridLayout_moveCard->addWidget(Player_aMoveTopToPlayFaceDown, 5, 1, 1, 1); + + gridLayout_20->addWidget(groupBox_moveCard, 0, 1, 1, 1); + + groupBox_view = new QGroupBox(tab_3); + groupBox_view->setObjectName("groupBox_view"); + gridLayout_view = new QGridLayout(groupBox_view); + gridLayout_view->setObjectName("gridLayout_view"); + lbl_Player_aViewGraveyard = new QLabel(groupBox_view); + lbl_Player_aViewGraveyard->setObjectName("lbl_Player_aViewGraveyard"); + + gridLayout_view->addWidget(lbl_Player_aViewGraveyard, 0, 0, 1, 1); + + Player_aViewGraveyard = new SequenceEdit("Player/aViewGraveyard", groupBox_view); + Player_aViewGraveyard->setObjectName("Player_aViewGraveyard"); + + gridLayout_view->addWidget(Player_aViewGraveyard, 0, 1, 1, 1); + + lbl_Player_aViewLibrary = new QLabel(groupBox_view); + lbl_Player_aViewLibrary->setObjectName("lbl_Player_aViewLibrary"); + + gridLayout_view->addWidget(lbl_Player_aViewLibrary, 1, 0, 1, 1); + + Player_aViewLibrary = new SequenceEdit("Player/aViewLibrary", groupBox_view); + Player_aViewLibrary->setObjectName("Player_aViewLibrary"); + + gridLayout_view->addWidget(Player_aViewLibrary, 1, 1, 1, 1); + + lbl_Player_aViewTopCards = new QLabel(groupBox_view); + lbl_Player_aViewTopCards->setObjectName("lbl_Player_aViewTopCards"); + + gridLayout_view->addWidget(lbl_Player_aViewTopCards, 2, 0, 1, 1); + + Player_aViewTopCards = new SequenceEdit("Player/aViewTopCards", groupBox_view); + Player_aViewTopCards->setObjectName("Player_aViewTopCards"); + + gridLayout_view->addWidget(Player_aViewTopCards, 2, 1, 1, 1); + + lbl_Player_aViewSideboard = new QLabel(groupBox_view); + lbl_Player_aViewSideboard->setObjectName("lbl_Player_aViewSideboard"); + + gridLayout_view->addWidget(lbl_Player_aViewSideboard, 3, 0, 1, 1); + + Player_aViewSideboard = new SequenceEdit("Player/aViewSideboard", groupBox_view); + Player_aViewSideboard->setObjectName("Player_aViewSideboard"); + + gridLayout_view->addWidget(Player_aViewSideboard, 3, 1, 1, 1); + + lbl_Player_aViewRfg = new QLabel(groupBox_view); + lbl_Player_aViewRfg->setObjectName("lbl_Player_aViewRfg"); + + gridLayout_view->addWidget(lbl_Player_aViewRfg, 4, 0, 1, 1); + + Player_aViewRfg = new SequenceEdit("Player/aViewRfg", groupBox_view); + Player_aViewRfg->setObjectName("Player_aViewRfg"); + + gridLayout_view->addWidget(Player_aViewRfg, 4, 1, 1, 1); + + lbl_GameView_aCloseMostRecentZoneView = new QLabel(groupBox_view); + lbl_GameView_aCloseMostRecentZoneView->setObjectName("lbl_GameView_aCloseMostRecentZoneView"); + + gridLayout_view->addWidget(lbl_GameView_aCloseMostRecentZoneView, 5, 0, 1, 1); + + GameView_aCloseMostRecentZoneView = new SequenceEdit("Player/aCloseMostRecentZoneView", groupBox_view); + GameView_aCloseMostRecentZoneView->setObjectName("GameView_aCloseMostRecentZoneView"); + + gridLayout_view->addWidget(GameView_aCloseMostRecentZoneView, 5, 1, 1, 1); + + gridLayout_20->addWidget(groupBox_view, 0, 2, 1, 1); + + groupBox_draw = new QGroupBox(tab_3); + groupBox_draw->setObjectName("groupBox_draw"); + gridLayout_draw = new QGridLayout(groupBox_draw); + gridLayout_draw->setObjectName("gridLayout_draw"); + lbl_Player_aMulligan = new QLabel(groupBox_draw); lbl_Player_aMulligan->setObjectName("lbl_Player_aMulligan"); - gridLayout_14->addWidget(lbl_Player_aMulligan, 4, 0, 1, 1); + gridLayout_draw->addWidget(lbl_Player_aMulligan, 4, 0, 1, 1); - Player_aMulligan = new SequenceEdit("Player/aMulligan", groupBox_14); + Player_aMulligan = new SequenceEdit("Player/aMulligan", groupBox_draw); Player_aMulligan->setObjectName("Player_aMulligan"); - gridLayout_14->addWidget(Player_aMulligan, 4, 1, 1, 1); + gridLayout_draw->addWidget(Player_aMulligan, 4, 1, 1, 1); - lbl_Player_aDrawCard = new QLabel(groupBox_14); + lbl_Player_aDrawCard = new QLabel(groupBox_draw); lbl_Player_aDrawCard->setObjectName("lbl_Player_aDrawCard"); - gridLayout_14->addWidget(lbl_Player_aDrawCard, 0, 0, 1, 1); + gridLayout_draw->addWidget(lbl_Player_aDrawCard, 0, 0, 1, 1); - Player_aDrawCard = new SequenceEdit("Player/aDrawCard", groupBox_14); + Player_aDrawCard = new SequenceEdit("Player/aDrawCard", groupBox_draw); Player_aDrawCard->setObjectName("Player_aDrawCard"); - gridLayout_14->addWidget(Player_aDrawCard, 0, 1, 1, 1); + gridLayout_draw->addWidget(Player_aDrawCard, 0, 1, 1, 1); - lbl_Player_aDrawCards = new QLabel(groupBox_14); + lbl_Player_aDrawCards = new QLabel(groupBox_draw); lbl_Player_aDrawCards->setObjectName("lbl_Player_aDrawCards"); - gridLayout_14->addWidget(lbl_Player_aDrawCards, 1, 0, 1, 1); + gridLayout_draw->addWidget(lbl_Player_aDrawCards, 1, 0, 1, 1); - Player_aDrawCards = new SequenceEdit("Player/aDrawCards", groupBox_14); + Player_aDrawCards = new SequenceEdit("Player/aDrawCards", groupBox_draw); Player_aDrawCards->setObjectName("Player_aDrawCards"); - gridLayout_14->addWidget(Player_aDrawCards, 1, 1, 1, 1); + gridLayout_draw->addWidget(Player_aDrawCards, 1, 1, 1, 1); - lbl_Player_aUndoDraw = new QLabel(groupBox_14); + lbl_Player_aUndoDraw = new QLabel(groupBox_draw); lbl_Player_aUndoDraw->setObjectName("lbl_Player_aUndoDraw"); - gridLayout_14->addWidget(lbl_Player_aUndoDraw, 2, 0, 1, 1); + gridLayout_draw->addWidget(lbl_Player_aUndoDraw, 2, 0, 1, 1); - Player_aUndoDraw = new SequenceEdit("Player/aUndoDraw", groupBox_14); + Player_aUndoDraw = new SequenceEdit("Player/aUndoDraw", groupBox_draw); Player_aUndoDraw->setObjectName("Player_aUndoDraw"); - gridLayout_14->addWidget(Player_aUndoDraw, 2, 1, 1, 1); + gridLayout_draw->addWidget(Player_aUndoDraw, 2, 1, 1, 1); - lbl_Player_aAlwaysRevealTopCard = new QLabel(groupBox_14); + lbl_Player_aAlwaysRevealTopCard = new QLabel(groupBox_draw); lbl_Player_aAlwaysRevealTopCard->setObjectName("lbl_Player_aAlwaysRevealTopCard"); - gridLayout_14->addWidget(lbl_Player_aAlwaysRevealTopCard, 3, 0, 1, 1); + gridLayout_draw->addWidget(lbl_Player_aAlwaysRevealTopCard, 3, 0, 1, 1); - Player_aAlwaysRevealTopCard = new SequenceEdit("Player/aAlwaysRevealTopCard", groupBox_14); + Player_aAlwaysRevealTopCard = new SequenceEdit("Player/aAlwaysRevealTopCard", groupBox_draw); Player_aAlwaysRevealTopCard->setObjectName("Player_aAlwaysRevealTopCard"); - gridLayout_14->addWidget(Player_aAlwaysRevealTopCard, 3, 1, 1, 1); - gridLayout_20->addWidget(groupBox_14, 0, 0, 1, 1); + gridLayout_draw->addWidget(Player_aAlwaysRevealTopCard, 3, 1, 1, 1); + gridLayout_20->addWidget(groupBox_draw, 1, 0, 1, 1); + + groupBox_moveDeck = new QGroupBox(tab_3); + groupBox_moveDeck->setObjectName("groupBox_moveDeck"); + gridLayout_moveDeck = new QGridLayout(groupBox_moveDeck); + gridLayout_moveDeck->setObjectName("gridLayout_moveDeck"); + lbl_Player_aMoveTopCardToGraveyard = new QLabel(groupBox_moveDeck); + lbl_Player_aMoveTopCardToGraveyard->setObjectName("lbl_Player_aMoveTopCardToGraveyard"); + + gridLayout_moveDeck->addWidget(lbl_Player_aMoveTopCardToGraveyard, 0, 0, 1, 1); + + Player_aMoveTopCardToGraveyard = new SequenceEdit("Player/aMoveTopCardToGraveyard", groupBox_moveDeck); + Player_aMoveTopCardToGraveyard->setObjectName("Player_aMoveTopCardToGraveyard"); + + gridLayout_moveDeck->addWidget(Player_aMoveTopCardToGraveyard, 0, 1, 1, 1); + + lbl_Player_aMoveTopCardsToGraveyard = new QLabel(groupBox_moveDeck); + lbl_Player_aMoveTopCardsToGraveyard->setObjectName("lbl_Player_aMoveTopCardsToGraveyard"); + + gridLayout_moveDeck->addWidget(lbl_Player_aMoveTopCardsToGraveyard, 1, 0, 1, 1); + + Player_aMoveTopCardsToGraveyard = new SequenceEdit("Player/aMoveTopCardsToGraveyard", groupBox_moveDeck); + Player_aMoveTopCardsToGraveyard->setObjectName("Player_aMoveTopCardsToGraveyard"); + + gridLayout_moveDeck->addWidget(Player_aMoveTopCardsToGraveyard, 1, 1, 1, 1); + + lbl_Player_aMoveTopCardToExile = new QLabel(groupBox_moveDeck); + lbl_Player_aMoveTopCardToExile->setObjectName("lbl_Player_aMoveTopCardToExile"); + + gridLayout_moveDeck->addWidget(lbl_Player_aMoveTopCardToExile, 2, 0, 1, 1); + + Player_aMoveTopCardToExile = new SequenceEdit("Player/aMoveTopCardToExile", groupBox_moveDeck); + Player_aMoveTopCardToExile->setObjectName("Player_aMoveTopCardToExile"); + + gridLayout_moveDeck->addWidget(Player_aMoveTopCardToExile, 2, 1, 1, 1); + + lbl_Player_aMoveTopCardsToExile = new QLabel(groupBox_moveDeck); + lbl_Player_aMoveTopCardsToExile->setObjectName("lbl_Player_aMoveTopCardsToExile"); + + gridLayout_moveDeck->addWidget(lbl_Player_aMoveTopCardsToExile, 3, 0, 1, 1); + + Player_aMoveTopCardsToExile = new SequenceEdit("Player/aMoveTopCardsToExile", groupBox_moveDeck); + Player_aMoveTopCardsToExile->setObjectName("Player_aMoveTopCardsToExile"); + + gridLayout_moveDeck->addWidget(Player_aMoveTopCardsToExile, 3, 1, 1, 1); + + gridLayout_20->addWidget(groupBox_moveDeck, 1, 1, 1, 1); + + groupBox_gameLobby = new QGroupBox(tab_3); + groupBox_gameLobby->setObjectName("groupBox_gameLobby"); + gridLayout_gameLobby = new QGridLayout(groupBox_gameLobby); + gridLayout_gameLobby->setObjectName("gridLayout_gameLobby"); + DeckViewContainer_loadRemoteButton = new SequenceEdit("DeckViewContainer/loadRemoteButton", groupBox_gameLobby); + DeckViewContainer_loadRemoteButton->setObjectName("DeckViewContainer_loadRemoteButton"); + + gridLayout_gameLobby->addWidget(DeckViewContainer_loadRemoteButton, 2, 1, 1, 1); + + DeckViewContainer_loadLocalButton = new SequenceEdit("DeckViewContainer/loadLocalButton", groupBox_gameLobby); + DeckViewContainer_loadLocalButton->setObjectName("DeckViewContainer_loadLocalButton"); + + gridLayout_gameLobby->addWidget(DeckViewContainer_loadLocalButton, 0, 1, 1, 1); + + lbl_DeckViewContainer_loadRemoteButton = new QLabel(groupBox_gameLobby); + lbl_DeckViewContainer_loadRemoteButton->setObjectName("lbl_DeckViewContainer_loadRemoteButton"); + + gridLayout_gameLobby->addWidget(lbl_DeckViewContainer_loadRemoteButton, 2, 0, 1, 1); + + lbl_DeckViewContainer_loadLocalButton = new QLabel(groupBox_gameLobby); + lbl_DeckViewContainer_loadLocalButton->setObjectName("lbl_DeckViewContainer_loadLocalButton"); + + gridLayout_gameLobby->addWidget(lbl_DeckViewContainer_loadLocalButton, 0, 0, 1, 1); + + gridLayout_20->addWidget(groupBox_gameLobby, 1, 2, 1, 1); + verticalSpacer_3 = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); gridLayout_20->addItem(verticalSpacer_3, 2, 1, 1, 1); tabWidget->addTab(tab_3, QString()); @@ -1747,12 +1805,13 @@ public: gridLayout_11->setSpacing(3); gridLayout_12->setSpacing(3); gridLayout_13->setSpacing(3); - gridLayout_14->setSpacing(3); - gridLayout_15->setSpacing(3); - gridLayout_16->setSpacing(3); + gridLayout_moveDeck->setSpacing(3); + gridLayout_draw->setSpacing(3); + gridLayout_moveCard->setSpacing(3); + gridLayout_view->setSpacing(3); + gridLayout_gameLobby->setSpacing(3); + gridLayout_gameplay->setSpacing(3); gridLayout_17->setSpacing(3); - gridLayout_18->setSpacing(3); - gridLayout_19->setSpacing(3); gridLayout_20->setSpacing(3); verticalLayout->setSpacing(3); @@ -1893,24 +1952,29 @@ public: lbl_Player_aSetAnnotation->setText(QApplication::translate("shortcutsTab", "Set annotation", 0)); tabWidget->setTabText(tabWidget->indexOf(tab_2), QApplication::translate("shortcutsTab", "Phases | P/T | Playing Area", 0)); - groupBox_15->setTitle(QApplication::translate("shortcutsTab", "Move card to", 0)); + groupBox_moveCard->setTitle(QApplication::translate("shortcutsTab", "Move selected card to", 0)); lbl_Player_aMoveToBottomLibrary->setText(QApplication::translate("shortcutsTab", "Bottom library", 0)); lbl_Player_aMoveToTopLibrary->setText(QApplication::translate("shortcutsTab", "Top library", 0)); lbl_Player_aMoveToGraveyard->setText(QApplication::translate("shortcutsTab", "Graveyard", 0)); lbl_Player_aMoveToExile->setText(QApplication::translate("shortcutsTab", "Exile", 0)); lbl_Player_aMoveToHand->setText(QApplication::translate("shortcutsTab", "Hand", 0)); lbl_Player_aMoveTopToPlayFaceDown->setText(QApplication::translate("shortcutsTab", "Play face down")); - groupBox_16->setTitle(QApplication::translate("shortcutsTab", "View", 0)); + groupBox_view->setTitle(QApplication::translate("shortcutsTab", "View", 0)); lbl_Player_aViewGraveyard->setText(QApplication::translate("shortcutsTab", "Graveyard", 0)); lbl_Player_aViewLibrary->setText(QApplication::translate("shortcutsTab", "Library", 0)); - lbl_Player_aViewTopCards->setText(QApplication::translate("shortcutsTab", "Tops card of library", 0)); + lbl_Player_aViewTopCards->setText(QApplication::translate("shortcutsTab", "Top cards of library", 0)); lbl_Player_aViewSideboard->setText(QApplication::translate("shortcutsTab", "Sideboard", 0)); lbl_Player_aViewRfg->setText(QApplication::translate("shortcutsTab", "Exile", 0)); lbl_GameView_aCloseMostRecentZoneView->setText(QApplication::translate("shortcutsTab", "Close recent view", 0)); - groupBox_17->setTitle(QApplication::translate("shortcutsTab", "Game Lobby", 0)); + groupBox_moveDeck->setTitle(QApplication::translate("shortcutsTab", "Move top card to", 0)); + lbl_Player_aMoveTopCardToGraveyard->setText(QApplication::translate("shortcutsTab", "Graveyard Once", 0)); + lbl_Player_aMoveTopCardsToGraveyard->setText(QApplication::translate("shortcutsTab", "Graveyard Multiple", 0)); + lbl_Player_aMoveTopCardToExile->setText(QApplication::translate("shortcutsTab", "Exile Once", 0)); + lbl_Player_aMoveTopCardsToExile->setText(QApplication::translate("shortcutsTab", "Exile Multiple", 0)); + groupBox_gameLobby->setTitle(QApplication::translate("shortcutsTab", "Game Lobby", 0)); lbl_DeckViewContainer_loadRemoteButton->setText(QApplication::translate("shortcutsTab", "Load remote deck", 0)); lbl_DeckViewContainer_loadLocalButton->setText(QApplication::translate("shortcutsTab", "Load local deck", 0)); - groupBox_18->setTitle(QApplication::translate("shortcutsTab", "Gameplay", 0)); + groupBox_gameplay->setTitle(QApplication::translate("shortcutsTab", "Gameplay", 0)); lbl_Player_aDrawArrow->setText(QApplication::translate("shortcutsTab", "Draw arrow", 0)); lbl_TabGame_aLeaveGame->setText(QApplication::translate("shortcutsTab", "Leave game", 0)); lbl_TabGame_aRemoveLocalArrows->setText(QApplication::translate("shortcutsTab", "Remove local arrows", 0)); @@ -1919,14 +1983,14 @@ public: lbl_TabGame_aRotateViewCW->setText(QApplication::translate("shortcutsTab", "Rotate view CW", 0)); lbl_Player_aShuffle->setText(QApplication::translate("shortcutsTab", "Shuffle library", 0)); lbl_TabGame_aRotateViewCCW->setText(QApplication::translate("shortcutsTab", "Rotate view CCW", 0)); - groupBox_14->setTitle(QApplication::translate("shortcutsTab", "Draw", 0)); + groupBox_draw->setTitle(QApplication::translate("shortcutsTab", "Draw", 0)); lbl_Player_aMulligan->setText(QApplication::translate("shortcutsTab", "Mulligan", 0)); lbl_Player_aDrawCard->setText(QApplication::translate("shortcutsTab", "Draw card", 0)); lbl_Player_aDrawCards->setText(QApplication::translate("shortcutsTab", "Draw cards", 0)); lbl_Player_aUndoDraw->setText(QApplication::translate("shortcutsTab", "Undo draw", 0)); lbl_Player_aAlwaysRevealTopCard->setText(QApplication::translate("shortcutsTab", "Always reveal top card", 0)); tabWidget->setTabText(tabWidget->indexOf(tab_3), - QApplication::translate("shortcutsTab", "Draw | Move | View | Gameplay", 0)); + QApplication::translate("shortcutsTab", "Gameplay | Draw | Move | View", 0)); tabWidget->setTabText(tabWidget->indexOf(tab_4), QApplication::translate("shortcutsTab", "Counters", 0)); faqLabel->setText(QString("%2") .arg(WIKI) diff --git a/cockatrice/src/shortcutssettings.cpp b/cockatrice/src/shortcutssettings.cpp index 66078c297..002ae8e59 100644 --- a/cockatrice/src/shortcutssettings.cpp +++ b/cockatrice/src/shortcutssettings.cpp @@ -4,19 +4,18 @@ #include #include -ShortcutsSettings::ShortcutsSettings(QString settingsPath, QObject *parent) : QObject(parent) +ShortcutsSettings::ShortcutsSettings(const QString &settingsPath, QObject *parent) : QObject(parent) { - this->settingsFilePath = std::move(settingsPath); - this->settingsFilePath.append("shortcuts.ini"); - fillDefaultShorcuts(); - shortCuts = QMap>(defaultShortCuts); + shortCuts = defaultShortCuts; + settingsFilePath = settingsPath; + settingsFilePath.append("shortcuts.ini"); bool exists = QFile(settingsFilePath).exists(); QSettings shortCutsFile(settingsFilePath, QSettings::IniFormat); if (exists) { - shortCutsFile.beginGroup("Custom"); + shortCutsFile.beginGroup(custom); const QStringList customKeys = shortCutsFile.allKeys(); QMap invalidItems; @@ -55,114 +54,123 @@ ShortcutsSettings::ShortcutsSettings(QString settingsPath, QObject *parent) : QO } } -QList ShortcutsSettings::getShortcut(QString name) +QList ShortcutsSettings::getDefaultShortcut(const QString &name) const +{ + return defaultShortCuts.value(name, QList()); +} + +QList ShortcutsSettings::getShortcut(const QString &name) const { if (shortCuts.contains(name)) { return shortCuts.value(name); } - return defaultShortCuts.value(name, QList()); + return getDefaultShortcut(name); } -QKeySequence ShortcutsSettings::getSingleShortcut(QString name) +QKeySequence ShortcutsSettings::getSingleShortcut(const QString &name) const { - return getShortcut(std::move(name)).at(0); + return getShortcut(name).at(0); } -QString ShortcutsSettings::getDefaultShortcutString(QString name) +QString ShortcutsSettings::getDefaultShortcutString(const QString &name) const { - return stringifySequence(defaultShortCuts.value(name)); + return stringifySequence(getDefaultShortcut(name)); } -QString ShortcutsSettings::getShortcutString(QString name) +QString ShortcutsSettings::getShortcutString(const QString &name) const { - return stringifySequence(shortCuts.value(name)); + return stringifySequence(getShortcut(name)); } -QString ShortcutsSettings::stringifySequence(QList Sequence) const +QString ShortcutsSettings::stringifySequence(const QList &Sequence) const { - QString stringSequence; + QStringList stringSequence; for (int i = 0; i < Sequence.size(); ++i) { stringSequence.append(Sequence.at(i).toString(QKeySequence::PortableText)); - if (i < Sequence.size() - 1) { - stringSequence.append(";"); - } } - return stringSequence; + return stringSequence.join(sep); } -QList ShortcutsSettings::parseSequenceString(QString stringSequence) +QList ShortcutsSettings::parseSequenceString(const QString &stringSequence) const { - QStringList Sequences = stringSequence.split(";"); QList SequenceList; - for (QStringList::const_iterator ss = Sequences.constBegin(); ss != Sequences.constEnd(); ++ss) { - SequenceList.append(QKeySequence(*ss, QKeySequence::PortableText)); + for (const QString &shortcut : stringSequence.split(sep)) { + SequenceList.append(QKeySequence(shortcut, QKeySequence::PortableText)); } return SequenceList; } -void ShortcutsSettings::setShortcuts(QString name, QList Sequence) +void ShortcutsSettings::setShortcuts(const QString &name, const QList &Sequence) { shortCuts[name] = Sequence; QSettings shortCutsFile(settingsFilePath, QSettings::IniFormat); - shortCutsFile.beginGroup("Custom"); - QString stringSequence = stringifySequence(Sequence); - shortCutsFile.setValue(name, stringSequence); + shortCutsFile.beginGroup(custom); + shortCutsFile.setValue(name, stringifySequence(Sequence)); shortCutsFile.endGroup(); - emit shortCutchanged(); + emit shortCutChanged(); } -void ShortcutsSettings::setShortcuts(QString name, QKeySequence Sequence) +void ShortcutsSettings::setShortcuts(const QString &name, const QKeySequence &Sequence) { - setShortcuts(std::move(name), QList() << Sequence); + setShortcuts(name, QList{Sequence}); } -void ShortcutsSettings::setShortcuts(QString name, QString Sequences) +void ShortcutsSettings::setShortcuts(const QString &name, const QString &Sequences) { - setShortcuts(std::move(name), parseSequenceString(std::move(Sequences))); + setShortcuts(name, parseSequenceString(Sequences)); } -bool ShortcutsSettings::isKeyAllowed(QString name, QString Sequences) +void ShortcutsSettings::resetAllShortcuts() +{ + shortCuts = defaultShortCuts; + QSettings shortCutsFile(settingsFilePath, QSettings::IniFormat); + shortCutsFile.beginGroup(custom); + shortCutsFile.remove(""); + shortCutsFile.endGroup(); + emit shortCutChanged(); + emit allShortCutsReset(); +} + +void ShortcutsSettings::clearAllShortcuts() +{ + QSettings shortCutsFile(settingsFilePath, QSettings::IniFormat); + shortCutsFile.beginGroup(custom); + for (auto it = shortCuts.begin(); it != shortCuts.end(); ++it) { + it.value() = parseSequenceString(""); + shortCutsFile.setValue(it.key(), ""); + } + shortCutsFile.endGroup(); + emit shortCutChanged(); + emit allShortCutsClear(); +} + +bool ShortcutsSettings::isKeyAllowed(const QString &name, const QString &Sequences) const { // if the shortcut is not to be used in deck-editor then it doesn't matter if (name.startsWith("Player")) { return true; } - QString checkSequence = Sequences.split(";").last(); - QStringList forbiddenKeys = (QStringList() << "Del" - << "Backspace" - << "Down" - << "Up" - << "Left" - << "Right" - << "Return" - << "Enter" - << "Menu" - << "Ctrl+Alt+-" - << "Ctrl+Alt+=" - << "Ctrl+Alt+[" - << "Ctrl+Alt+]" - << "Tab" - << "Space" - << "Shift+S" - << "Shift+Left" - << "Shift+Right"); + QString checkSequence = Sequences.split(sep).last(); + QStringList forbiddenKeys{"Del", "Backspace", "Down", "Up", "Left", "Right", + "Return", "Enter", "Menu", "Ctrl+Alt+-", "Ctrl+Alt+=", "Ctrl+Alt+[", + "Ctrl+Alt+]", "Tab", "Space", "Shift+S", "Shift+Left", "Shift+Right"}; return !forbiddenKeys.contains(checkSequence); } -bool ShortcutsSettings::isValid(QString name, QString Sequences) +bool ShortcutsSettings::isValid(const QString &name, const QString &Sequences) const { - QString checkSequence = Sequences.split(";").last(); + QString checkSequence = Sequences.split(sep).last(); QString checkKey = name.left(name.indexOf("/")); QList allKeys = shortCuts.keys(); for (const auto &key : allKeys) { if (key.startsWith(checkKey) || key.startsWith("MainWindow") || checkKey.startsWith("MainWindow")) { QString storedSequence = stringifySequence(shortCuts.value(key)); - QStringList stringSequences = storedSequence.split(";"); + QStringList stringSequences = storedSequence.split(sep); if (stringSequences.contains(checkSequence)) { return false; } @@ -170,161 +178,3 @@ bool ShortcutsSettings::isValid(QString name, QString Sequences) } return true; } - -void ShortcutsSettings::resetAllShortcuts() -{ - for (auto it = defaultShortCuts.begin(); it != defaultShortCuts.end(); ++it) { - setShortcuts(it.key(), it.value()); - } - emit allShortCutsReset(); -} - -void ShortcutsSettings::clearAllShortcuts() -{ - for (auto it = shortCuts.begin(); it != shortCuts.end(); ++it) { - setShortcuts(it.key(), ""); - } - emit allShortCutsClear(); -} - -void ShortcutsSettings::fillDefaultShorcuts() -{ - defaultShortCuts["MainWindow/aCheckCardUpdates"] = parseSequenceString(""); - defaultShortCuts["MainWindow/aConnect"] = parseSequenceString("Ctrl+L"); - defaultShortCuts["MainWindow/aDeckEditor"] = parseSequenceString(""); - defaultShortCuts["MainWindow/aDisconnect"] = parseSequenceString(""); - defaultShortCuts["MainWindow/aExit"] = parseSequenceString(""); - defaultShortCuts["MainWindow/aFullScreen"] = parseSequenceString("Ctrl+F"); - defaultShortCuts["MainWindow/aRegister"] = parseSequenceString(""); - defaultShortCuts["MainWindow/aSettings"] = parseSequenceString(""); - defaultShortCuts["MainWindow/aSinglePlayer"] = parseSequenceString(""); - defaultShortCuts["MainWindow/aWatchReplay"] = parseSequenceString(""); - - defaultShortCuts["TabDeckEditor/aAnalyzeDeck"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aClearFilterAll"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aClearFilterOne"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aClose"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aDecrement"] = parseSequenceString("-"); - defaultShortCuts["TabDeckEditor/aManageSets"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aEditTokens"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aExportDeckDecklist"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aIncrement"] = parseSequenceString("+"); - defaultShortCuts["TabDeckEditor/aLoadDeck"] = parseSequenceString("Ctrl+O"); - defaultShortCuts["TabDeckEditor/aLoadDeckFromClipboard"] = parseSequenceString("Ctrl+Shift+V"); - defaultShortCuts["TabDeckEditor/aNewDeck"] = parseSequenceString("Ctrl+N"); - defaultShortCuts["TabDeckEditor/aOpenCustomFolder"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aPrintDeck"] = parseSequenceString("Ctrl+P"); - defaultShortCuts["TabDeckEditor/aRemoveCard"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aResetLayout"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aSaveDeck"] = parseSequenceString("Ctrl+S"); - defaultShortCuts["TabDeckEditor/aSaveDeckAs"] = parseSequenceString(""); - defaultShortCuts["TabDeckEditor/aSaveDeckToClipboard"] = parseSequenceString("Ctrl+Shift+C"); - defaultShortCuts["TabDeckEditor/aSaveDeckToClipboardRaw"] = parseSequenceString("Ctrl+Shift+R"); - - defaultShortCuts["DeckViewContainer/loadLocalButton"] = parseSequenceString("Ctrl+O"); - defaultShortCuts["DeckViewContainer/loadRemoteButton"] = parseSequenceString("Ctrl+Alt+O"); - - defaultShortCuts["Player/aDec"] = parseSequenceString("F11"); - defaultShortCuts["Player/aInc"] = parseSequenceString("F12"); - defaultShortCuts["Player/aSet"] = parseSequenceString("Ctrl+L"); - defaultShortCuts["Player/aCloseMostRecentZoneView"] = parseSequenceString("Esc"); - defaultShortCuts["Player/IncP"] = parseSequenceString("Ctrl++"); - defaultShortCuts["Player/aAlwaysRevealTopCard"] = parseSequenceString("Ctrl+N"); - defaultShortCuts["Player/aAttach"] = parseSequenceString("Ctrl+Alt+A"); - defaultShortCuts["Player/aCCGreen"] = parseSequenceString(""); - defaultShortCuts["Player/aCCRed"] = parseSequenceString(""); - defaultShortCuts["Player/aCCYellow"] = parseSequenceString(""); - defaultShortCuts["Player/aClone"] = parseSequenceString("Ctrl+J"); - defaultShortCuts["Player/aCreateAnotherToken"] = parseSequenceString("Ctrl+G"); - defaultShortCuts["Player/aCreateToken"] = parseSequenceString("Ctrl+T"); - defaultShortCuts["Player/aCreateRelatedTokens"] = parseSequenceString("Ctrl+Shift+T"); - defaultShortCuts["Player/aDecP"] = parseSequenceString("Ctrl+-"); - defaultShortCuts["Player/aDecPT"] = parseSequenceString("Ctrl+Alt+-"); - defaultShortCuts["Player/aDecT"] = parseSequenceString("Alt+-"); - defaultShortCuts["Player/aDoesntUntap"] = parseSequenceString(""); - defaultShortCuts["Player/aDrawArrow"] = parseSequenceString(""); - defaultShortCuts["Player/aDrawCard"] = parseSequenceString("Ctrl+D"); - defaultShortCuts["Player/aDrawCards"] = parseSequenceString("Ctrl+E"); - defaultShortCuts["Player/aFlip"] = parseSequenceString(""); - defaultShortCuts["Player/aIncPT"] = parseSequenceString("Ctrl+Alt++"); - defaultShortCuts["Player/aIncT"] = parseSequenceString("Alt++"); - defaultShortCuts["Player/aMoveToBottomLibrary"] = parseSequenceString(""); - defaultShortCuts["Player/aMoveToExile"] = parseSequenceString(""); - defaultShortCuts["Player/aMoveToGraveyard"] = parseSequenceString("Ctrl+Del"); - defaultShortCuts["Player/aMoveToHand"] = parseSequenceString(""); - defaultShortCuts["Player/aMoveToTopLibrary"] = parseSequenceString(""); - defaultShortCuts["Player/aMulligan"] = parseSequenceString("Ctrl+M"); - defaultShortCuts["Player/aPeek"] = parseSequenceString(""); - defaultShortCuts["Player/aPlay"] = parseSequenceString(""); - defaultShortCuts["Player/aRCGreen"] = parseSequenceString(""); - defaultShortCuts["Player/aRCRed"] = parseSequenceString(""); - defaultShortCuts["Player/aRCYellow"] = parseSequenceString(""); - defaultShortCuts["Player/aRollDie"] = parseSequenceString("Ctrl+I"); - defaultShortCuts["Player/aSCGreen"] = parseSequenceString(""); - defaultShortCuts["Player/aSCRed"] = parseSequenceString(""); - defaultShortCuts["Player/aSCYellow"] = parseSequenceString(""); - defaultShortCuts["Player/aSetAnnotation"] = parseSequenceString(""); - defaultShortCuts["Player/aSetPT"] = parseSequenceString("Ctrl+P"); - defaultShortCuts["Player/aResetPT"] = parseSequenceString("Ctrl+Alt+0"); - defaultShortCuts["Player/aShuffle"] = parseSequenceString("Ctrl+S"); - defaultShortCuts["Player/aTap"] = parseSequenceString(""); - defaultShortCuts["Player/aUnattach"] = parseSequenceString(""); - defaultShortCuts["Player/aUndoDraw"] = parseSequenceString("Ctrl+Shift+D"); - defaultShortCuts["Player/aUntapAll"] = parseSequenceString("Ctrl+U"); - defaultShortCuts["Player/aViewGraveyard"] = parseSequenceString("F4"); - defaultShortCuts["Player/aViewLibrary"] = parseSequenceString("F3"); - defaultShortCuts["Player/aViewRfg"] = parseSequenceString(""); - defaultShortCuts["Player/aViewSideboard"] = parseSequenceString("Ctrl+F3"); - defaultShortCuts["Player/aViewTopCards"] = parseSequenceString("Ctrl+W"); - defaultShortCuts["Player/aConcede"] = parseSequenceString("F2"); - defaultShortCuts["Player/aLeaveGame"] = parseSequenceString("Ctrl+Q"); - defaultShortCuts["Player/aNextPhase"] = parseSequenceString("Ctrl+Space;Tab"); - defaultShortCuts["Player/aNextTurn"] = parseSequenceString("Ctrl+Return;Ctrl+Enter"); - defaultShortCuts["Player/aRemoveLocalArrows"] = parseSequenceString("Ctrl+R"); - defaultShortCuts["Player/aRotateViewCCW"] = parseSequenceString(""); - defaultShortCuts["Player/aRotateViewCW"] = parseSequenceString(""); - defaultShortCuts["Player/phase0"] = parseSequenceString("F5"); - defaultShortCuts["Player/phase1"] = parseSequenceString(""); - defaultShortCuts["Player/phase10"] = parseSequenceString("F10"); - defaultShortCuts["Player/phase2"] = parseSequenceString("F6"); - defaultShortCuts["Player/phase3"] = parseSequenceString("F7"); - defaultShortCuts["Player/phase4"] = parseSequenceString("F8"); - defaultShortCuts["Player/phase5"] = parseSequenceString(""); - defaultShortCuts["Player/phase6"] = parseSequenceString(""); - defaultShortCuts["Player/phase7"] = parseSequenceString(""); - defaultShortCuts["Player/phase8"] = parseSequenceString(""); - defaultShortCuts["Player/phase9"] = parseSequenceString("F9"); - - defaultShortCuts["Player/aIncCounter_w"] = parseSequenceString(""); - defaultShortCuts["Player/aDecCounter_w"] = parseSequenceString(""); - defaultShortCuts["Player/aSetCounter_w"] = parseSequenceString(""); - - defaultShortCuts["Player/aIncCounter_u"] = parseSequenceString(""); - defaultShortCuts["Player/aDecCounter_u"] = parseSequenceString(""); - defaultShortCuts["Player/aSetCounter_u"] = parseSequenceString(""); - - defaultShortCuts["Player/aIncCounter_b"] = parseSequenceString(""); - defaultShortCuts["Player/aDecCounter_b"] = parseSequenceString(""); - defaultShortCuts["Player/aSetCounter_b"] = parseSequenceString(""); - - defaultShortCuts["Player/aIncCounter_r"] = parseSequenceString(""); - defaultShortCuts["Player/aDecCounter_r"] = parseSequenceString(""); - defaultShortCuts["Player/aSetCounter_r"] = parseSequenceString(""); - - defaultShortCuts["Player/aIncCounter_g"] = parseSequenceString(""); - defaultShortCuts["Player/aDecCounter_g"] = parseSequenceString(""); - defaultShortCuts["Player/aSetCounter_g"] = parseSequenceString(""); - - defaultShortCuts["Player/aIncCounter_x"] = parseSequenceString(""); - defaultShortCuts["Player/aDecCounter_x"] = parseSequenceString(""); - defaultShortCuts["Player/aSetCounter_x"] = parseSequenceString(""); - - defaultShortCuts["Player/aIncCounter_storm"] = parseSequenceString(""); - defaultShortCuts["Player/aDecCounter_storm"] = parseSequenceString(""); - defaultShortCuts["Player/aSetCounter_storm"] = parseSequenceString(""); - - defaultShortCuts["tab_room/aClearChat"] = parseSequenceString("F12"); - defaultShortCuts["DlgLoadDeckFromClipboard/refreshButton"] = parseSequenceString("F5"); - defaultShortCuts["Player/aResetLayout"] = parseSequenceString(""); - defaultShortCuts["Player/aMoveTopToPlayFaceDown"] = parseSequenceString("Ctrl+Shift+E"); -} diff --git a/cockatrice/src/shortcutssettings.h b/cockatrice/src/shortcutssettings.h index 6d799448c..902db3d74 100644 --- a/cockatrice/src/shortcutssettings.h +++ b/cockatrice/src/shortcutssettings.h @@ -1,8 +1,8 @@ #ifndef SHORTCUTSSETTINGS_H #define SHORTCUTSSETTINGS_H +#include #include -#include #include #include @@ -10,37 +10,185 @@ class ShortcutsSettings : public QObject { Q_OBJECT public: - ShortcutsSettings(QString settingsFilePath, QObject *parent = nullptr); + ShortcutsSettings(const QString &settingsFilePath, QObject *parent = nullptr); - QList getShortcut(QString name); - QKeySequence getSingleShortcut(QString name); + QList getDefaultShortcut(const QString &name) const; + QList getShortcut(const QString &name) const; + QKeySequence getSingleShortcut(const QString &name) const; + QString getDefaultShortcutString(const QString &name) const; + QString getShortcutString(const QString &name) const; - QString getDefaultShortcutString(QString name); - QString getShortcutString(QString name); + void setShortcuts(const QString &name, const QList &Sequence); + void setShortcuts(const QString &name, const QKeySequence &Sequence); + void setShortcuts(const QString &name, const QString &Sequences); - void setShortcuts(QString name, QList Sequence); - void setShortcuts(QString name, QKeySequence Sequence); - void setShortcuts(QString name, QString Sequences); - - bool isKeyAllowed(QString name, QString Sequences); - bool isValid(QString name, QString Sequences); + bool isKeyAllowed(const QString &name, const QString &Sequences) const; + bool isValid(const QString &name, const QString &Sequences) const; void resetAllShortcuts(); void clearAllShortcuts(); signals: - void shortCutchanged(); + void shortCutChanged(); void allShortCutsReset(); void allShortCutsClear(); private: + const QChar sep = ';'; + const QString custom = "Custom"; QString settingsFilePath; - QMap> shortCuts; - QMap> defaultShortCuts; - void fillDefaultShorcuts(); + QHash> shortCuts; - QString stringifySequence(QList Sequence) const; - QList parseSequenceString(QString stringSequence); + QString stringifySequence(const QList &Sequence) const; + QList parseSequenceString(const QString &stringSequence) const; + + const QHash> defaultShortCuts{ + {"MainWindow/aCheckCardUpdates", parseSequenceString("")}, + {"MainWindow/aConnect", parseSequenceString("Ctrl+L")}, + {"MainWindow/aDeckEditor", parseSequenceString("")}, + {"MainWindow/aDisconnect", parseSequenceString("")}, + {"MainWindow/aExit", parseSequenceString("")}, + {"MainWindow/aFullScreen", parseSequenceString("Ctrl+F")}, + {"MainWindow/aRegister", parseSequenceString("")}, + {"MainWindow/aSettings", parseSequenceString("")}, + {"MainWindow/aSinglePlayer", parseSequenceString("")}, + {"MainWindow/aWatchReplay", parseSequenceString("")}, + + {"TabDeckEditor/aAnalyzeDeck", parseSequenceString("")}, + {"TabDeckEditor/aClearFilterAll", parseSequenceString("")}, + {"TabDeckEditor/aClearFilterOne", parseSequenceString("")}, + {"TabDeckEditor/aClose", parseSequenceString("")}, + {"TabDeckEditor/aDecrement", parseSequenceString("-")}, + {"TabDeckEditor/aManageSets", parseSequenceString("")}, + {"TabDeckEditor/aEditTokens", parseSequenceString("")}, + {"TabDeckEditor/aExportDeckDecklist", parseSequenceString("")}, + {"TabDeckEditor/aIncrement", parseSequenceString("+")}, + {"TabDeckEditor/aLoadDeck", parseSequenceString("Ctrl+O")}, + {"TabDeckEditor/aLoadDeckFromClipboard", parseSequenceString("Ctrl+Shift+V")}, + {"TabDeckEditor/aNewDeck", parseSequenceString("Ctrl+N")}, + {"TabDeckEditor/aOpenCustomFolder", parseSequenceString("")}, + {"TabDeckEditor/aPrintDeck", parseSequenceString("Ctrl+P")}, + {"TabDeckEditor/aRemoveCard", parseSequenceString("")}, + {"TabDeckEditor/aResetLayout", parseSequenceString("")}, + {"TabDeckEditor/aSaveDeck", parseSequenceString("Ctrl+S")}, + {"TabDeckEditor/aSaveDeckAs", parseSequenceString("")}, + {"TabDeckEditor/aSaveDeckToClipboard", parseSequenceString("Ctrl+Shift+C")}, + {"TabDeckEditor/aSaveDeckToClipboardRaw", parseSequenceString("Ctrl+Shift+R")}, + + {"DeckViewContainer/loadLocalButton", parseSequenceString("Ctrl+O")}, + {"DeckViewContainer/loadRemoteButton", parseSequenceString("Ctrl+Alt+O")}, + + {"Player/aSet", parseSequenceString("Ctrl+L")}, + {"Player/aAlwaysRevealTopCard", parseSequenceString("Ctrl+N")}, + {"Player/aCloseMostRecentZoneView", parseSequenceString("Esc")}, + {"Player/aDrawCard", parseSequenceString("Ctrl+D")}, + {"Player/aDrawCards", parseSequenceString("Ctrl+E")}, + {"Player/aDec", parseSequenceString("F11")}, + {"Player/aInc", parseSequenceString("F12")}, + {"Player/aMoveTopCardToGraveyard", parseSequenceString("")}, + {"Player/aMoveTopCardsToGraveyard", parseSequenceString("Ctrl+Shift+M")}, + {"Player/aMoveTopCardToExile", parseSequenceString("")}, + {"Player/aMoveTopCardsToExile", parseSequenceString("")}, + {"Player/aMoveTopToPlayFaceDown", parseSequenceString("Ctrl+Shift+E")}, + {"Player/aMulligan", parseSequenceString("Ctrl+M")}, + {"Player/aPeek", parseSequenceString("")}, + {"Player/aPlay", parseSequenceString("")}, + {"Player/aResetLayout", parseSequenceString("")}, + {"Player/aRollDie", parseSequenceString("Ctrl+I")}, + {"Player/aShuffle", parseSequenceString("Ctrl+S")}, + {"Player/aUndoDraw", parseSequenceString("Ctrl+Shift+D")}, + {"Player/aUntapAll", parseSequenceString("Ctrl+U")}, + {"Player/aViewGraveyard", parseSequenceString("F4")}, + {"Player/aViewLibrary", parseSequenceString("F3")}, + {"Player/aViewRfg", parseSequenceString("")}, + {"Player/aViewSideboard", parseSequenceString("Ctrl+F3")}, + {"Player/aViewTopCards", parseSequenceString("Ctrl+W")}, + + {"Player/aAttach", parseSequenceString("Ctrl+Alt+A")}, + {"Player/aClone", parseSequenceString("Ctrl+J")}, + {"Player/aCreateAnotherToken", parseSequenceString("Ctrl+G")}, + {"Player/aCreateToken", parseSequenceString("Ctrl+T")}, + {"Player/aCreateRelatedTokens", parseSequenceString("Ctrl+Shift+T")}, + {"Player/aDoesntUntap", parseSequenceString("")}, + {"Player/aDrawArrow", parseSequenceString("")}, + {"Player/aFlip", parseSequenceString("")}, + {"Player/aMoveToBottomLibrary", parseSequenceString("")}, + {"Player/aMoveToExile", parseSequenceString("")}, + {"Player/aMoveToGraveyard", parseSequenceString("Ctrl+Del")}, + {"Player/aMoveToHand", parseSequenceString("")}, + {"Player/aMoveToTopLibrary", parseSequenceString("")}, + {"Player/aSetAnnotation", parseSequenceString("")}, + {"Player/aTap", parseSequenceString("")}, + {"Player/aUnattach", parseSequenceString("")}, + + {"Player/aCCGreen", parseSequenceString("")}, + {"Player/aCCRed", parseSequenceString("")}, + {"Player/aCCYellow", parseSequenceString("")}, + {"Player/aRCGreen", parseSequenceString("")}, + {"Player/aRCRed", parseSequenceString("")}, + {"Player/aRCYellow", parseSequenceString("")}, + {"Player/aSCGreen", parseSequenceString("")}, + {"Player/aSCRed", parseSequenceString("")}, + {"Player/aSCYellow", parseSequenceString("")}, + + {"Player/aDecP", parseSequenceString("Ctrl+-")}, + {"Player/aDecPT", parseSequenceString("Ctrl+Alt+-")}, + {"Player/aDecT", parseSequenceString("Alt+-")}, + {"Player/aIncP", parseSequenceString("Ctrl++")}, + {"Player/aIncPT", parseSequenceString("Ctrl+Alt++")}, + {"Player/aIncT", parseSequenceString("Alt++")}, + {"Player/aSetPT", parseSequenceString("Ctrl+P")}, + {"Player/aResetPT", parseSequenceString("Ctrl+Alt+0")}, + + {"Player/aConcede", parseSequenceString("F2")}, + {"Player/aLeaveGame", parseSequenceString("Ctrl+Q")}, + {"Player/aNextPhase", parseSequenceString("Ctrl+Space;Tab")}, + {"Player/aNextTurn", parseSequenceString("Ctrl+Return;Ctrl+Enter")}, + {"Player/aRemoveLocalArrows", parseSequenceString("Ctrl+R")}, + {"Player/aRotateViewCCW", parseSequenceString("")}, + {"Player/aRotateViewCW", parseSequenceString("")}, + {"Player/phase0", parseSequenceString("F5")}, + {"Player/phase1", parseSequenceString("")}, + {"Player/phase10", parseSequenceString("F10")}, + {"Player/phase2", parseSequenceString("F6")}, + {"Player/phase3", parseSequenceString("F7")}, + {"Player/phase4", parseSequenceString("F8")}, + {"Player/phase5", parseSequenceString("")}, + {"Player/phase6", parseSequenceString("")}, + {"Player/phase7", parseSequenceString("")}, + {"Player/phase8", parseSequenceString("")}, + {"Player/phase9", parseSequenceString("F9")}, + + {"Player/aDecCounter_w", parseSequenceString("")}, + {"Player/aIncCounter_w", parseSequenceString("")}, + {"Player/aSetCounter_w", parseSequenceString("")}, + + {"Player/aDecCounter_u", parseSequenceString("")}, + {"Player/aIncCounter_u", parseSequenceString("")}, + {"Player/aSetCounter_u", parseSequenceString("")}, + + {"Player/aDecCounter_b", parseSequenceString("")}, + {"Player/aIncCounter_b", parseSequenceString("")}, + {"Player/aSetCounter_b", parseSequenceString("")}, + + {"Player/aDecCounter_r", parseSequenceString("")}, + {"Player/aIncCounter_r", parseSequenceString("")}, + {"Player/aSetCounter_r", parseSequenceString("")}, + + {"Player/aDecCounter_g", parseSequenceString("")}, + {"Player/aIncCounter_g", parseSequenceString("")}, + {"Player/aSetCounter_g", parseSequenceString("")}, + + {"Player/aDecCounter_x", parseSequenceString("")}, + {"Player/aIncCounter_x", parseSequenceString("")}, + {"Player/aSetCounter_x", parseSequenceString("")}, + + {"Player/aDecCounter_storm", parseSequenceString("")}, + {"Player/aIncCounter_storm", parseSequenceString("")}, + {"Player/aSetCounter_storm", parseSequenceString("")}, + + {"tab_room/aClearChat", parseSequenceString("F12")}, + {"DlgLoadDeckFromClipboard/refreshButton", parseSequenceString("F5")}}; }; #endif // SHORTCUTSSETTINGS_H diff --git a/cockatrice/src/tab_deck_editor.cpp b/cockatrice/src/tab_deck_editor.cpp index d44f8bab6..79556bdab 100644 --- a/cockatrice/src/tab_deck_editor.cpp +++ b/cockatrice/src/tab_deck_editor.cpp @@ -590,7 +590,7 @@ TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent) this->installEventFilter(this); retranslateUi(); - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); QTimer::singleShot(0, this, SLOT(loadLayout())); diff --git a/cockatrice/src/tab_game.cpp b/cockatrice/src/tab_game.cpp index 33be857f3..9fdc79b17 100644 --- a/cockatrice/src/tab_game.cpp +++ b/cockatrice/src/tab_game.cpp @@ -130,7 +130,7 @@ DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent) setLayout(deckViewLayout); retranslateUi(); - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); } @@ -375,7 +375,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay) createReplayMenuItems(); createViewMenuItems(); retranslateUi(); - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); messageLog->logReplayStarted(gameInfo.game_id()); @@ -414,7 +414,7 @@ TabGame::TabGame(TabSupervisor *_tabSupervisor, createMenuItems(); createViewMenuItems(); retranslateUi(); - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); // append game to rooms game list for others to see diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index 603c44723..bdca76c9f 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -811,7 +811,7 @@ MainWindow::MainWindow(QWidget *parent) createTrayIcon(); } - connect(&settingsCache->shortcuts(), SIGNAL(shortCutchanged()), this, SLOT(refreshShortcuts())); + connect(&settingsCache->shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts())); refreshShortcuts(); connect(db, SIGNAL(cardDatabaseLoadingFailed()), this, SLOT(cardDatabaseLoadingFailed())); From f1563c56042561d15b23b261b53e4469eca66cfb Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sun, 25 Nov 2018 11:25:47 +0100 Subject: [PATCH 21/62] add fedora docker image to travis compilation (#3447) * add fedora docker image to travis compilation fixes #1746 * add dockerfile * would you like to please install for me? [y/n] * remove bc dependency * save cache separately for different docker builds * add development packages to fedora dockerfile * add package names * use env values to make these all look the same * set docker image name correctly * add missing dependency * minor oversight, add %% to remove all braces in check schema --- .ci/Fedora29/Dockerfile | 17 +++++++ .ci/UbuntuBionic/Dockerfile | 1 - .travis.yml | 79 ++++++++++++++++++++++-------- servatrice/check_schema_version.sh | 8 +-- 4 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 .ci/Fedora29/Dockerfile diff --git a/.ci/Fedora29/Dockerfile b/.ci/Fedora29/Dockerfile new file mode 100644 index 000000000..eafdfe5ff --- /dev/null +++ b/.ci/Fedora29/Dockerfile @@ -0,0 +1,17 @@ +FROM fedora:29 + +RUN dnf install -y \ + @development-tools \ + ccache \ + cmake \ + desktop-file-utils \ + file \ + gcc-c++ \ + hicolor-icon-theme \ + libappstream-glib \ + protobuf-devel \ + qt5-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + sqlite-devel \ + wget \ + zlib-devel \ + && dnf clean all diff --git a/.ci/UbuntuBionic/Dockerfile b/.ci/UbuntuBionic/Dockerfile index 65ef1f6d1..9c41fb638 100644 --- a/.ci/UbuntuBionic/Dockerfile +++ b/.ci/UbuntuBionic/Dockerfile @@ -1,7 +1,6 @@ FROM ubuntu:bionic RUN apt-get update && apt-get install -y --no-install-recommends \ - bc \ build-essential \ clang-format \ file \ diff --git a/.travis.yml b/.travis.yml index 21a7a55d3..ef741da85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,33 +1,15 @@ language: cpp compiler: gcc -cache: ccache matrix: include: - #Ubuntu Bionic (on docker) - - name: Ubuntu Bionic (Debug) - if: tag IS NOT present - services: docker - before_install: docker build -t img .ci/UbuntuBionic - script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" - --mount "type=bind,source=$HOME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" - img - bash .ci/travis-compile.sh --server --debug - - name: Ubuntu Bionic (Release) - if: (branch = master AND NOT type = pull_request) OR tag IS present - services: docker - before_install: docker build -t img .ci/UbuntuBionic - script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" - --mount "type=bind,source=$HOME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" - img - bash .ci/travis-compile.sh --server --package "Ubuntu18.04" --release - #Ubuntu Xenial (Debug only) - name: Ubuntu Xenial (Debug) if: tag IS NOT present os: linux dist: xenial group: stable + cache: ccache addons: apt: packages: @@ -43,27 +25,84 @@ matrix: - libqt5websockets5-dev script: bash ./.ci/travis-compile.sh --format --server --test --debug + #Ubuntu Bionic (on docker) + - name: Ubuntu Bionic (Debug) + if: tag IS NOT present + services: docker + env: NAME=UbuntuBionic + cache: + directories: + - $HOME/$NAME/ + before_install: docker build -t "cockatrice_${NAME,,}" .ci/$NAME && mkdir -p $HOME/$NAME/.ccache + script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" + --mount "type=bind,source=$HOME/$NAME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" + "cockatrice_${NAME,,}" + bash .ci/travis-compile.sh --server --debug + + - name: Ubuntu Bionic (Release) + if: (branch = master AND NOT type = pull_request) OR tag IS present + services: docker + env: NAME=UbuntuBionic + cache: + directories: + - $HOME/$NAME/ + before_install: docker build -t "cockatrice_${NAME,,}" .ci/$NAME && mkdir -p $HOME/$NAME/.ccache + script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" + --mount "type=bind,source=$HOME/$NAME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" + "cockatrice_${NAME,,}" + bash .ci/travis-compile.sh --server --package "$NAME" --release + + #Fedora 29 (on docker) + - name: Fedora 29 (Debug) + if: tag IS NOT present + services: docker + env: NAME=Fedora29 + cache: + directories: + - $HOME/$NAME/ + before_install: docker build -t "cockatrice_${NAME,,}" .ci/$NAME && mkdir -p $HOME/$NAME/.ccache + script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" + --mount "type=bind,source=$HOME/$NAME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" + "cockatrice_${NAME,,}" + bash .ci/travis-compile.sh --server --debug + + - name: Fedora 29 (Release) + if: (branch = master AND NOT type = pull_request) OR tag IS present + services: docker + env: NAME=Fedora29 + cache: + directories: + - $HOME/$NAME/ + before_install: docker build -t "cockatrice_${NAME,,}" .ci/$NAME && mkdir -p $HOME/$NAME/.ccache + script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" + --mount "type=bind,source=$HOME/$NAME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" + "cockatrice_${NAME,,}" + bash .ci/travis-compile.sh --server --package "$NAME" --release + #macOS - name: macOS (Debug) if: tag IS NOT present os: osx osx_image: xcode8 + cache: ccache before_install: - brew update - brew install ccache - brew install protobuf --without-python@2 - brew install qt script: bash ./.ci/travis-compile.sh --server --install --debug + - name: macOS (Release) if: (branch = master AND NOT type = pull_request) OR tag IS present os: osx osx_image: xcode8 + cache: ccache before_install: - brew update - brew install ccache - brew install protobuf --without-python@2 - brew install qt - script: bash ./.ci/travis-compile.sh --server --package "" --release + script: bash ./.ci/travis-compile.sh --server --package "$TRAVIS_OS_NAME" --release # Builds for pull requests skip the deployment step altogether diff --git a/servatrice/check_schema_version.sh b/servatrice/check_schema_version.sh index d59edc7a2..ed8772349 100755 --- a/servatrice/check_schema_version.sh +++ b/servatrice/check_schema_version.sh @@ -2,13 +2,15 @@ set -e -schema_ver="$(grep 'INSERT INTO cockatrice_schema_version' servatrice/servatrice.sql | sed 's/.*VALUES(//' | sed 's/).*//')" +version_line="$(grep 'INSERT INTO cockatrice_schema_version' servatrice/servatrice.sql)" +version_line="${version_line#*VALUES(}" +declare -i schema_ver="${version_line%%)*}" latest_migration="$(ls -1 servatrice/migrations/ | tail -n1)" xtoysql="${latest_migration#servatrice_}" xtoy="${xtoysql%.sql}" -old_ver="$(echo ${xtoy%%_to_*} | bc)" -new_ver="$(echo ${xtoy##*_to_} | bc)" +declare -i old_ver="10#${xtoy%_to_*}" #declare as integer with base 10, numbers with a leading 0 are normally interpreted as base 16 +declare -i new_ver="10#${xtoy#*_to_}" if ((old_ver >= new_ver)); then echo "New version $new_ver is not newer than $old_ver" From ed2a3f92e0cffab2bf06758af499e8e0d0b604ec Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Mon, 26 Nov 2018 01:29:24 +0100 Subject: [PATCH 22/62] fix travis format checking again (#3451) Set the exit code properly, "$?" does not work after "if !". Move "set -e" to after format checks. --- .ci/travis-compile.sh | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index 8a5293453..a3553f5f7 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -2,8 +2,6 @@ # This script is to be used in .travis.yaml from the project root directory, do not use it from somewhere else. -set -e - # Read arguments while [[ "$@" ]]; do case "$1" in @@ -53,8 +51,11 @@ done # Check formatting using clang-format if [[ $CHECK_FORMAT ]]; then echo "Checking your code using clang-format..." - if ! diff="$(./clangify.sh --diff --cf-version)"; then - [[ $? == 1 ]] && cat <&2 + ;; + esac fi +set -e + # Setup ./servatrice/check_schema_version.sh mkdir -p build From 952f13dec4ffd2a7c65b8433e372febcaea08c12 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 28 Nov 2018 21:51:36 +0100 Subject: [PATCH 23/62] update macos (#3461) Set travis image to xcode9.2: this image uses sierra instead of el_capitan, el_capitan no longer builds. Disable homebrew autoupdates, this gives a major speedup. --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ef741da85..304e76af3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -83,24 +83,24 @@ matrix: - name: macOS (Debug) if: tag IS NOT present os: osx - osx_image: xcode8 + osx_image: xcode9.2 + env: HOMEBREW_NO_AUTO_UPDATE=1 cache: ccache before_install: - - brew update - brew install ccache - - brew install protobuf --without-python@2 + - brew install protobuf - brew install qt script: bash ./.ci/travis-compile.sh --server --install --debug - name: macOS (Release) if: (branch = master AND NOT type = pull_request) OR tag IS present os: osx - osx_image: xcode8 + osx_image: xcode9.2 + env: HOMEBREW_NO_AUTO_UPDATE=1 cache: ccache before_install: - - brew update - brew install ccache - - brew install protobuf --without-python@2 + - brew install protobuf - brew install qt script: bash ./.ci/travis-compile.sh --server --package "$TRAVIS_OS_NAME" --release From e921cf88f978cedb471e4df2d51697009a321eb9 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Thu, 6 Dec 2018 01:34:56 +0100 Subject: [PATCH 24/62] place the os nametag after the version (#3465) When i added the appended os nametags I didn't account for the dots inside the version, this will place the nametag before the last dot instead of after the first dot. --- .ci/travis-compile.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index a3553f5f7..443e54bd3 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -137,6 +137,6 @@ if [[ $MAKE_PACKAGE ]]; then echo "could not find package" >&2 exit 1 fi - mv "$path/$file" "$path/${file%%.*}-$PACKAGE_NAME.${file#*.}" + mv "$path/$file" "$path/${file%.*}-$PACKAGE_NAME.${file##*.}" fi fi From 3a05bcfb29db1c920930abfbca43f65d35c3e47d Mon Sep 17 00:00:00 2001 From: tooomm Date: Fri, 7 Dec 2018 17:48:06 +0100 Subject: [PATCH 25/62] fix deployment on travis (#3468) ## Short roundup of the initial problem Deployment conditions weren't met, because the $BUILDTYPE variable doesn't exist anymore. @ebbit1q changed that, and there is a flag now that you pass to the compile script instead. ## What will change with this Pull Request? - Remove that condition It's not needed because we make sure via the build config itself that on tags no debug builds are run. This was a double check basically to prevent debug builds beeing deployed. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 304e76af3..966676cd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -122,7 +122,7 @@ deploy: on: tags: true repo: Cockatrice/Cockatrice - condition: $BUILDTYPE = Release && $TRAVIS_TAG =~ ([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}-beta(\.([2-9]|[1-9][0-9]))?$ # regex to match semver naming convention for beta pre-releases + condition: $TRAVIS_TAG =~ ([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}-beta(\.([2-9]|[1-9][0-9]))?$ # regex to match semver naming convention for beta pre-releases # Deploy configuration for "stable" releases - provider: releases @@ -137,7 +137,7 @@ deploy: on: tags: true repo: Cockatrice/Cockatrice - condition: $BUILDTYPE = Release && $TRAVIS_TAG =~ ([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}$ # regex to match semver naming convention for stable full releases + condition: $TRAVIS_TAG =~ ([0-9]|[1-9][0-9])(\.([0-9]|[1-9][0-9])){2}$ # regex to match semver naming convention for stable full releases notifications: From cee69705d800e3cb3677766a2c1dfdbae55884e1 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 12 Dec 2018 13:26:13 +0100 Subject: [PATCH 26/62] fix rpm builds in travis (#3472) * add rpm build option to travis compile * better argument parsing --- .ci/Fedora29/Dockerfile | 1 + .ci/travis-compile.sh | 11 +++++++++-- .travis.yml | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.ci/Fedora29/Dockerfile b/.ci/Fedora29/Dockerfile index eafdfe5ff..77ee4e9f3 100644 --- a/.ci/Fedora29/Dockerfile +++ b/.ci/Fedora29/Dockerfile @@ -11,6 +11,7 @@ RUN dnf install -y \ libappstream-glib \ protobuf-devel \ qt5-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + rpm-build \ sqlite-devel \ wget \ zlib-devel \ diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index 443e54bd3..a82f537aa 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -16,9 +16,13 @@ while [[ "$@" ]]; do '--package') MAKE_PACKAGE=1 shift - if [[ $1 != -* ]]; then + if [[ $# != 0 && $1 != -* ]]; then PACKAGE_NAME="$1" shift + if [[ $# != 0 && $1 != -* ]]; then + PACKAGE_TYPE="$1" + shift + fi fi ;; '--server') @@ -107,6 +111,9 @@ fi if [[ $BUILDTYPE ]]; then flags+=" -DCMAKE_BUILD_TYPE=$BUILDTYPE" fi +if [[ $PACKAGE_TYPE ]]; then + flags+=" -DCPACK_GENERATOR=$PACKAGE_TYPE" +fi # Add qt install location when using brew if [[ $(uname) == "Darwin" ]]; then @@ -133,7 +140,7 @@ if [[ $MAKE_PACKAGE ]]; then found=$(find . -maxdepth 1 -type f -name "Cockatrice-*.*" -print -quit) path=${found%/*} file=${found##*/} - if [[ $file != *.* ]]; then + if [[ ! $file ]]; then echo "could not find package" >&2 exit 1 fi diff --git a/.travis.yml b/.travis.yml index 966676cd7..239e98ea0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -77,7 +77,7 @@ matrix: script: docker run --mount "type=bind,source=$(pwd),target=/src" -w="/src" --mount "type=bind,source=$HOME/$NAME/.ccache,target=/.ccache" -e "CCACHE_DIR=/.ccache" "cockatrice_${NAME,,}" - bash .ci/travis-compile.sh --server --package "$NAME" --release + bash .ci/travis-compile.sh --server --package "$NAME" "RPM" --release #macOS - name: macOS (Debug) From 65f41e520e6ab1f3e6e31442aebe6e1cf8b48511 Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Fri, 21 Dec 2018 00:05:03 +0100 Subject: [PATCH 27/62] [WIP] Basic mtgjsonv4 support (#3458) * Basic mtgjsonv4 support * Fix set type * [WIP] Oracle: use zx instead of zip * clanfigy fixes * Fix reading last block of xz * Added back zip support * [WIP] adding xz on ci + fixes * typo * resolve conflict * Make gcc an happy puppy * test appveyor build * appveyor maybe * Appveyor: add xz bindir * Update ssl version (the old one is not available anymore) * Windows is a really shitty platform to code on. * test vcpkg * again * gosh * nowarn * warning 2 * static * Maybe * cmake fix * fsck this pain * FindWin32SslRuntime: add vcpkg path * Appveyor: cache support, force usable of openssl from vcpkg * updated as suggested * ouch * Import card uuids and expose this property as !uuid! for card image download * Minor style fixes * address changed URL --- .appveyor.yml | 60 +---- .ci/Fedora29/Dockerfile | 1 + .ci/UbuntuBionic/Dockerfile | 1 + .travis.yml | 9 +- CMakeLists.txt | 3 +- README.md | 3 +- clangify.sh | 3 +- cmake/FindWin32SslRuntime.cmake | 4 +- cockatrice/CMakeLists.txt | 2 + cockatrice/src/carddatabase.cpp | 9 +- cockatrice/src/carddatabase.h | 15 +- .../src/carddbparser/cockatricexml3.cpp | 9 +- cockatrice/src/decklistmodel.cpp | 2 +- cockatrice/src/pictureloader.cpp | 2 + doc/cards.xsd | 1 + oracle/CMakeLists.txt | 33 ++- oracle/src/lzma/decompress.cpp | 250 ++++++++++++++++++ oracle/src/lzma/decompress.h | 19 ++ oracle/src/oracleimporter.cpp | 60 ++--- oracle/src/oracleimporter.h | 2 +- oracle/src/oraclewizard.cpp | 51 +++- servatrice/CMakeLists.txt | 2 + 22 files changed, 419 insertions(+), 122 deletions(-) create mode 100644 oracle/src/lzma/decompress.cpp create mode 100644 oracle/src/lzma/decompress.h diff --git a/.appveyor.yml b/.appveyor.yml index 1e143afa7..f76b3b321 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -24,66 +24,25 @@ clone_depth: 50 #same as travis, see https://www.appveyor.com/blog/2014/06/04 image: Visual Studio 2017 cache: - - c:\openssl-release - - c:\protobuf-release - - c:\zlib-release -# TODO: set dependency on ps skript file (in ./ci) / "-> appveyor.yml" maybe not ideal -# that way when we update specific cached tools there (like protobuf or zlib), cache will be newly created automatically -# https://www.appveyor.com/docs/build-cache/#cleaning-up-cache - + - c:\Tools\vcpkg\installed environment: - openssl_ver: 1.0.2p - protobuf_ver: 3.6.1 - zlib_ver: 1.2.11 - matrix: - target_arch: win64 qt_ver: 5.9\msvc2017_64 cmake_generator: Visual Studio 15 2017 Win64 cmake_toolset: v141,host=x64 - vc_arch: amd64 + vcpkg_arch: x64 - target_arch: win32 qt_ver: 5.9\msvc2015 # Qt doesn't provide a msvc2017_32 cmake_generator: Visual Studio 15 2017 cmake_toolset: v141 - vc_arch: amd64_x86 - + vcpkg_arch: x86 install: - - ps: | - if (Test-Path c:\openssl-release) { - echo "using openssl from cache" - } else { - if ($env:target_arch -eq "win64") { # 64bit filename - # echo "downloading 64bit version of openssl" - Invoke-WebRequest "https://indy.fulgan.com/SSL/openssl-$env:openssl_ver-x64_86-win64.zip" -OutFile c:\openssl-$env:openssl_ver.zip - } else { # 32bit filename - # echo "downloading 32bit version of openssl" - Invoke-WebRequest "https://indy.fulgan.com/SSL/openssl-$env:openssl_ver-i386-win32.zip" -OutFile c:\openssl-$env:openssl_ver.zip - } - Expand-Archive -Path c:\openssl-$env:openssl_ver.zip -DestinationPath c:\openssl-release - Set-Location -Path C:\openssl-release - } - if (Test-Path c:\protobuf-release) { - echo "using protobuf from cache" - } else { - Invoke-WebRequest "https://github.com/protocolbuffers/protobuf/releases/download/v$env:protobuf_ver/protobuf-cpp-$env:protobuf_ver.zip" -OutFile c:\protobuf-cpp-$env:protobuf_ver.zip - Expand-Archive -Path c:\protobuf-cpp-$env:protobuf_ver.zip -DestinationPath c:\ - Set-Location -Path C:\protobuf-$env:protobuf_ver\cmake - cmake . -G "$env:cmake_generator" -T "$env:cmake_toolset" -Dprotobuf_BUILD_TESTS=0 -Dprotobuf_MSVC_STATIC_RUNTIME=0 -DCMAKE_INSTALL_PREFIX=c:/protobuf-release - msbuild INSTALL.vcxproj /p:Configuration=Release - } - if (Test-Path c:\zlib-release) { - echo "using zlib from cache" - } else { - Invoke-WebRequest "https://github.com/madler/zlib/archive/v$env:zlib_ver.zip" -OutFile c:\zlib-$env:zlib_ver.zip - Expand-Archive -Path c:\zlib-$env:zlib_ver.zip -DestinationPath c:\ - Set-Location -Path C:\zlib-$env:zlib_ver - cmake . -G "$env:cmake_generator" -T "$env:cmake_toolset" -DCMAKE_INSTALL_PREFIX=c:/zlib-release - msbuild INSTALL.vcxproj /p:Configuration=Release - } + - vcpkg remove --outdated --recurse + - vcpkg install openssl protobuf liblzma zlib --triplet %vcpkg_arch%-windows services: - mysql @@ -92,13 +51,10 @@ build_script: - ps: | New-Item -ItemType directory -Path $env:APPVEYOR_BUILD_FOLDER\build Set-Location -Path $env:APPVEYOR_BUILD_FOLDER\build - $zlibdir = "c:\zlib-release" - $openssldir = "C:\openssl-release" - $protodir = "c:\protobuf-release" - $protoc = "c:\protobuf-release\bin\protoc.exe" + $vcpkgbindir = "C:\Tools\vcpkg\installed\$env:vcpkg_arch-windows\bin" $mysqldll = "c:\Program Files\MySQL\MySQL Server 5.7\lib\libmysql.dll" cmake --version - cmake .. -G "$env:cmake_generator" -T "$env:cmake_toolset" "-DCMAKE_PREFIX_PATH=c:/Qt/$env:qt_ver;$protodir;$zlibdir;$openssldir" "-DWITH_SERVER=1" "-DPROTOBUF_PROTOC_EXECUTABLE=$protoc" "-DMYSQLCLIENT_LIBRARIES=$mysqldll" + cmake .. -G "$env:cmake_generator" -T "$env:cmake_toolset" "-DCMAKE_TOOLCHAIN_FILE=c:/tools/vcpkg/scripts/buildsystems/vcpkg.cmake" "-DCMAKE_PREFIX_PATH=c:/Qt/$env:qt_ver;$vcpkgbindir" "-DWITH_SERVER=1" "-DMYSQLCLIENT_LIBRARIES=$mysqldll" - msbuild PACKAGE.vcxproj /p:Configuration=Release - ps: | $exe = dir -name *.exe @@ -110,7 +66,7 @@ build_script: (New-Object PSObject | Add-Member -PassThru NoteProperty bin $new_name | Add-Member -PassThru NoteProperty cmake $cmake_name | Add-Member -PassThru NoteProperty commit $env:APPVEYOR_REPO_COMMIT) | ConvertTo-JSON | Out-File -FilePath "latest-$env:target_arch" -Encoding ASCII Push-AppveyorArtifact "latest-$env:target_arch" $version = $matches['content'] - + test: off diff --git a/.ci/Fedora29/Dockerfile b/.ci/Fedora29/Dockerfile index 77ee4e9f3..7b0e9cf68 100644 --- a/.ci/Fedora29/Dockerfile +++ b/.ci/Fedora29/Dockerfile @@ -15,4 +15,5 @@ RUN dnf install -y \ sqlite-devel \ wget \ zlib-devel \ + xz-devel \ && dnf clean all diff --git a/.ci/UbuntuBionic/Dockerfile b/.ci/UbuntuBionic/Dockerfile index 9c41fb638..96e120a8d 100644 --- a/.ci/UbuntuBionic/Dockerfile +++ b/.ci/UbuntuBionic/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ git \ ccache \ cmake \ + liblzma-dev \ libprotobuf-dev \ libqt5multimedia5-plugins \ libqt5svg5-dev \ diff --git a/.travis.yml b/.travis.yml index 239e98ea0..b20c35a97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ matrix: packages: - libprotobuf-dev - protobuf-compiler + - liblzma-dev - qt5-default - qttools5-dev - qttools5-dev-tools @@ -87,9 +88,7 @@ matrix: env: HOMEBREW_NO_AUTO_UPDATE=1 cache: ccache before_install: - - brew install ccache - - brew install protobuf - - brew install qt + - brew install ccache protobuf qt xz script: bash ./.ci/travis-compile.sh --server --install --debug - name: macOS (Release) @@ -99,9 +98,7 @@ matrix: env: HOMEBREW_NO_AUTO_UPDATE=1 cache: ccache before_install: - - brew install ccache - - brew install protobuf - - brew install qt + - brew install ccache protobuf qt xz script: bash ./.ci/travis-compile.sh --server --package "$TRAVIS_OS_NAME" --release diff --git a/CMakeLists.txt b/CMakeLists.txt index d5f75540e..f55835061 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,8 @@ option(WARNING_AS_ERROR "Treat warnings as errors in debug builds" ON) IF(MSVC) # Visual Studio: # Maximum optimization - set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD") + # Disable warning C4251 + set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD /wd4251") # Generate complete debugging information #set(CMAKE_CXX_FLAGS_DEBUG "/Zi") ELSEIF (CMAKE_COMPILER_IS_GNUCXX) diff --git a/README.md b/README.md index cfcf41fdb..82f11d28a 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,9 @@ Dependencies: *(for minimum requirements search our [CMake file](https://github. - [protobuf](https://github.com/google/protobuf) - [CMake](https://www.cmake.org/) -Oracle can optionally use zlib to load zipped files: +Oracle can optionally use zlib and xz to load compressed files: - [zlib](https://www.zlib.net/) +- [xz](https://tukaani.org/xz/) To compile: diff --git a/clangify.sh b/clangify.sh index ca5b4ace9..ce81a1140 100755 --- a/clangify.sh +++ b/clangify.sh @@ -15,7 +15,8 @@ include=("common" \ exclude=("cockatrice/src/qt-json" \ "servatrice/src/smtp" \ "common/sfmt" \ -"oracle/src/zip") +"oracle/src/zip" \ +"oracle/src/lzma") exts=("cpp" "h") cf_cmd="clang-format" branch="origin/master" diff --git a/cmake/FindWin32SslRuntime.cmake b/cmake/FindWin32SslRuntime.cmake index 9cca2f7bc..79ba688d4 100644 --- a/cmake/FindWin32SslRuntime.cmake +++ b/cmake/FindWin32SslRuntime.cmake @@ -2,7 +2,7 @@ # will be needed by Qt in order to access https urls. if (WIN32) - # Get standard installation paths for OpenSSL under Windows + # Get standard installation paths for OpenSSL under Windows # http://www.slproweb.com/products/Win32OpenSSL.html @@ -15,6 +15,7 @@ if (WIN32) ) file(TO_CMAKE_PATH "$ENV{PROGRAMFILES}" _programfiles) set(_OPENSSL_ROOT_PATHS + "C:/Tools/vcpkg/installed/x64-windows/bin" "${_programfiles}/OpenSSL-Win64" "C:/OpenSSL-Win64/" ) @@ -28,6 +29,7 @@ if (WIN32) ) file(TO_CMAKE_PATH "$ENV{PROGRAMFILES}" _programfiles) set(_OPENSSL_ROOT_PATHS + "C:/Tools/vcpkg/installed/x86-windows/bin" "${_programfiles}/OpenSSL" "${_programfiles}/OpenSSL-Win32" "C:/OpenSSL/" diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 064e17701..3c62074d9 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -254,6 +254,8 @@ if(WIN32) set(plugin_dest_dir Plugins) set(qtconf_dest_dir .) + install(DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll") + # qt5 plugins: audio, iconengines, imageformats, platforms, printsupport install(DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} COMPONENT Runtime FILES_MATCHING REGEX "(audio|iconengines|imageformats|platforms|printsupport)/.*[^d]\\.dll") diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index 9b2137791..f4995db5f 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -224,13 +224,14 @@ CardInfo::CardInfo(const QString &_name, const SetList &_sets, const QStringMap &_customPicURLs, MuidMap _muIds, + QStringMap _uuIds, QStringMap _collectorNumbers, QStringMap _rarities) : name(_name), isToken(_isToken), sets(_sets), manacost(_manacost), cmc(_cmc), cardtype(_cardtype), powtough(_powtough), text(_text), colors(_colors), relatedCards(_relatedCards), reverseRelatedCards(_reverseRelatedCards), setsNames(), upsideDownArt(_upsideDownArt), loyalty(_loyalty), - customPicURLs(_customPicURLs), muIds(std::move(_muIds)), collectorNumbers(std::move(_collectorNumbers)), - rarities(std::move(_rarities)), cipt(_cipt), tableRow(_tableRow) + customPicURLs(_customPicURLs), muIds(std::move(_muIds)), uuIds(std::move(_uuIds)), + collectorNumbers(std::move(_collectorNumbers)), rarities(std::move(_rarities)), cipt(_cipt), tableRow(_tableRow) { pixmapCacheKey = QLatin1String("card_") + name; simpleName = CardInfo::simplifyName(name); @@ -260,12 +261,13 @@ CardInfoPtr CardInfo::newInstance(const QString &_name, const SetList &_sets, const QStringMap &_customPicURLs, MuidMap _muIds, + QStringMap _uuIds, QStringMap _collectorNumbers, QStringMap _rarities) { CardInfoPtr ptr(new CardInfo(_name, _isToken, _manacost, _cmc, _cardtype, _powtough, _text, _colors, _relatedCards, _reverseRelatedCards, _upsideDownArt, _loyalty, _cipt, _tableRow, _sets, - _customPicURLs, std::move(_muIds), std::move(_collectorNumbers), + _customPicURLs, std::move(_muIds), std::move(_uuIds), std::move(_collectorNumbers), std::move(_rarities))); ptr->setSmartPointer(ptr); @@ -440,6 +442,7 @@ void CardDatabase::addCard(CardInfoPtr card) QString setName = set->getCorrectedShortName(); sameCard->setSet(set); sameCard->setMuId(setName, card->getMuId(setName)); + sameCard->setUuId(setName, card->getUuId(setName)); sameCard->setRarity(setName, card->getRarity(setName)); sameCard->setSetNumber(setName, card->getCollectorNumber(setName)); } diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index c58c1ad83..8aa89e0b2 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -149,6 +149,7 @@ private: QString loyalty; QStringMap customPicURLs; MuidMap muIds; + QStringMap uuIds; QStringMap collectorNumbers; QStringMap rarities; bool cipt; @@ -172,7 +173,8 @@ public: int _tableRow = 0, const SetList &_sets = SetList(), const QStringMap &_customPicURLs = QStringMap(), - MuidMap muids = MuidMap(), + MuidMap _muids = MuidMap(), + QStringMap _uuIds = QStringMap(), QStringMap _collectorNumbers = QStringMap(), QStringMap _rarities = QStringMap()); ~CardInfo() override; @@ -193,7 +195,8 @@ public: int _tableRow = 0, const SetList &_sets = SetList(), const QStringMap &_customPicURLs = QStringMap(), - MuidMap muids = MuidMap(), + MuidMap _muids = MuidMap(), + QStringMap _uuIds = QStringMap(), QStringMap _collectorNumbers = QStringMap(), QStringMap _rarities = QStringMap()); @@ -310,6 +313,10 @@ public: { return muIds.value(set); } + QString getUuId(const QString &set) const + { + return uuIds.value(set); + } QString getCollectorNumber(const QString &set) const { return collectorNumbers.value(set); @@ -344,6 +351,10 @@ public: { muIds.insert(_set, _muId); } + void setUuId(const QString &_set, const QString &_uuId) + { + uuIds.insert(_set, _uuId); + } void setSetNumber(const QString &_set, const QString &_setNumber) { collectorNumbers.insert(_set, _setNumber); diff --git a/cockatrice/src/carddbparser/cockatricexml3.cpp b/cockatrice/src/carddbparser/cockatricexml3.cpp index d98202ac6..6a6300cf4 100644 --- a/cockatrice/src/carddbparser/cockatricexml3.cpp +++ b/cockatrice/src/carddbparser/cockatricexml3.cpp @@ -133,7 +133,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) QList relatedCards, reverseRelatedCards; QStringMap customPicURLs; MuidMap muids; - QStringMap collectorNumbers, rarities; + QStringMap uuids, collectorNumbers, rarities; SetList sets; int tableRow = 0; bool cipt = false; @@ -164,6 +164,10 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) muids[setName] = attrs.value("muId").toString().toInt(); } + if (attrs.hasAttribute("muId")) { + uuids[setName] = attrs.value("uuId").toString(); + } + if (attrs.hasAttribute("picURL")) { customPicURLs[setName] = attrs.value("picURL").toString(); } @@ -232,7 +236,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) CardInfoPtr newCard = CardInfo::newInstance( name, isToken, manacost, cmc, type, pt, text, colors, relatedCards, reverseRelatedCards, upsideDown, - loyalty, cipt, tableRow, sets, customPicURLs, muids, collectorNumbers, rarities); + loyalty, cipt, tableRow, sets, customPicURLs, muids, uuids, collectorNumbers, rarities); emit addCard(newCard); } } @@ -274,6 +278,7 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in tmpSet = sets[i]->getShortName(); xml.writeAttribute("rarity", info->getRarity(tmpSet)); xml.writeAttribute("muId", QString::number(info->getMuId(tmpSet))); + xml.writeAttribute("uuId", info->getUuId(tmpSet)); tmpString = info->getCollectorNumber(tmpSet); if (!tmpString.isEmpty()) { diff --git a/cockatrice/src/decklistmodel.cpp b/cockatrice/src/decklistmodel.cpp index e8b4b95ff..8b6e7bf54 100644 --- a/cockatrice/src/decklistmodel.cpp +++ b/cockatrice/src/decklistmodel.cpp @@ -324,7 +324,7 @@ QModelIndex DeckListModel::addCard(const QString &cardName, const QString &zoneN // and default values for all fields info = CardInfo::newInstance(cardName, false, nullptr, nullptr, "unknown", nullptr, nullptr, QStringList(), QList(), QList(), false, 0, false, 0, - SetList(), QStringMap(), MuidMap(), QStringMap(), QStringMap()); + SetList(), QStringMap(), MuidMap(), QStringMap(), QStringMap(), QStringMap()); } else { return {}; } diff --git a/cockatrice/src/pictureloader.cpp b/cockatrice/src/pictureloader.cpp index 341ad970d..7d3e32ff6 100644 --- a/cockatrice/src/pictureloader.cpp +++ b/cockatrice/src/pictureloader.cpp @@ -267,6 +267,7 @@ QString PictureToLoad::transformUrl(QString urlTemplate) const if (set) { transformMap["!cardid!"] = QString::number(card->getMuId(set->getShortName())); + transformMap["!uuid!"] = card->getUuId(set->getShortName()); transformMap["!collectornumber!"] = card->getCollectorNumber(set->getShortName()); transformMap["!setcode!"] = set->getShortName(); transformMap["!setcode_lower!"] = set->getShortName().toLower(); @@ -274,6 +275,7 @@ QString PictureToLoad::transformUrl(QString urlTemplate) const transformMap["!setname_lower!"] = set->getLongName().toLower(); } else { transformMap["!cardid!"] = QString(); + transformMap["!uuid!"] = QString(); transformMap["!collectornumber!"] = QString(); transformMap["!setcode!"] = QString(); transformMap["!setcode_lower!"] = QString(); diff --git a/doc/cards.xsd b/doc/cards.xsd index d3721e9bb..89ae6eb95 100644 --- a/doc/cards.xsd +++ b/doc/cards.xsd @@ -39,6 +39,7 @@ + diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 5b3567fcc..ec7b5d171 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -85,18 +85,33 @@ IF(ZLIB_FOUND) src/zip/unzip.cpp src/zip/zipglobal.cpp ) - ELSE() MESSAGE(STATUS "Oracle: zlib not found; ZIP support disabled") ENDIF() +# LibLZMA is required to support xz files +FIND_PACKAGE(LibLZMA) +IF(LIBLZMA_FOUND) + INCLUDE_DIRECTORIES(${LIBLZMA_INCLUDE_DIRS}) + ADD_DEFINITIONS("-DHAS_LZMA") + + set(oracle_SOURCES ${oracle_SOURCES} + src/lzma/decompress.cpp + ) +ELSE() + MESSAGE(STATUS "Oracle: LibLZMA not found; xz support disabled") +ENDIF() + # Build oracle binary and link it ADD_EXECUTABLE(oracle WIN32 MACOSX_BUNDLE ${oracle_SOURCES} ${oracle_QM} ${oracle_RESOURCES_RCC} ${oracle_MOC_SRCS}) +TARGET_LINK_LIBRARIES(oracle ${ORACLE_QT_MODULES}) IF(ZLIB_FOUND) - TARGET_LINK_LIBRARIES(oracle ${ORACLE_QT_MODULES} ${ZLIB_LIBRARIES}) -ELSE() - TARGET_LINK_LIBRARIES(oracle ${ORACLE_QT_MODULES}) + TARGET_LINK_LIBRARIES(oracle ${ZLIB_LIBRARIES}) +ENDIF() + +IF(LIBLZMA_FOUND) + TARGET_LINK_LIBRARIES(oracle ${LIBLZMA_LIBRARIES}) ENDIF() if(UNIX) @@ -163,14 +178,8 @@ IF(WIN32) set(plugin_dest_dir Plugins) set(qtconf_dest_dir .) list(APPEND libSearchDirs ${QT_LIBRARY_DIR}) - IF(ZLIB_FOUND) - # look for dll in the bin/ directory (gnuwin32 package) - get_filename_component(ZLIB_DLL_DIR "${ZLIB_INCLUDE_DIRS}/../bin/" REALPATH) - list(APPEND libSearchDirs ${ZLIB_DLL_DIR}) - # look for dll in the lib/ directory (nuget package) - get_filename_component(ZLIB_DLL_DIR "${ZLIB_LIBRARY}" DIRECTORY) - list(APPEND libSearchDirs ${ZLIB_DLL_DIR}) - ENDIF() + + install(DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll") # qt5 plugins: iconengines, platforms diff --git a/oracle/src/lzma/decompress.cpp b/oracle/src/lzma/decompress.cpp new file mode 100644 index 000000000..718cde207 --- /dev/null +++ b/oracle/src/lzma/decompress.cpp @@ -0,0 +1,250 @@ +/* + * Simple routing to extract a single file from a xz archive + * Heavily based from doc/examples/02_decompress.c obtained from + * the official xz git repository: git.tukaani.org/xz.git + * The license from the original file header follows + * + * Author: Lasse Collin + * This file has been put into the public domain. + * You can do whatever you want with this file. + */ + + +#include +#include + +#include "decompress.h" + +XzDecompressor::XzDecompressor(QObject *parent) + : QObject(parent) +{ + +} + +bool XzDecompressor::decompress(QBuffer *in, QBuffer *out) +{ + lzma_stream strm = LZMA_STREAM_INIT; + bool success; + + if (!init_decoder(&strm)) { + return false; + } + + success = internal_decompress(&strm, in, out); + + // Free the memory allocated for the decoder. This only needs to be + // done after the last file. + lzma_end(&strm); + + return success; +} + +bool XzDecompressor::init_decoder(lzma_stream *strm) +{ + // Initialize a .xz decoder. The decoder supports a memory usage limit + // and a set of flags. + // + // The memory usage of the decompressor depends on the settings used + // to compress a .xz file. It can vary from less than a megabyte to + // a few gigabytes, but in practice (at least for now) it rarely + // exceeds 65 MiB because that's how much memory is required to + // decompress files created with "xz -9". Settings requiring more + // memory take extra effort to use and don't (at least for now) + // provide significantly better compression in most cases. + // + // Memory usage limit is useful if it is important that the + // decompressor won't consume gigabytes of memory. The need + // for limiting depends on the application. In this example, + // no memory usage limiting is used. This is done by setting + // the limit to UINT64_MAX. + // + // The .xz format allows concatenating compressed files as is: + // + // echo foo | xz > foobar.xz + // echo bar | xz >> foobar.xz + // + // When decompressing normal standalone .xz files, LZMA_CONCATENATED + // should always be used to support decompression of concatenated + // .xz files. If LZMA_CONCATENATED isn't used, the decoder will stop + // after the first .xz stream. This can be useful when .xz data has + // been embedded inside another file format. + // + // Flags other than LZMA_CONCATENATED are supported too, and can + // be combined with bitwise-or. See lzma/container.h + // (src/liblzma/api/lzma/container.h in the source package or e.g. + // /usr/include/lzma/container.h depending on the install prefix) + // for details. + lzma_ret ret = lzma_stream_decoder( + strm, UINT64_MAX, LZMA_CONCATENATED); + + // Return successfully if the initialization went fine. + if (ret == LZMA_OK) + return true; + + // Something went wrong. The possible errors are documented in + // lzma/container.h (src/liblzma/api/lzma/container.h in the source + // package or e.g. /usr/include/lzma/container.h depending on the + // install prefix). + // + // Note that LZMA_MEMLIMIT_ERROR is never possible here. If you + // specify a very tiny limit, the error will be delayed until + // the first headers have been parsed by a call to lzma_code(). + const char *msg; + switch (ret) { + case LZMA_MEM_ERROR: + msg = "Memory allocation failed"; + break; + + case LZMA_OPTIONS_ERROR: + msg = "Unsupported decompressor flags"; + break; + + default: + // This is most likely LZMA_PROG_ERROR indicating a bug in + // this program or in liblzma. It is inconvenient to have a + // separate error message for errors that should be impossible + // to occur, but knowing the error code is important for + // debugging. That's why it is good to print the error code + // at least when there is no good error message to show. + msg = "Unknown error, possibly a bug"; + break; + } + + qDebug() << "Error initializing the decoder:" << msg << "(error code " << ret << ")"; + return false; +} + + +bool XzDecompressor::internal_decompress(lzma_stream *strm, QBuffer *in, QBuffer *out) +{ + // When LZMA_CONCATENATED flag was used when initializing the decoder, + // we need to tell lzma_code() when there will be no more input. + // This is done by setting action to LZMA_FINISH instead of LZMA_RUN + // in the same way as it is done when encoding. + // + // When LZMA_CONCATENATED isn't used, there is no need to use + // LZMA_FINISH to tell when all the input has been read, but it + // is still OK to use it if you want. When LZMA_CONCATENATED isn't + // used, the decoder will stop after the first .xz stream. In that + // case some unused data may be left in strm->next_in. + lzma_action action = LZMA_RUN; + + uint8_t inbuf[BUFSIZ]; + uint8_t outbuf[BUFSIZ]; + qint64 bytesAvailable; + + strm->next_in = NULL; + strm->avail_in = 0; + strm->next_out = outbuf; + strm->avail_out = sizeof(outbuf); + while (true) { + if (strm->avail_in == 0) { + strm->next_in = inbuf; + bytesAvailable = in->bytesAvailable(); + if(bytesAvailable == 0) { + // Once the end of the input file has been reached, + // we need to tell lzma_code() that no more input + // will be coming. As said before, this isn't required + // if the LZMA_CONCATENATED flag isn't used when + // initializing the decoder. + action = LZMA_FINISH; + } else if(bytesAvailable >= BUFSIZ) { + in->read((char*) inbuf, BUFSIZ); + strm->avail_in = BUFSIZ; + } else { + in->read((char*) inbuf, bytesAvailable); + strm->avail_in = bytesAvailable; + } + } + + lzma_ret ret = lzma_code(strm, action); + + if (strm->avail_out == 0 || ret == LZMA_STREAM_END) { + qint64 write_size = sizeof(outbuf) - strm->avail_out; + + if (out->write((char *) outbuf, write_size) != write_size) { + qDebug() << "Write error"; + return false; + } + + strm->next_out = outbuf; + strm->avail_out = sizeof(outbuf); + } + + if (ret != LZMA_OK) { + // Once everything has been decoded successfully, the + // return value of lzma_code() will be LZMA_STREAM_END. + // + // It is important to check for LZMA_STREAM_END. Do not + // assume that getting ret != LZMA_OK would mean that + // everything has gone well or that when you aren't + // getting more output it must have successfully + // decoded everything. + if (ret == LZMA_STREAM_END) + return true; + + // It's not LZMA_OK nor LZMA_STREAM_END, + // so it must be an error code. See lzma/base.h + // (src/liblzma/api/lzma/base.h in the source package + // or e.g. /usr/include/lzma/base.h depending on the + // install prefix) for the list and documentation of + // possible values. Many values listen in lzma_ret + // enumeration aren't possible in this example, but + // can be made possible by enabling memory usage limit + // or adding flags to the decoder initialization. + const char *msg; + switch (ret) { + case LZMA_MEM_ERROR: + msg = "Memory allocation failed"; + break; + + case LZMA_FORMAT_ERROR: + // .xz magic bytes weren't found. + msg = "The input is not in the .xz format"; + break; + + case LZMA_OPTIONS_ERROR: + // For example, the headers specify a filter + // that isn't supported by this liblzma + // version (or it hasn't been enabled when + // building liblzma, but no-one sane does + // that unless building liblzma for an + // embedded system). Upgrading to a newer + // liblzma might help. + // + // Note that it is unlikely that the file has + // accidentally became corrupt if you get this + // error. The integrity of the .xz headers is + // always verified with a CRC32, so + // unintentionally corrupt files can be + // distinguished from unsupported files. + msg = "Unsupported compression options"; + break; + + case LZMA_DATA_ERROR: + msg = "Compressed file is corrupt"; + break; + + case LZMA_BUF_ERROR: + // Typically this error means that a valid + // file has got truncated, but it might also + // be a damaged part in the file that makes + // the decoder think the file is truncated. + // If you prefer, you can use the same error + // message for this as for LZMA_DATA_ERROR. + msg = "Compressed file is truncated or " + "otherwise corrupt"; + break; + + default: + // This is most likely LZMA_PROG_ERROR. + msg = "Unknown error, possibly a bug"; + break; + } + + qDebug() << "Decoder error:" << msg << "(error code " << ret << ")"; + return false; + } + } +} + diff --git a/oracle/src/lzma/decompress.h b/oracle/src/lzma/decompress.h new file mode 100644 index 000000000..f0e315f8b --- /dev/null +++ b/oracle/src/lzma/decompress.h @@ -0,0 +1,19 @@ +#ifndef XZ_DECOMPRESS_H +#define XZ_DECOMPRESS_H + +#include +#include + +class XzDecompressor : public QObject +{ + Q_OBJECT +public: + XzDecompressor(QObject *parent = 0); + ~XzDecompressor() { }; + bool decompress(QBuffer *in, QBuffer *out); +private: + bool init_decoder(lzma_stream *strm); + bool internal_decompress(lzma_stream *strm, QBuffer *in, QBuffer *out); +}; + +#endif diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 90c908f91..4ac937cc3 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -19,7 +19,7 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data) setsMap = QtJson::Json::parse(QString(data), ok).toMap(); if (!ok) { qDebug() << "error: QtJson::Json::parse()"; - return 0; + return false; } QListIterator it(setsMap.values()); @@ -33,7 +33,7 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data) while (it.hasNext()) { map = it.next().toMap(); - edition = map.value("code").toString(); + edition = map.value("code").toString().toUpper(); editionLong = map.value("name").toString(); editionCards = map.value("cards"); setType = map.value("type").toString(); @@ -57,6 +57,7 @@ CardInfoPtr OracleImporter::addCard(const QString &setName, QString cardName, bool isToken, int cardId, + QString &cardUuId, QString &setNumber, QString &cardCost, QString &cmc, @@ -124,30 +125,13 @@ CardInfoPtr OracleImporter::addCard(const QString &setName, cards.insert(cardName, card); } card->setMuId(setName, cardId); + card->setUuId(setName, cardUuId); card->setSetNumber(setName, setNumber); card->setRarity(setName, rarity); return card; } -void OracleImporter::extractColors(const QStringList &in, QStringList &out) -{ - foreach (QString c, in) { - if (c == "White") - out << "W"; - else if (c == "Blue") - out << "U"; - else if (c == "Black") - out << "B"; - else if (c == "Red") - out << "R"; - else if (c == "Green") - out << "G"; - else - qDebug() << "error: unknown color:" << c; - } -} - int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) { int cards = 0; @@ -164,6 +148,7 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) QList relatedCards; QList reverseRelatedCards; // dummy int cardId; + QString cardUuId; QString setNumber; QString rarity; QString cardLoyalty; @@ -173,15 +158,20 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) while (it.hasNext()) { map = it.next().toMap(); + /* Currently used layouts are: + * augment, double_faced_token, flip, host, leveler, meld, normal, planar, + * saga, scheme, split, token, transform, vanguard + */ QString layout = map.value("layout").toString(); // don't import tokens from the json file if (layout == "token") continue; - if (layout == "split" || layout == "aftermath") { + // Aftermath card layout seems to have been integrated in "split" + if (layout == "split") { // Enqueue split card for later handling - cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; + cardId = map.contains("multiverseId") ? map.value("multiverseId").toInt() : 0; if (cardId) splitCards.insertMulti(cardId, map); continue; @@ -190,16 +180,18 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) // normal cards handling cardName = map.contains("name") ? map.value("name").toString() : QString(""); cardCost = map.contains("manaCost") ? map.value("manaCost").toString() : QString(""); - cmc = map.contains("cmc") ? map.value("cmc").toString() : QString("0"); + cmc = map.contains("convertedManaCost") ? map.value("convertedManaCost").toString() : QString("0"); cardType = map.contains("type") ? map.value("type").toString() : QString(""); cardPT = map.contains("power") || map.contains("toughness") ? map.value("power").toString() + QString('/') + map.value("toughness").toString() : QString(""); cardText = map.contains("text") ? map.value("text").toString() : QString(""); - cardId = map.contains("multiverseid") ? map.value("multiverseid").toInt() : 0; + cardId = map.contains("multiverseId") ? map.value("multiverseId").toInt() : 0; + cardUuId = map.contains("uuid") ? map.value("uuid").toString() : QString(""); setNumber = map.contains("number") ? map.value("number").toString() : QString(""); rarity = map.contains("rarity") ? map.value("rarity").toString() : QString(""); cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toString() : QString(""); + colors = map.contains("colors") ? map.value("colors").toStringList() : QStringList(); relatedCards = QList(); if (map.contains("names")) foreach (const QString &name, map.value("names").toStringList()) { @@ -214,11 +206,8 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) upsideDown = false; } - colors.clear(); - extractColors(map.value("colors").toStringList(), colors); - CardInfoPtr card = - addCard(set->getShortName(), cardName, false, cardId, setNumber, cardCost, cmc, cardType, cardPT, + addCard(set->getShortName(), cardName, false, cardId, cardUuId, setNumber, cardCost, cmc, cardType, cardPT, cardLoyalty, cardText, colors, relatedCards, reverseRelatedCards, upsideDown, rarity); if (!set->contains(card)) { @@ -250,6 +239,7 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) cardType = ""; cardPT = ""; cardText = ""; + cardUuId = ""; setNumber = ""; rarity = ""; cardLoyalty = ""; @@ -269,10 +259,10 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) cardCost += prefix; cardCost += map.value("manaCost").toString(); } - if (map.contains("cmc")) { + if (map.contains("convertedManaCost")) { if (!cmc.isEmpty()) cmc += prefix; - cmc += map.value("cmc").toString(); + cmc += map.value("convertedManaCost").toString(); } if (map.contains("type")) { if (!cardType.isEmpty()) @@ -289,6 +279,10 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) cardText += prefix2; cardText += map.value("text").toString(); } + if (map.contains("uuid")) { + if (cardUuId.isEmpty()) + cardUuId = map.value("uuid").toString(); + } if (map.contains("number")) { if (setNumber.isEmpty()) setNumber = map.value("number").toString(); @@ -298,7 +292,7 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) rarity = map.value("rarity").toString(); } - extractColors(map.value("colors").toStringList(), colors); + colors << map.value("colors").toStringList(); } colors.removeDuplicates(); @@ -313,8 +307,8 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) // add the card CardInfoPtr card = - addCard(set->getShortName(), cardName, false, muid, setNumber, cardCost, cmc, cardType, cardPT, cardLoyalty, - cardText, colors, relatedCards, reverseRelatedCards, upsideDown, rarity); + addCard(set->getShortName(), cardName, false, muid, cardUuId, setNumber, cardCost, cmc, cardType, cardPT, + cardLoyalty, cardText, colors, relatedCards, reverseRelatedCards, upsideDown, rarity); if (!set->contains(card)) { card->addToSet(set); diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 538a327e2..68a7b62be 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -61,6 +61,7 @@ private: QString cardName, bool isToken, int cardId, + QString &cardUuId, QString &setNumber, QString &cardCost, QString &cmc, @@ -93,7 +94,6 @@ public: } protected: - void extractColors(const QStringList &in, QStringList &out); void sortColors(QStringList &colors); }; diff --git a/oracle/src/oraclewizard.cpp b/oracle/src/oraclewizard.cpp index 9cf24cd88..ef2445447 100644 --- a/oracle/src/oraclewizard.cpp +++ b/oracle/src/oraclewizard.cpp @@ -26,11 +26,22 @@ #include "settingscache.h" #include "version_string.h" -#define ZIP_SIGNATURE "PK" -#define ALLSETS_URL_FALLBACK "https://mtgjson.com/json/AllSets.json" +#ifdef HAS_LZMA +#include "lzma/decompress.h" +#endif #ifdef HAS_ZLIB #include "zip/unzip.h" +#endif + +#define ZIP_SIGNATURE "PK" +// Xz stream header: 0xFD + "7zXZ" +#define XZ_SIGNATURE "\xFD\x37\x7A\x58\x5A" +#define ALLSETS_URL_FALLBACK "https://mtgjson.com/json/AllSets.json" + +#ifdef HAS_LZMA +#define ALLSETS_URL "https://mtgjson.com/json/AllSets.json.xz" +#elif defined(HAS_ZLIB) #define ALLSETS_URL "https://mtgjson.com/json/AllSets.json.zip" #else #define ALLSETS_URL "https://mtgjson.com/json/AllSets.json" @@ -249,11 +260,14 @@ void LoadSetsPage::actLoadSetsFile() QFileDialog dialog(this, tr("Load sets file")); dialog.setFileMode(QFileDialog::ExistingFile); + QString extensions = "*.json"; #ifdef HAS_ZLIB - dialog.setNameFilter(tr("Sets JSON file (*.json *.zip)")); -#else - dialog.setNameFilter(tr("Sets JSON file (*.json)")); + extensions += " *.zip"; #endif +#ifdef HAS_LZMA + extensions += " *.xz"; +#endif + dialog.setNameFilter(tr("Sets JSON file (%1)").arg(extensions)); if (!fileLineEdit->text().isEmpty() && QFile::exists(fileLineEdit->text())) { dialog.selectFile(fileLineEdit->text()); @@ -383,7 +397,32 @@ void LoadSetsPage::readSetsFromByteArray(QByteArray data) progressBar->show(); // unzip the file if needed - if (data.startsWith(ZIP_SIGNATURE)) { + if (data.startsWith(XZ_SIGNATURE)) { +#ifdef HAS_LZMA + // zipped file + auto *inBuffer = new QBuffer(&data); + auto *outBuffer = new QBuffer(this); + inBuffer->open(QBuffer::ReadOnly); + outBuffer->open(QBuffer::WriteOnly); + XzDecompressor xz; + if (!xz.decompress(inBuffer, outBuffer)) { + zipDownloadFailed(tr("Xz extraction failed.")); + return; + } + + future = QtConcurrent::run(wizard()->importer, &OracleImporter::readSetsFromByteArray, outBuffer->data()); + watcher.setFuture(future); + return; +#else + zipDownloadFailed(tr("Sorry, this version of Oracle does not support xz compressed files.")); + + wizard()->enableButtons(); + setEnabled(true); + progressLabel->hide(); + progressBar->hide(); + return; +#endif + } else if (data.startsWith(ZIP_SIGNATURE)) { #ifdef HAS_ZLIB // zipped file auto *inBuffer = new QBuffer(&data); diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 248030025..f1bf60925 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -164,6 +164,8 @@ if(WIN32) set(plugin_dest_dir Plugins) set(qtconf_dest_dir .) + install(DIRECTORY "${CMAKE_BINARY_DIR}/${PROJECT_NAME}/${CMAKE_BUILD_TYPE}/" DESTINATION ./ FILES_MATCHING PATTERN "*.dll") + # qt5 plugins: platforms, sqldrivers/mysql install(DIRECTORY "${QT_PLUGINS_DIR}/" DESTINATION ${plugin_dest_dir} COMPONENT Runtime FILES_MATCHING REGEX "(platforms/.*|sqldrivers/qsqlmysql)\\.dll" From 94fe64c6c19b1144d2474e4a4a319c43fcc8cae5 Mon Sep 17 00:00:00 2001 From: Zach H Date: Thu, 20 Dec 2018 18:26:00 -0500 Subject: [PATCH 28/62] Update translations (#3475) Signed-off-by: Zach Halpern --- .gitignore | 1 + .../translations/cockatrice_en@pirate.ts | 1764 +++++++------- cockatrice/translations/cockatrice_fr.ts | 156 +- cockatrice/translations/cockatrice_ja.ts | 176 +- cockatrice/translations/cockatrice_ru.ts | 1776 +++++++------- cockatrice/translations/cockatrice_sv.ts | 2149 +++++++++-------- cockatrice/translations/cockatrice_zh-Hans.ts | 1682 +++++++------ oracle/translations/oracle_fr.ts | 151 +- oracle/translations/oracle_ja.ts | 2 +- oracle/translations/oracle_zh-Hans.ts | 97 +- 10 files changed, 4224 insertions(+), 3730 deletions(-) diff --git a/.gitignore b/.gitignore index aa0867979..1bb149f82 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ mysql.cnf .idea/ *.aps cmake-build-debug/ +preferences/ diff --git a/cockatrice/translations/cockatrice_en@pirate.ts b/cockatrice/translations/cockatrice_en@pirate.ts index 618933c09..7c070715d 100644 --- a/cockatrice/translations/cockatrice_en@pirate.ts +++ b/cockatrice/translations/cockatrice_en@pirate.ts @@ -20,62 +20,62 @@ AppearanceSettingsPage - + Theme settings Theme settin's - + Current theme: - + Card rendering C-arrr-d Scribblin' - + Display card names on cards having a picture - + Scale cards on mouse over - + 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: @@ -85,17 +85,17 @@ ban &user name - + Curse da landlubber's name ban &IP address - + Curse da landlubbers port o call ban client I&D - + Curse da landlubber's Jolly Roger @@ -188,6 +188,29 @@ This be only saved for captains and cannot be seen by t' brutally banned sc + + BetaReleaseChannel + + + Beta Releases + + + + + No reply received from the release update server. + + + + + Invalid reply received from the release update server. + + + + + No reply received from the file update server. + + + CardDatabaseModel @@ -242,32 +265,37 @@ This be only saved for captains and cannot be seen by t' brutally banned sc CardInfoText - + + Unknown card: + + + + Name: Name: - + Mana cost: Magic tax: - + Color(s): Color(s): - + Card type: Type 'o magic: - + P / T: P / T: - + Loyalty: @@ -411,58 +439,58 @@ This be only saved for captains and cannot be seen by t' brutally banned sc DeckEditorSettingsPage - - + + Update Spoilers - - Updating Spoilers + + Updating... - + Choose path Map yer scurvy way - + Spoilers - + Download Spoilers Automatically - + Spoiler Location: - + Hey, something's here finally! YARR CAP'N, THAR BE A NEW SHIP SAILIN'! - + Last Updated - + Spoilers download automatically on launch - + Press the button to manually update without relaunching - + Do not close settings until manual update complete @@ -470,12 +498,12 @@ This be only saved for captains and cannot be seen by t' brutally banned sc DeckListModel - + Number - + Card @@ -537,136 +565,129 @@ This be only saved for captains and cannot be seen by t' brutally banned sc - - DevReleaseChannel - - - Development snapshots - - - - - No reply received from the release update server. - - - - - Invalid reply received from the release update server. - - - - - No reply received from the file update server. - - - DlgConnect - + New Host - + &Host: &Cap'n - + Known Hosts - Name: - - - - - &Port: + Refresh the server list with known public servers + Name: + + + + + &Port: + + + + Player &name: Matey &name: - + P&assword: &A stupendously secret code: - + &Save password Record t' &stupendously secret code - + A&uto connect - + Automatically connect to the most recent login when Cockatrice opens - - - Public Servers - - - - - Forgot password - - - Connect + If you have any trouble connecting or registering then contact the server staff for help! - - Cancel + + + Webpage - - Server + + Forgot Password + + + + + &Connect + + + + + Server Contact + + + + + Connect to Server + Server + + + + Login - - Connect to server - - - - + Connection Warning - + You need to name your new connection profile. - + Connect Warning - + The player name can't be empty. + + + Downloading... + + DlgCreateGame @@ -858,7 +879,7 @@ This be only saved for captains and cannot be seen by t' brutally banned sc DlgEditAvatar - + No image chosen. No pic be picked! @@ -879,17 +900,17 @@ To remove your current avatar, confirm without choosing a new image. - + Open Image Open Pic - + Image Files (*.png *.jpg *.bmp) - + Invalid image chosen. Invalid pic be picked! @@ -1369,12 +1390,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgSettings - + Unknown Error loading card database Cap'n? Thar be a unknown problem wi' t' dock - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -1385,7 +1406,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 @@ -1396,7 +1417,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -1405,21 +1426,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 http://github.com/Cockatrice/Cockatrice/issues @@ -1428,63 +1449,81 @@ Would you like to change your database location setting? - - - + + + Error Cap'n? Thar be a problem - + 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 Cap'n's orders - + General - + Appearance - + User Interface - + Deck Editor Dock - + Chat - + Sound - + Shortcuts + + DlgTipOfTheDay + + + Next + + + + + Previous + + + + + Tip of the Day + + + DlgUpdate @@ -1821,32 +1860,32 @@ You may have to build from source yourself. - + Type - + Description - + Creator Cap'n - + Restrictions - + Players Mateys - + Spectators Landlubbers @@ -1854,852 +1893,844 @@ You may have to build from source yourself. GeneralSettingsPage - - - - - + + + + + Choose path - + Success - + Downloaded card pictures have been reset. - + Error Cap'n? Thar be a problem - + One or more downloaded card pictures could not be cleared. - + Personal settings Cap'n's orders for t' cap'n - + Language: - + Download card pictures on the fly - + Paths (editing disabled in portable mode) - + Paths - + Decks directory: - + Replays directory: - + Pictures directory: - + Card database: Dock: - + Token database: - + Picture cache size: - + Primary download URL: - + Fallback download URL: - + How to set a custom picture url - + Reset/clear downloaded pictures - + Update channel - + Notify if a feature supported by the server is missing in my client - - + + Reset - - - Logger - - Client Operating System - - - - - Build Architecture - - - - - Qt Version + + Show tips on startup 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 Cap'n's thrown ye outta tha ship! - + Expected end time: %1 - + This ban lasts indefinitely. - + Scheduled server shutdown. - - + + Invalid username. Yarrr, that ain't ye name ye scurvy landlubber! - + 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. - + Number of players Number o' mateys - + Please enter the number of players. Me hearty, enter t' number o' mateys - - + + Player %1 Matey %1 - + Load replay - + About Cockatrice - + Cockatrice Webpage - + Project Manager: - + Past Project Managers: - + Developers: - + Our Developers - + Help Develop! - + Translators: - + Help Translate! - + Support: - + Report an Issue - + Troubleshooting - + F.A.Q. - - - - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + + + + Error Cap'n? Thar be a problem - + Server timeout T' ship be timin' out! - + Failed Login - + 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 ID's. 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 try closing and reopening your client and try again. If the error persists try updating your client to the most recent build and if need be contact your software provider. - + Account activation - + 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 - + 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. - + Your client seems to be missing features this server requires for connection. - + Version - + Our Translators - + Your account has not been activated yet. You need to provide the activation token received in the activation email. - + The email address provider used during registration has been blacklisted for use on this server. - + 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. The code be too piddly to be stupendously secret. - + Registration failed for a technical problem on the server. - + 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... Boarding %1 - + Registering to %1 as %2... - + Disconnected Disembarked - + Connected, logging in at %1 - - - + + + Requesting forgot password to %1 as %2... - + &Connect... &Board ye ship... - + &Disconnect &Disembark - + Start &local game... Built &local ship - + &Watch replay... - + &Deck editor &Dock - + &Full screen - + &Register to server... - + &Settings... Cap'n'&s orders - - + + &Exit Dis&embark - + A&ctions - + &Cockatrice - + C&ard Database - + Open custom image folder - + Open custom sets folder - + Add custom sets/cards - + Edit &tokens... - + &About Cockatrice - + + &Tip of the Day + + + + Check for Client Updates - + View &debug log - + &Help - + Check for card updates... - + 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 AYE! - - + + No NAY! - + Open settings - + New sets found - + %1 new set(s) found in the card database Set code(s): %2 Do you want to enable it/them? - + View sets - + Welcome - - - + + + Information - + A card database update is already running. A dock update be runnin' already! - + Unable to run the card database updater: Cap'n! We can't run the dock updater: - + failed to start. - + crashed. - + timed out. be timin' out! - + write error. Cap'n? Thar be a writin' problem wi' t' ship. - + read error. Cap'n? Thar be a readin' problem wi' t' ship. - + unknown error. Cap'n? Thar be a unknown problem wi' t' ship. - + The card database updater exited with an error: %1 The dock updater sunk. She be havin' a problem: %1 - + Update completed successfully. Cockatrice will now reload the card database. - + You can only import XML databases at this time. - - - + + + Forgot Password - + Your password has been reset successfully, you now may 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. - - - - + + + + Load sets/cards - + &Manage sets... - + 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. - + 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. @@ -2707,18 +2738,18 @@ To update your client, go to Help -> Check for Updates. - + Selected file cannot be found. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. @@ -2841,6 +2872,16 @@ Cockatrice will now reload the card database. %1 plays %2%3. + + + %1 turns %2 face-down. + + + + + %1 turns %2 face-up. + + %1 has left the game (%2). @@ -3096,16 +3137,6 @@ Cockatrice will now reload the card database. %1 draws their initial hand. - - - %1 flips %2 face-down. - - - - - %1 flips %2 face-up. - - %1 destroys %2. @@ -3275,79 +3306,79 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Add message - + Message: - + Chat settings Cap'n's orders for t' chat - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered 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 @@ -3413,478 +3444,485 @@ Cockatrice will now reload the card database. Player - + Reveal top cards of library - + Number of cards: (max. %1) - + &View graveyard &View Davy Jones' Locker - + &View exile - + Player "%1" Matey "%1" - - - - + + + + &Graveyard Davy Jones' Locker - - - - + + + + &Exile - - - &Move hand to... - - - - - - &Top of library + &Move hand to... - + + &Top of library + + + + + + + &Bottom of library - + &Move graveyard to... - - - - + + + + &Hand &Hand - + &Move exile to... - + &View library &View library - + View &top cards of library... View &top cards o' library... - + Reveal &library to... - + Reveal t&op cards to... - + &Always reveal top card - + O&pen deck in deck editor O&pen flagship in dock - + &View sideboard &View sideplank - + &Draw card &Draw card - + D&raw cards... - + &Undo last draw - + Take &mulligan - + &Shuffle - + Play top card &face down - + Move top cards to &graveyard... Move top cards to Davy Jones' Locker - + Move top cards to &exile... - + Put top card on &bottom - + Put bottom card &in graveyard - + &Reveal hand to... &Reveal hand to... - + Reveal r&andom card to... - + Reveal random card to... - + &Sideboard &Sideplank - + &Library &Library - + &Counters Pie&ces o' eight - + &Untap all permanents - + R&oll die... - + &Create token... - + C&reate another token - + Cr&eate predefined token - + S&ay - + C&ard - + &All players - + &Play - + &Hide - + Play &Face Down - + Toggle &normal untapping - - &Flip - - - - + &Peek at card face - + &Clone - + Attac&h to card... - + Unattac&h - + &Draw arrow... - + &Increase power - + &Decrease power - + I&ncrease toughness - + D&ecrease toughness - + In&crease power and toughness - + Dec&rease power and toughness - + Set &power and toughness... - + &Set annotation... - + Red Blood red - + Yellow Fool's gold - + Green Emerald green - + X cards from the top of library... - - + + C&reate another %1 token - + Create tokens - - - - - - + + + + + + Token: - + Place card X cards from top of library - + How many cards from the top of the deck should this card be placed: - - + + View related cards + + + + + Attach to - + All tokens - + View top cards of library View top cards o' library - + &Tap / Untap + Turn sideways or back again - + + T&urn Over + Turn face up/face down + + + + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... - + Number of cards: Number o' cards: - + Draw cards - - - - - + + + + + Number: - + Move top cards to grave - + Move top cards to exile - + Roll die - + Number of sides: - + Set power/toughness - + Please enter the new PT: - + Set annotation - + Please enter the new annotation: - + Set counters Set pieces o' eight @@ -3892,37 +3930,37 @@ Cockatrice will now reload the card database. QMenuBar - + Services - + Hide %1 - + Hide Others - + Show All - + Preferences... - + Quit %1 - + About %1 @@ -3930,18 +3968,18 @@ Cockatrice will now reload the card database. QObject - + Cockatrice card database (*.xml) - + All files (*.*) - + Cockatrice replays (*.cor) @@ -4058,10 +4096,15 @@ Cockatrice will now reload the card database. SequenceEdit - + Shortcut already in use + + + Invalid key + + SetsModel @@ -4091,6 +4134,21 @@ Cockatrice will now reload the card database. + + ShortcutsSettings + + + Your configuration file contained invalid shortcuts. +Please check your shortcut settings! + + + + + The following shortcuts have been set to default: + + + + ShortcutsTab @@ -4104,12 +4162,12 @@ Cockatrice will now reload the card database. - + Clear all default shortcuts - + Do you really want to clear all shortcuts? @@ -4135,27 +4193,27 @@ Cockatrice will now reload the card database. SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings Cap'n's orders for t' shanties - + Master volume @@ -4163,48 +4221,48 @@ Cockatrice will now reload the card database. SpoilerBackgroundUpdater - + Spoilers season has ended - + Deleting spoiler.xml. Please run Oracle - - + + Spoilers download failed - + No internet connection - + Error - + Spoilers already up to date - + No new spoilers added - + Spoilers have been updated! - + Last change: @@ -4213,7 +4271,7 @@ Cockatrice will now reload the card database. StableReleaseChannel - Stable releases + Stable Releases @@ -4278,231 +4336,246 @@ Cockatrice will now reload the card database. TabDeckEditor - + &Clear all filters - + Delete selected - + Deck &name: Flagship &name: - + &Comments: - + Hash: - + &New deck Freshly formed flagship - + &Load deck... Find a flagship... - + &Save deck Record flag&ship - + Save deck &as... Record flagship &as... - + Load deck from cl&ipboard... Find a flagship from t' cl&ipplank - + &Print deck... &Print flagship... - + Search by card name - + + Add to Deck + + + + + Add to Sideboard + + + + + Show Related cards + + + + Save deck to clipboard - + Annotated - + Not Annotated - + &Send deck to online service - + Create decklist (decklist.org) - + Analyze deck (deckstats.net) - + Analyze deck (tappedout.net) - + &Close - + Add card to &maindeck Add card to t' &mainplank - + Add card to &sideboard - + &Remove row - + &Increment number - + &Decrement number - + &Deck Editor &Dock - - + + Card Info - - + + Deck - - + + Filters - + &View - - - + + + Visible - - - + + + Floating - + Reset layout - + Deck: %1 Flagship: %1 - + Are you sure? - + The decklist has been modified. Do you want to save the changes? - + Load deck Find a flagship - - - - - + + + + + Error Cap'n? Thar be a problem - + The deck could not be saved. T' flagship be failin' to be recordin'! - - + + The deck could not be saved. Please check that the directory is writable and try again. - + Save deck Record flagship - + There are no cards in your deck to be exported - + No deck was selected to be saved. @@ -4599,175 +4672,175 @@ Please enter a name: TabGame - - - + + + Replay - - + + Game - - + + Card Info - - + + Player List - - + + Messages - - + + Replay Timeline - + &Phases - + &Game - + Next &phase - + Next &turn - + &Remove all local arrows - + Rotate View Cl&ockwise - + Rotate View Co&unterclockwise - + Game &information - + &Concede - + &Leave game - + C&lose replay - + &Say: - + &View - - - + + + Visible - - - + + + Floating - + Reset layout - + Concede - + Are you sure you want to concede this game? - + Leave game - + Are you sure you want to leave this game? - + You are flooding the game. Please wait a couple of seconds. - + kicked by game host or moderator - + player left the game - + player disconnected from server - + reason unknown - + You have been kicked out of the game. @@ -5125,22 +5198,22 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Are you sure? - + There are still open games. Are you sure you want to quit? - + 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. @@ -5148,38 +5221,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 to moderator. 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) @@ -5217,6 +5290,21 @@ Please refrain from engaging in this activity or further actions may be taken ag + + TipsOfTheDay + + + File does not exist. + + + + + + Failed to open file. + + + + UpdateDownloader @@ -5541,42 +5629,42 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings - + Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating - + &Double-click cards to play them (instead of single-click) - + &Play all nonlands onto the stack (not the battlefield) by default - + Annotate card text on tokens - + Animation settings Cap'n's orders for t' things that be movin' - + &Tap/untap animation @@ -5646,97 +5734,127 @@ Please refrain from engaging in this activity or further actions may be taken ag WndSets - + Move selected set to the top Move selected fleet to the top - + Move selected set up Move selected fleet up - + Move selected set down Move selected fleet down - + Move selected set to the bottom Move selected fleet to the bottom - - Enable all sets - Enable all fleets - - - - Disable all sets - Disable all fleets - - - - Enable selected set(s) + + Search by set name, code, or type - - Disable selected set(s) + + Default order - - Deck Editor - - - - - Only cards in enabled sets will appear in the deck editor card list - - - - - Card Art - - - - - Image priority is decided in the following order - - - - - The - - - - - CUSTOM Folder - - - - - Enabled Sets (Top to Bottom) + + Restore original art priority order + Enable all sets + Enable all fleets + + + + Disable all sets + Disable all fleets + + + + Enable selected set(s) + + + + + Disable selected set(s) + + + + + Deck Editor + + + + + Only cards in enabled sets will appear in the deck editor card list + + + + + Card Art + + + + + Image priority is decided in the following order + + + + + The + + + + + CUSTOM Folder + + + + + Enabled Sets (Top to Bottom) + + + + Disabled Sets (Top to Bottom) - + + Warning: + + + + + While the set list is sorted by any of the columns, custom art priority setting is disabled. + + + + + To disable sorting click on the same column header again until this message disappears. + + + + Manage sets - + Success - + The sets database has been saved successfully. @@ -5767,7 +5885,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English English, arr! (Pirate English) @@ -5840,97 +5958,113 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Load deck (clipboard) - + Clear all filters - + New deck - + Clear selected filter - + Open custom pic folder - + Close - + Print deck - + Delete card - + Edit tokens - + Reset layout - + Add card - + Save deck - + Remove card - + Save deck as - + Load deck - - + + Counters - + Life - + + + + + + + + + + + + + + Set + + + - + @@ -5938,16 +6072,15 @@ Please refrain from engaging in this activity or further actions may be taken ag - - Set + + Add - - + @@ -5955,455 +6088,440 @@ Please refrain from engaging in this activity or further actions may be taken ag - Add - - - - - - - - - - - - - - - + Remove - + Red - + Green - + Yellow - + Storm - + W - + U - + B - + R - + G - + X - + Main Window | Deck Editor - + Power / Toughness - + Power and Toughness - + Add (+1/+1) - + Remove (-1/-1) - + Toughness - + Remove (-0/-1) - + Add (+0/+1) - + Power - + Remove (-1/-0) - + Add (+1/+0) - + Game Phases - + Untap - + Upkeep - - + + Draw - + Main 1 - + Start combat - + Attack - + Block - + Damage - + End combat - + Main 2 - + End - + Next phase - + Next turn - + Playing Area - + Manage sets - + Export deck - + Save deck (clip) - + Save deck (clip; no annotations) - + Tap / Untap Card - + Untap all - + Toggle untap - + Flip card - + Peek card - + Play card - + Attach card - + Unattach card - + Clone card - + Create token - + Create all related tokens - + Create another token - + Set annotation - + Phases | P/T | Playing Area - + Move card to - + Bottom library - + Top library - - + + Graveyard - - + + Exile - + Hand - + View - + Library - + Tops card of library - + Sideboard - + Close recent view - + Game Lobby - + Load remote deck - + Load local deck - + Gameplay - + Draw arrow - + Leave game - + Remove local arrows - + Concede - + Parley - + Roll dice - + Toss da bones - + Rotate view CW - + Shuffle library - + Rotate view CCW - + Mulligan - + Draw card - + Draw cards - + Undo draw - + Always reveal top card - + Draw | Move | View | Gameplay - + How to set custom shortcuts - + Restore all default shortcuts - + Clear all shortcuts diff --git a/cockatrice/translations/cockatrice_fr.ts b/cockatrice/translations/cockatrice_fr.ts index 58b1cf7c7..5cc54b167 100644 --- a/cockatrice/translations/cockatrice_fr.ts +++ b/cockatrice/translations/cockatrice_fr.ts @@ -193,22 +193,22 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Beta Releases - + version bêta No reply received from the release update server. - + Le serveur de mise à jour ne répond pas. Invalid reply received from the release update server. - + Réponse reçue du serveur de mise à jour de version invalide . No reply received from the file update server. - + Le serveur de mise à jour de fichier ne répond pas. @@ -442,47 +442,47 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Update Spoilers - + mettre à jour les spoilers Updating... - + Mise à jours Choose path - + Choisir le chemin Spoilers - + Spoilers Download Spoilers Automatically - + Télécharger automatiquement les spoilers Spoiler Location: - + Emplacement des spoilers: Hey, something's here finally! - + Hey, quelque chose est enfin là! Last Updated - + dernière mise à jour Spoilers download automatically on launch - + spoilers téléchargé automatiquement au lancement @@ -585,7 +585,7 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Refresh the server list with known public servers - + Actualiser la liste de serveurs avec des serveurs publics connus @@ -625,33 +625,33 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa If you have any trouble connecting or registering then contact the server staff for help! - + Si vous rencontrez des difficultés pour vous connecter ou vous inscrire, contactez le personnel du serveur pour obtenir de l'aide! Webpage - + page internet Forgot Password - + Mot de passe oublié &Connect - + &Connexion Server Contact - + Contact du serveur Connect to Server - + Connecter au serveur @@ -686,7 +686,7 @@ Cette information sera consultable uniquement par les modérateurs et ne sera pa Downloading... - + Téléchargement @@ -1041,7 +1041,8 @@ Pour enlever votre avatar actuel, confirmez sans choisir une nouvelle image. 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. - + Le nom choisi est en conflit avec une carte ou un jeton existant. +Assurez-vous d'activer les set de 'jeton' dans le menu "gérer les sets" afin de les afficher correctement. @@ -1536,17 +1537,17 @@ Voulez vous changer les paramètres d'emplacement de base de données ? Next - + suivant Previous - + précédent Tip of the Day - + Conseil du jour @@ -1637,7 +1638,7 @@ Please visit the download page to update manually. Released - + Sortie @@ -1715,7 +1716,7 @@ Vous devez compiler les sources vous-même. Clear log when closing - + Effacer le journal lors de la fermeture @@ -1728,7 +1729,7 @@ Vous devez compiler les sources vous-même. Type your filter here - + Tapez votre filtre ici @@ -2041,7 +2042,7 @@ Vous devez compiler les sources vous-même. Show tips on startup - + Afficher les astuces au démarrage @@ -2589,7 +2590,7 @@ La version locale est %1, la nouvelle version est %2. &Tip of the Day - + &Conseil du jour @@ -2764,14 +2765,14 @@ Cockatrice va maintenant recharger de nouveau la base de données de cartes. &Manage sets... - + &Gérer les sets... 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. - + Salut! On dirait que vous utilisez cette version de Cockatrice pour la première fois. Tous les sets de la base de données de cartes ont été activés. Pour plus d'informations sur la modification de l'ordre des sets ou la désactivation de sets spécifiques et de leurs effets, consultez le menu "Gérer les sets". @@ -2920,12 +2921,12 @@ Cockactrice va maintenant recharger la base de données de cartes. %1 turns %2 face-down. - + %1 tourner %2 face cachée. %1 turns %2 face-up. - + %1 tourner %2 face visible. @@ -3862,7 +3863,7 @@ Cockactrice va maintenant recharger la base de données de cartes. View related cards - + Voir les cartes associées @@ -3890,7 +3891,7 @@ Cockactrice va maintenant recharger la base de données de cartes. T&urn Over Turn face up/face down - + Retourner @@ -4148,7 +4149,7 @@ Cockactrice va maintenant recharger la base de données de cartes. Invalid key - + Clé invalide @@ -4185,13 +4186,14 @@ Cockactrice va maintenant recharger la base de données de cartes. Your configuration file contained invalid shortcuts. Please check your shortcut settings! - + Votre fichier de configuration contenait des raccourcis invalides. Merci de vérifier vos paramètres de raccourci ! The following shortcuts have been set to default: - + Les raccourcis suivants ont été définis par défaut: + @@ -4268,48 +4270,48 @@ Please check your shortcut settings! Spoilers season has ended - + La saison des spoilers est terminée Deleting spoiler.xml. Please run Oracle - + Suppression de spoiler.xml. S'il vous plaît exécuter Oracle Spoilers download failed - + Échec du téléchargement des spoilers No internet connection - + Pas de connexion Internet Error - + Erreur Spoilers already up to date - + Spoilers déjà à jour No new spoilers added - + Aucun nouveau spoilers ajouté Spoilers have been updated! - + Les spoilers ont été mis à jour! Last change: - + Dernier changement: @@ -4317,7 +4319,7 @@ Please check your shortcut settings! Stable Releases - + Version stable @@ -4438,57 +4440,57 @@ Please check your shortcut settings! Search by card name - + Rechercher par nom de carte Add to Deck - + Ajouter au Deck Add to Sideboard - + Ajouter à la réserve Show Related cards - + Afficher les cartes associées Save deck to clipboard - + Enregistrer le deck dans le presse-papier Annotated - + Annoté Not Annotated - + sans annotation &Send deck to online service - + & Envoyer le Deck au service en ligne Create decklist (decklist.org) - + Créer une liste de deck (decklist.org) Analyze deck (deckstats.net) - + Analyser le Deck (deckstats.net) Analyze deck (tappedout.net) - + Analyser le deck (tappedout.net) @@ -4619,12 +4621,12 @@ Vérifiez que le répertoire ne soit pas en lecture seule et réessayez. There are no cards in your deck to be exported - + Il n'y a pas de cartes dans le deck à exporter No deck was selected to be saved. - + Aucun deck n'a été sélectionné pour être sauvegardé. @@ -5077,7 +5079,7 @@ Plus vous entrez d'informations, meilleurs seront les résultats. Private message from - + Message privé de @@ -5350,13 +5352,15 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre File does not exist. - + Le fichier n'existe pas. + Failed to open file. - + Échec de l'ouverture du fichier. + @@ -5810,17 +5814,17 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Search by set name, code, or type - + Rechercher par ensemble de nom, code ou type Default order - + Ordre par défaut Restore original art priority order - + Restaurer l'ordre originale de priorité artistique @@ -5885,22 +5889,22 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Warning: - + Attention: While the set list is sorted by any of the columns, custom art priority setting is disabled. - + lorsque la liste est triée dans une des colonnes, la priorité personnalisée est désactivée. To disable sorting click on the same column header again until this message disappears. - + Pour désactiver le tri, cliquez à nouveau sur la même en-tête de colonne jusqu'à ce que ce message disparaisse. Manage sets - + Gérer les sets @@ -6330,22 +6334,22 @@ Merci de ne pas recommencer ou d'autres mesures peuvent être prises contre Manage sets - + Gérer les sets Export deck - + Exporter le Deck Save deck (clip) - + Sauvegarder le Deck (du presse-papier) Save deck (clip; no annotations) - + Enregistrer le Deck (du presse papier; sans annotations) diff --git a/cockatrice/translations/cockatrice_ja.ts b/cockatrice/translations/cockatrice_ja.ts index 02011a2fa..d6c6f13e5 100644 --- a/cockatrice/translations/cockatrice_ja.ts +++ b/cockatrice/translations/cockatrice_ja.ts @@ -267,7 +267,7 @@ This is only saved for moderators and cannot be seen by the banned person. Unknown card: - + 謎のカード: @@ -585,7 +585,7 @@ This is only saved for moderators and cannot be seen by the banned person. Refresh the server list with known public servers - + デフォルトの公開サーバーリストに更新します @@ -625,33 +625,33 @@ This is only saved for moderators and cannot be seen by the banned person. If you have any trouble connecting or registering then contact the server staff for help! - + 接続や登録に問題がある場合は、サーバー管理者にお問い合わせください。 Webpage - + ウェブページ Forgot Password - + パスワード忘れちゃった &Connect - + 接続 Server Contact - + サーバーの連絡先 Connect to Server - + サーバーに接続 @@ -1030,7 +1030,7 @@ To remove your current avatar, confirm without choosing a new image. Please enter the name of the token: - トークン名を入力してください: + トークン名を入力: @@ -1771,7 +1771,7 @@ You may have to build from source yourself. The game does not exist any more. - このゲームは既に存在しません。 + このゲームはすでに存在しません。 @@ -2818,12 +2818,12 @@ Cockatrice will now reload the card database. %1 is now watching the game. - %1が観戦に参加した。 + %1 が観戦に参加した。 %1 has loaded a deck (%2). - %1はデッキをロードした。ハッシュ:%2 + %1 はデッキをロードした。ハッシュ:%2 @@ -2889,57 +2889,57 @@ Cockatrice will now reload the card database. %1 gives %2 control over %3. - %1は、%2に《%3》のコントロールを渡した。 + %1 は %2 に %3 のコントロールを渡した。 %1 puts %2 into play tapped%3. - %1は《%2》を%3タップ状態でプレイした。 + %1 は %2 を%3タップ状態でプレイした。 %1 puts %2 into play%3. - %1は《%2》を%3プレイした。 + %1 は %2 を%3プレイした。 %1 exiles %2%3. - %1は《%2》を%3追放した。 + %1 は %2 を%3追放した。 %1 puts %2%3 into their library %4 cards from the top. - %1は《%2》を%3ライブラリーの一番上から%4枚目に置いた。 + %1 は %2 を%3ライブラリーの一番上から%4枚目に置いた。 %1 moves %2%3 to sideboard. - %1は《%2》を%3サイドボードに置いた。 + %1 は %2 を%3サイドボードに置いた。 %1 plays %2%3. - %1は《%2》を%3プレイした。 + %1 は %2 を%3プレイした。 %1 turns %2 face-down. - %1は《%2》を裏向きにした。 + %1 は %2 を裏向きにした。 %1 turns %2 face-up. - %1は《%2》を表向きにした。 + %1 は %2 を表向きにした。 %1 has left the game (%2). - %1はゲームから離脱した(%2)。 + %1 はゲームから離脱した(%2)。 %1 is not watching the game any more (%2). - %1が観戦から離脱した(%2)。 + %1 が観戦から離脱した(%2)。 @@ -3004,7 +3004,7 @@ Cockatrice will now reload the card database. %1's turn. - ■%1のターン■ + ■ %1 のターン■ @@ -3024,12 +3024,12 @@ Cockatrice will now reload the card database. %1 is now keeping the top card %2 revealed. - %1は、%2一番上のカードを公開した状態でゲームをプレイしている。 + %1 は%2一番上のカードを公開した状態でゲームをプレイしている。 %1 is not revealing the top card %2 any longer. - %1は、%2一番上のカードの公開を終えた。 + %1 は%2一番上のカードの公開を終えた。 @@ -3039,47 +3039,47 @@ Cockatrice will now reload the card database. %1 has joined the game. - %1がゲームに参加した。 + %1 がゲームに参加した。 %1 is ready to start the game. - %1はゲーム開始の準備が完了した!! + %1 はゲーム開始の準備が完了した!! %1 is not ready to start the game any more. - %1は準備完了を解除した。 + %1 は準備完了を解除した。 %1 has locked their sideboard. - %1はサイドボードをロックした。 + %1 はサイドボードをロックした。 %1 has unlocked their sideboard. - %1はサイドボードを解禁した。 + %1 はサイドボードを解禁した。 %1 has conceded the game. - %1が投了した!!! + %1 が投了した!!! %1 has restored connection to the game. - %1がゲームに再接続した。 + %1 がゲームに再接続した。 %1 has lost connection to the game. - %1はゲームから切断された。 + %1 はゲームから切断された。 %1 shuffles %2. - %1は%2を切り直した。 + %1 は%2を切り直した。 @@ -3094,27 +3094,27 @@ Cockatrice will now reload the card database. %1 flipped a coin. It landed as %2. - %1はコインを投げた。結果は……【%2】だ! + %1 はコインを投げた。結果は……【%2】だ! %1 rolls a %2 with a %3-sided die. - %1は%3面ダイスをふり、【%2】を出した。 + %1 は%3面ダイスをふり、【%2】を出した。 %1 draws %2 card(s). - %1は%2枚カードを引いた。 + %1 は%2枚カードを引いた。 %1 undoes their last draw. - %1は最後に引いたカードを戻した。 + %1 は最後に引いたカードを戻した。 %1 undoes their last draw (%2). - %1は最後に引いたカード (《%2》) を戻した 。 + %1 は最後に引いたカード ( %2 ) を戻した 。 @@ -3154,202 +3154,202 @@ Cockatrice will now reload the card database. %1 puts %2%3 into their graveyard. - %1は《%2》を%3墓地に置いた。 + %1 は %2 を%3墓地に置いた。 %1 moves %2%3 to their hand. - %1は《%2》を%3手札に加えた。 + % 1は %2 を%3手札に加えた。 %1 puts %2%3 into their library. - %1は《%2》を%3ライブラリーに加えた。 + %1 は %2 を%3ライブラリーに加えた。 %1 puts %2%3 on bottom of their library. - %1は《%2》を%3ライブラリーの一番下に置いた。 + %1 は %2 を%3ライブラリーの一番下に置いた。 %1 puts %2%3 on top of their library. - %1は《%2》を%3ライブラリーの一番上に置いた。 + %1 は %2 を%3ライブラリーの一番上に置いた。 %1 takes a mulligan to %2. - %1は%2枚にマリガンした。 + %1 は%2枚にマリガンした。 %1 draws their initial hand. - %1は初期手札を引いた。 + %1 は初期手札を引いた。 %1 destroys %2. - %1は《%2》を取り除いた。 + %1 は %2 を取り除いた。 %1 unattaches %2. - %1は《%2》をはずした。 + %1 は %2 をはずした。 %1 creates token: %2%3. - %1はトークン:《%2》を%3作成した。 + %1 はトークン: %2 を%3作成した。 %1 points from their %2 to themselves. - %1は《%2》から自分自身へ対象を指定した。 + %1 は %2 から自分自身へ対象を指定した。 %1 points from their %2 to %3. - %1は、《%2》から%3へ対象を指定した。 + %1 は %2 から %3 へ対象を指定した。 %1 points from %2's %3 to themselves. - %1は、%2の《%3》から自分自身へ対象を指定した。 + %1 は %2 の %3 から自分自身へ対象を指定した。 %1 points from %2's %3 to %4. - %1は、%2の《%3》から%4へ対象を指定した。 + %1 は %2 の %3 から %4 へ対象を指定した。 %1 points from their %2 to their %3. - %1は、《%2》から%3へ対象を指定した。 + %1 は %2 から %3 へ対象を指定した。 %1 points from their %2 to %3's %4. - %1は、《%2》から%3の《%4》へ対象を指定した。 + %1 は %2 から %3 の %4 へ対象を指定した。 %1 points from %2's %3 to their own %4. - %1は、%2の《%3》から《%4》へ対象を指定した。 + %1 は %2 の %3 から %4 へ対象を指定した。 %1 points from %2's %3 to %4's %5. - %1は、%2の《%3》から%4の《%5》へ対象を指定した。 + %1 は %2 の %3 から %4 の %5 へ対象を指定した。 %1 places %2 %3 counter(s) on %4 (now %5). - %1は《%4》の上に%3カウンターを%2個置いた (計%5個) 。 + %1 は %4 の上に%3カウンターを%2個置いた (計%5個) 。 %1 removes %2 %3 counter(s) from %4 (now %5). - %1は《%4》の上から%3カウンターを%2個取り除いた (計%5個) 。 + %1 は %4 の上から%3カウンターを%2個取り除いた (計%5個) 。 %1 taps their permanents. - %1は自分のコントロールするパーマネントをタップした。 + %1 は自分のコントロールするパーマネントをタップした。 %1 untaps their permanents. - %1は自分のコントロールするパーマネントをアンタップした。 + %1 は自分のコントロールするパーマネントをアンタップした。 %1 taps %2. - %1は《%2》をタップした。 + %1 は %2 をタップした。 %1 untaps %2. - %1は《%2》をアンタップした。 + %1 は %2 をアンタップした。 %1 sets counter %2 to %3 (%4%5). - %1は%2カウンターを%3に設定した (%4%5)。 + %1 は%2カウンターを%3に設定した (%4%5)。 %1 sets %2 to not untap normally. - %1は《%2》をアンタップ・ステップの間にアンタップしないようにした。 + %1 は %2 をアンタップ・ステップの間にアンタップしないようにした。 %1 sets %2 to untap normally. - %1は《%2》を通常どおりアンタップするように設定した。 + %1 は %2 を通常どおりアンタップするように設定した。 %1 sets PT of %2 to %3. - %1は《%2》のP/Tを%3にした。 + %1 は %2 のP/Tを%3にした。 %1 sets annotation of %2 to %3. - %1は《%2》に注釈をつけた (%3)。 + %1 は %2 に注釈をつけた (%3)。 %1 is looking at %2. - %1は%2を見ている。 + %1 は%2を見ている。 %1 is looking at the top %2 card(s) %3. - %1は%3上から%2枚のカードを見ている。 + %1 は%3上から%2枚のカードを見ている。 %1 stops looking at %2. - %1は%2を見るのをやめた。 + %1 は%2を見るのをやめた。 %1 reveals %2 to %3. - %1は、%3に%2を見せた。 + %1 は %3 に%2を見せた。 %1 reveals %2. - %1は%2を公開した。 + %1 は%2を公開した。 %1 randomly reveals %2%3 to %4. - %1は%3無作為に《%2》を選び、%4に見せた。 + %1 は%3無作為に %2 を選び、%4に見せた。 %1 randomly reveals %2%3. - %1は%3無作為に《%2》を選び、公開した。 + %1 は%3無作為に %2 を選び、公開した。 %1 peeks at face down card #%2. - %1は裏向きのカード#%2の表面を確認した。 + %1 は裏向きのカード#%2の表面を確認した。 %1 peeks at face down card #%2: %3. - %1は裏向きのカード#%2の表面を確認した (《%3》) 。 + %1 は裏向きのカード#%2の表面を確認した ( %3 ) 。 %1 reveals %2%3 to %4. - %1は%3《%2》を%4に見せた。 + %1 は%3 %2 を %4 に見せた。 %1 reveals %2%3. - %1は%3《%2》を公開した + %1 は%3 %2 を公開した。 @@ -3515,7 +3515,7 @@ Cockatrice will now reload the card database. Player "%1" - プレイヤー "%1" + プレイヤー: %1 @@ -3836,7 +3836,7 @@ Cockatrice will now reload the card database. C&reate another %1 token - 別の%1トークンを出す + 別の %1 トークンを出す @@ -4274,7 +4274,7 @@ Please check your shortcut settings! Spoilers season has ended - + スポイラーシーズンが終了しました @@ -5063,7 +5063,7 @@ The more information you put in, the more specific your results will be. Private &chat - プライベートチャット + 個人チャット @@ -5073,7 +5073,7 @@ The more information you put in, the more specific your results will be. %1 - Private chat - %1 - プライベートチャット + %1 - 個人チャット @@ -5893,17 +5893,17 @@ Please refrain from engaging in this activity or further actions may be taken ag Warning: - + 警告: While the set list is sorted by any of the columns, custom art priority setting is disabled. - + セットリストがいずれかの列でソートされている間、セット読み込み順の優先度設定は無効です。 To disable sorting click on the same column header again until this message disappears. - + このメッセージが消えるまで同じ列見出しをクリックし続けると、ソートを無効にすることができます。 diff --git a/cockatrice/translations/cockatrice_ru.ts b/cockatrice/translations/cockatrice_ru.ts index 709166f7e..310765244 100644 --- a/cockatrice/translations/cockatrice_ru.ts +++ b/cockatrice/translations/cockatrice_ru.ts @@ -20,62 +20,62 @@ AppearanceSettingsPage - + Theme settings - Выбор темы + Настройки темы - + Current theme: - Тема + Текущая тема: - + Card rendering Отрисовка карт - + Display card names on cards having a picture - Отображать название карты поверх изображения + Отображать название карты вместе с изображением - + Scale cards on mouse over Увеличивать карты при наведении мыши - + 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: Максимальный размер шрифта для текста карт: @@ -188,6 +188,29 @@ This is only saved for moderators and cannot be seen by the banned person.Необходимо ввести ID клиента при выборе опции бана по ID. + + BetaReleaseChannel + + + Beta Releases + Бета-версии + + + + No reply received from the release update server. + Нет ответа от сервера обновления версий. + + + + Invalid reply received from the release update server. + Неправильный ответ от сервера обновления версий. + + + + No reply received from the file update server. + Нет ответа от сервера обновления файлов. + + CardDatabaseModel @@ -242,32 +265,37 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoText - + + Unknown card: + Неизвестная карта: + + + Name: Название: - + Mana cost: Мановая стоимость: - + Color(s): Цвет(а): - + Card type: Тип: - + P / T: Сила/Выносливость: - + Loyalty: Преданность: @@ -411,71 +439,71 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorSettingsPage - - + + Update Spoilers - + Обновить спойлеры - - Updating Spoilers - + + Updating... + Обновление... - + Choose path - + Выбрать путь - + Spoilers - + Спойлеры - + Download Spoilers Automatically - + Скачивать спойлеры автоматически - + Spoiler Location: - + Расположение спойлеров: - + Hey, something's here finally! - + Хей, здесь что-то наконец есть! - + Last Updated - + Последнее обновление - + Spoilers download automatically on launch - + Спойлеры скачиваются автоматически при запуске - + Press the button to manually update without relaunching - + Нажмите, чтобы обновить вручную без перезапуска - + Do not close settings until manual update complete - + Не закрывать настройки до завершения ручного обновления DeckListModel - + Number Номер - + Card Название @@ -537,136 +565,129 @@ This is only saved for moderators and cannot be seen by the banned person.Выбранный файл не может быть загружен - - DevReleaseChannel - - - Development snapshots - Снимки разработчика - - - - No reply received from the release update server. - Сервер обновлений версии не отвечает. - - - - Invalid reply received from the release update server. - Ошибка отклика сервера обновлений версии. - - - - No reply received from the file update server. - Сервер обновлений файлов не отвечает. - - DlgConnect - + New Host Новый хост - + &Host: &Хост: - + Known Hosts Известные Хосты + Refresh the server list with known public servers + + + + Name: Название: - + &Port: &Порт: - + Player &name: &Имя пользователя: - + P&assword: П&ароль: - + &Save password &Сохранить пароль - + A&uto connect Присоединяться автоматически - + Automatically connect to the most recent login when Cockatrice opens Автоматически подключится с чаще запускаемым логином при открытии Кокатрис - - Public Servers - + + If you have any trouble connecting or registering then contact the server staff for help! + Если у вас есть любая проблема с присоединением или регистрацией - свяжитесь с сотрудниками для помощи! - - Forgot password + + + Webpage + Веб-страница + + + + Forgot Password Забыли пароль - - Connect - Соединение + + &Connect + &Подключиться - - Cancel - Отменить + + Server Contact + - + + Connect to Server + Подключиться к серверу + + + Server Сервер - + Login Логин - - Connect to server - Соединение - - - + Connection Warning Предупреждение при соединении - + You need to name your new connection profile. Назовите ваш профиль для соединения. - + Connect Warning Ошибка при соединении - + The player name can't be empty. Необходимо указать имя пользователя. + + + Downloading... + Загрузка... + DlgCreateGame @@ -858,7 +879,7 @@ This is only saved for moderators and cannot be seen by the banned person.DlgEditAvatar - + No image chosen. Изображение не выбрано. @@ -880,17 +901,17 @@ To remove your current avatar, confirm without choosing a new image. Сменить аватар - + Open Image Открыть изображение - + Image Files (*.png *.jpg *.bmp) Файлы изображений (*.png *.jpg *.bmp) - + Invalid image chosen. Выбрано не подходящее изображение. @@ -1370,12 +1391,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgSettings - + Unknown Error loading card database Неизвестная ошибка при загрузке базы карт. - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -1389,7 +1410,7 @@ Cockatrice не может корректно работать с испорче Вы хотите сменить расположение файла базы карт? - + Your card database version is too old. This can cause problems loading card information or images @@ -1403,7 +1424,7 @@ Would you like to change your database location setting? Вы хотите сменить расположение файла базы карт? - + Your card database did not finish loading Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -1414,7 +1435,7 @@ Would you like to change your database location setting? Вы хотите сменить расположение файла базы карт? - + File Error loading your card database. Would you like to change your database location setting? @@ -1422,7 +1443,7 @@ 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? @@ -1430,7 +1451,7 @@ Would you like to change your database location setting? Вы хотите сменить расположение файла базы карт? - + Unknown card database load status Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues @@ -1441,63 +1462,81 @@ 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 Интерфейс - + Deck Editor Редактор колод - + Chat Чат - + Sound Звук - + Shortcuts Горячие клавиши + + DlgTipOfTheDay + + + Next + + + + + Previous + + + + + Tip of the Day + + + DlgUpdate @@ -1834,32 +1873,32 @@ You may have to build from source yourself. Возраст - + Type Тип - + Description Описание - + Creator Создатель - + Restrictions Ограничения - + Players Количество игроков - + Spectators Зрители @@ -1867,202 +1906,189 @@ You may have to build from source yourself. GeneralSettingsPage - - - - - + + + + + Choose path Путь - + Success Выполнено - + Downloaded card pictures have been reset. Загруженные картинки были обновлены - + Error Ошибка - + One or more downloaded card pictures could not be cleared. Одно или более загруженных изображений карт не может быть удалено. - + Personal settings Настройки пользователя - + Language: Язык: - + Download card pictures on the fly Загружать изображения карт автоматически - + Paths (editing disabled in portable mode) - + Paths Пути расположения - + Decks directory: Колоды: - + Replays directory: Повторы: - + Pictures directory: Изображения карт: - + Card database: База данных карт: - + Token database: База данных токенов: - + Picture cache size: Размер кеша изображений: - + Primary download URL: Основной путь загрузки: - + Fallback download URL: Запасной путь загрузки: - + How to set a custom picture url Как установить пользовательский путь загрузки - + Reset/clear downloaded pictures Обновить/удалить загруженные картинки - + Update channel Обновить канал - + Notify if a feature supported by the server is missing in my client Предупреждать, если функции, используемые сервером, отсутствуют в этом клиенте. - - + + Reset Сбросить - - - Logger - - Client Operating System - - - - - Build Architecture - - - - - Qt Version + + Show tips on startup 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 Ожидаемое время: %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 Ваше подключение было прервано сервером. Причина: %1 - + The server is going to be restarted in %n minute(s). All running games will be lost. Reason for shutdown: %1 @@ -2077,535 +2103,540 @@ Reason for shutdown: %1 Причина перезагрузки: %1 - + Scheduled server shutdown Плановый перерыв в работе сервера - - + + Success Успешно - + Registration accepted. Will now login. Регистрация прошла успешно. Идет соединение. - + Account activation accepted. Will now login. Активация аккаунта прошла успешно. Идет соединение. - + Number of players Количество игроков - + Please enter the number of players. Введите количество игроков. - - + + Player %1 Игрок %1 - + Load replay Загрузить повтор - + About Cockatrice О программе - + Cockatrice Webpage Страница Cockatrice - + Project Manager: Руководитель проекта: - + Past Project Managers: Предыдущие руководители проекта: - + Developers: Разработчики: - + Our Developers Наши разработчики - + Help Develop! Помочь в разработке! - + Translators: Переводчики: - + Help Translate! Помочь с переводом! - + Support: Поддержать проект: - + Report an Issue Сообщить о проблеме - + Troubleshooting Устранение неполадок - + F.A.Q. ЧАВо - - - - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + + + + Error Ошибка - + Server timeout Нет связи с сервером - + Failed Login Не могу подключиться - + 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. Вы забанены до %1. - - + + You are banned indefinitely. Вы забанены бессрочно. - + This server requires user registration. Do you want to register now? На этом сервере требуется регистрация. Вы хотите зарегистрироваться сейчас? - + This server requires client ID's. 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. На этом сервере используется ID клиентов. Ваш клиент не может сгенерировать ID, либо ваш клиент был модифицирован. Пожалуйста, перезапустите клиент и попробуйте снова. - + An internal error has occurred, please try closing and reopening your client and try again. If the error persists try updating your client to the most recent build and if need be contact your software provider. Возникла внутренняя ошибка. Пожалуйста, перезапустите клиент. Если ошибка повторится, попробуйте обновить клиент до последней версии или свяжитесь с разработчиком. - + Account activation Активация аккаунта - + Server Full Сервер заполнен - + Unknown login error: %1 Неизвестная ошибка входа: %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 длиной в %1 - %2 символов - + can %1 contain lowercase characters %1 может содержать строчные буквы - - - + + + NOT НЕ - + can %1 contain uppercase characters %1 может содержать заглавные буквы - + can %1 contain numeric characters %1 может содержать цифры - + can contain the following punctuation: %1 Может содержать следующие символы: %1 - + first character can %1 be a punctuation mark Первый знак %1 может быть знаком пунктуации - + can not contain any of the following words: %1 не может содержать следующие слова: %1 - + can not match any of the following expressions: %1 не может соответствовать следующим выражениям: %1 - + You may only use A-Z, a-z, 0-9, _, ., and - in your username. Имя пользователя может включать только следующие символы: A-Z, a-z, 0-9, "_", "." и "-" - - - - - - + + + + + + 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. При регистрации необходимо указать действующий e-mail. - + Your client seems to be missing features this server requires for connection. Ваш клиент не поддерживает некоторые функции, необходимые для соединения с сервером. - + Version - + Our Translators Наши переводчики - + Your account has not been activated yet. You need to provide the activation token received in the activation email. Ваш аккаунт ещё не активирован. Вам нужно предоставить жетон/код активации, полученный по электронной почте. - + The email address provider used during registration has been blacklisted for use on this server. Домен электронной почты, использованной при регистрации, занесён в чёрный список на этом сервере. - + 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. Регистрация не удалась из-за технических неполадок на сервере. - + Unknown registration error: %1 Неизвестная ошибка при регистрации: %1 - + Account activation failed Не удалось активировать аккаунт - + Socket error: %1 Ошибка сокета: %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. Вы пытаетесь подключиться к несуществующему серверу. Пожалуйста, обновите Cockatrice или выберите другой сервер. Локальная версия %1, удаленная версия %2. - + Your Cockatrice client is obsolete. Please update your Cockatrice version. Local version is %1, remote version is %2. Ваш клиент Cockatrice устарел. Пожалуйста, обновите Cockatrice. Локальная версия %1, удаленная версия %2. - + Connecting to %1... Подключение к %1... - + Registering to %1 as %2... Регистрация на %1 как %2... - + Disconnected Подключение прервано - + Connected, logging in at %1 Соединение установлено, выполняется вход на %1 - - - + + + Requesting forgot password to %1 as %2... Запрашиваем восстановление пароля у %1 от %2... - + &Connect... Подключение... &С - + &Disconnect П&рервать подключение - + Start &local game... &Начать локальную игру... - + &Watch replay... &Смотреть повтор... - + &Deck editor Редактор &колод - + &Full screen Полный экран &F - + &Register to server... &Регистрация на сервере... - + &Settings... &Настройки - - + + &Exit &Выход - + A&ctions &Действия - + &Cockatrice &Cockatrice - + C&ard Database &База карт - + Open custom image folder Открыть папку с изображениями - + Open custom sets folder Открыть папку с изданиями - + Add custom sets/cards Добавить издания/карты - + Edit &tokens... Редактировать &фишки - + &About Cockatrice О про&грамме - + + &Tip of the Day + + + + Check for Client Updates - + View &debug log Просмотреть &журнал отладки - + &Help &Справка - + Check for card updates... Проверить обновления базы карт... - + 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" Cockatrice не может загрузить базу карт. Обновить вашу базу карт сейчас? Если Вы не уверены, или впервые запустили программу, нажмите "Да" - - + + Yes Да - - + + No Нет - + Open settings Открыть настройки - + New sets found Найдены новые издания - + %1 new set(s) found in the card database Set code(s): %2 Do you want to enable it/them? @@ -2614,124 +2645,124 @@ Do you want to enable it/them? Включить их отображение? - + View sets Просмотреть издания - + Welcome Добро пожаловать - - - + + + Information Информация - + A card database update is already running. Обновление базы карт уже идет. - + Unable to run the card database updater: Не удалось запустить обновление базы карт: - + failed to start. ошибка при запуске. - + crashed. прервано. - + timed out. превышен лимит ожидания. - + write error. ошибка записи. - + read error. ошибка чтения. - + unknown error. неизвестная ошибка. - + The card database updater exited with an error: %1 Обновление базы карт прервано с ошибкой: %1 - + Update completed successfully. Cockatrice will now reload the card database. Обновление успешно завершено. Сейчас Cockatrice перезагрузит базу карт. - + You can only import XML databases at this time. На данный момент вы только можете предостовлять базы данных в формате XML. - - - + + + Forgot Password Забыли пароль - + Your password has been reset successfully, you now may 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. Запрос активации получен. Вам отправлен код активации. Пожалуйста, проверьте свой e-mail. - - - - + + + + Load sets/cards Загрузить издания/карты - + &Manage sets... - + 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. - + 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. @@ -2742,19 +2773,19 @@ To update your client, go to Help -> Check for Updates. Чтобы обновить клиент, перейдите в Справка-> Проверка обновлений. - + Selected file cannot be found. Выбранный файл не найден. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. Новые издания/карты успешно добавлены. Сейчас Cockatrice перезагрузит базу карт. - + Sets/cards failed to import. Не удалось импортировать издания/карты. @@ -2877,6 +2908,16 @@ Cockatrice will now reload the card database. %1 plays %2%3. %1 разыгрывает %2%3. + + + %1 turns %2 face-down. + + + + + %1 turns %2 face-up. + + %1 has left the game (%2). @@ -3132,16 +3173,6 @@ Cockatrice will now reload the card database. %1 draws their initial hand. %1 берет свои стартовые руки. - - - %1 flips %2 face-down. - %1 переворачивает %2 лицом вниз. - - - - %1 flips %2 face-up. - %1 переворачивает %2 лицом вверх. - %1 destroys %2. @@ -3311,79 +3342,79 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Add message Добавить сообщение - + Message: Сообщение: - + Chat settings Настройки чата - + Custom alert words Пользовательские предупреждения - + Enable chat mentions Включить упоминания в чате - + Enable mention completer Включить автозавершение упоминаний - + In-game message macros Макросы сообщений игрового чата - + Ignore chat room messages sent by unregistered users Не уведомлять о сообщениях в чате от незарегистрированных пользователей - + Ignore private messages sent by unregistered 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 Разделять слова пробелом, только для букв и цифр @@ -3449,478 +3480,485 @@ Cockatrice will now reload the card database. Player - + Reveal top cards of library Показать верхние карты библиотеки - + Number of cards: (max. %1) Количество карт: (макс. %1) - + &View graveyard &Посмотреть кладбище - + &View exile По&смотреть карты в изгнании - + Player "%1" Игрок "%1" - - - - + + + + &Graveyard &Кладбище - - - - + + + + &Exile &Изгнание - + &Move hand to... &Поместить руку.. - - - - - - &Top of library - &на верх библиотеки - - + + &Top of library + &на верх библиотеки + + + + + + &Bottom of library &вниз библиотеки - + &Move graveyard to... &Поместить карты кладбища.. - - - - + + + + &Hand Р&ука - + &Move exile to... &Поместить карты из изгнания.. - + &View library Просмортеть &библиотеку - + View &top cards of library... Посмтореть верхние карт&ы... - + Reveal &library to... Показать &библиотеку.. - + Reveal t&op cards to... Показать &верхние карты.. - + &Always reveal top card &Всегда показывать верхнюю карту - + O&pen deck in deck editor &Открыть колоду в редакторе - + &View sideboard Просмотреть &сайд - + &Draw card В&зять карту - + D&raw cards... Вз&ять карты... - + &Undo last draw &Отменить последнее взятие - + Take &mulligan Взять стра&ховку - + &Shuffle Переме&шать - + Play top card &face down Разыграть верхнюю карту &рубашкой вверх - + Move top cards to &graveyard... Поместить верхние карты на кладби&ще... - + Move top cards to &exile... Поместить верхние карты в и&згнание... - + Put top card on &bottom Поместить верхн&юю карту на дно - + Put bottom card &in graveyard Поместить нижнюю карту &на кладбище - + &Reveal hand to... Показать руку игроку.. &R - + Reveal r&andom card to... Показать случайную карту игроку... - + Reveal random card to... - + &Sideboard &Сайд - + &Library &Библиотека - + &Counters &Жетоны - + &Untap all permanents &Развернуть все перманенты - + R&oll die... Бросить &кубик... - + &Create token... Создать &фишку... - + C&reate another token Создать &еще одну фишку - + Cr&eate predefined token Создать готовый токен - + S&ay Ска&зать - + C&ard Ка&рта - + &All players &Все игроки - + &Play Разыграть - + &Hide Скрыть - + Play &Face Down Разыграть рубашкой вверх - + Toggle &normal untapping Переключить &разворот как обычно - - &Flip - &Перевернуть - - - + &Peek at card face Посмотреть &лицевую сторону - + &Clone &Копировать - + Attac&h to card... При&соединить - + Unattac&h &Открепить - + &Draw arrow... &Нарисовать стрелку - + &Increase power Увеличить силу - + &Decrease power Уменьшить силу - + I&ncrease toughness Увеличить защиту - + D&ecrease toughness Уменьшить защиту - + In&crease power and toughness У&величить силу и защиту - + Dec&rease power and toughness У&меньшить силу и защиту - + Set &power and toughness... Установить силу и защиту - + &Set annotation... Добавить пометку - + Red Красный - + Yellow Желтый - + Green Зеленый - + X cards from the top of library... - - + + C&reate another %1 token - + Create tokens - - - - - - + + + + + + Token: Код: - + Place card X cards from top of library - + How many cards from the top of the deck should this card be placed: - - + + View related cards + + + + + Attach to - + All tokens Все фишки - + View top cards of library Просмотр верхних карт - + &Tap / Untap + Turn sideways or back again - + + T&urn Over + Turn face up/face down + + + + &Add counter (%1) &Добавить жетон (%1) - + &Remove counter (%1) &Удалить жетон (%1) - + &Set counters (%1)... &Установить жетоны (%1)... - + Number of cards: Количество: - + Draw cards Взять карты - - - - - + + + + + Number: Количество: - + Move top cards to grave Поместить верхние карты на кладбище - + Move top cards to exile Поместить верхние карты в изгнание - + Roll die Бросить кубик - + Number of sides: Количество граней: - + Set power/toughness Установить Силу/Защиту - + Please enter the new PT: Введите новые Силу/Защиту: - + Set annotation Пометка - + Please enter the new annotation: Введите текст: - + Set counters Установить жетоны @@ -3928,37 +3966,37 @@ Cockatrice will now reload the card database. QMenuBar - + Services Сервисы - + Hide %1 Скрыть %1 - + Hide Others Скрыть остальные - + Show All Показать все - + Preferences... Параметры... - + Quit %1 Выйти %1 - + About %1 О %1 @@ -3966,18 +4004,18 @@ Cockatrice will now reload the card database. QObject - + Cockatrice card database (*.xml) База карт Cockatrice (*.xml) - + All files (*.*) Все файлы (*.*) - + Cockatrice replays (*.cor) Записи игр Cockatrice (*.cor) @@ -4094,10 +4132,15 @@ Cockatrice will now reload the card database. SequenceEdit - + Shortcut already in use Сочетание клавиш уже используется + + + Invalid key + + SetsModel @@ -4127,6 +4170,21 @@ Cockatrice will now reload the card database. Дата релиза + + ShortcutsSettings + + + Your configuration file contained invalid shortcuts. +Please check your shortcut settings! + + + + + The following shortcuts have been set to default: + + + + ShortcutsTab @@ -4140,12 +4198,12 @@ Cockatrice will now reload the card database. Вы действительно хотите восстановить все сочетания клавиш по умолчанию? - + Clear all default shortcuts Очистить все сочетания клавиш по умолчанию - + Do you really want to clear all shortcuts? Вы действительно хотите очистить все сочетания клавиш? @@ -4171,27 +4229,27 @@ Cockatrice will now reload the card database. SoundSettingsPage - + Enable &sounds Включить &звуки - + Current sounds theme: Звуковая тема: - + Test system sound engine Протестировать системный звук - + Sound settings Настройки звука - + Master volume Громкости @@ -4199,48 +4257,48 @@ Cockatrice will now reload the card database. SpoilerBackgroundUpdater - + Spoilers season has ended - + Deleting spoiler.xml. Please run Oracle - - + + Spoilers download failed - + No internet connection - + Error - + Spoilers already up to date - + No new spoilers added - + Spoilers have been updated! - + Last change: @@ -4249,8 +4307,8 @@ Cockatrice will now reload the card database. StableReleaseChannel - Stable releases - Стабильные версии + Stable Releases + @@ -4314,233 +4372,248 @@ Cockatrice will now reload the card database. TabDeckEditor - + &Clear all filters Очистить все фильтры - + Delete selected Удалить выбранное - + Deck &name: Имя колоды: - + &Comments: Комментарии: - + Hash: Хэш: - + &New deck Новая колода - + &Load deck... Загрузить колоду... - + &Save deck Сохранить колоду - + Save deck &as... Сохранить колоду как... - + Load deck from cl&ipboard... Загрузить колоду из буфера... - + &Print deck... Распечатать колоду... - + Search by card name - + + Add to Deck + + + + + Add to Sideboard + + + + + Show Related cards + + + + Save deck to clipboard - + Annotated - + Not Annotated - + &Send deck to online service - + Create decklist (decklist.org) - + Analyze deck (deckstats.net) - + Analyze deck (tappedout.net) - + &Close Закрыть - + Add card to &maindeck Добавить карту в основную колоду - + Add card to &sideboard Добавить карту в сайдборд - + &Remove row Удалить строку - + &Increment number Увеличить количество - + &Decrement number Уменьшить количество - + &Deck Editor Редактор колод - - + + Card Info Информация о карте - - + + Deck Колода - - + + Filters Фильтры - + &View Вид - - - + + + Visible Видимый - - - + + + Floating Плавающий - + Reset layout Сбросить слой - + Deck: %1 Колода: %1 - + Are you sure? Вы уверены? - + The decklist has been modified. Do you want to save the changes? Деклист был изменен. Вы хотите сохранить изменения? - + Load deck Загрузить колоду - - - - - + + + + + Error Ошибка - + The deck could not be saved. Колода не может быть сохранена. - - + + The deck could not be saved. Please check that the directory is writable and try again. Колода не может быть сохранена. Пожалуйста, проверьте расположение файла и попробуйте снова. - + Save deck Сохранить колоду - + There are no cards in your deck to be exported - + No deck was selected to be saved. Для сохранения не выбрано ни одной колоды @@ -4638,175 +4711,175 @@ Please enter a name: TabGame - - - + + + Replay Запись игры - - + + Game Игра - - + + Card Info Информация о карте - - + + Player List Список игроков - - + + Messages Сообщения - - + + Replay Timeline Полоса прокрутки записи - + &Phases &Фазы - + &Game &Игра - + Next &phase Следующая &фаза - + Next &turn Следующий &ход - + &Remove all local arrows &Удалить все указатели - + Rotate View Cl&ockwise Повернуть вид по часовой стрелке - + Rotate View Co&unterclockwise Повернуть вид против часовой стрелки - + Game &information Информация &об игре - + &Concede &Сдаться... - + &Leave game Покинуть и&гру - + C&lose replay З&акрыть повтор - + &Say: Ска&зать: - + &View Вид - - - + + + Visible Видимый - - - + + + Floating Плавающий - + Reset layout Сбросить слой - + Concede Сдаться - + Are you sure you want to concede this game? Вы точно хотите сдаться? - + Leave game Покинуть игру - + Are you sure you want to leave this game? Вы уверены, что хотите уйти? - + You are flooding the game. Please wait a couple of seconds. Кажется, Вы нафлудили. Пожалуйста, подождите пару секунд. - + kicked by game host or moderator - + player left the game игрок покинул игру - + player disconnected from server игрок отсоединился от сервера - + reason unknown причина неизвестна - + You have been kicked out of the game. Вы были отключены от игры @@ -5165,22 +5238,22 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Are you sure? Вы уверены? - + There are still open games. Are you sure you want to quit? У вас все еще есть активные игры. Уверены, что хотите выйти? - + 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. @@ -5191,38 +5264,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 to moderator. 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. Вы получили предупреждение по причине %1. Пожалуйста, воздержитесь от подобных действий, иначе в ваш адрес будут применены дальнейшие меры. При возникновении каких-либо вопросов напишите личное сообщение любому модератору. - + You have received the following message from the server. (custom messages like these could be untranslated) Вы получили сообщение от сервера. @@ -5261,6 +5334,21 @@ Please refrain from engaging in this activity or further actions may be taken ag Невозможно проанализировать колоду + + TipsOfTheDay + + + File does not exist. + + + + + + Failed to open file. + + + + UpdateDownloader @@ -5585,42 +5673,42 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings Основные настройки интерфейса - + Enable notifications in taskbar Включить оповещения в панели задач. - + Notify in the taskbar for game events while you are spectating Уведомлять об игровых событиях в панели задач, когда вы наблюдаете за игрой - + &Double-click cards to play them (instead of single-click) &Двойной клик, чтобы разыграть карту (вместо одинарного) - + &Play all nonlands onto the stack (not the battlefield) by default &Помещать все заклинания в стек при разыгрывании (вместо поля боя) - + Annotate card text on tokens Изменить текст карты на токенах - + Animation settings Настройки анимации - + &Tap/untap animation &Анимировать поворот/разворот карты @@ -5690,97 +5778,127 @@ 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 Переместить выбранное издание в конец. - - Enable all sets - Включить все издания - - - - Disable all sets - Отключить все издания - - - - Enable selected set(s) - Включить выбранные издания - - - - Disable selected set(s) - Отключить выбранные издания - - - - Deck Editor - Редактор колод - - - - Only cards in enabled sets will appear in the deck editor card list - Только карты включенных изданий появятся в редакторе колод - - - - Card Art + + Search by set name, code, or type - - Image priority is decided in the following order + + Default order - - The - - - - - CUSTOM Folder - - - - - Enabled Sets (Top to Bottom) + + Restore original art priority order + Enable all sets + Включить все издания + + + + Disable all sets + Отключить все издания + + + + Enable selected set(s) + Включить выбранные издания + + + + Disable selected set(s) + Отключить выбранные издания + + + + Deck Editor + Редактор колод + + + + Only cards in enabled sets will appear in the deck editor card list + Только карты включенных изданий появятся в редакторе колод + + + + Card Art + + + + + Image priority is decided in the following order + + + + + The + + + + + CUSTOM Folder + + + + + Enabled Sets (Top to Bottom) + + + + Disabled Sets (Top to Bottom) - + + Warning: + + + + + While the set list is sorted by any of the columns, custom art priority setting is disabled. + + + + + To disable sorting click on the same column header again until this message disappears. + + + + Manage sets - + Success Выполнено - + The sets database has been saved successfully. База данных изданий успешно сохранена. @@ -5811,7 +5929,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English Русский (Russian) @@ -5884,97 +6002,113 @@ Please refrain from engaging in this activity or further actions may be taken ag Анализировать колоду - + Load deck (clipboard) Загрузить колоду (буфер) - + Clear all filters Отключить все фильтры - + New deck Новая колода - + Clear selected filter Отключить выбранный фильтр - + Open custom pic folder Открыть папку авторских картинок - + Close Закрыть - + Print deck Распечатать колоду - + Delete card Удалить карту - + Edit tokens Изменить фишки - + Reset layout Переназначить вид - + Add card Добавить карту - + Save deck Сохранить колоду - + Remove card Удалить карту - + Save deck as Сохранить колоду как - + Load deck Загрузить колоду - - + + Counters Жетоны - + Life Жизни - + + + + + + + + + + + + + + Set + Установить С/В + + - + @@ -5982,16 +6116,15 @@ Please refrain from engaging in this activity or further actions may be taken ag - - Set - Установить С/В + + Add + Добавить - - + @@ -5999,455 +6132,440 @@ Please refrain from engaging in this activity or further actions may be taken ag - Add - Добавить - - - - - - - - - - - - - - + Remove Удалить - + Red Красный - + Green Зеленый - + Yellow Желтый - + Storm - + W Белый - + U Синий - + B Черный - + R Красный - + G Зеленый - + X - + Main Window | Deck Editor Главное окно | Редактор - + Power / Toughness Сила/Выносливость - + Power and Toughness Сила и Выносливость - + Add (+1/+1) Добавить (+1/+1) - + Remove (-1/-1) Уменьшить (-1/-1) - + Toughness Выносливость - + Remove (-0/-1) Уменьшить (-0/-1) - + Add (+0/+1) Добавить (+0/+1) - + Power Сила - + Remove (-1/-0) Уменьшить (-1/-0) - + Add (+1/+0) Добавить (+1/+0) - + Game Phases Игровые фазы - + Untap Шаг разворота - + Upkeep Шаг поддержки - - + + Draw Шаг взятия карты - + Main 1 Первая основная фаза - + Start combat Шаг начала боя - + Attack Атака - + Block Блок - + Damage Повреждения - + End combat Конец боя - + Main 2 Вторая основная фаза - + End Конец хода - + Next phase Следующая фаза - + Next turn Предыдущая фаза - + Playing Area Игровое поле - + Manage sets - + Export deck Экспортировать колоду - + Save deck (clip) - + Save deck (clip; no annotations) - + Tap / Untap Card Повернуть / развернуть карту - + Untap all Развернуть все - + Toggle untap Переключить разворот - + Flip card Перевернуть карту - + Peek card Посмотреть карту - + Play card Разыграть карту - + Attach card Прикрепить карту - + Unattach card Открепить карту - + Clone card Копировать карту - + Create token Создать фишку - + Create all related tokens Создать все связанные фишки - + Create another token Создать другую фишку - + Set annotation Добавить пометку - + Phases | P/T | Playing Area Фазы | Сила/Защита | Игровое поле - + Move card to Переместить карту в - + Bottom library Низ библиотеки - + Top library Верх библиотеки - - + + Graveyard Кладбище - - + + Exile Изгнание - + Hand Рука - + View Вид - + Library Библиотека - + Tops card of library Верхние карты библиотеки - + Sideboard Сайдборд - + Close recent view Закрыть предыдущий вид - + Game Lobby Вкладка игры - + Load remote deck Загрузить колоду с сервера - + Load local deck Загрузить &колоду с диска - + Gameplay Процесс игры - + Draw arrow Нарисовать стрелку - + Leave game Покинуть игру - + Remove local arrows Удалить все стрелки - + Concede Сдаться - + Roll dice Бросить кубик - + Rotate view CW Повернуть вид по часовой стрелке - + Shuffle library Перемешать библиотеку - + Rotate view CCW Повернуть вид против часовой стрелки - + Mulligan Муллиган - + Draw card Взять карту - + Draw cards Взять карты - + Undo draw Отменить взятие карты - + Always reveal top card Держать открытой верхнюю карту - + Draw | Move | View | Gameplay Нарисовать | Переместить | Вид | Процесс игры - + How to set custom shortcuts Как установитьт пользовательские сочетания клавиш - + Restore all default shortcuts Восстановить сочетания клавиш по умолчанию - + Clear all shortcuts Очистить все сочетания клавиш diff --git a/cockatrice/translations/cockatrice_sv.ts b/cockatrice/translations/cockatrice_sv.ts index 710be8fce..5773df2b7 100644 --- a/cockatrice/translations/cockatrice_sv.ts +++ b/cockatrice/translations/cockatrice_sv.ts @@ -20,64 +20,64 @@ AppearanceSettingsPage - + Theme settings - + Temainställningar - + Current theme: - + Aktuellt tema: - + Card rendering Kortrendering - + Display card names on cards having a picture Visa kortnamn på kort som har bilder - + Scale cards on mouse over - + Skala kort på musen över - + Hand layout Handlayout - + Display hand horizontally (wastes space) Visa hand horisontellt (slösar plats) - + Enable left justification - + Aktivera vänsterjustering - + Table grid layout Bordets rutnätlayout - + Invert vertical coordinate Invertera vertikal koordinat - + Minimum player count for multi-column layout: Minst antal spelare för multi-kolumnlayout: - + Maximum font size for information displayed on cards: - + Maximal teckenstorlek för information som visas på kort: @@ -95,7 +95,7 @@ ban client I&D - + förbjuda klienten I & D @@ -170,23 +170,46 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. You have to select a name-based, IP-based, clientId based, or some combination of the three to place a ban. - + Du måste välja en namnbaserad, IP-baserad, clientId-baserad eller en kombination av de tre för att lägga ett förbud. You must have a value in the name ban when selecting the name ban checkbox. - + Du måste ha ett värde i namnförbudet när du markerar kryssrutan för namnförbud. You must have a value in the ip ban when selecting the ip ban checkbox. - + Du måste ha ett värde i ip-förbudet när du markerar kryssrutan ip-banan. You must have a value in the clientid ban when selecting the clientid ban checkbox. + Du måste ha ett värde i clientidförbudet när du markerar kryssrutan för klientidförbud. + + + + BetaReleaseChannel + + + Beta Releases + + + No reply received from the release update server. + Inget svar mottaget från uppdateringsservern. + + + + Invalid reply received from the release update server. + Ogiltigt svar mottaget från uppdateringsserveren. + + + + No reply received from the file update server. + Inget svar mottaget från filuppdateringsservern. + CardDatabaseModel @@ -242,32 +265,37 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. CardInfoText - + + Unknown card: + Okänt kort: + + + Name: Namn: - + Mana cost: Manakostnad: - + Color(s): Färg(er): - + Card type: Korttyp: - + P / T: P / T: - + Loyalty: Lojalitet: @@ -387,7 +415,7 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. their sideboard look at zone - + deras skänk @@ -399,7 +427,7 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. their sideboard nominative - + deras skänk @@ -411,71 +439,71 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. DeckEditorSettingsPage - - + + Update Spoilers - + Uppdatera Spoilers - - Updating Spoilers - + + Updating... + Uppdatering ... - + Choose path - + Välj sökväg - + Spoilers - + Download Spoilers Automatically - + Ladda ner Spoilers automatiskt - + Spoiler Location: - + Spoiler Plats: - + Hey, something's here finally! - + Hej, något är här äntligen! - + Last Updated - + Senast uppdaterad - + Spoilers download automatically on launch - + Spoilers laddas ner automatiskt vid lanseringen - + Press the button to manually update without relaunching - + Tryck på knappen för att manuellt uppdatera utan omstart - + Do not close settings until manual update complete - + Stäng inte inställningarna förrän manuell uppdatering är klar DeckListModel - + Number Nummer - + Card Kort @@ -499,12 +527,12 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. Load deck... - + Lastdäck ... Load remote deck... - + Ladda fjärrdäck ... @@ -537,136 +565,129 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. Den valda filen kunde ej laddas. - - DevReleaseChannel - - - Development snapshots - - - - - No reply received from the release update server. - - - - - Invalid reply received from the release update server. - - - - - No reply received from the file update server. - - - DlgConnect - + New Host Ny värd - + &Host: &Värd: - + Known Hosts - + Kända värdar - Name: - + Refresh the server list with known public servers + Uppdatera servern listan med kända offentliga servrar - + + Name: + Namn: + + + &Port: &Port: - + Player &name: Spelar&namn: - + P&assword: &Lösenord: - + &Save password &Spara lösenord - + A&uto connect - + Automatically connect to the most recent login when Cockatrice opens - - - - - Public Servers - - - - - Forgot password - + Koppla automatiskt till den senaste inloggningen när Cockatrice öppnas - Connect - + If you have any trouble connecting or registering then contact the server staff for help! + Om du har problem med att ansluta eller registrera kontaktar du serverns personal för hjälp! - - Cancel - Avbryt + + + Webpage + Webbsida - + + Forgot Password + Glömt ditt lösenord + + + + &Connect + &Ansluta + + + + Server Contact + Serverkontakt + + + + Connect to Server + Anslut till servern + + + Server - + Login - + Logga in - - Connect to server - Anslut till server - - - + Connection Warning - + Anslutning Varning - + You need to name your new connection profile. - + Du måste ange din nya anslutningsprofil. - + Connect Warning Uppkopplingsvarning - + The player name can't be empty. Fältet för spelarnamn kan inte lämnas tomt. + + + Downloading... + Hämtar ... + DlgCreateGame @@ -858,7 +879,7 @@ Detta sparas endast för moderatorer och kan inte ses av den bannlysta personen. DlgEditAvatar - + No image chosen. Ingen bild vald @@ -880,17 +901,17 @@ För att radera din nuvarande avatar, klicka "bekräfta" utan att väl Ändra avatar - + Open Image Öppna bild - + Image Files (*.png *.jpg *.bmp) Bildfiler (*.png *.jpg *.bmp) - + Invalid image chosen. Ogiltigt val av bild. @@ -1020,7 +1041,8 @@ För att radera din nuvarande avatar, klicka "bekräfta" utan att väl 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. - + Det valda namnet står i konflikt med ett befintligt kort eller token. +Se till att du aktiverar "Token" i dialogrutan "Hantera uppsättningar" för att visa dem korrekt. @@ -1101,7 +1123,7 @@ Make sure to enable the 'Token' set in the "Manage sets" dia Restrictions - + begränsningar @@ -1115,22 +1137,22 @@ Make sure to enable the 'Token' set in the "Manage sets" dia Forgot Password Challenge Warning - + Glömt lösenord Utmaning Varning Oops, looks like something has gone wrong. Please restart the forgot password process by using the forgot password button on the connection screen. - + Oj, ser ut som att något har gått fel. Vänligen starta om glömt lösenordsprocessen med hjälp av knappen Glömt lösenord på anslutningsskärmen. &Host: - + &Värd: &Port: - + &Hamn: @@ -1140,17 +1162,17 @@ Make sure to enable the 'Token' set in the "Manage sets" dia Email: - + E-post: Forgot Password Challenge - + Glömt lösenordsutmaning The email address can't be empty. - + E-postadressen kan inte vara tom. @@ -1158,12 +1180,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia &Host: - + &Värd: &Port: - + &Hamn: @@ -1173,17 +1195,17 @@ Make sure to enable the 'Token' set in the "Manage sets" dia Forgot Password Request - + Glömt lösenordsförfrågan Forgot Password Request Warning - + Glömt lösenordsförfrågan Varning The player name can't be empty. - + Spelarens namn kan inte vara tomt. @@ -1195,22 +1217,22 @@ Make sure to enable the 'Token' set in the "Manage sets" dia Forgot Password Reset Warning - + Glömt lösenord Reset Warning Opps, looks like something has gone wrong. Please re-start the forgot password process by using the forgot password button on the connection screen. - + Opps, ser ut som något har gått fel. Vänligen starta glömt lösenordsprocessen med hjälp av knappen Glömt lösenord på anslutningsskärmen. &Host: - + &Värd: &Port: - + &Hamn: @@ -1220,38 +1242,38 @@ Make sure to enable the 'Token' set in the "Manage sets" dia Token: - + Tecken: New Password: - + Nytt lösenord: Forgot Password Reset - + Glömt lösenordsåterställning The player name can't be empty. - + Spelarens namn kan inte vara tomt. The token can't be empty. - + Token kan inte vara tomt. The new password can't be empty. - + Det nya lösenordet kan inte vara tomt. The passwords do not match. - + Lösenorden stämmer inte överens. @@ -1370,12 +1392,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgSettings - + Unknown Error loading card database Ett okänt fel har inträffat vid laddande av kortdatabasen - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -1392,7 +1414,7 @@ Möjligen kan du behöva starta om oracle för att uppdatera din kortdatabas. Vill du ändra dina databaslokaliseringsinställningar? - + Your card database version is too old. This can cause problems loading card information or images @@ -1409,7 +1431,7 @@ Detta kan oftast fixas genom att starta om oracle för att uppdatera din kortdat Vill du ändra dina kortdatabaslokaliseringsinställningar? - + Your card database did not finish loading Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -1422,7 +1444,7 @@ Var god skicka en hjälpförfrågan på http://github.com/Cockatrice/Cockatrice/ Vill du ändra dina kortdatabaslokaliseringsinställningar? - + File Error loading your card database. Would you like to change your database location setting? @@ -1431,7 +1453,7 @@ Would you like to change your database location setting? Vill du ändra dina kortdatabaslokaliseringsinställningar? - + Your card database was loaded but contains no cards. Would you like to change your database location setting? @@ -1440,7 +1462,7 @@ Would you like to change your database location setting? Vill du ändra dina kortdatabaslokaliseringsinställningar? - + Unknown card database load status Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues @@ -1453,63 +1475,81 @@ Var god skicka en hjälpförfrågan på http://github.com/Cockatrice/Cockatrice/ Vill du ändra dina kortdatabaslokaliseringsinställningar? - - - + + + Error Fel - + The path to your deck directory is invalid. Would you like to go back and set the correct path? Sökvägen till din lekkatalog är ogiltig. Vill du gå tillbaka och ange den korrekta sökvägen? - + The path to your card pictures directory is invalid. Would you like to go back and set the correct path? Sökvägen till din kortbildsdatabas är ogiltig. Vill du gå tillbaka och ange den korrekta sökvägen? - + Settings Inställningar - + General Allmänt - + Appearance Utseende - + User Interface Gränssnitt - + Deck Editor Leklådan - + Chat Chatt - + Sound Ljud - + Shortcuts Genvägar + + DlgTipOfTheDay + + + Next + Nästa + + + + Previous + Tidigare + + + + Tip of the Day + Dagens Tips + + DlgUpdate @@ -1523,17 +1563,17 @@ Vill du ändra dina kortdatabaslokaliseringsinställningar? Current release channel - + Nuvarande släppkanal Reinstall - + Installera Cancel Download - + Avbryt Ladda ner @@ -1544,108 +1584,110 @@ Vill du ändra dina kortdatabaslokaliseringsinställningar? Cockatrice was not built with SSL support, therefore you cannot download updates automatically! Please visit the download page to update manually. - + Cockatrice byggdes inte med SSL-stöd, därför kan du inte ladda ner uppdateringar automatiskt! +Besök sidan för nedladdning för att uppdatera manuellt. Finished checking for updates - + Slutfört efter uppdateringar No Update Available - + Ingen uppdatering tillgänglig Cockatrice is up to date! - + Cockatris är aktuell! You are already running the latest version available in the chosen release channel. - + Du kör redan den senaste versionen i den valda utgåvan. Current version - + Aktuell version Selected release channel - + Vald utgivningskanal Update Available - + Uppdatering tillgänglig A new version of Cockatrice is available! - + En ny version av Cockatrice är tillgänglig! New version - + Ny version Released - + Släppte Changelog - + Ändringslogg Do you want to update now? - + Vill du uppdatera nu? Unfortunately there are no download packages available for your operating system. You may have to build from source yourself. - + Tyvärr finns inga nedladdningspaket tillgängliga för ditt operativsystem. +Du kanske måste bygga från källan själv. Please check the download page manually and visit the wiki for instructions on compiling. - + Vänligen kontrollera nedladdningssidan manuellt och besök wiki för instruktioner om sammanställning. An error occurred while checking for updates: - + Ett fel uppstod när du letade efter uppdateringar: An error occurred while downloading an update: - + Ett fel uppstod när du hämtade en uppdatering: Cockatrice is unable to open the installer. - + Cockatrice kan inte öppna installatören. Try to update manually by closing Cockatrice and running the installer. - + Försök att uppdatera manuellt genom att stänga Cockatrice och köra installationsprogrammet. Download location - + Hämta plats @@ -1675,12 +1717,12 @@ You may have to build from source yourself. Clear log when closing - + Tydlig logg vid stängning Debug Log - + Felsökningslogg @@ -1688,7 +1730,7 @@ You may have to build from source yourself. Type your filter here - + Skriv ditt filter här @@ -1797,7 +1839,7 @@ You may have to build from source yourself. New - + Ny @@ -1843,35 +1885,35 @@ You may have to build from source yourself. Age - + Ålder - + Type - + Typ - + Description Beskrivning - + Creator Skapare - + Restrictions Begränsningar - + Players Spelare - + Spectators Åskådare @@ -1879,202 +1921,189 @@ You may have to build from source yourself. GeneralSettingsPage - - - - - + + + + + Choose path Välj sökväg - + Success Framgång - + Downloaded card pictures have been reset. De nedladdade kortbilderna har återställts. - + Error Fel - + One or more downloaded card pictures could not be cleared. En eller fler nedladdade kortbilder kunde inte rensas. - + Personal settings Personliga inställningar - + Language: Språk: - + Download card pictures on the fly Ladda ner kortbilder på direkten - + Paths (editing disabled in portable mode) - + Banor (redigering inaktiverad i bärbart läge) - + Paths Sökvägar - + Decks directory: Lekkatalog: - + Replays directory: Repriskatalog: - + Pictures directory: Bildkatalog: - + Card database: Kortdatabas: - + Token database: Token-databas: - + Picture cache size: Storlek på bild-cache: - + Primary download URL: Primär nedladdnings-URL: - + Fallback download URL: Reservnedladdnings-URL: - + How to set a custom picture url Hur man väljer en egen bild-url - - Reset/clear downloaded pictures - - - - - Update channel - - - - - Notify if a feature supported by the server is missing in my client - - - - + Reset/clear downloaded pictures + Återställ / ta bort nedladdade bilder + + + + Update channel + Uppdatera kanal + + + + Notify if a feature supported by the server is missing in my client + Meddela om en funktion som stöds av servern saknas i min klient + + + + Reset Återställ - - - Logger - - Client Operating System - - - - - Build Architecture - - - - - Qt Version - + + Show tips on startup + Visa tips vid uppstart MainWindow - - + + The server has reached its maximum user capacity, please check back later. Servern har nått sin maximala kapacitet för användare, var god försök igen vid ett senare tillfälle. - + There are too many concurrent connections from your address. Din adress har för många uppkopplingar samtidigt. - + Banned by moderator Bannlyst av moderator - + Expected end time: %1 Förväntad sluttid: %1 - + This ban lasts indefinitely. Denna bannlysning varar för evigt. - + Scheduled server shutdown. Schemalagd serverstängning. - - + + Invalid username. Ogiltigt användarnamn. - + You have been logged out due to logging in at another location. Du loggades ut på grund av att du loggade in via en annan plats. - + Connection closed Uppkoppling avslutad - + The server has terminated your connection. Reason: %1 Servern har avslutat din uppkoppling. Anledning: %1 - + The server is going to be restarted in %n minute(s). All running games will be lost. Reason for shutdown: %1 @@ -2085,656 +2114,661 @@ Alla pågående spel kommer att gå förlorade. Anledning till nedstängning: %1 - + Scheduled server shutdown Schemalagd serverstängning - - + + Success Framgång - + Registration accepted. Will now login. Registrering lyckad. Inloggning påbörjad. - + Account activation accepted. Will now login. Aktivering tillåten. Inloggning påbörjad. - + Number of players Antal spelare - + Please enter the number of players. Vänligen ange antal spelare. - - + + Player %1 Spelare %1 - + Load replay Ladda repris - + About Cockatrice Om Cockatrice - + Cockatrice Webpage - - - Project Manager: - - - - - Past Project Managers: - - - - - Developers: - - - Our Developers - + Project Manager: + Projektledare: + Past Project Managers: + Tidigare projektledare: + + + + Developers: + utvecklare: + + + + Our Developers + Våra utvecklare + + + Help Develop! - + Translators: Översättare: - + Help Translate! - + Support: - + Report an Issue - + Troubleshooting - + F.A.Q. - - - - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + + + + Error Fel - + Server timeout Server timeout - + Failed Login - + 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. Det finns redan en aktiv session med det användarnamnet. Vänligen stäng den sessionen först och försök igen. - - + + You are banned until %1. Du är bannlyst till %1. - - + + You are banned indefinitely. Du är bannlyst för evigt. - + This server requires user registration. Do you want to register now? - + This server requires client ID's. 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 try closing and reopening your client and try again. If the error persists try updating your client to the most recent build and if need be contact your software provider. - + Account activation - + Server Full - + Unknown login error: %1 Okänt inloggningsfel: %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 - + 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. - + Your client seems to be missing features this server requires for connection. - + Version - + Our Translators - + Your account has not been activated yet. You need to provide the activation token received in the activation email. - + The email address provider used during registration has been blacklisted for use on this server. - + 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. - + Unknown registration error: %1 - + Account activation failed - + Socket error: %1 Socketfel: %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. Du försöker koppla upp dig till en föråldrad server. Vänligen nergradera din version av Cockatrice eller koppla upp dig till en lämplig server. Lokal version är %1, avlägsen version är %2. - + Your Cockatrice client is obsolete. Please update your Cockatrice version. Local version is %1, remote version is %2. Din version av Cockatrice är föråldrad. Vänligen uppdatera din version av Cockatrice. Lokal version är %1, avlägsen version är %2. - + Connecting to %1... Ansluter till %1... - + Registering to %1 as %2... - + Disconnected Frånkopplad - + Connected, logging in at %1 Uppkopplad, loggar in hos %1 - - - + + + Requesting forgot password to %1 as %2... - + &Connect... &Anslut... - + &Disconnect &Frånkoppla - + Start &local game... Starta &lokalt spel... - + &Watch replay... &Titta på repris... - + &Deck editor Lek&redigeraren - + &Full screen &Fullskärmsläge - + &Register to server... - + &Settings... &Inställningar... - - + + &Exit A&vsluta - + A&ctions - + &Cockatrice &Cockatrice - + C&ard Database - + Open custom image folder - + Open custom sets folder - + Add custom sets/cards - + Edit &tokens... - + &About Cockatrice &Om Cockatrice - + + &Tip of the Day + + + + Check for Client Updates - + View &debug log - + &Help &Hjälp - + Check for card updates... - + 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 - + %1 new set(s) found in the card database Set code(s): %2 Do you want to enable it/them? - + View sets - + Welcome - - - + + + Information - + A card database update is already running. - + Unable to run the card database updater: - + failed to start. - + crashed. - + timed out. - + write error. - + read error. - + unknown error. - + The card database updater exited with an error: %1 - + Update completed successfully. Cockatrice will now reload the card database. - + You can only import XML databases at this time. - - - + + + Forgot Password - + Your password has been reset successfully, you now may 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. - - - - + + + + Load sets/cards - + &Manage sets... - + 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. - + 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. @@ -2742,18 +2776,18 @@ To update your client, go to Help -> Check for Updates. - + Selected file cannot be found. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. @@ -2876,6 +2910,16 @@ Cockatrice will now reload the card database. %1 plays %2%3. %1 spelar %2%3. + + + %1 turns %2 face-down. + + + + + %1 turns %2 face-up. + + %1 has left the game (%2). @@ -3131,16 +3175,6 @@ Cockatrice will now reload the card database. %1 draws their initial hand. - - - %1 flips %2 face-down. - - - - - %1 flips %2 face-up. - - %1 destroys %2. @@ -3310,79 +3344,79 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Add message Lägg till meddelande - + Message: Meddelande: - + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered 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 @@ -3448,478 +3482,485 @@ Cockatrice will now reload the card database. Player - + Reveal top cards of library - + Number of cards: (max. %1) - + &View graveyard &Titta på kyrkogård - + &View exile &Titta på exil - + Player "%1" Spelare "%1" - - - - + + + + &Graveyard &Kyrkogård - - - - + + + + &Exile &Exil - - - &Move hand to... - - - - - - &Top of library + &Move hand to... - + + &Top of library + + + + + + + &Bottom of library - + &Move graveyard to... - - - - + + + + &Hand &Hand - + &Move exile to... - + &View library &Titta på leken - + View &top cards of library... Titta på de &översta korten i leken... - + Reveal &library to... - + Reveal t&op cards to... - + &Always reveal top card &Avslöja alltid det översta kortet - + O&pen deck in deck editor &Öppna lek i lekredigeraren - + &View sideboard &Titta på sidbrädan - + &Draw card &Dra kort - + D&raw cards... D&ra kort... - + &Undo last draw &Ångra senaste drag - + Take &mulligan &Mulligan - + &Shuffle &Blanda - + Play top card &face down - + Move top cards to &graveyard... Flytta översta korten till &kyrkogården... - + Move top cards to &exile... Flytta översta korten till &exil... - + Put top card on &bottom Placera översta kortet &underst - + Put bottom card &in graveyard - + &Reveal hand to... - + Reveal r&andom card to... - + Reveal random card to... - + &Sideboard S&idbräda - + &Library &Lek - + &Counters &Poletter - + &Untap all permanents Tappa upp alla perma&nenta kort - + R&oll die... Rulla t&ärning... - + &Create token... &Skapa jetong... - + C&reate another token S&kapa en till jetong - + Cr&eate predefined token Skapa fördefinierad &jetong - + S&ay S&äg - + C&ard K&ort - + &All players A&lla spelare - + &Play &Spela - + &Hide &Göm - + Play &Face Down - + Toggle &normal untapping Växla &normal upptappning - - &Flip - &Vänd - - - + &Peek at card face Kika på kort&framsida - + &Clone &Klona - + Attac&h to card... &Fäst på kort... - + Unattac&h Se&parera - + &Draw arrow... &Rita pil... - + &Increase power &Öka power - + &Decrease power &Minska power - + I&ncrease toughness Öka toug&hness - + D&ecrease toughness Minska t&oughness - + In&crease power and toughness Öka power o&ch toughness - + Dec&rease power and toughness M&inska power och toughness - + Set &power and toughness... An&ge power och toughness... - + &Set annotation... Ang&e annotering... - + Red - + Yellow - + Green - + X cards from the top of library... - - + + C&reate another %1 token - + Create tokens - - - - - - + + + + + + Token: - + Place card X cards from top of library - + How many cards from the top of the deck should this card be placed: - - + + View related cards + + + + + Attach to - + All tokens - + View top cards of library Titta på de översta korten i leken - + &Tap / Untap + Turn sideways or back again - + + T&urn Over + Turn face up/face down + + + + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... - + Number of cards: Antal kort: - + Draw cards Dra kort - - - - - + + + + + Number: Antal: - + Move top cards to grave Flytta översta korten till kyrkogården - + Move top cards to exile Flytta översta korten till exil - + Roll die Rulla tärning - + Number of sides: Antal sidor: - + Set power/toughness Ange power/toughness - + Please enter the new PT: Vänligen ange ny PT: - + Set annotation Ange annotering - + Please enter the new annotation: Vänligen ange den nya annoteringen: - + Set counters Placera poletter @@ -3927,37 +3968,37 @@ Cockatrice will now reload the card database. QMenuBar - + Services - + Hide %1 - + Hide Others - + Show All - + Preferences... - + Quit %1 - + About %1 @@ -3965,18 +4006,18 @@ Cockatrice will now reload the card database. QObject - + Cockatrice card database (*.xml) - + All files (*.*) Alla filer (*.*) - + Cockatrice replays (*.cor) Cockatricerepriser (*.cor) @@ -4093,10 +4134,15 @@ Cockatrice will now reload the card database. SequenceEdit - + Shortcut already in use + + + Invalid key + + SetsModel @@ -4126,6 +4172,21 @@ Cockatrice will now reload the card database. + + ShortcutsSettings + + + Your configuration file contained invalid shortcuts. +Please check your shortcut settings! + + + + + The following shortcuts have been set to default: + + + + ShortcutsTab @@ -4139,12 +4200,12 @@ Cockatrice will now reload the card database. - + Clear all default shortcuts - + Do you really want to clear all shortcuts? @@ -4170,27 +4231,27 @@ Cockatrice will now reload the card database. SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -4198,48 +4259,48 @@ Cockatrice will now reload the card database. SpoilerBackgroundUpdater - + Spoilers season has ended - + Deleting spoiler.xml. Please run Oracle - - + + Spoilers download failed - + No internet connection - + Error - + Spoilers already up to date - + No new spoilers added - + Spoilers have been updated! - + Last change: @@ -4248,7 +4309,7 @@ Cockatrice will now reload the card database. StableReleaseChannel - Stable releases + Stable Releases @@ -4313,232 +4374,247 @@ Cockatrice will now reload the card database. TabDeckEditor - + &Clear all filters - + Delete selected - + Deck &name: &Leknamn: - + &Comments: &Kommentarer: - + Hash: Hash: - + &New deck &Ny lek - + &Load deck... &Ladda lek... - + &Save deck S&para lek - + Save deck &as... Spa&ra lek som... - + Load deck from cl&ipboard... Ladda lek &från urklipp... - + &Print deck... Skri&v ut lek... - + Search by card name - + + Add to Deck + + + + + Add to Sideboard + + + + + Show Related cards + + + + Save deck to clipboard - + Annotated - + Not Annotated - + &Send deck to online service - + Create decklist (decklist.org) - + Analyze deck (deckstats.net) - + Analyze deck (tappedout.net) - + &Close S&täng - + Add card to &maindeck Lägg till kort till &huvudlek - + Add card to &sideboard Lägg till kort i sidbr&äda - + &Remove row Ta bort ra&d - + &Increment number &Öka antal - + &Decrement number &Minska antal - + &Deck Editor - - + + Card Info - - + + Deck - - + + Filters - + &View - - - + + + Visible - - - + + + Floating - + Reset layout - + Deck: %1 Lek: %1 - + Are you sure? Är du säker? - + The decklist has been modified. Do you want to save the changes? Denna leklista har modifierats. Vill du spara ändringarna? - + Load deck Ladda lek - - - - - + + + + + Error Fel - + The deck could not be saved. Leken kunde inte sparas. - - + + The deck could not be saved. Please check that the directory is writable and try again. Leken kunde inte sparas. Vänligen se till att katalogen är skrivbar och försök igen. - + Save deck Spara lek - + There are no cards in your deck to be exported - + No deck was selected to be saved. @@ -4635,175 +4711,175 @@ Please enter a name: TabGame - - - + + + Replay - - + + Game - - + + Card Info - - + + Player List - - + + Messages - - + + Replay Timeline - + &Phases &Faser - + &Game &Spel - + Next &phase Nästa &fas - + Next &turn Nästa &tur - + &Remove all local arrows Ta &bort alla lokala pilar - + Rotate View Cl&ockwise - + Rotate View Co&unterclockwise - + Game &information Spel&information - + &Concede &Ge upp - + &Leave game &Lämna spel - + C&lose replay S&täng repris - + &Say: S&äg: - + &View - - - + + + Visible - - - + + + Floating - + Reset layout - + Concede Ge upp - + Are you sure you want to concede this game? Är du säker på att du vill ge upp detta spel? - + Leave game Lämna spel - + Are you sure you want to leave this game? Är du säker på att du vill lämna detta spel? - + You are flooding the game. Please wait a couple of seconds. - + kicked by game host or moderator - + player left the game - + player disconnected from server - + reason unknown - + You have been kicked out of the game. @@ -5161,22 +5237,22 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Are you sure? - + There are still open games. Are you sure you want to quit? - + 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. @@ -5184,38 +5260,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 to moderator. 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) @@ -5253,6 +5329,21 @@ Please refrain from engaging in this activity or further actions may be taken ag + + TipsOfTheDay + + + File does not exist. + + + + + + Failed to open file. + + + + UpdateDownloader @@ -5577,42 +5668,42 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings Allmänna gränssnittsinställningar - + Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating - + &Double-click cards to play them (instead of single-click) &Dubbelklicka på kort för att spela dem (istället för enkelklick) - + &Play all nonlands onto the stack (not the battlefield) by default - + Annotate card text on tokens - + Animation settings Animationsinställningar - + &Tap/untap animation &Tappnings/Upptappningsanimation @@ -5682,97 +5773,127 @@ 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 - - Enable all sets + + Search by set name, code, or type - - Disable all sets + + Default order - - Enable selected set(s) - - - - - Disable selected set(s) - - - - - Deck Editor - - - - - Only cards in enabled sets will appear in the deck editor card list - - - - - Card Art - - - - - Image priority is decided in the following order - - - - - The - - - - - CUSTOM Folder - - - - - Enabled Sets (Top to Bottom) + + Restore original art priority order + Enable all sets + + + + + Disable all sets + + + + + Enable selected set(s) + + + + + Disable selected set(s) + + + + + Deck Editor + + + + + Only cards in enabled sets will appear in the deck editor card list + + + + + Card Art + + + + + Image priority is decided in the following order + + + + + The + + + + + CUSTOM Folder + + + + + Enabled Sets (Top to Bottom) + + + + Disabled Sets (Top to Bottom) - + + Warning: + + + + + While the set list is sorted by any of the columns, custom art priority setting is disabled. + + + + + To disable sorting click on the same column header again until this message disappears. + + + + Manage sets - + Success - + The sets database has been saved successfully. @@ -5803,7 +5924,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English Svenska (Swedish) @@ -5876,97 +5997,113 @@ Please refrain from engaging in this activity or further actions may be taken ag - + Load deck (clipboard) - + Clear all filters - + New deck - + Clear selected filter - + Open custom pic folder - + Close - + Print deck - + Delete card - + Edit tokens - + Reset layout - + Add card - + Save deck - + Remove card - + Save deck as - + Load deck - - + + Counters - + Life - + + + + + + + + + + + + + + Set + + + - + @@ -5974,16 +6111,15 @@ Please refrain from engaging in this activity or further actions may be taken ag - - Set + + Add - - + @@ -5991,457 +6127,442 @@ Please refrain from engaging in this activity or further actions may be taken ag - Add - - - - - - - - - - - - - - - + Remove - + Red - + Green - + Yellow - + Storm - + W - + U - + B - + R - + G - + X - + Main Window | Deck Editor - + Power / Toughness - + Power and Toughness - + Add (+1/+1) - + Remove (-1/-1) - + Toughness - + Remove (-0/-1) - + Add (+0/+1) - + Power - + Remove (-1/-0) - + Add (+1/+0) - + Game Phases - + Untap - + Upkeep - - + + Draw - + Main 1 - + Start combat - + Attack - + Block - + Damage - + End combat - + Main 2 - + End - + Next phase - + Next turn - + Playing Area - + Manage sets - + Export deck - + Save deck (clip) - + Save deck (clip; no annotations) - + Tap / Untap Card - + Untap all - + Toggle untap - + Flip card - - - Peek card - - - - - Play card - - - - - Attach card - - - - - Unattach card - - - Clone card - + Peek card + Peek-kort - Create token - + Play card + Spelkort - Create all related tokens - + Attach card + Fäst kort - Create another token - + Unattach card + Unattach-kortet - Set annotation - + Clone card + Klon kort + Create token + Skapa token + + + + Create all related tokens + Skapa alla relaterade tokens + + + + Create another token + Skapa en annan token + + + + Set annotation + Ange anteckningar + + + Phases | P/T | Playing Area - + Move card to - + Flytta kortet till - + Bottom library - + Top library - + Top bibliotek - - + + Graveyard - + Kyrkogård - - + + Exile - + Exil - + Hand - - - View - - - - - Library - - - - - Tops card of library - - - - - Sideboard - - - Close recent view - + View + Se - + + Library + Bibliotek + + + + Tops card of library + Tops kort av biblioteket + + + + Sideboard + Skänk + + + + Close recent view + Stäng ny vy nyligen + + + Game Lobby - - - Load remote deck - - - - - Load local deck - - - - - Gameplay - - - - - Draw arrow - - - - - Leave game - - - - - Remove local arrows - - - Concede - + Load remote deck + Ladda fjärrdäck - Roll dice - + Load local deck + Ladda lokala däck + Gameplay + Gameplay + + + + Draw arrow + Rita pilen + + + + Leave game + Lämna spelet + + + + Remove local arrows + Ta bort lokala pilar + + + + Concede + Medge + + + + Roll dice + Kasta tärning + + + Rotate view CW - + Shuffle library - + Blanda biblioteket - + Rotate view CCW - + Mulligan - - - Draw card - - - - - Draw cards - - - - - Undo draw - - - - - Always reveal top card - - - - - Draw | Move | View | Gameplay - - - How to set custom shortcuts - + Draw card + Rita kort - Restore all default shortcuts - + Draw cards + Rita kort + Undo draw + Ångra rita + + + + Always reveal top card + Alltid avslöja bästa kort + + + + Draw | Move | View | Gameplay + Rita | Flytta | Visa | gameplay + + + + How to set custom shortcuts + Så här ställer du in anpassade genvägar + + + + Restore all default shortcuts + Återställ alla standardgenvägar + + + Clear all shortcuts - + Rensa alla genvägar \ No newline at end of file diff --git a/cockatrice/translations/cockatrice_zh-Hans.ts b/cockatrice/translations/cockatrice_zh-Hans.ts index b7b114940..d9bb8b940 100644 --- a/cockatrice/translations/cockatrice_zh-Hans.ts +++ b/cockatrice/translations/cockatrice_zh-Hans.ts @@ -20,62 +20,62 @@ AppearanceSettingsPage - + Theme settings 主题设置 - + Current theme: 当前主题 - + Card rendering 牌面 - + Display card names on cards having a picture 显示有图卡牌的名称 - + Scale cards on mouse over 卡牌随指针缩放 - + 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: 卡牌上显示的最大字号 @@ -188,6 +188,29 @@ This is only saved for moderators and cannot be seen by the banned person.在禁用客户端ID时必须填写一个客户端ID。 + + BetaReleaseChannel + + + Beta Releases + 测试版本 + + + + No reply received from the release update server. + 未收到更新服务器的响应。 + + + + Invalid reply received from the release update server. + 收到更新服务器的无效响应 + + + + No reply received from the file update server. + 未收到文件更新服务器的响应 + + CardDatabaseModel @@ -242,32 +265,37 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoText - + + Unknown card: + 未知牌: + + + Name: 名称: - + Mana cost: 法术力费用: - + Color(s): 颜色 - + Card type: 卡牌类别: - + P / T: 力量/防御力: - + Loyalty: 忠诚值: @@ -411,58 +439,58 @@ This is only saved for moderators and cannot be seen by the banned person. DeckEditorSettingsPage - - + + Update Spoilers 更新预览 - - Updating Spoilers - 更新预览中 + + Updating... + 更新中... - + Choose path 选择路径 - + Spoilers 偷跑 - + Download Spoilers Automatically 自动下载预览 - + Spoiler Location: Spoiler位置: - + Hey, something's here finally! 嘿,终于有东西来了! - + Last Updated 最后更新 - + Spoilers download automatically on launch 自动下载Spoilers运行 - + Press the button to manually update without relaunching 按下按钮手动更新而不重新启动 - + Do not close settings until manual update complete 手动更新完成之前,请勿关闭设置 @@ -470,12 +498,12 @@ This is only saved for moderators and cannot be seen by the banned person. DeckListModel - + Number 数量 - + Card 卡牌 @@ -537,136 +565,129 @@ This is only saved for moderators and cannot be seen by the banned person.此文件无法被载入 - - DevReleaseChannel - - - Development snapshots - 开发快照 - - - - No reply received from the release update server. - 未收到更新服务器的响应。 - - - - Invalid reply received from the release update server. - 收到更新服务器的无效响应。 - - - - No reply received from the file update server. - 未收到文件更新服务器的响应。 - - DlgConnect - + New Host 新主机 - + &Host: 主机: - + Known Hosts 已知主机: + Refresh the server list with known public servers + 从已知的服务器更新服务器列表 + + + Name: 名称: - + &Port: 端口: - + Player &name: 玩家名字: - + P&assword: 密码: - + &Save password 记住密码 - + A&uto connect 自动连接 - + Automatically connect to the most recent login when Cockatrice opens 当Cockatrice启动时自动连接到最近的登录 - - Public Servers - 公共服务器 + + If you have any trouble connecting or registering then contact the server staff for help! + 如果你在连接或者注册上遇到了任何麻烦,可以联系服务器的工作人员获得帮助。 - - Forgot password + + + Webpage + 网页 + + + + Forgot Password 忘记密码 - - Connect + + &Connect 连接 - - Cancel - 取消 + + Server Contact + 联系服务器 - + + Connect to Server + 连接服务器 + + + Server 服务器 - + Login 登录 - - Connect to server - 连接服务器 - - - + Connection Warning 连接警告 - + You need to name your new connection profile. 你需要给连接配置文件命名。 - + Connect Warning 连接警告 - + The player name can't be empty. 玩家名称不能为空 + + + Downloading... + 下载中... + DlgCreateGame @@ -858,7 +879,7 @@ This is only saved for moderators and cannot be seen by the banned person.DlgEditAvatar - + No image chosen. 未选择图像 @@ -880,17 +901,17 @@ To remove your current avatar, confirm without choosing a new image. 改变头像 - + Open Image 打开图片 - + Image Files (*.png *.jpg *.bmp) 图片文件(*.png *.jpg *.bmp格式) - + Invalid image chosen. 所选图片不可用。 @@ -1371,12 +1392,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgSettings - + Unknown Error loading card database 读取卡牌数据库时出现未知错误 - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -1393,7 +1414,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 @@ -1410,7 +1431,7 @@ Would you like to change your database location setting? 您想要重新设置卡牌数据库路径么? - + Your card database did not finish loading Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -1423,7 +1444,7 @@ Would you like to change your database location setting? 您想要重新设置卡牌数据库路径么? - + File Error loading your card database. Would you like to change your database location setting? @@ -1432,7 +1453,7 @@ 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? @@ -1441,7 +1462,7 @@ Would you like to change your database location setting? 您想要重新设置卡牌数据库路径么? - + Unknown card database load status Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues @@ -1452,63 +1473,81 @@ 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 用户界面 - + Deck Editor 套牌编辑 - + Chat 聊天 - + Sound 声音 - + Shortcuts 快捷键 + + DlgTipOfTheDay + + + Next + 下一个 + + + + Previous + 已知 + + + + Tip of the Day + 每日提示 + + DlgUpdate @@ -1847,32 +1886,32 @@ You may have to build from source yourself. 时长 - + Type 类型 - + Description 描述 - + Creator 创建者 - + Restrictions 限制 - + Players 玩家 - + Spectators 观看者 @@ -1880,202 +1919,189 @@ You may have to build from source yourself. GeneralSettingsPage - - - - - + + + + + Choose path 选择路径 - + Success 成功 - + Downloaded card pictures have been reset. 下载的卡牌图片已被重置。 - + Error 错误 - + One or more downloaded card pictures could not be cleared. 1个或多个卡牌图片未能被清除。 - + Personal settings 个人设置 - + Language: 语言: - + Download card pictures on the fly 下载卡牌上的图片 - + Paths (editing disabled in portable mode) 路径(在便携模式下禁止编辑) - + Paths 路径 - + Decks directory: 套牌路径: - + Replays directory: 游戏录像目录: - + Pictures directory: 图片目录: - + Card database: 卡牌数据库: - + Token database: 衍生物数据库: - + Picture cache size: 图片缓存大小: - + Primary download URL: 首选下载链接: - + Fallback download URL: 备用下载链接: - + How to set a custom picture url 如何设置自定义图片链接 - + Reset/clear downloaded pictures 重置/清除 已下载的图片 - + Update channel 更新通道 - + Notify if a feature supported by the server is missing in my client 当客户端缺少服务器支持的特性时提示我 - - + + Reset 重置 - - - Logger - - Client Operating System - 客户端操作系统 - - - - Build Architecture - 构建架构 - - - - Qt Version - QT版本 + + Show tips on startup + 显示启动时提示 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 预计结束时间: %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 服务器中断连接. 原因: %1 - + The server is going to be restarted in %n minute(s). All running games will be lost. Reason for shutdown: %1 @@ -2084,507 +2110,512 @@ Reason for shutdown: %1 关闭原因: %1 - + Scheduled server shutdown 预定服务器关闭 - - + + Success 成功 - + Registration accepted. Will now login. 注册已成功。 现在登陆。 - + Account activation accepted. Will now login. 账号已激活。 现在登陆 - + Number of players 玩家人数 - + Please enter the number of players. 请输入玩家的数量. - - + + Player %1 玩家 %1 - + Load replay 载入游戏录像 - + About Cockatrice 关于Cockatrice鸡蛇 - + Cockatrice Webpage Cockatrice鸡蛇网站 - + Project Manager: 项目经理: - + Past Project Managers: 前项目经理: - + Developers: 开发者: - + Our Developers 我们的开发者: - + Help Develop! 协助开发! - + Translators: 翻译者: - + Help Translate! 协助翻译! - + Support: 支持: - + Report an Issue 报告错误 - + Troubleshooting 排除故障 - + F.A.Q. 问答 - - - - - - - - - - - - + + + + + + + + + - - - - - - + + + + + + + + + Error 错误 - + Server timeout 服务器超时 - + Failed Login 登录失败 - + 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. 你被禁止直到: %1. - - + + You are banned indefinitely. 你被永久封禁. - + This server requires user registration. Do you want to register now? 服务器需要注册。现在注册吗? - + This server requires client ID's. 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. 服务器需要客户端ID。您的客户端未能生成ID或您正在运行修改过的客户端。 请关闭并重新打开客户端。 - + An internal error has occurred, please try closing and reopening your client and try again. If the error persists try updating your client to the most recent build and if need be contact your software provider. 出现内部错误,请关闭并重启客户端再尝试。如果错误仍然出现请将您的客户端升级到最新版本,如有需要请联系您的软件提供商。 - + Account activation 账号激活 - + Server Full 服务器已满 - + Unknown login error: %1 未知的登陆错误: %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 在%1 - %2个字符之间。 - + can %1 contain lowercase characters 能包含 %1 个小写字母 - - - + + + NOT - + can %1 contain uppercase characters 能包含%1个小写字母 - + can %1 contain numeric characters 能包含%1个数字 - + can contain the following punctuation: %1 能包含以下符号:%1 - + first character can %1 be a punctuation mark 第一个字符不能为%1 标点符号 - + can not contain any of the following words: %1 不能包含以下词:%1 - + can not match any of the following expressions: %1 不能包含以下语句:%1 - + You may only use A-Z, a-z, 0-9, _, ., and - in your username. 您可以使用A-Z,a-z,0-9,_,.以及-作为用户名。 - - - - - - + + + + + + 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. 注册时必须提供一个有效的电子邮箱。 - + Your client seems to be missing features this server requires for connection. 你的客户端缺少连接这个服务器所需要的要素 - + Version 版本 - + Our Translators 翻译者名单 - + Your account has not been activated yet. You need to provide the activation token received in the activation email. 你的账号还未激活。 请提供激活邮件中的验证码。 - + The email address provider used during registration has been blacklisted for use on this server. 注册时使用的电子邮箱的提供方已被此服务器列入黑名单 - + 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. 由于服务器出现技术问题,注册失败。 - + Unknown registration error: %1 未知的注册错误:%1 - + Account activation failed 账号激活失败 - + Socket error: %1 接口错误: %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. 你正在连接到一个过时的服务器,请下调你的版本或者连接到一个匹配的服务器. 你当前的版本 %1, 服务器版本 %2. - + Your Cockatrice client is obsolete. Please update your Cockatrice version. Local version is %1, remote version is %2. 你使用的客户端意见过时,请使用更新你版本. 你当前的版本 %1, 服务器版本 %2. - + Connecting to %1... 连接到 %1... - + Registering to %1 as %2... 正在将 %2 注册到 %1 - + Disconnected 断开连接 - + Connected, logging in at %1 连接登陆到 %1 - - - + + + Requesting forgot password to %1 as %2... 正在将%2向%1申请忘记密码流程。 - + &Connect... 连接... - + &Disconnect 断开连接 - + Start &local game... 开始本地游戏... - + &Watch replay... 观看录像... - + &Deck editor 套牌编辑 - + &Full screen 全屏 - + &Register to server... 注册到服务器 - + &Settings... 设置... - - + + &Exit 退出 - + A&ctions 动作 - + &Cockatrice Cockatrice鸡蛇 - + C&ard Database 卡牌数据库 - + Open custom image folder 打开自定义图片文件夹 - + Open custom sets folder 打开自定义系列文件夹 - + Add custom sets/cards 添加自定义系列/卡牌 - + Edit &tokens... 编辑衍生物 - + &About Cockatrice 关于Cockatrice鸡蛇 - + + &Tip of the Day + &每日提示 + + + Check for Client Updates 检查客户端更新 - + View &debug log 查看错误日志 - + &Help 帮助 - + Check for card updates... 检查卡牌更新 - + 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" @@ -2593,29 +2624,29 @@ If unsure or first time user, choose "Yes" 如果不确定或者您是第一次使用,请选择“是” - - + + Yes - - + + No - + Open settings 打开设置 - + New sets found 发现新系列 - + %1 new set(s) found in the card database Set code(s): %2 Do you want to enable it/them? @@ -2624,117 +2655,117 @@ Do you want to enable it/them? 要将它们生效吗? - + View sets 查看系列 - + Welcome 欢迎 - - - + + + Information 信息 - + A card database update is already running. 数据库更新已经在运行中。 - + Unable to run the card database updater: 无法运行卡组数据库更新器: - + failed to start. 无法开始。 - + crashed. 程序崩溃。 - + timed out. 超时 - + write error. 写入错误。 - + read error. 读取错误。 - + unknown error. 未知错误 - + The card database updater exited with an error: %1 卡牌数据库更新器退出,错误:%1 - + Update completed successfully. Cockatrice will now reload the card database. 更新已完毕。 Cockatrice鸡蛇现在会重新载入卡组数据库。 - + You can only import XML databases at this time. 当前只能导入XML格式数据库。 - - - + + + Forgot Password 忘记密码 - + Your password has been reset successfully, you now may 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. 激活请求已收到,请查看你的电子邮箱获取激活验证码。 - - - - + + + + Load sets/cards 载入系列/卡牌 - + &Manage sets... 系列管理 - + 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. @@ -2743,7 +2774,7 @@ Read more about changing the set order or disabling specific sets and consequent 了解更多用“系列管理”窗口更改系列顺序或者禁用某系列的信息。 - + 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. @@ -2753,19 +2784,19 @@ To update your client, go to Help -> Check for Updates. 要升级客户端,请点击 帮助->检查更新 - + Selected file cannot be found. 找不到选择的文件。 - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. 新的系列/卡牌已添加成功。 Cockatrice鸡蛇现在会重新载入卡组数据库。 - + Sets/cards failed to import. 系列/卡牌导入失败。 @@ -2888,6 +2919,16 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 %1 plays %2%3. %1使用 %2%3。 + + + %1 turns %2 face-down. + %1回合%2翻为面朝下。 + + + + %1 turns %2 face-up. + %1回合%2翻为面朝上。 + %1 has left the game (%2). @@ -3143,16 +3184,6 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 %1 draws their initial hand. %1 抓了起始手牌 - - - %1 flips %2 face-down. - %1将%2翻为面朝下。 - - - - %1 flips %2 face-up. - %1将%2翻为面朝下。 - %1 destroys %2. @@ -3322,79 +3353,79 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 MessagesSettingsPage - + Add message 添加信息 - + Message: 信息: - + Chat settings 聊天设置 - + Custom alert words 自定义警告语 - + Enable chat mentions 允许聊天中提到某人 - + Enable mention completer 允许自动完成提名 - + In-game message macros 游戏内消息宏 - + Ignore chat room messages sent by unregistered users 忽略未注册用户的聊天室消息。 - + Ignore private messages sent by unregistered users 忽略未注册用户发出的私人消息 - - + + Invert text color 反转文本颜色 - + Enable desktop notifications for private messages 开启私人消息桌面提醒 - + Enable desktop notification for mentions 开启聊天提名桌面提醒 - + Enable room message history on join 开启加入聊天室时的历史消息 - - + + (Color is hexadecimal) (颜色为16进制) - + Separate words with a space, alphanumeric characters only 将单词以空格区分,仅支持字母 @@ -3460,478 +3491,485 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 Player - + Reveal top cards of library 查看牌库顶的牌 - + Number of cards: (max. %1) 卡牌数量:(最大%1) - + &View graveyard 查看坟墓场 - + &View exile 查看放逐区 - + Player "%1" 玩家 "%1" - - - - + + + + &Graveyard 坟墓场 - - - - + + + + &Exile 放逐区 - + &Move hand to... 将手牌移动到 - - - - - - &Top of library - 牌库顶 - - + + &Top of library + 牌库顶 + + + + + + &Bottom of library 牌库底 - + &Move graveyard to... 将坟墓场的牌移到 - - - - + + + + &Hand 手牌 - + &Move exile to... 将放逐区的卡移到 - + &View library 查看套牌 - + View &top cards of library... 检视牌库顶的牌 - + Reveal &library to... 将套牌展示给 - + Reveal t&op cards to... 将牌库顶牌展示给 - + &Always reveal top card 一直展示牌库顶牌 - + O&pen deck in deck editor 打开套牌编辑 - + &View sideboard 查看备牌 - + &Draw card 抓牌 - + D&raw cards... 抓多张牌... - + &Undo last draw 撤销最后抓牌 - + Take &mulligan 抓起始手牌 - + &Shuffle 切洗套牌 - + Play top card &face down 将牌库顶牌面朝下打出 - + Move top cards to &graveyard... 将牌库顶的牌置入坟墓场... - + Move top cards to &exile... 将牌库顶的牌置入放逐区... - + Put top card on &bottom 将牌库顶牌放到牌库底 - + Put bottom card &in graveyard 将牌库底牌置入坟墓场 - + &Reveal hand to... 将手牌展示给 - + Reveal r&andom card to... 展示随机卡牌到 - + Reveal random card to... 随机展示牌给... - + &Sideboard 备牌 - + &Library 牌库 - + &Counters 数值 - + &Untap all permanents 重置所有永久物 - + R&oll die... 抛骰子... - + &Create token... 创造一个衍生物... - + C&reate another token 将另一个衍生物放置进场 - + Cr&eate predefined token 将预设衍生物放置进场 - + S&ay - + C&ard 卡牌 - + &All players 全部玩家 - + &Play 开始 - + &Hide 隐藏 - + Play &Face Down 面朝下打出 - + Toggle &normal untapping 锁定通常重置 - - &Flip - 翻面 - - - + &Peek at card face 查看卡牌背面 - + &Clone 复制 - + Attac&h to card... 结附卡牌... - + Unattac&h 取消结附 - + &Draw arrow... 划箭头... - + &Increase power 增加力量 - + &Decrease power 降低防御 - + I&ncrease toughness 降低防御 - + D&ecrease toughness 降低防御 - + In&crease power and toughness 增加力量和防御 - + Dec&rease power and toughness 降低力量和防御 - + Set &power and toughness... 设置力量和防御... - + &Set annotation... 设置注释... - + Red - + Yellow - + Green 绿 - + X cards from the top of library... 从牌库顶X张牌... - - + + C&reate another %1 token 创造%1个衍生物 - + Create tokens 创造一个衍生物 - - - - - - + + + + + + Token: 衍生物: - + Place card X cards from top of library 查看牌库顶X张牌 - + How many cards from the top of the deck should this card be placed: 多少张牌应该放在牌库顶: - - + + View related cards + 查看关联牌 + + + + Attach to 结附于 - + All tokens 所有衍生物 - + View top cards of library 查看牌库顶的牌 - + &Tap / Untap + Turn sideways or back again 横置/重置 - + + T&urn Over + Turn face up/face down + 回合结束 + + + &Add counter (%1) 增加指示物 (%1) - + &Remove counter (%1) 移除指示物 (%1) - + &Set counters (%1)... 设置数值 (%1)... - + Number of cards: 卡牌数量: - + Draw cards 抓牌 - - - - - + + + + + Number: 数值: - + Move top cards to grave 将牌库顶的牌置入坟墓场 - + Move top cards to exile 将牌库顶的牌置入放逐区 - + Roll die 抛骰子 - + Number of sides: 面数: - + Set power/toughness 设置力量和防御 - + Please enter the new PT: 请输入力量和防御值: - + Set annotation 设置注释 - + Please enter the new annotation: 请输入注释: - + Set counters 设置数值 @@ -3939,37 +3977,37 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 QMenuBar - + Services 服务 - + Hide %1 隐藏 %1 - + Hide Others 隐藏其他 - + Show All 展示所有 - + Preferences... 参数 - + Quit %1 退出%1 - + About %1 关于 %1 @@ -3977,18 +4015,18 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 QObject - + Cockatrice card database (*.xml) Cockatrice鸡蛇卡牌数据库 (*.xml) - + All files (*.*) 全部文件 (*.*) - + Cockatrice replays (*.cor) 鸡蛇录像文件 (*.cor) @@ -4105,10 +4143,15 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 SequenceEdit - + Shortcut already in use 快捷键已被使用 + + + Invalid key + 无效密钥 + SetsModel @@ -4138,6 +4181,23 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 发布日期 + + ShortcutsSettings + + + Your configuration file contained invalid shortcuts. +Please check your shortcut settings! + 您的配置文件包涵了无效的快捷键 +请检查您的快捷键设置 + + + + The following shortcuts have been set to default: + + 以下快捷方式被设置为默认状态: + + + ShortcutsTab @@ -4151,12 +4211,12 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 您要恢复所有默认快捷键吗? - + Clear all default shortcuts 清除所有快捷键 - + Do you really want to clear all shortcuts? 您要清除所有快捷键吗? @@ -4182,27 +4242,27 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 SoundSettingsPage - + Enable &sounds 启用声效 - + Current sounds theme: 当前声效主题: - + Test system sound engine 测试系统声效 - + Sound settings 声效设置 - + Master volume 主音量 @@ -4210,48 +4270,48 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 SpoilerBackgroundUpdater - + Spoilers season has ended 偷跑时间已结束 - + Deleting spoiler.xml. Please run Oracle 删除spoiler.xml。请运行Oracle - - + + Spoilers download failed Spoilers下载错误 - + No internet connection 没有网络连接 - + Error 错误 - + Spoilers already up to date Spoilers更新已准备好 - + No new spoilers added 没有新的spoilers被添加 - + Spoilers have been updated! Spoilers已更新! - + Last change: 最后变更: @@ -4260,7 +4320,7 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 StableReleaseChannel - Stable releases + Stable Releases 稳定版本 @@ -4325,233 +4385,248 @@ Cockatrice鸡蛇现在会重新载入卡组数据库。 TabDeckEditor - + &Clear all filters 清除所有筛选项目 - + Delete selected 删除所选项 - + Deck &name: 套牌名称: - + &Comments: 评论: - + Hash: 哈希值: - + &New deck 创建新套牌 - + &Load deck... 读取套牌... - + &Save deck 保存套牌 - + Save deck &as... 另存为套牌... - + Load deck from cl&ipboard... 从剪贴板读取套牌... - + &Print deck... 打印套牌... - + Search by card name 按卡牌名称搜索 - + + Add to Deck + 添加至套牌 + + + + Add to Sideboard + 添加至备牌 + + + + Show Related cards + 显示关联牌 + + + Save deck to clipboard 保存卡组到剪切板 - + Annotated 注释 - + Not Annotated 没有注释 - + &Send deck to online service 发送卡组到在线服务器 - + Create decklist (decklist.org) 创建卡组(decklist.org) - + Analyze deck (deckstats.net) 分析卡组(deckstats.net) - + Analyze deck (tappedout.net) 分析卡组(tappedout.net) - + &Close 关闭 - + Add card to &maindeck 添加卡牌到主要套牌 - + Add card to &sideboard 添加卡牌到备牌 - + &Remove row 移除卡牌 - + &Increment number 添加卡牌张数 - + &Decrement number 减少卡牌张数 - + &Deck Editor 套牌编辑 - - + + Card Info 卡牌信息 - - + + Deck 套牌 - - + + Filters 过滤器 - + &View 视图 - - - + + + Visible 可见 - - - + + + Floating 移动窗口 - + Reset layout 重置界面 - + Deck: %1 套牌: %1 - + Are you sure? 你确定吗? - + The decklist has been modified. Do you want to save the changes? 套牌列表已被修改。 你想要保存更改吗? - + Load deck 读取套牌 - - - - - + + + + + Error 错误 - + The deck could not be saved. 未能保存套牌。 - - + + The deck could not be saved. Please check that the directory is writable and try again. 套牌未能保存. 请检查目录是否可用后再重试. - + Save deck 保存套牌 - + There are no cards in your deck to be exported 你的卡组中没有卡可以导出 - + No deck was selected to be saved. 没有选择卡组用于保存 @@ -4649,175 +4724,175 @@ Please enter a name: TabGame - - - + + + Replay 录像 - - + + Game 游戏 - - + + Card Info 卡牌信息 - - + + Player List 玩家列表 - - + + Messages 消息 - - + + Replay Timeline 录像时间线 - + &Phases 阶段 - + &Game 游戏 - + Next &phase 下个阶段 - + Next &turn 下个回合 - + &Remove all local arrows 重置所有箭头 - + Rotate View Cl&ockwise 顺时针旋转视角 - + Rotate View Co&unterclockwise 逆时针旋转视角 - + Game &information 游戏信息 - + &Concede 放弃游戏 - + &Leave game 离开游戏 - + C&lose replay 关闭游戏录像 - + &Say: 说: - + &View 查看 - - - + + + Visible 可见 - - - + + + Floating 移动窗口 - + Reset layout 重置界面 - + Concede 放弃游戏 - + Are you sure you want to concede this game? 你确定放弃这个游戏? - + Leave game 离开游戏 - + Are you sure you want to leave this game? 你确定离开这这个游戏? - + You are flooding the game. Please wait a couple of seconds. 您正在刷屏。请稍等几秒钟。 - + kicked by game host or moderator 被游戏主机或管理器踢出 - + player left the game 玩家离开游戏 - + player disconnected from server 玩家与服务器断开连接 - + reason unknown 原因不明 - + You have been kicked out of the game. 你已被踢出游戏。 @@ -5176,22 +5251,22 @@ The more information you put in, the more specific your results will be. TabSupervisor - + Are you sure? 你确定吗? - + There are still open games. Are you sure you want to quit? 游戏还在继续, 你确定要退出吗? - + 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. @@ -5202,39 +5277,39 @@ 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 to moderator. 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. 你因为%1被警告。 请杜绝此类行为否则会被采取进一步行动。如果有任何疑问,请私信版主。 - + You have received the following message from the server. (custom messages like these could be untranslated) 你收到来自服务器的如下消息。 @@ -5273,6 +5348,23 @@ Please refrain from engaging in this activity or further actions may be taken ag 无法分析套牌。 + + TipsOfTheDay + + + File does not exist. + + 文件不存在 + + + + + Failed to open file. + + 打开文件失败。 + + + UpdateDownloader @@ -5597,42 +5689,42 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings 通用接口设置 - + Enable notifications in taskbar 开启任务栏提醒 - + Notify in the taskbar for game events while you are spectating 观看时在任务栏提示游戏信息 - + &Double-click cards to play them (instead of single-click) 双击卡牌开始 (而不是单击开始) - + &Play all nonlands onto the stack (not the battlefield) by default 默认将所有非地牌加入堆叠(不是战场) - + Annotate card text on tokens 用卡牌信息给衍生物标注 - + Animation settings 动画设置 - + &Tap/untap animation 横置/重置 动画 @@ -5702,97 +5794,127 @@ 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 套牌编辑器 - + Only cards in enabled sets will appear in the deck editor card list 套牌编辑器卡牌列表将只显示已启用卡组牌张 - + Card Art 牌张风格 - + Image priority is decided in the following order 图片使用的优先级由以下规则决定 - + The 这个 - + CUSTOM Folder 自定义文件夹 - + Enabled Sets (Top to Bottom) 启用的系列(从上到下) - + Disabled Sets (Top to Bottom) 禁用的系列(从上到下) - + + Warning: + 警告: + + + + While the set list is sorted by any of the columns, custom art priority setting is disabled. + 虽然所有列以系列列表排序,但是自定义优先设置被禁用。 + + + + To disable sorting click on the same column header again until this message disappears. + 若要禁用排序,请再次单击同一列标题,直到该消息消失为止。 + + + Manage sets 系列管理 - + Success 成功 - + The sets database has been saved successfully. 系列数据库已保存。 @@ -5823,7 +5945,7 @@ Please refrain from engaging in this activity or further actions may be taken ag i18n - + English 简体中文 (Chinese Simplified) @@ -5896,97 +6018,113 @@ Please refrain from engaging in this activity or further actions may be taken ag 分析套牌 - + Load deck (clipboard) 载入套牌(从剪贴板) - + Clear all filters 清除所有筛选项目 - + New deck 创建新套牌 - + Clear selected filter 清除所选的筛选项目 - + Open custom pic folder 打开自定义图片文件夹 - + Close 关闭 - + Print deck 打印套牌 - + Delete card 删除卡牌 - + Edit tokens 编辑衍生物... - + Reset layout 重置界面 - + Add card 添加卡牌 - + Save deck 保存套牌 - + Remove card 移除卡牌 - + Save deck as 另存为套牌... - + Load deck 读取套牌... - - + + Counters 指示物 - + Life 生命值 - + + + + + + + + + + + + + + Set + 系列 + + - + @@ -5994,16 +6132,15 @@ Please refrain from engaging in this activity or further actions may be taken ag - - Set - 系列 + + Add + 添加 - - + @@ -6011,455 +6148,440 @@ Please refrain from engaging in this activity or further actions may be taken ag - Add - 添加 - - - - - - - - - - - - - - + Remove 移除 - + Red - + Green 绿 - + Yellow - + Storm 风暴 - + W - + U - + B - + R - + G 绿 - + X 可变法术力 - + Main Window | Deck Editor 主窗口 | 套牌编辑器 - + Power / Toughness 力量/防御力 - + Power and Toughness 力量和防御力 - + Add (+1/+1) 添加 (+1/+1) - + Remove (-1/-1) 移除 (-1/-1) - + Toughness 防御力 - + Remove (-0/-1) 移除 (-1/-0) - + Add (+0/+1) 添加 (+0/+1) - + Power 力量 - + Remove (-1/-0) 移除 (-1/-0) - + Add (+1/+0) 添加 (+1/+0) - + Game Phases 游戏阶段 - + Untap 重置 - + Upkeep 维持 - - + + Draw - + Main 1 主要阶段1 - + Start combat 开始战斗 - + Attack 攻击 - + Block 阻挡 - + Damage 伤害 - + End combat 结束战斗 - + Main 2 主要阶段2 - + End 回合结束 - + Next phase 下个阶段 - + Next turn 下个回合 - + Playing Area 游戏区域 - + Manage sets 系列管理 - + Export deck 导出卡组 - + Save deck (clip) 保存卡组(剪切板) - + Save deck (clip; no annotations) 保存卡组(剪切板,没有注释) - + Tap / Untap Card 横置/重置卡牌 - + Untap all 重置所有 - + Toggle untap 锁定重置状态 - + Flip card 翻转卡牌 - + Peek card 查看卡牌 - + Play card 使用卡牌 - + Attach card 结附卡牌 - + Unattach card 取消结附 - + Clone card 复制卡牌 - + Create token 创造一个衍生物... - + Create all related tokens 创造所有相关的衍生物 - + Create another token 创造另一个衍生物 - + Set annotation 设置注释 - + Phases | P/T | Playing Area 阶段 | 力/防 | 游戏区域 - + Move card to 将卡移到 - + Bottom library 牌库底 - + Top library 牌库顶 - - + + Graveyard 坟墓场 - - + + Exile 放逐区 - + Hand 手牌 - + View 查看 - + Library 牌库 - + Tops card of library 牌库顶的卡牌 - + Sideboard 备牌 - + Close recent view 关闭最近的查看 - + Game Lobby 游戏大厅 - + Load remote deck 载入服务器上的套牌 - + Load local deck 从本地载入套牌 - + Gameplay 游戏 - + Draw arrow 划箭头... - + Leave game 离开游戏 - + Remove local arrows 移除本地箭头 - + Concede 放弃游戏 - + Roll dice 抛骰子 - + Rotate view CW 顺时针旋转视角 - + Shuffle library 切洗套牌 - + Rotate view CCW 逆时针旋转视角 - + Mulligan 起手牌 - + Draw card 抓牌 - + Draw cards 抓牌 - + Undo draw 撤销抓牌 - + Always reveal top card 一直展示牌库顶牌 - + Draw | Move | View | Gameplay 抓牌|移动|查看|游戏 - + How to set custom shortcuts 如何设置自定义快捷键 - + Restore all default shortcuts 恢复所有默认快捷键 - + Clear all shortcuts 清除所有快捷键 diff --git a/oracle/translations/oracle_fr.ts b/oracle/translations/oracle_fr.ts index bf9323ae6..db961967d 100644 --- a/oracle/translations/oracle_fr.ts +++ b/oracle/translations/oracle_fr.ts @@ -9,7 +9,7 @@ This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Cet assistant va importer la liste des éditions, cartes et jetons qui seront utilisés par Cockatrice. @@ -87,7 +87,7 @@ Downloading (0MB) - Téléchargement (0MB) + Téléchargement en cours (0MB) @@ -148,97 +148,97 @@ LoadSpoilersPage - + Downloading (%1MB) - + Téléchargement en cours (%1MB) - - + + Error - + Erreur - + Network error: %1. - + Erreur réseau: %1. - + The provided URL is not valid. - + L'URL fournie n'est pas valide - + Downloading (0MB) - - - - - Spoilers source selection - + Téléchargement en cours (0MB) - Please specify a spoiler source. - + Spoilers source selection + Sélection de la source des spoilers - - Download URL: - + + Please specify a spoiler source. + Veuillez spécifier une source de spoiler. + Download URL: + URL de téléchargement : + + + Restore default URL - + Restorer l'URL par défaut LoadTokensPage - + Tokens source selection sélection de la source des jetons - + Please specify a source for the list of tokens. - + Veuillez spécifier une source pour la liste des jetons. - + Download URL: URL de téléchargement: - + Restore default URL Restorer l'URL par défaut - - + + Error Erreur - + The provided URL is not valid. L'URL fournie n'est pas valide. - + Downloading (0MB) Téléchargement (0MB) - + Downloading (%1MB) Téléchargement (%1MB) - + Network error: %1. Erreur réseau : %1. @@ -246,7 +246,7 @@ OracleImporter - + Dummy set containing tokens Fausse édition contenant les jetons. @@ -268,7 +268,7 @@ SaveSetsPage - + Error Erreur @@ -293,126 +293,131 @@ Sauvergarder au chemin par défaut (recommendé) - + + &Save + Sauvegarder + + + Import finished: %1 cards. Import terminé: %1 cartes. - + %1: %2 cards imported %1: %2 cartes ajoutées. - + Save card database Sauvegarder la base de carte - + XML; card database (*.xml) XML; base de donnée de carte (*.xml) - + Success Importation réussi - + The card database has been saved successfully to %1 La base de donnée de carte à été correctement sauvegardée dans %1 - + The file could not be saved to %1 Le fichier n'a pu être sauvegarder au chemin '%1' SaveSpoilersPage - - - Spoilers imported - - + Spoilers imported + Spoilers importés + + + The spoilers file has been imported. Press "Save" to save the imported spoilers to the Cockatrice card database. - + Le fichier spoilers a été importé. Appuyez sur "Enregistrer" pour enregistrer les spoilers importés dans la base de données des cartes Cockatrice. - + Save to the default path (recommended) - - - - - Save spoiler database - + Enregistrer dans le chemin par défaut (recommandé) + Save spoiler database + Enregistrer la base de données des spoiler + + + XML; card database (*.xml) - + XML; base de données de cartes (* .xml) - + Error - + Erreur - + The file could not be saved to %1 - + Le fichier n'a pas pu être enregistré dans %1 SaveTokensPage - + Tokens imported Jetons importés - + The tokens has been imported. Press "Save" to save the imported tokens to the Cockatrice tokens database. Les jetons ont été importés. Pressez sur "Sauvegarder" pour sauver les jetons importés dans la base de données des jetons Cockatrice. - + Save to the default path (recommended) Sauvergarder au chemin par défaut (recommendé) - + Save token database Sauvegarder la base des jetons - + XML; token database (*.xml) XML; bases de données des jetons (*.xml) - + Success Réussite - + The token database has been saved successfully to %1 La base de donnée de jetons a été correctement sauvegardée dans %1 - + Error Érreur - + The file could not be saved to %1 Le fichier n'a pas pu être sauvegardé dans %1 @@ -561,7 +566,7 @@ i18n - + English Français (French) @@ -571,7 +576,7 @@ Only run in spoiler mode - + Ne fonctionne qu'en mode spoiler \ No newline at end of file diff --git a/oracle/translations/oracle_ja.ts b/oracle/translations/oracle_ja.ts index f4ea1f61f..dbd9c97b8 100644 --- a/oracle/translations/oracle_ja.ts +++ b/oracle/translations/oracle_ja.ts @@ -295,7 +295,7 @@ &Save - + 保存 diff --git a/oracle/translations/oracle_zh-Hans.ts b/oracle/translations/oracle_zh-Hans.ts index 47e335ad5..15e2b6d60 100644 --- a/oracle/translations/oracle_zh-Hans.ts +++ b/oracle/translations/oracle_zh-Hans.ts @@ -148,48 +148,48 @@ LoadSpoilersPage - + Downloading (%1MB) 下载中(%1MB) - - + + Error 错误 - + Network error: %1. 网络错误:%1 - + The provided URL is not valid. 提供的链接无效。 - + Downloading (0MB) 下载中(0MB) - + Spoilers source selection 选择spoiler资源文件 - + Please specify a spoiler source. 请指定一个spoiler资源文件 - + Download URL: 下载链接: - + Restore default URL 恢复默认链接 @@ -197,48 +197,48 @@ LoadTokensPage - + Tokens source selection 衍生物资源选择 - + Please specify a source for the list of tokens. 请衍生物列表指定一个资源文件 - + Download URL: 下载链接: - + Restore default URL 恢复默认链接 - - + + Error 错误 - + The provided URL is not valid. 提供的链接无效。 - + Downloading (0MB) 下载中(0MB) - + Downloading (%1MB) 下载中(%1MB) - + Network error: %1. 网络错误:%1 @@ -246,7 +246,7 @@ OracleImporter - + Dummy set containing tokens 包含衍生物的虚拟系列 @@ -268,7 +268,7 @@ SaveSetsPage - + Error 错误 @@ -293,39 +293,44 @@ 保存到默认路径(推荐) - + + &Save + 保存 + + + Import finished: %1 cards. 导入成功:%1张卡牌。 - + %1: %2 cards imported %1:%2张卡牌被导入 - + Save card database 保存卡牌数据库 - + XML; card database (*.xml) XML;卡牌数据库(*.xml) - + Success 成功 - + The card database has been saved successfully to %1 卡牌数据库已保存到 %1 - + The file could not be saved to %1 文件无法被保存到%1 @@ -333,37 +338,37 @@ SaveSpoilersPage - + Spoilers imported 导入spoilers  - + The spoilers file has been imported. Press "Save" to save the imported spoilers to the Cockatrice card database. spoilers文件已导入,请按保存按钮将导入的spoilers储存到鸡蛇卡牌数据库中。 - + Save to the default path (recommended) 保存到默认路径(推荐) - + Save spoiler database 保存spoiler数据库 - + XML; card database (*.xml) XML;卡牌数据库(*.xml) - + Error 错误 - + The file could not be saved to %1 文件无法被保存到%1 @@ -371,49 +376,49 @@ SaveTokensPage - + Tokens imported 衍生物已导入 - + The tokens has been imported. Press "Save" to save the imported tokens to the Cockatrice tokens database. 衍生物已导入。请点击“保存”按钮将衍生物保存到Cockatrice鸡蛇衍生物数据库。 - + Save to the default path (recommended) 保存到默认路径(推荐) - + Save token database 保存衍生物数据库 - + XML; token database (*.xml) XML; 衍生物数据库 (*.xml) - + Success 成功 - + The token database has been saved successfully to %1 衍生物数据库已经保到 %1 - + Error 错误 - + The file could not be saved to %1 文件无法被保存到%1 @@ -562,7 +567,7 @@ i18n - + English 简体中文 (Chinese Simplified) From 4616dd47bcf659a2522726151b295abd5dc9da1f Mon Sep 17 00:00:00 2001 From: Zach Halpern Date: Thu, 20 Dec 2018 18:26:46 -0500 Subject: [PATCH 29/62] preferences sym not dir --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1bb149f82..a58ddf74b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ mysql.cnf .idea/ *.aps cmake-build-debug/ -preferences/ +preferences From ab1c4cb1d7df51021b198899b2717f434cd39f72 Mon Sep 17 00:00:00 2001 From: Zach H Date: Thu, 20 Dec 2018 18:49:53 -0500 Subject: [PATCH 30/62] Use scryfall urls (#3474) --- cockatrice/src/pictureloader.cpp | 6 ++++++ cockatrice/src/settingscache.h | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/pictureloader.cpp b/cockatrice/src/pictureloader.cpp index 7d3e32ff6..8d2c4b2c6 100644 --- a/cockatrice/src/pictureloader.cpp +++ b/cockatrice/src/pictureloader.cpp @@ -25,6 +25,10 @@ // never cache more than 300 cards at once for a single deck #define CACHED_CARD_PER_DECK_MAX 300 +// Other URLs we can use (TODO: Make this less messy) +#define GATHERER_DEFAULT "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card" +#define GATHERER_FALLBACK "http://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card" + class PictureToLoad::SetDownloadPriorityComparator { public: @@ -48,6 +52,8 @@ PictureToLoad::PictureToLoad(CardInfoPtr _card) : card(std::move(_card)) /* #2479 will expand this into a list of Urls */ urlTemplates.append(settingsCache->getPicUrl()); urlTemplates.append(settingsCache->getPicUrlFallback()); + urlTemplates.append(GATHERER_DEFAULT); + urlTemplates.append(GATHERER_FALLBACK); if (card) { sortedSets = card->getSets(); diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index 1f21a8c2b..b3d82889d 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -13,9 +13,10 @@ class ReleaseChannel; -// the falbacks are used for cards without a muid -#define PIC_URL_DEFAULT "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card" -#define PIC_URL_FALLBACK "http://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card" +// Fallbacks used for cards w/o MultiverseId +#define PIC_URL_DEFAULT "https://api.scryfall.com/cards/multiverse/!cardid!?format=image" +#define PIC_URL_FALLBACK "https://api.scryfall.com/cards/named?fuzzy=!name!&format=image" + // size should be a multiple of 64 #define PIXMAPCACHE_SIZE_DEFAULT 2047 #define PIXMAPCACHE_SIZE_MIN 64 From 59300e61fc2653d90b63e55d8a39215d5ffeeaae Mon Sep 17 00:00:00 2001 From: Caledor Date: Fri, 28 Dec 2018 21:59:11 +0100 Subject: [PATCH 31/62] Minor translation issue fix (#3481) --- cockatrice/src/sequenceEdit/ui_shortcutstab.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/sequenceEdit/ui_shortcutstab.h b/cockatrice/src/sequenceEdit/ui_shortcutstab.h index 17c786a8d..69b1a0afe 100644 --- a/cockatrice/src/sequenceEdit/ui_shortcutstab.h +++ b/cockatrice/src/sequenceEdit/ui_shortcutstab.h @@ -1983,7 +1983,7 @@ public: lbl_TabGame_aRotateViewCW->setText(QApplication::translate("shortcutsTab", "Rotate view CW", 0)); lbl_Player_aShuffle->setText(QApplication::translate("shortcutsTab", "Shuffle library", 0)); lbl_TabGame_aRotateViewCCW->setText(QApplication::translate("shortcutsTab", "Rotate view CCW", 0)); - groupBox_draw->setTitle(QApplication::translate("shortcutsTab", "Draw", 0)); + groupBox_draw->setTitle(QApplication::translate("shortcutsTab", "Drawing", 0)); lbl_Player_aMulligan->setText(QApplication::translate("shortcutsTab", "Mulligan", 0)); lbl_Player_aDrawCard->setText(QApplication::translate("shortcutsTab", "Draw card", 0)); lbl_Player_aDrawCards->setText(QApplication::translate("shortcutsTab", "Draw cards", 0)); From 40f787be14e2ccb4531abe920953ca95cce8f511 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Mon, 31 Dec 2018 00:51:03 +0100 Subject: [PATCH 32/62] add an ignore button to sort warning (#3463) * add an ignore button to sort warning * change warning message * formatting --- cockatrice/src/window_sets.cpp | 32 +++++++++++++++++++++++++------- cockatrice/src/window_sets.h | 6 +++++- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/cockatrice/src/window_sets.cpp b/cockatrice/src/window_sets.cpp index a637f0243..95ac6ba4f 100644 --- a/cockatrice/src/window_sets.cpp +++ b/cockatrice/src/window_sets.cpp @@ -139,13 +139,20 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent) hintsGroupBox = new QGroupBox(tr("Hints")); hintsGroupBox->setLayout(hintsGrid); - sortWarning = new QLabel; - sortWarning->setWordWrap(true); - sortWarning->setText( - "" + tr("Warning: ") + "
" + - tr("While the set list is sorted by any of the columns, custom art priority setting is disabled.") + "
" + - tr("To disable sorting click on the same column header again until this message disappears.")); - sortWarning->setStyleSheet("QLabel { background-color:red;}"); + sortWarning = new QGroupBox(tr("Note")); + QGridLayout *sortWarningLayout = new QGridLayout; + sortWarningText = new QLabel; + sortWarningText->setWordWrap(true); + sortWarningText->setText(tr("Sorting by column allows you to find a set while not changing set priority.") + " " + + tr("To enable ordering again, click the column header until this message disappears.")); + sortWarningLayout->addWidget(sortWarningText, 0, 0, 1, 2); + sortWarningButton = new QPushButton; + sortWarningButton->setText(tr("Use the current sorting as the set priority instead")); + sortWarningButton->setToolTip(tr("Sorts the set priority using the same column")); + sortWarningButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + connect(sortWarningButton, SIGNAL(released()), this, SLOT(actIgnoreWarning())); + sortWarningLayout->addWidget(sortWarningButton, 1, 0); + sortWarning->setLayout(sortWarningLayout); sortWarning->setVisible(false); buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); @@ -254,6 +261,17 @@ void WndSets::actSort(int index) } } +void WndSets::actIgnoreWarning() +{ + if (sortIndex < 0) { + return; + } + model->sort(sortIndex, sortOrder); + view->header()->setSortIndicator(SORT_RESET, Qt::DescendingOrder); + sortIndex = -1; + sortWarning->setVisible(false); +} + void WndSets::actDisableSortButtons(int index) { if (index != SORT_RESET) { diff --git a/cockatrice/src/window_sets.h b/cockatrice/src/window_sets.h index d8ce32915..1013bc43b 100644 --- a/cockatrice/src/window_sets.h +++ b/cockatrice/src/window_sets.h @@ -31,7 +31,10 @@ private: QAction *aUp, *aDown, *aBottom, *aTop; QToolBar *setsEditToolBar; QDialogButtonBox *buttonBox; - QLabel *labNotes, *searchLabel, *sortWarning; + QLabel *labNotes, *searchLabel; + QGroupBox *sortWarning; + QLabel *sortWarningText; + QPushButton *sortWarningButton; QLineEdit *searchField; QGridLayout *mainLayout; QHBoxLayout *filterBox; @@ -67,6 +70,7 @@ private slots: void actRestoreOriginalOrder(); void actDisableResetButton(const QString &filterText); void actSort(int index); + void actIgnoreWarning(); }; #endif From 2621cc09acbf052fae7b857c3b1a83e0b495377d Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Mon, 31 Dec 2018 01:05:40 +0100 Subject: [PATCH 33/62] Optimize json parsing (#3480) --- cockatrice/src/qt-json/json.cpp | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cockatrice/src/qt-json/json.cpp b/cockatrice/src/qt-json/json.cpp index faf6601b3..a5dd69c20 100644 --- a/cockatrice/src/qt-json/json.cpp +++ b/cockatrice/src/qt-json/json.cpp @@ -1,28 +1,28 @@ /* Copyright 2011 Eeli Reilin. All rights reserved. * - * Redistribution and use in source and binary forms, with or without + * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * - * 1. Redistributions of source code must retain the above copyright notice, + * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation + * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - * EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * The views and conclusions contained in the software and documentation - * are those of the authors and should not be interpreted as representing + * The views and conclusions contained in the software and documentation + * are those of the authors and should not be interpreted as representing * official policies, either expressed or implied, of Eeli Reilin. */ @@ -503,11 +503,12 @@ QVariant Json::parseNumber(const QString &json, int &index) */ int Json::lastIndexOfNumber(const QString &json, int index) { + static const QString numericCharacters("0123456789+-.eE"); int lastIndex; for(lastIndex = index; lastIndex < json.size(); lastIndex++) { - if(QString("0123456789+-.eE").indexOf(json[lastIndex]) == -1) + if(numericCharacters.indexOf(json[lastIndex]) == -1) { break; } @@ -521,9 +522,10 @@ int Json::lastIndexOfNumber(const QString &json, int index) */ void Json::eatWhitespace(const QString &json, int &index) { + static const QString whitespaceChars(" \t\n\r"); for(; index < json.size(); index++) { - if(QString(" \t\n\r").indexOf(json[index]) == -1) + if(whitespaceChars.indexOf(json[index]) == -1) { break; } From 84f6da103f64396335d693d676344613f3f5c0cd Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sun, 6 Jan 2019 19:46:18 +0100 Subject: [PATCH 34/62] remove all dos line endings (#3489) --- cmake/FindVCredistRuntime.cmake | 72 +- cmake/FindWin32SslRuntime.cmake | 142 +- oracle/src/zip/unzip.cpp | 2854 +++++++++++++-------------- oracle/src/zip/unzip.h | 304 +-- oracle/src/zip/unzip_p.h | 260 +-- oracle/src/zip/zip.cpp | 3238 +++++++++++++++---------------- oracle/src/zip/zip.h | 316 +-- oracle/src/zip/zip_p.h | 266 +-- oracle/src/zip/zipentry_p.h | 182 +- oracle/src/zip/zipglobal.cpp | 304 +-- oracle/src/zip/zipglobal.h | 154 +- 11 files changed, 4046 insertions(+), 4046 deletions(-) diff --git a/cmake/FindVCredistRuntime.cmake b/cmake/FindVCredistRuntime.cmake index 9c3d335b8..a9d074456 100644 --- a/cmake/FindVCredistRuntime.cmake +++ b/cmake/FindVCredistRuntime.cmake @@ -1,36 +1,36 @@ -# Find the MS Visual Studio VC redistributable package - -if (WIN32) - set(VCREDISTRUNTIME_FOUND "NO") - - if(CMAKE_SIZEOF_VOID_P EQUAL 8) # 64-bit - set(REDIST_ARCH x64) - else() - set(REDIST_ARCH x86) - endif() - - set(REDIST_FILE vcredist_${REDIST_ARCH}.exe) - - set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) - include(InstallRequiredSystemLibraries) - - # Check if the list contains minimum one element, to get the path from - list(LENGTH CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS libsCount) - if (libsCount GREATER 0) - list(GET CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS 0 _path) - - get_filename_component(_path ${_path} DIRECTORY) - get_filename_component(_path ${_path}/../../ ABSOLUTE) - - if (EXISTS "${_path}/${REDIST_FILE}") # VS 2017 - set(VCREDISTRUNTIME_FOUND "YES") - set(VCREDISTRUNTIME_FILE ${_path}/${REDIST_FILE}) - endif() - endif() - - if(VCREDISTRUNTIME_FOUND) - message(STATUS "Found VCredist ${VCREDISTRUNTIME_FILE}") - else() - message(WARNING "Could not find VCredist package. It's not required for compiling, but needs to be available at runtime.") - endif() -endif() +# Find the MS Visual Studio VC redistributable package + +if (WIN32) + set(VCREDISTRUNTIME_FOUND "NO") + + if(CMAKE_SIZEOF_VOID_P EQUAL 8) # 64-bit + set(REDIST_ARCH x64) + else() + set(REDIST_ARCH x86) + endif() + + set(REDIST_FILE vcredist_${REDIST_ARCH}.exe) + + set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) + include(InstallRequiredSystemLibraries) + + # Check if the list contains minimum one element, to get the path from + list(LENGTH CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS libsCount) + if (libsCount GREATER 0) + list(GET CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS 0 _path) + + get_filename_component(_path ${_path} DIRECTORY) + get_filename_component(_path ${_path}/../../ ABSOLUTE) + + if (EXISTS "${_path}/${REDIST_FILE}") # VS 2017 + set(VCREDISTRUNTIME_FOUND "YES") + set(VCREDISTRUNTIME_FILE ${_path}/${REDIST_FILE}) + endif() + endif() + + if(VCREDISTRUNTIME_FOUND) + message(STATUS "Found VCredist ${VCREDISTRUNTIME_FILE}") + else() + message(WARNING "Could not find VCredist package. It's not required for compiling, but needs to be available at runtime.") + endif() +endif() diff --git a/cmake/FindWin32SslRuntime.cmake b/cmake/FindWin32SslRuntime.cmake index 79ba688d4..37ffd0b0e 100644 --- a/cmake/FindWin32SslRuntime.cmake +++ b/cmake/FindWin32SslRuntime.cmake @@ -1,71 +1,71 @@ -# Find the OpenSSL runtime libraries (.dll) for Windows that -# will be needed by Qt in order to access https urls. - -if (WIN32) - # Get standard installation paths for OpenSSL under Windows - - # http://www.slproweb.com/products/Win32OpenSSL.html - - if( CMAKE_SIZEOF_VOID_P EQUAL 8 ) - # target win64 - set(_OPENSSL_ROOT_HINTS - ${OPENSSL_ROOT_DIR} - "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\OpenSSL (64-bit)_is1;Inno Setup: App Path]" - ENV OPENSSL_ROOT_DIR - ) - file(TO_CMAKE_PATH "$ENV{PROGRAMFILES}" _programfiles) - set(_OPENSSL_ROOT_PATHS - "C:/Tools/vcpkg/installed/x64-windows/bin" - "${_programfiles}/OpenSSL-Win64" - "C:/OpenSSL-Win64/" - ) - unset(_programfiles) - else( CMAKE_SIZEOF_VOID_P EQUAL 8 ) - # target win32 - set(_OPENSSL_ROOT_HINTS - ${OPENSSL_ROOT_DIR} - "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\OpenSSL (32-bit)_is1;Inno Setup: App Path]" - ENV OPENSSL_ROOT_DIR - ) - file(TO_CMAKE_PATH "$ENV{PROGRAMFILES}" _programfiles) - set(_OPENSSL_ROOT_PATHS - "C:/Tools/vcpkg/installed/x86-windows/bin" - "${_programfiles}/OpenSSL" - "${_programfiles}/OpenSSL-Win32" - "C:/OpenSSL/" - "C:/OpenSSL-Win32/" - ) - unset(_programfiles) - endif( CMAKE_SIZEOF_VOID_P EQUAL 8 ) - -else () - set(_OPENSSL_ROOT_HINTS - ${OPENSSL_ROOT_DIR} - ENV OPENSSL_ROOT_DIR - ) -endif () - -set(_OPENSSL_ROOT_HINTS_AND_PATHS - HINTS ${_OPENSSL_ROOT_HINTS} - PATHS ${_OPENSSL_ROOT_PATHS} - ) - -# For OpenSSL < 1.1, they are named libeay32 and ssleay32 and even if the dll is 64bit, it's still suffixed as *32.dll -# For OpenSSL >= 1.1, they are named libcrypto and libssl with no suffix -FIND_FILE(WIN32SSLRUNTIME_LIBEAY NAMES libeay32.dll libcrypto.dll ${_OPENSSL_ROOT_HINTS_AND_PATHS}) -FIND_FILE(WIN32SSLRUNTIME_SSLEAY NAMES ssleay32.dll libssl.dll ${_OPENSSL_ROOT_HINTS_AND_PATHS}) - - -IF(WIN32SSLRUNTIME_LIBEAY AND WIN32SSLRUNTIME_SSLEAY) - SET(WIN32SSLRUNTIME_LIBRARIES "${WIN32SSLRUNTIME_LIBEAY}" "${WIN32SSLRUNTIME_SSLEAY}") - SET(WIN32SSLRUNTIME_FOUND "YES") - message(STATUS "Found OpenSSL ${WIN32SSLRUNTIME_LIBRARIES}") -ELSE() - SET(WIN32SSLRUNTIME_FOUND "NO") - message(WARNING "Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime.") -ENDIF() - -MARK_AS_ADVANCED( - WIN32SSLRUNTIME_LIBEAY - WIN32SSLRUNTIME_SSLEAY - ) +# Find the OpenSSL runtime libraries (.dll) for Windows that +# will be needed by Qt in order to access https urls. + +if (WIN32) + # Get standard installation paths for OpenSSL under Windows + + # http://www.slproweb.com/products/Win32OpenSSL.html + + if( CMAKE_SIZEOF_VOID_P EQUAL 8 ) + # target win64 + set(_OPENSSL_ROOT_HINTS + ${OPENSSL_ROOT_DIR} + "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\OpenSSL (64-bit)_is1;Inno Setup: App Path]" + ENV OPENSSL_ROOT_DIR + ) + file(TO_CMAKE_PATH "$ENV{PROGRAMFILES}" _programfiles) + set(_OPENSSL_ROOT_PATHS + "C:/Tools/vcpkg/installed/x64-windows/bin" + "${_programfiles}/OpenSSL-Win64" + "C:/OpenSSL-Win64/" + ) + unset(_programfiles) + else( CMAKE_SIZEOF_VOID_P EQUAL 8 ) + # target win32 + set(_OPENSSL_ROOT_HINTS + ${OPENSSL_ROOT_DIR} + "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\OpenSSL (32-bit)_is1;Inno Setup: App Path]" + ENV OPENSSL_ROOT_DIR + ) + file(TO_CMAKE_PATH "$ENV{PROGRAMFILES}" _programfiles) + set(_OPENSSL_ROOT_PATHS + "C:/Tools/vcpkg/installed/x86-windows/bin" + "${_programfiles}/OpenSSL" + "${_programfiles}/OpenSSL-Win32" + "C:/OpenSSL/" + "C:/OpenSSL-Win32/" + ) + unset(_programfiles) + endif( CMAKE_SIZEOF_VOID_P EQUAL 8 ) + +else () + set(_OPENSSL_ROOT_HINTS + ${OPENSSL_ROOT_DIR} + ENV OPENSSL_ROOT_DIR + ) +endif () + +set(_OPENSSL_ROOT_HINTS_AND_PATHS + HINTS ${_OPENSSL_ROOT_HINTS} + PATHS ${_OPENSSL_ROOT_PATHS} + ) + +# For OpenSSL < 1.1, they are named libeay32 and ssleay32 and even if the dll is 64bit, it's still suffixed as *32.dll +# For OpenSSL >= 1.1, they are named libcrypto and libssl with no suffix +FIND_FILE(WIN32SSLRUNTIME_LIBEAY NAMES libeay32.dll libcrypto.dll ${_OPENSSL_ROOT_HINTS_AND_PATHS}) +FIND_FILE(WIN32SSLRUNTIME_SSLEAY NAMES ssleay32.dll libssl.dll ${_OPENSSL_ROOT_HINTS_AND_PATHS}) + + +IF(WIN32SSLRUNTIME_LIBEAY AND WIN32SSLRUNTIME_SSLEAY) + SET(WIN32SSLRUNTIME_LIBRARIES "${WIN32SSLRUNTIME_LIBEAY}" "${WIN32SSLRUNTIME_SSLEAY}") + SET(WIN32SSLRUNTIME_FOUND "YES") + message(STATUS "Found OpenSSL ${WIN32SSLRUNTIME_LIBRARIES}") +ELSE() + SET(WIN32SSLRUNTIME_FOUND "NO") + message(WARNING "Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime.") +ENDIF() + +MARK_AS_ADVANCED( + WIN32SSLRUNTIME_LIBEAY + WIN32SSLRUNTIME_SSLEAY + ) diff --git a/oracle/src/zip/unzip.cpp b/oracle/src/zip/unzip.cpp index 6640e0a98..716952c69 100755 --- a/oracle/src/zip/unzip.cpp +++ b/oracle/src/zip/unzip.cpp @@ -1,1427 +1,1427 @@ -/**************************************************************************** -** Filename: unzip.cpp -** Last updated [dd/mm/yyyy]: 08/07/2010 -** -** pkzip 2.0 decompression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -#include "unzip.h" -#include "unzip_p.h" -#include "zipentry_p.h" - -#include -#include -#include -#include -#include - -// You can remove this #include if you replace the qDebug() statements. -#include - -/*! - \class UnZip unzip.h - - \brief PKZip 2.0 file decompression. - Compatibility with later versions is not ensured as they may use - unsupported compression algorithms. - Versions after 2.7 may have an incompatible header format and thus be - completely incompatible. -*/ - -/*! \enum UnZip::ErrorCode The result of a decompression operation. - \value UnZip::Ok No error occurred. - \value UnZip::ZlibInit Failed to init or load the zlib library. - \value UnZip::ZlibError The zlib library returned some error. - \value UnZip::OpenFailed Unable to create or open a device. - \value UnZip::PartiallyCorrupted Corrupted zip archive - some files could be extracted. - \value UnZip::Corrupted Corrupted or invalid zip archive. - \value UnZip::WrongPassword Unable to decrypt a password protected file. - \value UnZip::NoOpenArchive No archive has been opened yet. - \value UnZip::FileNotFound Unable to find the requested file in the archive. - \value UnZip::ReadFailed Reading of a file failed. - \value UnZip::WriteFailed Writing of a file failed. - \value UnZip::SeekFailed Seek failed. - \value UnZip::CreateDirFailed Could not create a directory. - \value UnZip::InvalidDevice A null device has been passed as parameter. - \value UnZip::InvalidArchive This is not a valid (or supported) ZIP archive. - \value UnZip::HeaderConsistencyError Local header record info does not match with the central directory record info. The archive may be corrupted. - - \value UnZip::Skip Internal use only. - \value UnZip::SkipAll Internal use only. -*/ - -/*! \enum UnZip::ExtractionOptions Some options for the file extraction methods. - \value UnZip::ExtractPaths Default. Does not ignore the path of the zipped files. - \value UnZip::SkipPaths Default. Ignores the path of the zipped files and extracts them all to the same root directory. - \value UnZip::VerifyOnly Doesn't actually extract files. - \value UnZip::NoSilentDirectoryCreation Doesn't attempt to silently create missing output directories. -*/ - -//! Local header size (excluding signature, excluding variable length fields) -#define UNZIP_LOCAL_HEADER_SIZE 26 -//! Central Directory file entry size (excluding signature, excluding variable length fields) -#define UNZIP_CD_ENTRY_SIZE_NS 42 -//! Data descriptor size (excluding signature) -#define UNZIP_DD_SIZE 12 -//! End Of Central Directory size (including signature, excluding variable length fields) -#define UNZIP_EOCD_SIZE 22 -//! Local header entry encryption header size -#define UNZIP_LOCAL_ENC_HEADER_SIZE 12 - -// Some offsets inside a CD record (excluding signature) -#define UNZIP_CD_OFF_VERSION_MADE 0 -#define UNZIP_CD_OFF_VERSION 2 -#define UNZIP_CD_OFF_GPFLAG 4 -#define UNZIP_CD_OFF_CMETHOD 6 -#define UNZIP_CD_OFF_MODT 8 -#define UNZIP_CD_OFF_MODD 10 -#define UNZIP_CD_OFF_CRC32 12 -#define UNZIP_CD_OFF_CSIZE 16 -#define UNZIP_CD_OFF_USIZE 20 -#define UNZIP_CD_OFF_NAMELEN 24 -#define UNZIP_CD_OFF_XLEN 26 -#define UNZIP_CD_OFF_COMMLEN 28 -#define UNZIP_CD_OFF_LHOFFSET 38 - -// Some offsets inside a local header record (excluding signature) -#define UNZIP_LH_OFF_VERSION 0 -#define UNZIP_LH_OFF_GPFLAG 2 -#define UNZIP_LH_OFF_CMETHOD 4 -#define UNZIP_LH_OFF_MODT 6 -#define UNZIP_LH_OFF_MODD 8 -#define UNZIP_LH_OFF_CRC32 10 -#define UNZIP_LH_OFF_CSIZE 14 -#define UNZIP_LH_OFF_USIZE 18 -#define UNZIP_LH_OFF_NAMELEN 22 -#define UNZIP_LH_OFF_XLEN 24 - -// Some offsets inside a data descriptor record (excluding signature) -#define UNZIP_DD_OFF_CRC32 0 -#define UNZIP_DD_OFF_CSIZE 4 -#define UNZIP_DD_OFF_USIZE 8 - -// Some offsets inside a EOCD record -#define UNZIP_EOCD_OFF_ENTRIES 6 -#define UNZIP_EOCD_OFF_CDOFF 12 -#define UNZIP_EOCD_OFF_COMMLEN 16 - -/*! - Max version handled by this API. - 0x14 = 2.0 --> full compatibility only up to this version; - later versions use unsupported features -*/ -#define UNZIP_VERSION 0x14 - -//! CRC32 routine -#define CRC32(c, b) crcTable[((int)c^b) & 0xff] ^ (c >> 8) - -OSDAB_BEGIN_NAMESPACE(Zip) - - -/************************************************************************ - ZipEntry -*************************************************************************/ - -/*! - ZipEntry constructor - initialize data. Type is set to File. -*/ -UnZip::ZipEntry::ZipEntry() -{ - compressedSize = uncompressedSize = crc32 = 0; - compression = NoCompression; - type = File; - encrypted = false; -} - - -/************************************************************************ - Private interface -*************************************************************************/ - -//! \internal -UnzipPrivate::UnzipPrivate() : - password(), - skipAllEncrypted(false), - headers(0), - device(0), - file(0), - uBuffer(0), - crcTable(0), - cdOffset(0), - eocdOffset(0), - cdEntryCount(0), - unsupportedEntryCount(0), - comment() -{ - uBuffer = (unsigned char*) buffer1; - crcTable = (quint32*) get_crc_table(); -} - -//! \internal -void UnzipPrivate::deviceDestroyed(QObject*) -{ - qDebug("Unexpected device destruction detected."); - do_closeArchive(); -} - -//! \internal Parses a Zip archive. -UnZip::ErrorCode UnzipPrivate::openArchive(QIODevice* dev) -{ - Q_ASSERT(!device); - Q_ASSERT(dev); - - if (!(dev->isOpen() || dev->open(QIODevice::ReadOnly))) { - qDebug() << "Unable to open device for reading"; - return UnZip::OpenFailed; - } - - device = dev; - if (device != file) - connect(device, SIGNAL(destroyed(QObject*)), this, SLOT(deviceDestroyed(QObject*))); - - UnZip::ErrorCode ec; - - ec = seekToCentralDirectory(); - if (ec != UnZip::Ok) { - closeArchive(); - return ec; - } - - //! \todo Ignore CD entry count? CD may be corrupted. - if (cdEntryCount == 0) { - return UnZip::Ok; - } - - bool continueParsing = true; - - while (continueParsing) { - if (device->read(buffer1, 4) != 4) { - if (headers) { - qDebug() << "Corrupted zip archive. Some files might be extracted."; - ec = headers->size() != 0 ? UnZip::PartiallyCorrupted : UnZip::Corrupted; - break; - } else { - closeArchive(); - qDebug() << "Corrupted or invalid zip archive. Closing."; - ec = UnZip::Corrupted; - break; - } - } - - if (! (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x01 && buffer1[3] == 0x02) ) - break; - - if ((ec = parseCentralDirectoryRecord()) != UnZip::Ok) - break; - } - - if (ec != UnZip::Ok) - closeArchive(); - - return ec; -} - -/* - \internal Parses a local header record and makes some consistency check - with the information stored in the Central Directory record for this entry - that has been previously parsed. - \todo Optional consistency check (as a ExtractionOptions flag) - - local file header signature 4 bytes (0x04034b50) - version needed to extract 2 bytes - general purpose bit flag 2 bytes - compression method 2 bytes - last mod file time 2 bytes - last mod file date 2 bytes - crc-32 4 bytes - compressed size 4 bytes - uncompressed size 4 bytes - file name length 2 bytes - extra field length 2 bytes - - file name (variable size) - extra field (variable size) -*/ -UnZip::ErrorCode UnzipPrivate::parseLocalHeaderRecord(const QString& path, const ZipEntryP& entry) -{ - Q_ASSERT(device); - - if (!device->seek(entry.lhOffset)) - return UnZip::SeekFailed; - - // Test signature - if (device->read(buffer1, 4) != 4) - return UnZip::ReadFailed; - - if ((buffer1[0] != 'P') || (buffer1[1] != 'K') || (buffer1[2] != 0x03) || (buffer1[3] != 0x04)) - return UnZip::InvalidArchive; - - if (device->read(buffer1, UNZIP_LOCAL_HEADER_SIZE) != UNZIP_LOCAL_HEADER_SIZE) - return UnZip::ReadFailed; - - /* - Check 3rd general purpose bit flag. - - "bit 3: If this bit is set, the fields crc-32, compressed size - and uncompressed size are set to zero in the local - header. The correct values are put in the data descriptor - immediately following the compressed data." - */ - bool hasDataDescriptor = entry.hasDataDescriptor(); - bool checkFailed = entry.compMethod != getUShort(uBuffer, UNZIP_LH_OFF_CMETHOD); - - if (!checkFailed) - checkFailed = entry.gpFlag[0] != uBuffer[UNZIP_LH_OFF_GPFLAG]; - if (!checkFailed) - checkFailed = entry.gpFlag[1] != uBuffer[UNZIP_LH_OFF_GPFLAG + 1]; - if (!checkFailed) - checkFailed = entry.modTime[0] != uBuffer[UNZIP_LH_OFF_MODT]; - if (!checkFailed) - checkFailed = entry.modTime[1] != uBuffer[UNZIP_LH_OFF_MODT + 1]; - if (!checkFailed) - checkFailed = entry.modDate[0] != uBuffer[UNZIP_LH_OFF_MODD]; - if (!checkFailed) - checkFailed = entry.modDate[1] != uBuffer[UNZIP_LH_OFF_MODD + 1]; - if (!hasDataDescriptor) - { - if (!checkFailed) - checkFailed = entry.crc != getULong(uBuffer, UNZIP_LH_OFF_CRC32); - if (!checkFailed) - checkFailed = entry.szComp != getULong(uBuffer, UNZIP_LH_OFF_CSIZE); - if (!checkFailed) - checkFailed = entry.szUncomp != getULong(uBuffer, UNZIP_LH_OFF_USIZE); - } - - if (checkFailed) - return UnZip::HeaderConsistencyError; - - // Check filename - quint16 szName = getUShort(uBuffer, UNZIP_LH_OFF_NAMELEN); - if (szName == 0) - return UnZip::HeaderConsistencyError; - - if (device->read(buffer2, szName) != szName) - return UnZip::ReadFailed; - - QString filename = QString::fromLatin1(buffer2, szName); - if (filename != path) { - qDebug() << "Filename in local header mismatches."; - return UnZip::HeaderConsistencyError; - } - - // Skip extra field - quint16 szExtra = getUShort(uBuffer, UNZIP_LH_OFF_XLEN); - if (szExtra != 0) { - if (!device->seek(device->pos() + szExtra)) - return UnZip::SeekFailed; - } - - entry.dataOffset = device->pos(); - - if (hasDataDescriptor) { - /* - The data descriptor has this OPTIONAL signature: PK\7\8 - We try to skip the compressed data relying on the size set in the - Central Directory record. - */ - if (!device->seek(device->pos() + entry.szComp)) - return UnZip::SeekFailed; - - // Read 4 bytes and check if there is a data descriptor signature - if (device->read(buffer2, 4) != 4) - return UnZip::ReadFailed; - - bool hasSignature = buffer2[0] == 'P' && buffer2[1] == 'K' && buffer2[2] == 0x07 && buffer2[3] == 0x08; - if (hasSignature) { - if (device->read(buffer2, UNZIP_DD_SIZE) != UNZIP_DD_SIZE) - return UnZip::ReadFailed; - } else { - if (device->read(buffer2 + 4, UNZIP_DD_SIZE - 4) != UNZIP_DD_SIZE - 4) - return UnZip::ReadFailed; - } - - // DD: crc, compressed size, uncompressed size - if ( - entry.crc != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CRC32) || - entry.szComp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CSIZE) || - entry.szUncomp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_USIZE) - ) - return UnZip::HeaderConsistencyError; - } - - return UnZip::Ok; -} - -/*! \internal Attempts to find the start of the central directory record. - - We seek the file back until we reach the "End Of Central Directory" - signature PK\5\6. - - end of central dir signature 4 bytes (0x06054b50) - number of this disk 2 bytes - number of the disk with the - start of the central directory 2 bytes - total number of entries in the - central directory on this disk 2 bytes - total number of entries in - the central directory 2 bytes - size of the central directory 4 bytes - offset of start of central - directory with respect to - the starting disk number 4 bytes - .ZIP file comment length 2 bytes - --- SIZE UNTIL HERE: UNZIP_EOCD_SIZE --- - .ZIP file comment (variable size) -*/ -UnZip::ErrorCode UnzipPrivate::seekToCentralDirectory() -{ - Q_ASSERT(device); - - qint64 length = device->size(); - qint64 offset = length - UNZIP_EOCD_SIZE; - - if (length < UNZIP_EOCD_SIZE) - return UnZip::InvalidArchive; - - if (!device->seek( offset )) - return UnZip::SeekFailed; - - if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE) - return UnZip::ReadFailed; - - bool eocdFound = (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x05 && buffer1[3] == 0x06); - - if (eocdFound) { - // Zip file has no comment (the only variable length field in the EOCD record) - eocdOffset = offset; - } else { - qint64 read; - char* p = 0; - - offset -= UNZIP_EOCD_SIZE; - - if (offset <= 0) - return UnZip::InvalidArchive; - - if (!device->seek( offset )) - return UnZip::SeekFailed; - - while ((read = device->read(buffer1, UNZIP_EOCD_SIZE)) >= 0) { - if ( (p = strstr(buffer1, "PK\5\6")) != 0) { - // Seek to the start of the EOCD record so we can read it fully - // Yes... we could simply read the missing bytes and append them to the buffer - // but this is far easier so heck it! - device->seek( offset + (p - buffer1) ); - eocdFound = true; - eocdOffset = offset + (p - buffer1); - - // Read EOCD record - if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE) - return UnZip::ReadFailed; - - break; - } - - // TODO: This is very slow and only a temporary bug fix. Need some pattern matching algorithm here. - offset -= 1 /*UNZIP_EOCD_SIZE*/; - if (offset <= 0) - return UnZip::InvalidArchive; - - if (!device->seek( offset )) - return UnZip::SeekFailed; - } - } - - if (!eocdFound) - return UnZip::InvalidArchive; - - // Parse EOCD to locate CD offset - offset = getULong((const unsigned char*)buffer1, UNZIP_EOCD_OFF_CDOFF + 4); - - cdOffset = offset; - - cdEntryCount = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_ENTRIES + 4); - - quint16 commentLength = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_COMMLEN + 4); - if (commentLength != 0) { - QByteArray c = device->read(commentLength); - if (c.count() != commentLength) - return UnZip::ReadFailed; - - comment = c; - } - - // Seek to the start of the CD record - if (!device->seek( cdOffset )) - return UnZip::SeekFailed; - - return UnZip::Ok; -} - -/*! - \internal Parses a central directory record. - - Central Directory record structure: - - [file header 1] - . - . - . - [file header n] - [digital signature] // PKZip 6.2 or later only - - File header: - - central file header signature 4 bytes (0x02014b50) - version made by 2 bytes - version needed to extract 2 bytes - general purpose bit flag 2 bytes - compression method 2 bytes - last mod file time 2 bytes - last mod file date 2 bytes - crc-32 4 bytes - compressed size 4 bytes - uncompressed size 4 bytes - file name length 2 bytes - extra field length 2 bytes - file comment length 2 bytes - disk number start 2 bytes - internal file attributes 2 bytes - external file attributes 4 bytes - relative offset of local header 4 bytes - - file name (variable size) - extra field (variable size) - file comment (variable size) -*/ -UnZip::ErrorCode UnzipPrivate::parseCentralDirectoryRecord() -{ - Q_ASSERT(device); - - // Read CD record - if (device->read(buffer1, UNZIP_CD_ENTRY_SIZE_NS) != UNZIP_CD_ENTRY_SIZE_NS) - return UnZip::ReadFailed; - - bool skipEntry = false; - - // Get compression type so we can skip non compatible algorithms - quint16 compMethod = getUShort(uBuffer, UNZIP_CD_OFF_CMETHOD); - - // Get variable size fields length so we can skip the whole record - // if necessary - quint16 szName = getUShort(uBuffer, UNZIP_CD_OFF_NAMELEN); - quint16 szExtra = getUShort(uBuffer, UNZIP_CD_OFF_XLEN); - quint16 szComment = getUShort(uBuffer, UNZIP_CD_OFF_COMMLEN); - - quint32 skipLength = szName + szExtra + szComment; - - UnZip::ErrorCode ec = UnZip::Ok; - - if ((compMethod != 0) && (compMethod != 8)) { - qDebug() << "Unsupported compression method. Skipping file."; - skipEntry = true; - } - - if (!skipEntry && szName == 0) { - qDebug() << "Skipping file with no name."; - skipEntry = true; - } - - QString filename; - if (device->read(buffer2, szName) != szName) { - ec = UnZip::ReadFailed; - skipEntry = true; - } else { - filename = QString::fromLatin1(buffer2, szName); - } - - // Unsupported features if version is bigger than UNZIP_VERSION - if (!skipEntry && buffer1[UNZIP_CD_OFF_VERSION] > UNZIP_VERSION) { - QString v = QString::number(buffer1[UNZIP_CD_OFF_VERSION]); - if (v.length() == 2) - v.insert(1, QLatin1Char('.')); - v = QString::fromLatin1("Unsupported PKZip version (%1). Skipping file: %2") - .arg(v, filename.isEmpty() ? QString::fromLatin1("") : filename); - qDebug() << v.toLatin1().constData(); - skipEntry = true; - } - - if (skipEntry) { - if (ec == UnZip::Ok) { - if (!device->seek( device->pos() + skipLength )) - ec = UnZip::SeekFailed; - unsupportedEntryCount++; - } - - return ec; - } - - ZipEntryP* h = new ZipEntryP; - h->compMethod = compMethod; - - h->gpFlag[0] = buffer1[UNZIP_CD_OFF_GPFLAG]; - h->gpFlag[1] = buffer1[UNZIP_CD_OFF_GPFLAG + 1]; - - h->modTime[0] = buffer1[UNZIP_CD_OFF_MODT]; - h->modTime[1] = buffer1[UNZIP_CD_OFF_MODT + 1]; - - h->modDate[0] = buffer1[UNZIP_CD_OFF_MODD]; - h->modDate[1] = buffer1[UNZIP_CD_OFF_MODD + 1]; - - h->crc = getULong(uBuffer, UNZIP_CD_OFF_CRC32); - h->szComp = getULong(uBuffer, UNZIP_CD_OFF_CSIZE); - h->szUncomp = getULong(uBuffer, UNZIP_CD_OFF_USIZE); - - // Skip extra field (if any) - if (szExtra != 0) { - if (!device->seek( device->pos() + szExtra )) { - delete h; - return UnZip::SeekFailed; - } - } - - // Read comment field (if any) - if (szComment != 0) { - if (device->read(buffer2, szComment) != szComment) { - delete h; - return UnZip::ReadFailed; - } - - h->comment = QString::fromLatin1(buffer2, szComment); - } - - h->lhOffset = getULong(uBuffer, UNZIP_CD_OFF_LHOFFSET); - - if (!headers) - headers = new QMap(); - headers->insert(filename, h); - - return UnZip::Ok; -} - -//! \internal Closes the archive and resets the internal status. -void UnzipPrivate::closeArchive() -{ - if (!device) { - Q_ASSERT(!file); - return; - } - - if (device != file) - disconnect(device, 0, this, 0); - - do_closeArchive(); -} - -//! \internal -void UnzipPrivate::do_closeArchive() -{ - skipAllEncrypted = false; - - if (headers) { - if (headers) - qDeleteAll(*headers); - delete headers; - headers = 0; - } - - device = 0; - - if (file) - delete file; - file = 0; - - cdOffset = eocdOffset = 0; - cdEntryCount = 0; - unsupportedEntryCount = 0; - - comment.clear(); -} - -//! \internal -UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, const ZipEntryP& entry, - const QDir& dir, UnZip::ExtractionOptions options) -{ - QString name(path); - QString dirname; - QString directory; - - const bool verify = (options & UnZip::VerifyOnly); - const int pos = name.lastIndexOf('/'); - - // This entry is for a directory - if (pos == name.length() - 1) { - if (verify) - return UnZip::Ok; - - if (options & UnZip::SkipPaths) - return UnZip::Ok; - - directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(name)); - if (!createDirectory(directory)) { - qDebug() << QString("Unable to create directory: %1").arg(directory); - return UnZip::CreateDirFailed; - } - - return UnZip::Ok; - } - - // Extract path from entry - if (verify) { - return extractFile(path, entry, 0, options); - } - - if (pos > 0) { - // get directory part - dirname = name.left(pos); - if (options & UnZip::SkipPaths) { - directory = dir.absolutePath(); - } else { - directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(dirname)); - if (!createDirectory(directory)) { - qDebug() << QString("Unable to create directory: %1").arg(directory); - return UnZip::CreateDirFailed; - } - } - name = name.right(name.length() - pos - 1); - } else { - directory = dir.absolutePath(); - } - - const bool silentDirectoryCreation = !(options & UnZip::NoSilentDirectoryCreation); - if (silentDirectoryCreation) { - if (!createDirectory(directory)) { - qDebug() << QString("Unable to create output directory %1").arg(directory); - return UnZip::CreateDirFailed; - } - } - - name = QString("%1/%2").arg(directory).arg(name); - - QFile outFile(name); - if (!outFile.open(QIODevice::WriteOnly)) { - qDebug() << QString("Unable to open %1 for writing").arg(name); - return UnZip::OpenFailed; - } - - UnZip::ErrorCode ec = extractFile(path, entry, &outFile, options); - outFile.close(); - - const QDateTime lastModified = convertDateTime(entry.modDate, entry.modTime); - const bool setTimeOk = OSDAB_ZIP_MANGLE(setFileTimestamp)(name, lastModified); - if (!setTimeOk) { - qDebug() << QString("Unable to set last modified time on file: %1").arg(name); - } - - if (ec != UnZip::Ok) { - if (!outFile.remove()) - qDebug() << QString("Unable to remove corrupted file: %1").arg(name); - } - - return ec; -} - -//! \internal -UnZip::ErrorCode UnzipPrivate::extractStoredFile( - const quint32 szComp, quint32** keys, quint32& myCRC, QIODevice* outDev, - UnZip::ExtractionOptions options) -{ - const bool verify = (options & UnZip::VerifyOnly); - const bool isEncrypted = keys != 0; - - uInt rep = szComp / UNZIP_READ_BUFFER; - uInt rem = szComp % UNZIP_READ_BUFFER; - uInt cur = 0; - - // extract data - qint64 read; - quint64 tot = 0; - - while ( (read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem)) > 0 ) { - if (isEncrypted) - decryptBytes(*keys, buffer1, read); - - myCRC = crc32(myCRC, uBuffer, read); - if (!verify) { - if (outDev->write(buffer1, read) != read) - return UnZip::WriteFailed; - } - - cur++; - tot += read; - if (tot == szComp) - break; - } - - return (read < 0) - ? UnZip::ReadFailed - : UnZip::Ok; -} - -//! \internal -UnZip::ErrorCode UnzipPrivate::inflateFile( - const quint32 szComp, quint32** keys, quint32& myCRC, QIODevice* outDev, - UnZip::ExtractionOptions options) -{ - const bool verify = (options & UnZip::VerifyOnly); - const bool isEncrypted = keys != 0; - Q_ASSERT(verify ? true : outDev != 0); - - uInt rep = szComp / UNZIP_READ_BUFFER; - uInt rem = szComp % UNZIP_READ_BUFFER; - uInt cur = 0; - - // extract data - qint64 read; - quint64 tot = 0; - - /* Allocate inflate state */ - z_stream zstr; - zstr.zalloc = Z_NULL; - zstr.zfree = Z_NULL; - zstr.opaque = Z_NULL; - zstr.next_in = Z_NULL; - zstr.avail_in = 0; - - int zret; - - // Use inflateInit2 with negative windowBits to get raw decompression - if ( (zret = inflateInit2_(&zstr, -MAX_WBITS, ZLIB_VERSION, sizeof(z_stream))) != Z_OK ) - return UnZip::ZlibError; - - int szDecomp; - - // Decompress until deflate stream ends or end of file - do { - read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem); - if (!read) - break; - - if (read < 0) { - (void)inflateEnd(&zstr); - return UnZip::ReadFailed; - } - - if (isEncrypted) - decryptBytes(*keys, buffer1, read); - - cur++; - tot += read; - - zstr.avail_in = (uInt) read; - zstr.next_in = (Bytef*) buffer1; - - // Run inflate() on input until output buffer not full - do { - zstr.avail_out = UNZIP_READ_BUFFER; - zstr.next_out = (Bytef*) buffer2;; - - zret = inflate(&zstr, Z_NO_FLUSH); - - switch (zret) { - case Z_NEED_DICT: - case Z_DATA_ERROR: - case Z_MEM_ERROR: - inflateEnd(&zstr); - return UnZip::WriteFailed; - default: - ; - } - - szDecomp = UNZIP_READ_BUFFER - zstr.avail_out; - if (!verify) { - if (outDev->write(buffer2, szDecomp) != szDecomp) { - inflateEnd(&zstr); - return UnZip::ZlibError; - } - } - - myCRC = crc32(myCRC, (const Bytef*) buffer2, szDecomp); - - } while (zstr.avail_out == 0); - - } while (zret != Z_STREAM_END); - - inflateEnd(&zstr); - return UnZip::Ok; -} - -//! \internal \p outDev is null if the VerifyOnly option is set -UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, const ZipEntryP& entry, - QIODevice* outDev, UnZip::ExtractionOptions options) -{ - const bool verify = (options & UnZip::VerifyOnly); - - Q_UNUSED(options); - Q_ASSERT(device); - Q_ASSERT(verify ? true : outDev != 0); - - if (!entry.lhEntryChecked) { - UnZip::ErrorCode ec = parseLocalHeaderRecord(path, entry); - entry.lhEntryChecked = true; - if (ec != UnZip::Ok) - return ec; - } - - if (!device->seek(entry.dataOffset)) - return UnZip::SeekFailed; - - // Encryption keys - quint32 keys[3]; - quint32 szComp = entry.szComp; - if (entry.isEncrypted()) { - UnZip::ErrorCode e = testPassword(keys, path, entry); - if (e != UnZip::Ok) - { - qDebug() << QString("Unable to decrypt %1").arg(path); - return e; - }//! Encryption header size - szComp -= UNZIP_LOCAL_ENC_HEADER_SIZE; // remove encryption header size - } - - if (szComp == 0) { - if (entry.crc != 0) - return UnZip::Corrupted; - return UnZip::Ok; - } - - quint32 myCRC = crc32(0L, Z_NULL, 0); - quint32* k = keys; - - UnZip::ErrorCode ec = UnZip::Ok; - if (entry.compMethod == 0) { - ec = extractStoredFile(szComp, entry.isEncrypted() ? &k : 0, myCRC, outDev, options); - } else if (entry.compMethod == 8) { - ec = inflateFile(szComp, entry.isEncrypted() ? &k : 0, myCRC, outDev, options); - } - - if (ec == UnZip::Ok && myCRC != entry.crc) - return UnZip::Corrupted; - - return UnZip::Ok; -} - -//! \internal Creates a new directory and all the needed parent directories. -bool UnzipPrivate::createDirectory(const QString& path) -{ - QDir d(path); - if (!d.exists() && !d.mkpath(path)) { - qDebug() << QString("Unable to create directory: %1").arg(path); - return false; - } - - return true; -} - -/*! - \internal Reads an quint32 (4 bytes) from a byte array starting at given offset. -*/ -quint32 UnzipPrivate::getULong(const unsigned char* data, quint32 offset) const -{ - quint32 res = (quint32) data[offset]; - res |= (((quint32)data[offset+1]) << 8); - res |= (((quint32)data[offset+2]) << 16); - res |= (((quint32)data[offset+3]) << 24); - - return res; -} - -/*! - \internal Reads an quint64 (8 bytes) from a byte array starting at given offset. -*/ -quint64 UnzipPrivate::getULLong(const unsigned char* data, quint32 offset) const -{ - quint64 res = (quint64) data[offset]; - res |= (((quint64)data[offset+1]) << 8); - res |= (((quint64)data[offset+2]) << 16); - res |= (((quint64)data[offset+3]) << 24); - res |= (((quint64)data[offset+1]) << 32); - res |= (((quint64)data[offset+2]) << 40); - res |= (((quint64)data[offset+3]) << 48); - res |= (((quint64)data[offset+3]) << 56); - - return res; -} - -/*! - \internal Reads an quint16 (2 bytes) from a byte array starting at given offset. -*/ -quint16 UnzipPrivate::getUShort(const unsigned char* data, quint32 offset) const -{ - return (quint16) data[offset] | (((quint16)data[offset+1]) << 8); -} - -/*! - \internal Return the next byte in the pseudo-random sequence - */ -int UnzipPrivate::decryptByte(quint32 key2) const -{ - quint16 temp = ((quint16)(key2) & 0xffff) | 2; - return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); -} - -/*! - \internal Update the encryption keys with the next byte of plain text - */ -void UnzipPrivate::updateKeys(quint32* keys, int c) const -{ - keys[0] = CRC32(keys[0], c); - keys[1] += keys[0] & 0xff; - keys[1] = keys[1] * 134775813L + 1; - keys[2] = CRC32(keys[2], ((int)keys[1]) >> 24); -} - -/*! - \internal Initialize the encryption keys and the random header according to - the given password. - */ -void UnzipPrivate::initKeys(const QString& pwd, quint32* keys) const -{ - keys[0] = 305419896L; - keys[1] = 591751049L; - keys[2] = 878082192L; - - QByteArray pwdBytes = pwd.toLatin1(); - int sz = pwdBytes.size(); - const char* ascii = pwdBytes.data(); - - for (int i = 0; i < sz; ++i) - updateKeys(keys, (int)ascii[i]); -} - -/*! - \internal Attempts to test a password without actually extracting a file. - The \p file parameter can be used in the user interface or for debugging purposes - as it is the name of the encrypted file for wich the password is being tested. -*/ -UnZip::ErrorCode UnzipPrivate::testPassword(quint32* keys, const QString& file, const ZipEntryP& header) -{ - Q_UNUSED(file); - Q_ASSERT(device); - - // read encryption keys - if (device->read(buffer1, 12) != 12) - return UnZip::Corrupted; - - // Replace this code if you want to i.e. call some dialog and ask the user for a password - initKeys(password, keys); - if (testKeys(header, keys)) - return UnZip::Ok; - - return UnZip::Skip; -} - -/*! - \internal Tests a set of keys on the encryption header. -*/ -bool UnzipPrivate::testKeys(const ZipEntryP& header, quint32* keys) -{ - char lastByte; - - // decrypt encryption header - for (int i = 0; i < 11; ++i) - updateKeys(keys, lastByte = buffer1[i] ^ decryptByte(keys[2])); - updateKeys(keys, lastByte = buffer1[11] ^ decryptByte(keys[2])); - - // if there is an extended header (bit in the gp flag) buffer[11] is a byte from the file time - // with no extended header we have to check the crc high-order byte - char c = ((header.gpFlag[0] & 0x08) == 8) ? header.modTime[1] : header.crc >> 24; - - return (lastByte == c); -} - -/*! - \internal Decrypts an array of bytes long \p read. -*/ -void UnzipPrivate::decryptBytes(quint32* keys, char* buffer, qint64 read) -{ - for (int i = 0; i < (int)read; ++i) - updateKeys(keys, buffer[i] ^= decryptByte(keys[2])); -} - -/*! - \internal Converts date and time values from ZIP format to a QDateTime object. -*/ -QDateTime UnzipPrivate::convertDateTime(const unsigned char date[2], const unsigned char time[2]) const -{ - QDateTime dt; - - // Usual PKZip low-byte to high-byte order - - // Date: 7 bits = years from 1980, 4 bits = month, 5 bits = day - quint16 year = (date[1] >> 1) & 127; - quint16 month = ((date[1] << 3) & 14) | ((date[0] >> 5) & 7); - quint16 day = date[0] & 31; - - // Time: 5 bits hour, 6 bits minutes, 5 bits seconds with a 2sec precision - quint16 hour = (time[1] >> 3) & 31; - quint16 minutes = ((time[1] << 3) & 56) | ((time[0] >> 5) & 7); - quint16 seconds = (time[0] & 31) * 2; - - dt.setDate(QDate(1980 + year, month, day)); - dt.setTime(QTime(hour, minutes, seconds)); - return dt; -} - - -/************************************************************************ - Public interface -*************************************************************************/ - -/*! - Creates a new Zip file decompressor. -*/ -UnZip::UnZip() : d(new UnzipPrivate) -{ -} - -/*! - Closes any open archive and releases used resources. -*/ -UnZip::~UnZip() -{ - closeArchive(); - delete d; -} - -/*! - Returns true if there is an open archive. -*/ -bool UnZip::isOpen() const -{ - return d->device; -} - -/*! - Opens a zip archive and reads the files list. Closes any previously opened archive. -*/ -UnZip::ErrorCode UnZip::openArchive(const QString& filename) -{ - closeArchive(); - - // closeArchive will destroy the file - d->file = new QFile(filename); - - if (!d->file->exists()) { - delete d->file; - d->file = 0; - return UnZip::FileNotFound; - } - - if (!d->file->open(QIODevice::ReadOnly)) { - delete d->file; - d->file = 0; - return UnZip::OpenFailed; - } - - return d->openArchive(d->file); -} - -/*! - Opens a zip archive and reads the entries list. - Closes any previously opened archive. - \warning The class takes DOES NOT take ownership of the device. -*/ -UnZip::ErrorCode UnZip::openArchive(QIODevice* device) -{ - closeArchive(); - - if (!device) { - qDebug() << "Invalid device."; - return UnZip::InvalidDevice; - } - - return d->openArchive(device); -} - -/*! - Closes the archive and releases all the used resources (like cached passwords). -*/ -void UnZip::closeArchive() -{ - d->closeArchive(); -} - -QString UnZip::archiveComment() const -{ - return d->comment; -} - -/*! - Returns a locale translated error string for a given error code. -*/ -QString UnZip::formatError(UnZip::ErrorCode c) const -{ - switch (c) - { - case Ok: return QCoreApplication::translate("UnZip", "ZIP operation completed successfully."); break; - case ZlibInit: return QCoreApplication::translate("UnZip", "Failed to initialize or load zlib library."); break; - case ZlibError: return QCoreApplication::translate("UnZip", "zlib library error."); break; - case OpenFailed: return QCoreApplication::translate("UnZip", "Unable to create or open file."); break; - case PartiallyCorrupted: return QCoreApplication::translate("UnZip", "Partially corrupted archive. Some files might be extracted."); break; - case Corrupted: return QCoreApplication::translate("UnZip", "Corrupted archive."); break; - case WrongPassword: return QCoreApplication::translate("UnZip", "Wrong password."); break; - case NoOpenArchive: return QCoreApplication::translate("UnZip", "No archive has been created yet."); break; - case FileNotFound: return QCoreApplication::translate("UnZip", "File or directory does not exist."); break; - case ReadFailed: return QCoreApplication::translate("UnZip", "File read error."); break; - case WriteFailed: return QCoreApplication::translate("UnZip", "File write error."); break; - case SeekFailed: return QCoreApplication::translate("UnZip", "File seek error."); break; - case CreateDirFailed: return QCoreApplication::translate("UnZip", "Unable to create a directory."); break; - case InvalidDevice: return QCoreApplication::translate("UnZip", "Invalid device."); break; - case InvalidArchive: return QCoreApplication::translate("UnZip", "Invalid or incompatible zip archive."); break; - case HeaderConsistencyError: return QCoreApplication::translate("UnZip", "Inconsistent headers. Archive might be corrupted."); break; - default: ; - } - - return QCoreApplication::translate("UnZip", "Unknown error."); -} - -/*! - Returns true if the archive contains a file with the given path and name. -*/ -bool UnZip::contains(const QString& file) const -{ - return d->headers ? d->headers->contains(file) : false; -} - -/*! - Returns complete paths of files and directories in this archive. -*/ -QStringList UnZip::fileList() const -{ - return d->headers ? d->headers->keys() : QStringList(); -} - -/*! - Returns information for each (correctly parsed) entry of this archive. -*/ -QList UnZip::entryList() const -{ - QList list; - if (!d->headers) - return list; - - for (QMap::ConstIterator it = d->headers->constBegin(); - it != d->headers->constEnd(); ++it) { - const ZipEntryP* entry = it.value(); - Q_ASSERT(entry != 0); - - ZipEntry z; - - z.filename = it.key(); - if (!entry->comment.isEmpty()) - z.comment = entry->comment; - z.compressedSize = entry->szComp; - z.uncompressedSize = entry->szUncomp; - z.crc32 = entry->crc; - z.lastModified = d->convertDateTime(entry->modDate, entry->modTime); - - z.compression = entry->compMethod == 0 ? NoCompression : entry->compMethod == 8 ? Deflated : UnknownCompression; - z.type = z.filename.endsWith("/") ? Directory : File; - - z.encrypted = entry->isEncrypted(); - - list.append(z); - } - - return list; -} - -/*! - Extracts the whole archive to a directory. -*/ -UnZip::ErrorCode UnZip::verifyArchive() -{ - return extractAll(QDir(), VerifyOnly); -} - -/*! - Extracts the whole archive to a directory. -*/ -UnZip::ErrorCode UnZip::extractAll(const QString& dirname, ExtractionOptions options) -{ - return extractAll(QDir(dirname), options); -} - -/*! - Extracts the whole archive to a directory. - Stops extraction at the first error. -*/ -UnZip::ErrorCode UnZip::extractAll(const QDir& dir, ExtractionOptions options) -{ - // this should only happen if we didn't call openArchive() yet - if (!d->device) - return NoOpenArchive; - - if (!d->headers) - return Ok; - - ErrorCode ec = Ok; - - QMap::ConstIterator it = d->headers->constBegin(); - const QMap::ConstIterator end = d->headers->constEnd(); - while (it != end) { - ZipEntryP* entry = it.value(); - Q_ASSERT(entry != 0); - if ((entry->isEncrypted()) && d->skipAllEncrypted) { - ++it; - continue; - } - - bool skip = false; - ec = d->extractFile(it.key(), *entry, dir, options); - switch (ec) { - case Corrupted: - qDebug() << "Corrupted entry" << it.key(); - break; - case CreateDirFailed: - break; - case Skip: - skip = true; - break; - case SkipAll: - skip = true; - d->skipAllEncrypted = true; - break; - default: - ; - } - - if (ec != Ok && !skip) { - break; - } - - ++it; - } - - return ec; -} - -/*! - Extracts a single file to a directory. -*/ -UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QString& dirname, ExtractionOptions options) -{ - return extractFile(filename, QDir(dirname), options); -} - -/*! - Extracts a single file to a directory. -*/ -UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QDir& dir, ExtractionOptions options) -{ - if (!d->device) - return NoOpenArchive; - if (!d->headers) - return FileNotFound; - - QMap::Iterator itr = d->headers->find(filename); - if (itr != d->headers->end()) { - ZipEntryP* entry = itr.value(); - Q_ASSERT(entry != 0); - return d->extractFile(itr.key(), *entry, dir, options); - } - - return FileNotFound; -} - -/*! - Extracts a single file to a directory. -*/ -UnZip::ErrorCode UnZip::extractFile(const QString& filename, QIODevice* outDev, ExtractionOptions options) -{ - if (!d->device) - return NoOpenArchive; - if (!d->headers) - return FileNotFound; - if (!outDev) - return InvalidDevice; - - QMap::Iterator itr = d->headers->find(filename); - if (itr != d->headers->end()) { - ZipEntryP* entry = itr.value(); - Q_ASSERT(entry != 0); - return d->extractFile(itr.key(), *entry, outDev, options); - } - - return FileNotFound; -} - -/*! - Extracts a list of files. - Stops extraction at the first error (but continues if a file does not exist in the archive). - */ -UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QString& dirname, ExtractionOptions options) -{ - if (!d->device) - return NoOpenArchive; - if (!d->headers) - return Ok; - - QDir dir(dirname); - ErrorCode ec; - - for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr) { - ec = extractFile(*itr, dir, options); - if (ec == FileNotFound) - continue; - if (ec != Ok) - return ec; - } - - return Ok; -} - -/*! - Extracts a list of files. - Stops extraction at the first error (but continues if a file does not exist in the archive). - */ -UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QDir& dir, ExtractionOptions options) -{ - if (!d->device) - return NoOpenArchive; - if (!d->headers) - return Ok; - - ErrorCode ec; - - for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr) { - ec = extractFile(*itr, dir, options); - if (ec == FileNotFound) - continue; - if (ec != Ok) - return ec; - } - - return Ok; -} - -/*! - Remove/replace this method to add your own password retrieval routine. -*/ -void UnZip::setPassword(const QString& pwd) -{ - d->password = pwd; -} - -OSDAB_END_NAMESPACE +/**************************************************************************** +** Filename: unzip.cpp +** Last updated [dd/mm/yyyy]: 08/07/2010 +** +** pkzip 2.0 decompression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#include "unzip.h" +#include "unzip_p.h" +#include "zipentry_p.h" + +#include +#include +#include +#include +#include + +// You can remove this #include if you replace the qDebug() statements. +#include + +/*! + \class UnZip unzip.h + + \brief PKZip 2.0 file decompression. + Compatibility with later versions is not ensured as they may use + unsupported compression algorithms. + Versions after 2.7 may have an incompatible header format and thus be + completely incompatible. +*/ + +/*! \enum UnZip::ErrorCode The result of a decompression operation. + \value UnZip::Ok No error occurred. + \value UnZip::ZlibInit Failed to init or load the zlib library. + \value UnZip::ZlibError The zlib library returned some error. + \value UnZip::OpenFailed Unable to create or open a device. + \value UnZip::PartiallyCorrupted Corrupted zip archive - some files could be extracted. + \value UnZip::Corrupted Corrupted or invalid zip archive. + \value UnZip::WrongPassword Unable to decrypt a password protected file. + \value UnZip::NoOpenArchive No archive has been opened yet. + \value UnZip::FileNotFound Unable to find the requested file in the archive. + \value UnZip::ReadFailed Reading of a file failed. + \value UnZip::WriteFailed Writing of a file failed. + \value UnZip::SeekFailed Seek failed. + \value UnZip::CreateDirFailed Could not create a directory. + \value UnZip::InvalidDevice A null device has been passed as parameter. + \value UnZip::InvalidArchive This is not a valid (or supported) ZIP archive. + \value UnZip::HeaderConsistencyError Local header record info does not match with the central directory record info. The archive may be corrupted. + + \value UnZip::Skip Internal use only. + \value UnZip::SkipAll Internal use only. +*/ + +/*! \enum UnZip::ExtractionOptions Some options for the file extraction methods. + \value UnZip::ExtractPaths Default. Does not ignore the path of the zipped files. + \value UnZip::SkipPaths Default. Ignores the path of the zipped files and extracts them all to the same root directory. + \value UnZip::VerifyOnly Doesn't actually extract files. + \value UnZip::NoSilentDirectoryCreation Doesn't attempt to silently create missing output directories. +*/ + +//! Local header size (excluding signature, excluding variable length fields) +#define UNZIP_LOCAL_HEADER_SIZE 26 +//! Central Directory file entry size (excluding signature, excluding variable length fields) +#define UNZIP_CD_ENTRY_SIZE_NS 42 +//! Data descriptor size (excluding signature) +#define UNZIP_DD_SIZE 12 +//! End Of Central Directory size (including signature, excluding variable length fields) +#define UNZIP_EOCD_SIZE 22 +//! Local header entry encryption header size +#define UNZIP_LOCAL_ENC_HEADER_SIZE 12 + +// Some offsets inside a CD record (excluding signature) +#define UNZIP_CD_OFF_VERSION_MADE 0 +#define UNZIP_CD_OFF_VERSION 2 +#define UNZIP_CD_OFF_GPFLAG 4 +#define UNZIP_CD_OFF_CMETHOD 6 +#define UNZIP_CD_OFF_MODT 8 +#define UNZIP_CD_OFF_MODD 10 +#define UNZIP_CD_OFF_CRC32 12 +#define UNZIP_CD_OFF_CSIZE 16 +#define UNZIP_CD_OFF_USIZE 20 +#define UNZIP_CD_OFF_NAMELEN 24 +#define UNZIP_CD_OFF_XLEN 26 +#define UNZIP_CD_OFF_COMMLEN 28 +#define UNZIP_CD_OFF_LHOFFSET 38 + +// Some offsets inside a local header record (excluding signature) +#define UNZIP_LH_OFF_VERSION 0 +#define UNZIP_LH_OFF_GPFLAG 2 +#define UNZIP_LH_OFF_CMETHOD 4 +#define UNZIP_LH_OFF_MODT 6 +#define UNZIP_LH_OFF_MODD 8 +#define UNZIP_LH_OFF_CRC32 10 +#define UNZIP_LH_OFF_CSIZE 14 +#define UNZIP_LH_OFF_USIZE 18 +#define UNZIP_LH_OFF_NAMELEN 22 +#define UNZIP_LH_OFF_XLEN 24 + +// Some offsets inside a data descriptor record (excluding signature) +#define UNZIP_DD_OFF_CRC32 0 +#define UNZIP_DD_OFF_CSIZE 4 +#define UNZIP_DD_OFF_USIZE 8 + +// Some offsets inside a EOCD record +#define UNZIP_EOCD_OFF_ENTRIES 6 +#define UNZIP_EOCD_OFF_CDOFF 12 +#define UNZIP_EOCD_OFF_COMMLEN 16 + +/*! + Max version handled by this API. + 0x14 = 2.0 --> full compatibility only up to this version; + later versions use unsupported features +*/ +#define UNZIP_VERSION 0x14 + +//! CRC32 routine +#define CRC32(c, b) crcTable[((int)c^b) & 0xff] ^ (c >> 8) + +OSDAB_BEGIN_NAMESPACE(Zip) + + +/************************************************************************ + ZipEntry +*************************************************************************/ + +/*! + ZipEntry constructor - initialize data. Type is set to File. +*/ +UnZip::ZipEntry::ZipEntry() +{ + compressedSize = uncompressedSize = crc32 = 0; + compression = NoCompression; + type = File; + encrypted = false; +} + + +/************************************************************************ + Private interface +*************************************************************************/ + +//! \internal +UnzipPrivate::UnzipPrivate() : + password(), + skipAllEncrypted(false), + headers(0), + device(0), + file(0), + uBuffer(0), + crcTable(0), + cdOffset(0), + eocdOffset(0), + cdEntryCount(0), + unsupportedEntryCount(0), + comment() +{ + uBuffer = (unsigned char*) buffer1; + crcTable = (quint32*) get_crc_table(); +} + +//! \internal +void UnzipPrivate::deviceDestroyed(QObject*) +{ + qDebug("Unexpected device destruction detected."); + do_closeArchive(); +} + +//! \internal Parses a Zip archive. +UnZip::ErrorCode UnzipPrivate::openArchive(QIODevice* dev) +{ + Q_ASSERT(!device); + Q_ASSERT(dev); + + if (!(dev->isOpen() || dev->open(QIODevice::ReadOnly))) { + qDebug() << "Unable to open device for reading"; + return UnZip::OpenFailed; + } + + device = dev; + if (device != file) + connect(device, SIGNAL(destroyed(QObject*)), this, SLOT(deviceDestroyed(QObject*))); + + UnZip::ErrorCode ec; + + ec = seekToCentralDirectory(); + if (ec != UnZip::Ok) { + closeArchive(); + return ec; + } + + //! \todo Ignore CD entry count? CD may be corrupted. + if (cdEntryCount == 0) { + return UnZip::Ok; + } + + bool continueParsing = true; + + while (continueParsing) { + if (device->read(buffer1, 4) != 4) { + if (headers) { + qDebug() << "Corrupted zip archive. Some files might be extracted."; + ec = headers->size() != 0 ? UnZip::PartiallyCorrupted : UnZip::Corrupted; + break; + } else { + closeArchive(); + qDebug() << "Corrupted or invalid zip archive. Closing."; + ec = UnZip::Corrupted; + break; + } + } + + if (! (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x01 && buffer1[3] == 0x02) ) + break; + + if ((ec = parseCentralDirectoryRecord()) != UnZip::Ok) + break; + } + + if (ec != UnZip::Ok) + closeArchive(); + + return ec; +} + +/* + \internal Parses a local header record and makes some consistency check + with the information stored in the Central Directory record for this entry + that has been previously parsed. + \todo Optional consistency check (as a ExtractionOptions flag) + + local file header signature 4 bytes (0x04034b50) + version needed to extract 2 bytes + general purpose bit flag 2 bytes + compression method 2 bytes + last mod file time 2 bytes + last mod file date 2 bytes + crc-32 4 bytes + compressed size 4 bytes + uncompressed size 4 bytes + file name length 2 bytes + extra field length 2 bytes + + file name (variable size) + extra field (variable size) +*/ +UnZip::ErrorCode UnzipPrivate::parseLocalHeaderRecord(const QString& path, const ZipEntryP& entry) +{ + Q_ASSERT(device); + + if (!device->seek(entry.lhOffset)) + return UnZip::SeekFailed; + + // Test signature + if (device->read(buffer1, 4) != 4) + return UnZip::ReadFailed; + + if ((buffer1[0] != 'P') || (buffer1[1] != 'K') || (buffer1[2] != 0x03) || (buffer1[3] != 0x04)) + return UnZip::InvalidArchive; + + if (device->read(buffer1, UNZIP_LOCAL_HEADER_SIZE) != UNZIP_LOCAL_HEADER_SIZE) + return UnZip::ReadFailed; + + /* + Check 3rd general purpose bit flag. + + "bit 3: If this bit is set, the fields crc-32, compressed size + and uncompressed size are set to zero in the local + header. The correct values are put in the data descriptor + immediately following the compressed data." + */ + bool hasDataDescriptor = entry.hasDataDescriptor(); + bool checkFailed = entry.compMethod != getUShort(uBuffer, UNZIP_LH_OFF_CMETHOD); + + if (!checkFailed) + checkFailed = entry.gpFlag[0] != uBuffer[UNZIP_LH_OFF_GPFLAG]; + if (!checkFailed) + checkFailed = entry.gpFlag[1] != uBuffer[UNZIP_LH_OFF_GPFLAG + 1]; + if (!checkFailed) + checkFailed = entry.modTime[0] != uBuffer[UNZIP_LH_OFF_MODT]; + if (!checkFailed) + checkFailed = entry.modTime[1] != uBuffer[UNZIP_LH_OFF_MODT + 1]; + if (!checkFailed) + checkFailed = entry.modDate[0] != uBuffer[UNZIP_LH_OFF_MODD]; + if (!checkFailed) + checkFailed = entry.modDate[1] != uBuffer[UNZIP_LH_OFF_MODD + 1]; + if (!hasDataDescriptor) + { + if (!checkFailed) + checkFailed = entry.crc != getULong(uBuffer, UNZIP_LH_OFF_CRC32); + if (!checkFailed) + checkFailed = entry.szComp != getULong(uBuffer, UNZIP_LH_OFF_CSIZE); + if (!checkFailed) + checkFailed = entry.szUncomp != getULong(uBuffer, UNZIP_LH_OFF_USIZE); + } + + if (checkFailed) + return UnZip::HeaderConsistencyError; + + // Check filename + quint16 szName = getUShort(uBuffer, UNZIP_LH_OFF_NAMELEN); + if (szName == 0) + return UnZip::HeaderConsistencyError; + + if (device->read(buffer2, szName) != szName) + return UnZip::ReadFailed; + + QString filename = QString::fromLatin1(buffer2, szName); + if (filename != path) { + qDebug() << "Filename in local header mismatches."; + return UnZip::HeaderConsistencyError; + } + + // Skip extra field + quint16 szExtra = getUShort(uBuffer, UNZIP_LH_OFF_XLEN); + if (szExtra != 0) { + if (!device->seek(device->pos() + szExtra)) + return UnZip::SeekFailed; + } + + entry.dataOffset = device->pos(); + + if (hasDataDescriptor) { + /* + The data descriptor has this OPTIONAL signature: PK\7\8 + We try to skip the compressed data relying on the size set in the + Central Directory record. + */ + if (!device->seek(device->pos() + entry.szComp)) + return UnZip::SeekFailed; + + // Read 4 bytes and check if there is a data descriptor signature + if (device->read(buffer2, 4) != 4) + return UnZip::ReadFailed; + + bool hasSignature = buffer2[0] == 'P' && buffer2[1] == 'K' && buffer2[2] == 0x07 && buffer2[3] == 0x08; + if (hasSignature) { + if (device->read(buffer2, UNZIP_DD_SIZE) != UNZIP_DD_SIZE) + return UnZip::ReadFailed; + } else { + if (device->read(buffer2 + 4, UNZIP_DD_SIZE - 4) != UNZIP_DD_SIZE - 4) + return UnZip::ReadFailed; + } + + // DD: crc, compressed size, uncompressed size + if ( + entry.crc != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CRC32) || + entry.szComp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_CSIZE) || + entry.szUncomp != getULong((unsigned char*)buffer2, UNZIP_DD_OFF_USIZE) + ) + return UnZip::HeaderConsistencyError; + } + + return UnZip::Ok; +} + +/*! \internal Attempts to find the start of the central directory record. + + We seek the file back until we reach the "End Of Central Directory" + signature PK\5\6. + + end of central dir signature 4 bytes (0x06054b50) + number of this disk 2 bytes + number of the disk with the + start of the central directory 2 bytes + total number of entries in the + central directory on this disk 2 bytes + total number of entries in + the central directory 2 bytes + size of the central directory 4 bytes + offset of start of central + directory with respect to + the starting disk number 4 bytes + .ZIP file comment length 2 bytes + --- SIZE UNTIL HERE: UNZIP_EOCD_SIZE --- + .ZIP file comment (variable size) +*/ +UnZip::ErrorCode UnzipPrivate::seekToCentralDirectory() +{ + Q_ASSERT(device); + + qint64 length = device->size(); + qint64 offset = length - UNZIP_EOCD_SIZE; + + if (length < UNZIP_EOCD_SIZE) + return UnZip::InvalidArchive; + + if (!device->seek( offset )) + return UnZip::SeekFailed; + + if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE) + return UnZip::ReadFailed; + + bool eocdFound = (buffer1[0] == 'P' && buffer1[1] == 'K' && buffer1[2] == 0x05 && buffer1[3] == 0x06); + + if (eocdFound) { + // Zip file has no comment (the only variable length field in the EOCD record) + eocdOffset = offset; + } else { + qint64 read; + char* p = 0; + + offset -= UNZIP_EOCD_SIZE; + + if (offset <= 0) + return UnZip::InvalidArchive; + + if (!device->seek( offset )) + return UnZip::SeekFailed; + + while ((read = device->read(buffer1, UNZIP_EOCD_SIZE)) >= 0) { + if ( (p = strstr(buffer1, "PK\5\6")) != 0) { + // Seek to the start of the EOCD record so we can read it fully + // Yes... we could simply read the missing bytes and append them to the buffer + // but this is far easier so heck it! + device->seek( offset + (p - buffer1) ); + eocdFound = true; + eocdOffset = offset + (p - buffer1); + + // Read EOCD record + if (device->read(buffer1, UNZIP_EOCD_SIZE) != UNZIP_EOCD_SIZE) + return UnZip::ReadFailed; + + break; + } + + // TODO: This is very slow and only a temporary bug fix. Need some pattern matching algorithm here. + offset -= 1 /*UNZIP_EOCD_SIZE*/; + if (offset <= 0) + return UnZip::InvalidArchive; + + if (!device->seek( offset )) + return UnZip::SeekFailed; + } + } + + if (!eocdFound) + return UnZip::InvalidArchive; + + // Parse EOCD to locate CD offset + offset = getULong((const unsigned char*)buffer1, UNZIP_EOCD_OFF_CDOFF + 4); + + cdOffset = offset; + + cdEntryCount = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_ENTRIES + 4); + + quint16 commentLength = getUShort((const unsigned char*)buffer1, UNZIP_EOCD_OFF_COMMLEN + 4); + if (commentLength != 0) { + QByteArray c = device->read(commentLength); + if (c.count() != commentLength) + return UnZip::ReadFailed; + + comment = c; + } + + // Seek to the start of the CD record + if (!device->seek( cdOffset )) + return UnZip::SeekFailed; + + return UnZip::Ok; +} + +/*! + \internal Parses a central directory record. + + Central Directory record structure: + + [file header 1] + . + . + . + [file header n] + [digital signature] // PKZip 6.2 or later only + + File header: + + central file header signature 4 bytes (0x02014b50) + version made by 2 bytes + version needed to extract 2 bytes + general purpose bit flag 2 bytes + compression method 2 bytes + last mod file time 2 bytes + last mod file date 2 bytes + crc-32 4 bytes + compressed size 4 bytes + uncompressed size 4 bytes + file name length 2 bytes + extra field length 2 bytes + file comment length 2 bytes + disk number start 2 bytes + internal file attributes 2 bytes + external file attributes 4 bytes + relative offset of local header 4 bytes + + file name (variable size) + extra field (variable size) + file comment (variable size) +*/ +UnZip::ErrorCode UnzipPrivate::parseCentralDirectoryRecord() +{ + Q_ASSERT(device); + + // Read CD record + if (device->read(buffer1, UNZIP_CD_ENTRY_SIZE_NS) != UNZIP_CD_ENTRY_SIZE_NS) + return UnZip::ReadFailed; + + bool skipEntry = false; + + // Get compression type so we can skip non compatible algorithms + quint16 compMethod = getUShort(uBuffer, UNZIP_CD_OFF_CMETHOD); + + // Get variable size fields length so we can skip the whole record + // if necessary + quint16 szName = getUShort(uBuffer, UNZIP_CD_OFF_NAMELEN); + quint16 szExtra = getUShort(uBuffer, UNZIP_CD_OFF_XLEN); + quint16 szComment = getUShort(uBuffer, UNZIP_CD_OFF_COMMLEN); + + quint32 skipLength = szName + szExtra + szComment; + + UnZip::ErrorCode ec = UnZip::Ok; + + if ((compMethod != 0) && (compMethod != 8)) { + qDebug() << "Unsupported compression method. Skipping file."; + skipEntry = true; + } + + if (!skipEntry && szName == 0) { + qDebug() << "Skipping file with no name."; + skipEntry = true; + } + + QString filename; + if (device->read(buffer2, szName) != szName) { + ec = UnZip::ReadFailed; + skipEntry = true; + } else { + filename = QString::fromLatin1(buffer2, szName); + } + + // Unsupported features if version is bigger than UNZIP_VERSION + if (!skipEntry && buffer1[UNZIP_CD_OFF_VERSION] > UNZIP_VERSION) { + QString v = QString::number(buffer1[UNZIP_CD_OFF_VERSION]); + if (v.length() == 2) + v.insert(1, QLatin1Char('.')); + v = QString::fromLatin1("Unsupported PKZip version (%1). Skipping file: %2") + .arg(v, filename.isEmpty() ? QString::fromLatin1("") : filename); + qDebug() << v.toLatin1().constData(); + skipEntry = true; + } + + if (skipEntry) { + if (ec == UnZip::Ok) { + if (!device->seek( device->pos() + skipLength )) + ec = UnZip::SeekFailed; + unsupportedEntryCount++; + } + + return ec; + } + + ZipEntryP* h = new ZipEntryP; + h->compMethod = compMethod; + + h->gpFlag[0] = buffer1[UNZIP_CD_OFF_GPFLAG]; + h->gpFlag[1] = buffer1[UNZIP_CD_OFF_GPFLAG + 1]; + + h->modTime[0] = buffer1[UNZIP_CD_OFF_MODT]; + h->modTime[1] = buffer1[UNZIP_CD_OFF_MODT + 1]; + + h->modDate[0] = buffer1[UNZIP_CD_OFF_MODD]; + h->modDate[1] = buffer1[UNZIP_CD_OFF_MODD + 1]; + + h->crc = getULong(uBuffer, UNZIP_CD_OFF_CRC32); + h->szComp = getULong(uBuffer, UNZIP_CD_OFF_CSIZE); + h->szUncomp = getULong(uBuffer, UNZIP_CD_OFF_USIZE); + + // Skip extra field (if any) + if (szExtra != 0) { + if (!device->seek( device->pos() + szExtra )) { + delete h; + return UnZip::SeekFailed; + } + } + + // Read comment field (if any) + if (szComment != 0) { + if (device->read(buffer2, szComment) != szComment) { + delete h; + return UnZip::ReadFailed; + } + + h->comment = QString::fromLatin1(buffer2, szComment); + } + + h->lhOffset = getULong(uBuffer, UNZIP_CD_OFF_LHOFFSET); + + if (!headers) + headers = new QMap(); + headers->insert(filename, h); + + return UnZip::Ok; +} + +//! \internal Closes the archive and resets the internal status. +void UnzipPrivate::closeArchive() +{ + if (!device) { + Q_ASSERT(!file); + return; + } + + if (device != file) + disconnect(device, 0, this, 0); + + do_closeArchive(); +} + +//! \internal +void UnzipPrivate::do_closeArchive() +{ + skipAllEncrypted = false; + + if (headers) { + if (headers) + qDeleteAll(*headers); + delete headers; + headers = 0; + } + + device = 0; + + if (file) + delete file; + file = 0; + + cdOffset = eocdOffset = 0; + cdEntryCount = 0; + unsupportedEntryCount = 0; + + comment.clear(); +} + +//! \internal +UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, const ZipEntryP& entry, + const QDir& dir, UnZip::ExtractionOptions options) +{ + QString name(path); + QString dirname; + QString directory; + + const bool verify = (options & UnZip::VerifyOnly); + const int pos = name.lastIndexOf('/'); + + // This entry is for a directory + if (pos == name.length() - 1) { + if (verify) + return UnZip::Ok; + + if (options & UnZip::SkipPaths) + return UnZip::Ok; + + directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(name)); + if (!createDirectory(directory)) { + qDebug() << QString("Unable to create directory: %1").arg(directory); + return UnZip::CreateDirFailed; + } + + return UnZip::Ok; + } + + // Extract path from entry + if (verify) { + return extractFile(path, entry, 0, options); + } + + if (pos > 0) { + // get directory part + dirname = name.left(pos); + if (options & UnZip::SkipPaths) { + directory = dir.absolutePath(); + } else { + directory = QString("%1/%2").arg(dir.absolutePath()).arg(QDir::cleanPath(dirname)); + if (!createDirectory(directory)) { + qDebug() << QString("Unable to create directory: %1").arg(directory); + return UnZip::CreateDirFailed; + } + } + name = name.right(name.length() - pos - 1); + } else { + directory = dir.absolutePath(); + } + + const bool silentDirectoryCreation = !(options & UnZip::NoSilentDirectoryCreation); + if (silentDirectoryCreation) { + if (!createDirectory(directory)) { + qDebug() << QString("Unable to create output directory %1").arg(directory); + return UnZip::CreateDirFailed; + } + } + + name = QString("%1/%2").arg(directory).arg(name); + + QFile outFile(name); + if (!outFile.open(QIODevice::WriteOnly)) { + qDebug() << QString("Unable to open %1 for writing").arg(name); + return UnZip::OpenFailed; + } + + UnZip::ErrorCode ec = extractFile(path, entry, &outFile, options); + outFile.close(); + + const QDateTime lastModified = convertDateTime(entry.modDate, entry.modTime); + const bool setTimeOk = OSDAB_ZIP_MANGLE(setFileTimestamp)(name, lastModified); + if (!setTimeOk) { + qDebug() << QString("Unable to set last modified time on file: %1").arg(name); + } + + if (ec != UnZip::Ok) { + if (!outFile.remove()) + qDebug() << QString("Unable to remove corrupted file: %1").arg(name); + } + + return ec; +} + +//! \internal +UnZip::ErrorCode UnzipPrivate::extractStoredFile( + const quint32 szComp, quint32** keys, quint32& myCRC, QIODevice* outDev, + UnZip::ExtractionOptions options) +{ + const bool verify = (options & UnZip::VerifyOnly); + const bool isEncrypted = keys != 0; + + uInt rep = szComp / UNZIP_READ_BUFFER; + uInt rem = szComp % UNZIP_READ_BUFFER; + uInt cur = 0; + + // extract data + qint64 read; + quint64 tot = 0; + + while ( (read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem)) > 0 ) { + if (isEncrypted) + decryptBytes(*keys, buffer1, read); + + myCRC = crc32(myCRC, uBuffer, read); + if (!verify) { + if (outDev->write(buffer1, read) != read) + return UnZip::WriteFailed; + } + + cur++; + tot += read; + if (tot == szComp) + break; + } + + return (read < 0) + ? UnZip::ReadFailed + : UnZip::Ok; +} + +//! \internal +UnZip::ErrorCode UnzipPrivate::inflateFile( + const quint32 szComp, quint32** keys, quint32& myCRC, QIODevice* outDev, + UnZip::ExtractionOptions options) +{ + const bool verify = (options & UnZip::VerifyOnly); + const bool isEncrypted = keys != 0; + Q_ASSERT(verify ? true : outDev != 0); + + uInt rep = szComp / UNZIP_READ_BUFFER; + uInt rem = szComp % UNZIP_READ_BUFFER; + uInt cur = 0; + + // extract data + qint64 read; + quint64 tot = 0; + + /* Allocate inflate state */ + z_stream zstr; + zstr.zalloc = Z_NULL; + zstr.zfree = Z_NULL; + zstr.opaque = Z_NULL; + zstr.next_in = Z_NULL; + zstr.avail_in = 0; + + int zret; + + // Use inflateInit2 with negative windowBits to get raw decompression + if ( (zret = inflateInit2_(&zstr, -MAX_WBITS, ZLIB_VERSION, sizeof(z_stream))) != Z_OK ) + return UnZip::ZlibError; + + int szDecomp; + + // Decompress until deflate stream ends or end of file + do { + read = device->read(buffer1, cur < rep ? UNZIP_READ_BUFFER : rem); + if (!read) + break; + + if (read < 0) { + (void)inflateEnd(&zstr); + return UnZip::ReadFailed; + } + + if (isEncrypted) + decryptBytes(*keys, buffer1, read); + + cur++; + tot += read; + + zstr.avail_in = (uInt) read; + zstr.next_in = (Bytef*) buffer1; + + // Run inflate() on input until output buffer not full + do { + zstr.avail_out = UNZIP_READ_BUFFER; + zstr.next_out = (Bytef*) buffer2;; + + zret = inflate(&zstr, Z_NO_FLUSH); + + switch (zret) { + case Z_NEED_DICT: + case Z_DATA_ERROR: + case Z_MEM_ERROR: + inflateEnd(&zstr); + return UnZip::WriteFailed; + default: + ; + } + + szDecomp = UNZIP_READ_BUFFER - zstr.avail_out; + if (!verify) { + if (outDev->write(buffer2, szDecomp) != szDecomp) { + inflateEnd(&zstr); + return UnZip::ZlibError; + } + } + + myCRC = crc32(myCRC, (const Bytef*) buffer2, szDecomp); + + } while (zstr.avail_out == 0); + + } while (zret != Z_STREAM_END); + + inflateEnd(&zstr); + return UnZip::Ok; +} + +//! \internal \p outDev is null if the VerifyOnly option is set +UnZip::ErrorCode UnzipPrivate::extractFile(const QString& path, const ZipEntryP& entry, + QIODevice* outDev, UnZip::ExtractionOptions options) +{ + const bool verify = (options & UnZip::VerifyOnly); + + Q_UNUSED(options); + Q_ASSERT(device); + Q_ASSERT(verify ? true : outDev != 0); + + if (!entry.lhEntryChecked) { + UnZip::ErrorCode ec = parseLocalHeaderRecord(path, entry); + entry.lhEntryChecked = true; + if (ec != UnZip::Ok) + return ec; + } + + if (!device->seek(entry.dataOffset)) + return UnZip::SeekFailed; + + // Encryption keys + quint32 keys[3]; + quint32 szComp = entry.szComp; + if (entry.isEncrypted()) { + UnZip::ErrorCode e = testPassword(keys, path, entry); + if (e != UnZip::Ok) + { + qDebug() << QString("Unable to decrypt %1").arg(path); + return e; + }//! Encryption header size + szComp -= UNZIP_LOCAL_ENC_HEADER_SIZE; // remove encryption header size + } + + if (szComp == 0) { + if (entry.crc != 0) + return UnZip::Corrupted; + return UnZip::Ok; + } + + quint32 myCRC = crc32(0L, Z_NULL, 0); + quint32* k = keys; + + UnZip::ErrorCode ec = UnZip::Ok; + if (entry.compMethod == 0) { + ec = extractStoredFile(szComp, entry.isEncrypted() ? &k : 0, myCRC, outDev, options); + } else if (entry.compMethod == 8) { + ec = inflateFile(szComp, entry.isEncrypted() ? &k : 0, myCRC, outDev, options); + } + + if (ec == UnZip::Ok && myCRC != entry.crc) + return UnZip::Corrupted; + + return UnZip::Ok; +} + +//! \internal Creates a new directory and all the needed parent directories. +bool UnzipPrivate::createDirectory(const QString& path) +{ + QDir d(path); + if (!d.exists() && !d.mkpath(path)) { + qDebug() << QString("Unable to create directory: %1").arg(path); + return false; + } + + return true; +} + +/*! + \internal Reads an quint32 (4 bytes) from a byte array starting at given offset. +*/ +quint32 UnzipPrivate::getULong(const unsigned char* data, quint32 offset) const +{ + quint32 res = (quint32) data[offset]; + res |= (((quint32)data[offset+1]) << 8); + res |= (((quint32)data[offset+2]) << 16); + res |= (((quint32)data[offset+3]) << 24); + + return res; +} + +/*! + \internal Reads an quint64 (8 bytes) from a byte array starting at given offset. +*/ +quint64 UnzipPrivate::getULLong(const unsigned char* data, quint32 offset) const +{ + quint64 res = (quint64) data[offset]; + res |= (((quint64)data[offset+1]) << 8); + res |= (((quint64)data[offset+2]) << 16); + res |= (((quint64)data[offset+3]) << 24); + res |= (((quint64)data[offset+1]) << 32); + res |= (((quint64)data[offset+2]) << 40); + res |= (((quint64)data[offset+3]) << 48); + res |= (((quint64)data[offset+3]) << 56); + + return res; +} + +/*! + \internal Reads an quint16 (2 bytes) from a byte array starting at given offset. +*/ +quint16 UnzipPrivate::getUShort(const unsigned char* data, quint32 offset) const +{ + return (quint16) data[offset] | (((quint16)data[offset+1]) << 8); +} + +/*! + \internal Return the next byte in the pseudo-random sequence + */ +int UnzipPrivate::decryptByte(quint32 key2) const +{ + quint16 temp = ((quint16)(key2) & 0xffff) | 2; + return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); +} + +/*! + \internal Update the encryption keys with the next byte of plain text + */ +void UnzipPrivate::updateKeys(quint32* keys, int c) const +{ + keys[0] = CRC32(keys[0], c); + keys[1] += keys[0] & 0xff; + keys[1] = keys[1] * 134775813L + 1; + keys[2] = CRC32(keys[2], ((int)keys[1]) >> 24); +} + +/*! + \internal Initialize the encryption keys and the random header according to + the given password. + */ +void UnzipPrivate::initKeys(const QString& pwd, quint32* keys) const +{ + keys[0] = 305419896L; + keys[1] = 591751049L; + keys[2] = 878082192L; + + QByteArray pwdBytes = pwd.toLatin1(); + int sz = pwdBytes.size(); + const char* ascii = pwdBytes.data(); + + for (int i = 0; i < sz; ++i) + updateKeys(keys, (int)ascii[i]); +} + +/*! + \internal Attempts to test a password without actually extracting a file. + The \p file parameter can be used in the user interface or for debugging purposes + as it is the name of the encrypted file for wich the password is being tested. +*/ +UnZip::ErrorCode UnzipPrivate::testPassword(quint32* keys, const QString& file, const ZipEntryP& header) +{ + Q_UNUSED(file); + Q_ASSERT(device); + + // read encryption keys + if (device->read(buffer1, 12) != 12) + return UnZip::Corrupted; + + // Replace this code if you want to i.e. call some dialog and ask the user for a password + initKeys(password, keys); + if (testKeys(header, keys)) + return UnZip::Ok; + + return UnZip::Skip; +} + +/*! + \internal Tests a set of keys on the encryption header. +*/ +bool UnzipPrivate::testKeys(const ZipEntryP& header, quint32* keys) +{ + char lastByte; + + // decrypt encryption header + for (int i = 0; i < 11; ++i) + updateKeys(keys, lastByte = buffer1[i] ^ decryptByte(keys[2])); + updateKeys(keys, lastByte = buffer1[11] ^ decryptByte(keys[2])); + + // if there is an extended header (bit in the gp flag) buffer[11] is a byte from the file time + // with no extended header we have to check the crc high-order byte + char c = ((header.gpFlag[0] & 0x08) == 8) ? header.modTime[1] : header.crc >> 24; + + return (lastByte == c); +} + +/*! + \internal Decrypts an array of bytes long \p read. +*/ +void UnzipPrivate::decryptBytes(quint32* keys, char* buffer, qint64 read) +{ + for (int i = 0; i < (int)read; ++i) + updateKeys(keys, buffer[i] ^= decryptByte(keys[2])); +} + +/*! + \internal Converts date and time values from ZIP format to a QDateTime object. +*/ +QDateTime UnzipPrivate::convertDateTime(const unsigned char date[2], const unsigned char time[2]) const +{ + QDateTime dt; + + // Usual PKZip low-byte to high-byte order + + // Date: 7 bits = years from 1980, 4 bits = month, 5 bits = day + quint16 year = (date[1] >> 1) & 127; + quint16 month = ((date[1] << 3) & 14) | ((date[0] >> 5) & 7); + quint16 day = date[0] & 31; + + // Time: 5 bits hour, 6 bits minutes, 5 bits seconds with a 2sec precision + quint16 hour = (time[1] >> 3) & 31; + quint16 minutes = ((time[1] << 3) & 56) | ((time[0] >> 5) & 7); + quint16 seconds = (time[0] & 31) * 2; + + dt.setDate(QDate(1980 + year, month, day)); + dt.setTime(QTime(hour, minutes, seconds)); + return dt; +} + + +/************************************************************************ + Public interface +*************************************************************************/ + +/*! + Creates a new Zip file decompressor. +*/ +UnZip::UnZip() : d(new UnzipPrivate) +{ +} + +/*! + Closes any open archive and releases used resources. +*/ +UnZip::~UnZip() +{ + closeArchive(); + delete d; +} + +/*! + Returns true if there is an open archive. +*/ +bool UnZip::isOpen() const +{ + return d->device; +} + +/*! + Opens a zip archive and reads the files list. Closes any previously opened archive. +*/ +UnZip::ErrorCode UnZip::openArchive(const QString& filename) +{ + closeArchive(); + + // closeArchive will destroy the file + d->file = new QFile(filename); + + if (!d->file->exists()) { + delete d->file; + d->file = 0; + return UnZip::FileNotFound; + } + + if (!d->file->open(QIODevice::ReadOnly)) { + delete d->file; + d->file = 0; + return UnZip::OpenFailed; + } + + return d->openArchive(d->file); +} + +/*! + Opens a zip archive and reads the entries list. + Closes any previously opened archive. + \warning The class takes DOES NOT take ownership of the device. +*/ +UnZip::ErrorCode UnZip::openArchive(QIODevice* device) +{ + closeArchive(); + + if (!device) { + qDebug() << "Invalid device."; + return UnZip::InvalidDevice; + } + + return d->openArchive(device); +} + +/*! + Closes the archive and releases all the used resources (like cached passwords). +*/ +void UnZip::closeArchive() +{ + d->closeArchive(); +} + +QString UnZip::archiveComment() const +{ + return d->comment; +} + +/*! + Returns a locale translated error string for a given error code. +*/ +QString UnZip::formatError(UnZip::ErrorCode c) const +{ + switch (c) + { + case Ok: return QCoreApplication::translate("UnZip", "ZIP operation completed successfully."); break; + case ZlibInit: return QCoreApplication::translate("UnZip", "Failed to initialize or load zlib library."); break; + case ZlibError: return QCoreApplication::translate("UnZip", "zlib library error."); break; + case OpenFailed: return QCoreApplication::translate("UnZip", "Unable to create or open file."); break; + case PartiallyCorrupted: return QCoreApplication::translate("UnZip", "Partially corrupted archive. Some files might be extracted."); break; + case Corrupted: return QCoreApplication::translate("UnZip", "Corrupted archive."); break; + case WrongPassword: return QCoreApplication::translate("UnZip", "Wrong password."); break; + case NoOpenArchive: return QCoreApplication::translate("UnZip", "No archive has been created yet."); break; + case FileNotFound: return QCoreApplication::translate("UnZip", "File or directory does not exist."); break; + case ReadFailed: return QCoreApplication::translate("UnZip", "File read error."); break; + case WriteFailed: return QCoreApplication::translate("UnZip", "File write error."); break; + case SeekFailed: return QCoreApplication::translate("UnZip", "File seek error."); break; + case CreateDirFailed: return QCoreApplication::translate("UnZip", "Unable to create a directory."); break; + case InvalidDevice: return QCoreApplication::translate("UnZip", "Invalid device."); break; + case InvalidArchive: return QCoreApplication::translate("UnZip", "Invalid or incompatible zip archive."); break; + case HeaderConsistencyError: return QCoreApplication::translate("UnZip", "Inconsistent headers. Archive might be corrupted."); break; + default: ; + } + + return QCoreApplication::translate("UnZip", "Unknown error."); +} + +/*! + Returns true if the archive contains a file with the given path and name. +*/ +bool UnZip::contains(const QString& file) const +{ + return d->headers ? d->headers->contains(file) : false; +} + +/*! + Returns complete paths of files and directories in this archive. +*/ +QStringList UnZip::fileList() const +{ + return d->headers ? d->headers->keys() : QStringList(); +} + +/*! + Returns information for each (correctly parsed) entry of this archive. +*/ +QList UnZip::entryList() const +{ + QList list; + if (!d->headers) + return list; + + for (QMap::ConstIterator it = d->headers->constBegin(); + it != d->headers->constEnd(); ++it) { + const ZipEntryP* entry = it.value(); + Q_ASSERT(entry != 0); + + ZipEntry z; + + z.filename = it.key(); + if (!entry->comment.isEmpty()) + z.comment = entry->comment; + z.compressedSize = entry->szComp; + z.uncompressedSize = entry->szUncomp; + z.crc32 = entry->crc; + z.lastModified = d->convertDateTime(entry->modDate, entry->modTime); + + z.compression = entry->compMethod == 0 ? NoCompression : entry->compMethod == 8 ? Deflated : UnknownCompression; + z.type = z.filename.endsWith("/") ? Directory : File; + + z.encrypted = entry->isEncrypted(); + + list.append(z); + } + + return list; +} + +/*! + Extracts the whole archive to a directory. +*/ +UnZip::ErrorCode UnZip::verifyArchive() +{ + return extractAll(QDir(), VerifyOnly); +} + +/*! + Extracts the whole archive to a directory. +*/ +UnZip::ErrorCode UnZip::extractAll(const QString& dirname, ExtractionOptions options) +{ + return extractAll(QDir(dirname), options); +} + +/*! + Extracts the whole archive to a directory. + Stops extraction at the first error. +*/ +UnZip::ErrorCode UnZip::extractAll(const QDir& dir, ExtractionOptions options) +{ + // this should only happen if we didn't call openArchive() yet + if (!d->device) + return NoOpenArchive; + + if (!d->headers) + return Ok; + + ErrorCode ec = Ok; + + QMap::ConstIterator it = d->headers->constBegin(); + const QMap::ConstIterator end = d->headers->constEnd(); + while (it != end) { + ZipEntryP* entry = it.value(); + Q_ASSERT(entry != 0); + if ((entry->isEncrypted()) && d->skipAllEncrypted) { + ++it; + continue; + } + + bool skip = false; + ec = d->extractFile(it.key(), *entry, dir, options); + switch (ec) { + case Corrupted: + qDebug() << "Corrupted entry" << it.key(); + break; + case CreateDirFailed: + break; + case Skip: + skip = true; + break; + case SkipAll: + skip = true; + d->skipAllEncrypted = true; + break; + default: + ; + } + + if (ec != Ok && !skip) { + break; + } + + ++it; + } + + return ec; +} + +/*! + Extracts a single file to a directory. +*/ +UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QString& dirname, ExtractionOptions options) +{ + return extractFile(filename, QDir(dirname), options); +} + +/*! + Extracts a single file to a directory. +*/ +UnZip::ErrorCode UnZip::extractFile(const QString& filename, const QDir& dir, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return FileNotFound; + + QMap::Iterator itr = d->headers->find(filename); + if (itr != d->headers->end()) { + ZipEntryP* entry = itr.value(); + Q_ASSERT(entry != 0); + return d->extractFile(itr.key(), *entry, dir, options); + } + + return FileNotFound; +} + +/*! + Extracts a single file to a directory. +*/ +UnZip::ErrorCode UnZip::extractFile(const QString& filename, QIODevice* outDev, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return FileNotFound; + if (!outDev) + return InvalidDevice; + + QMap::Iterator itr = d->headers->find(filename); + if (itr != d->headers->end()) { + ZipEntryP* entry = itr.value(); + Q_ASSERT(entry != 0); + return d->extractFile(itr.key(), *entry, outDev, options); + } + + return FileNotFound; +} + +/*! + Extracts a list of files. + Stops extraction at the first error (but continues if a file does not exist in the archive). + */ +UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QString& dirname, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return Ok; + + QDir dir(dirname); + ErrorCode ec; + + for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr) { + ec = extractFile(*itr, dir, options); + if (ec == FileNotFound) + continue; + if (ec != Ok) + return ec; + } + + return Ok; +} + +/*! + Extracts a list of files. + Stops extraction at the first error (but continues if a file does not exist in the archive). + */ +UnZip::ErrorCode UnZip::extractFiles(const QStringList& filenames, const QDir& dir, ExtractionOptions options) +{ + if (!d->device) + return NoOpenArchive; + if (!d->headers) + return Ok; + + ErrorCode ec; + + for (QStringList::ConstIterator itr = filenames.constBegin(); itr != filenames.constEnd(); ++itr) { + ec = extractFile(*itr, dir, options); + if (ec == FileNotFound) + continue; + if (ec != Ok) + return ec; + } + + return Ok; +} + +/*! + Remove/replace this method to add your own password retrieval routine. +*/ +void UnZip::setPassword(const QString& pwd) +{ + d->password = pwd; +} + +OSDAB_END_NAMESPACE diff --git a/oracle/src/zip/unzip.h b/oracle/src/zip/unzip.h index e095a2287..7171c8b8f 100755 --- a/oracle/src/zip/unzip.h +++ b/oracle/src/zip/unzip.h @@ -1,152 +1,152 @@ -/**************************************************************************** -** Filename: unzip.h -** Last updated [dd/mm/yyyy]: 27/03/2011 -** -** pkzip 2.0 decompression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -#ifndef OSDAB_UNZIP__H -#define OSDAB_UNZIP__H - -#include "zipglobal.h" - -#include -#include -#include - -#include - -class QDir; -class QFile; -class QIODevice; -class QString; -class QStringList; - -OSDAB_BEGIN_NAMESPACE(Zip) - -class UnzipPrivate; - -class OSDAB_ZIP_EXPORT UnZip -{ -public: - enum ErrorCode - { - Ok, - ZlibInit, - ZlibError, - OpenFailed, - PartiallyCorrupted, - Corrupted, - WrongPassword, - NoOpenArchive, - FileNotFound, - ReadFailed, - WriteFailed, - SeekFailed, - CreateDirFailed, - InvalidDevice, - InvalidArchive, - HeaderConsistencyError, - - Skip, SkipAll // internal use only - }; - - enum ExtractionOption - { - ExtractPaths = 0x0001, - SkipPaths = 0x0002, - VerifyOnly = 0x0004, - NoSilentDirectoryCreation = 0x0008 - }; - Q_DECLARE_FLAGS(ExtractionOptions, ExtractionOption) - - enum CompressionMethod - { - NoCompression, Deflated, UnknownCompression - }; - - enum FileType - { - File, Directory - }; - - struct ZipEntry - { - ZipEntry(); - - QString filename; - QString comment; - - quint32 compressedSize; - quint32 uncompressedSize; - quint32 crc32; - - QDateTime lastModified; - - CompressionMethod compression; - FileType type; - - bool encrypted; - }; - - UnZip(); - virtual ~UnZip(); - - bool isOpen() const; - - ErrorCode openArchive(const QString& filename); - ErrorCode openArchive(QIODevice* device); - void closeArchive(); - - QString archiveComment() const; - - QString formatError(UnZip::ErrorCode c) const; - - bool contains(const QString& file) const; - - QStringList fileList() const; - QList entryList() const; - - ErrorCode verifyArchive(); - - ErrorCode extractAll(const QString& dirname, ExtractionOptions options = ExtractPaths); - ErrorCode extractAll(const QDir& dir, ExtractionOptions options = ExtractPaths); - - ErrorCode extractFile(const QString& filename, const QString& dirname, ExtractionOptions options = ExtractPaths); - ErrorCode extractFile(const QString& filename, const QDir& dir, ExtractionOptions options = ExtractPaths); - ErrorCode extractFile(const QString& filename, QIODevice* device, ExtractionOptions options = ExtractPaths); - - ErrorCode extractFiles(const QStringList& filenames, const QString& dirname, ExtractionOptions options = ExtractPaths); - ErrorCode extractFiles(const QStringList& filenames, const QDir& dir, ExtractionOptions options = ExtractPaths); - - void setPassword(const QString& pwd); - -private: - UnzipPrivate* d; -}; - -Q_DECLARE_OPERATORS_FOR_FLAGS(UnZip::ExtractionOptions) - -OSDAB_END_NAMESPACE - -#endif // OSDAB_UNZIP__H +/**************************************************************************** +** Filename: unzip.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 decompression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#ifndef OSDAB_UNZIP__H +#define OSDAB_UNZIP__H + +#include "zipglobal.h" + +#include +#include +#include + +#include + +class QDir; +class QFile; +class QIODevice; +class QString; +class QStringList; + +OSDAB_BEGIN_NAMESPACE(Zip) + +class UnzipPrivate; + +class OSDAB_ZIP_EXPORT UnZip +{ +public: + enum ErrorCode + { + Ok, + ZlibInit, + ZlibError, + OpenFailed, + PartiallyCorrupted, + Corrupted, + WrongPassword, + NoOpenArchive, + FileNotFound, + ReadFailed, + WriteFailed, + SeekFailed, + CreateDirFailed, + InvalidDevice, + InvalidArchive, + HeaderConsistencyError, + + Skip, SkipAll // internal use only + }; + + enum ExtractionOption + { + ExtractPaths = 0x0001, + SkipPaths = 0x0002, + VerifyOnly = 0x0004, + NoSilentDirectoryCreation = 0x0008 + }; + Q_DECLARE_FLAGS(ExtractionOptions, ExtractionOption) + + enum CompressionMethod + { + NoCompression, Deflated, UnknownCompression + }; + + enum FileType + { + File, Directory + }; + + struct ZipEntry + { + ZipEntry(); + + QString filename; + QString comment; + + quint32 compressedSize; + quint32 uncompressedSize; + quint32 crc32; + + QDateTime lastModified; + + CompressionMethod compression; + FileType type; + + bool encrypted; + }; + + UnZip(); + virtual ~UnZip(); + + bool isOpen() const; + + ErrorCode openArchive(const QString& filename); + ErrorCode openArchive(QIODevice* device); + void closeArchive(); + + QString archiveComment() const; + + QString formatError(UnZip::ErrorCode c) const; + + bool contains(const QString& file) const; + + QStringList fileList() const; + QList entryList() const; + + ErrorCode verifyArchive(); + + ErrorCode extractAll(const QString& dirname, ExtractionOptions options = ExtractPaths); + ErrorCode extractAll(const QDir& dir, ExtractionOptions options = ExtractPaths); + + ErrorCode extractFile(const QString& filename, const QString& dirname, ExtractionOptions options = ExtractPaths); + ErrorCode extractFile(const QString& filename, const QDir& dir, ExtractionOptions options = ExtractPaths); + ErrorCode extractFile(const QString& filename, QIODevice* device, ExtractionOptions options = ExtractPaths); + + ErrorCode extractFiles(const QStringList& filenames, const QString& dirname, ExtractionOptions options = ExtractPaths); + ErrorCode extractFiles(const QStringList& filenames, const QDir& dir, ExtractionOptions options = ExtractPaths); + + void setPassword(const QString& pwd); + +private: + UnzipPrivate* d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(UnZip::ExtractionOptions) + +OSDAB_END_NAMESPACE + +#endif // OSDAB_UNZIP__H diff --git a/oracle/src/zip/unzip_p.h b/oracle/src/zip/unzip_p.h index 21e9f2a2a..c389fcd6b 100755 --- a/oracle/src/zip/unzip_p.h +++ b/oracle/src/zip/unzip_p.h @@ -1,130 +1,130 @@ -/**************************************************************************** -** Filename: unzip_p.h -** Last updated [dd/mm/yyyy]: 27/03/2011 -** -** pkzip 2.0 decompression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Zip/UnZip API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -#ifndef OSDAB_UNZIP_P__H -#define OSDAB_UNZIP_P__H - -#include "unzip.h" -#include "zipentry_p.h" - -#include -#include - -// zLib authors suggest using larger buffers (128K or 256K) for (de)compression (especially for inflate()) -// we use a 256K buffer here - if you want to use this code on a pre-iceage mainframe please change it ;) -#define UNZIP_READ_BUFFER (256*1024) - -OSDAB_BEGIN_NAMESPACE(Zip) - -class UnzipPrivate : public QObject -{ - Q_OBJECT - -public: - UnzipPrivate(); - - // Replace this with whatever else you use to store/retrieve the password. - QString password; - - bool skipAllEncrypted; - - QMap* headers; - - QIODevice* device; - QFile* file; - - char buffer1[UNZIP_READ_BUFFER]; - char buffer2[UNZIP_READ_BUFFER]; - - unsigned char* uBuffer; - const quint32* crcTable; - - // Central Directory (CD) offset - quint32 cdOffset; - // End of Central Directory (EOCD) offset - quint32 eocdOffset; - - // Number of entries in the Central Directory (as to the EOCD record) - quint16 cdEntryCount; - - // The number of detected entries that have been skipped because of a non compatible format - quint16 unsupportedEntryCount; - - QString comment; - - UnZip::ErrorCode openArchive(QIODevice* device); - - UnZip::ErrorCode seekToCentralDirectory(); - UnZip::ErrorCode parseCentralDirectoryRecord(); - UnZip::ErrorCode parseLocalHeaderRecord(const QString& path, const ZipEntryP& entry); - - void closeArchive(); - - UnZip::ErrorCode extractFile(const QString& path, const ZipEntryP& entry, const QDir& dir, UnZip::ExtractionOptions options); - UnZip::ErrorCode extractFile(const QString& path, const ZipEntryP& entry, QIODevice* device, UnZip::ExtractionOptions options); - - UnZip::ErrorCode testPassword(quint32* keys, const QString& file, const ZipEntryP& header); - bool testKeys(const ZipEntryP& header, quint32* keys); - - bool createDirectory(const QString& path); - - inline void decryptBytes(quint32* keys, char* buffer, qint64 read); - - inline quint32 getULong(const unsigned char* data, quint32 offset) const; - inline quint64 getULLong(const unsigned char* data, quint32 offset) const; - inline quint16 getUShort(const unsigned char* data, quint32 offset) const; - inline int decryptByte(quint32 key2) const; - inline void updateKeys(quint32* keys, int c) const; - inline void initKeys(const QString& pwd, quint32* keys) const; - - inline QDateTime convertDateTime(const unsigned char date[2], const unsigned char time[2]) const; - -private slots: - void deviceDestroyed(QObject*); - -private: - UnZip::ErrorCode extractStoredFile(const quint32 szComp, quint32** keys, - quint32& myCRC, QIODevice* outDev, UnZip::ExtractionOptions options); - UnZip::ErrorCode inflateFile(const quint32 szComp, quint32** keys, - quint32& myCRC, QIODevice* outDev, UnZip::ExtractionOptions options); - void do_closeArchive(); -}; - -OSDAB_END_NAMESPACE - -#endif // OSDAB_UNZIP_P__H +/**************************************************************************** +** Filename: unzip_p.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 decompression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Zip/UnZip API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef OSDAB_UNZIP_P__H +#define OSDAB_UNZIP_P__H + +#include "unzip.h" +#include "zipentry_p.h" + +#include +#include + +// zLib authors suggest using larger buffers (128K or 256K) for (de)compression (especially for inflate()) +// we use a 256K buffer here - if you want to use this code on a pre-iceage mainframe please change it ;) +#define UNZIP_READ_BUFFER (256*1024) + +OSDAB_BEGIN_NAMESPACE(Zip) + +class UnzipPrivate : public QObject +{ + Q_OBJECT + +public: + UnzipPrivate(); + + // Replace this with whatever else you use to store/retrieve the password. + QString password; + + bool skipAllEncrypted; + + QMap* headers; + + QIODevice* device; + QFile* file; + + char buffer1[UNZIP_READ_BUFFER]; + char buffer2[UNZIP_READ_BUFFER]; + + unsigned char* uBuffer; + const quint32* crcTable; + + // Central Directory (CD) offset + quint32 cdOffset; + // End of Central Directory (EOCD) offset + quint32 eocdOffset; + + // Number of entries in the Central Directory (as to the EOCD record) + quint16 cdEntryCount; + + // The number of detected entries that have been skipped because of a non compatible format + quint16 unsupportedEntryCount; + + QString comment; + + UnZip::ErrorCode openArchive(QIODevice* device); + + UnZip::ErrorCode seekToCentralDirectory(); + UnZip::ErrorCode parseCentralDirectoryRecord(); + UnZip::ErrorCode parseLocalHeaderRecord(const QString& path, const ZipEntryP& entry); + + void closeArchive(); + + UnZip::ErrorCode extractFile(const QString& path, const ZipEntryP& entry, const QDir& dir, UnZip::ExtractionOptions options); + UnZip::ErrorCode extractFile(const QString& path, const ZipEntryP& entry, QIODevice* device, UnZip::ExtractionOptions options); + + UnZip::ErrorCode testPassword(quint32* keys, const QString& file, const ZipEntryP& header); + bool testKeys(const ZipEntryP& header, quint32* keys); + + bool createDirectory(const QString& path); + + inline void decryptBytes(quint32* keys, char* buffer, qint64 read); + + inline quint32 getULong(const unsigned char* data, quint32 offset) const; + inline quint64 getULLong(const unsigned char* data, quint32 offset) const; + inline quint16 getUShort(const unsigned char* data, quint32 offset) const; + inline int decryptByte(quint32 key2) const; + inline void updateKeys(quint32* keys, int c) const; + inline void initKeys(const QString& pwd, quint32* keys) const; + + inline QDateTime convertDateTime(const unsigned char date[2], const unsigned char time[2]) const; + +private slots: + void deviceDestroyed(QObject*); + +private: + UnZip::ErrorCode extractStoredFile(const quint32 szComp, quint32** keys, + quint32& myCRC, QIODevice* outDev, UnZip::ExtractionOptions options); + UnZip::ErrorCode inflateFile(const quint32 szComp, quint32** keys, + quint32& myCRC, QIODevice* outDev, UnZip::ExtractionOptions options); + void do_closeArchive(); +}; + +OSDAB_END_NAMESPACE + +#endif // OSDAB_UNZIP_P__H diff --git a/oracle/src/zip/zip.cpp b/oracle/src/zip/zip.cpp index aa33bfc70..5b0177293 100755 --- a/oracle/src/zip/zip.cpp +++ b/oracle/src/zip/zip.cpp @@ -1,1619 +1,1619 @@ -/**************************************************************************** -** Filename: zip.cpp -** Last updated [dd/mm/yyyy]: 01/02/2007 -** -** pkzip 2.0 file compression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -#include "zip.h" -#include "zip_p.h" -#include "zipentry_p.h" - -// we only use this to seed the random number generator -#include - -#include -#include -#include -#include -#include -#include -#include - -// You can remove this #include if you replace the qDebug() statements. -#include - - -/*! #define OSDAB_ZIP_NO_PNG_RLE to disable the use of Z_RLE compression strategy with - PNG files (achieves slightly better compression levels according to the authors). -*/ -// #define OSDAB_ZIP_NO_PNG_RLE - -#define OSDAB_ZIP_NO_DEBUG - -//! Local header size (including signature, excluding variable length fields) -#define ZIP_LOCAL_HEADER_SIZE 30 -//! Encryption header size -#define ZIP_LOCAL_ENC_HEADER_SIZE 12 -//! Data descriptor size (signature included) -#define ZIP_DD_SIZE_WS 16 -//! Central Directory record size (signature included) -#define ZIP_CD_SIZE 46 -//! End of Central Directory record size (signature included) -#define ZIP_EOCD_SIZE 22 - -// Some offsets inside a local header record (signature included) -#define ZIP_LH_OFF_VERS 4 -#define ZIP_LH_OFF_GPFLAG 6 -#define ZIP_LH_OFF_CMET 8 -#define ZIP_LH_OFF_MODT 10 -#define ZIP_LH_OFF_MODD 12 -#define ZIP_LH_OFF_CRC 14 -#define ZIP_LH_OFF_CSIZE 18 -#define ZIP_LH_OFF_USIZE 22 -#define ZIP_LH_OFF_NAMELEN 26 -#define ZIP_LH_OFF_XLEN 28 - -// Some offsets inside a data descriptor record (including signature) -#define ZIP_DD_OFF_CRC32 4 -#define ZIP_DD_OFF_CSIZE 8 -#define ZIP_DD_OFF_USIZE 12 - -// Some offsets inside a Central Directory record (including signature) -#define ZIP_CD_OFF_MADEBY 4 -#define ZIP_CD_OFF_VERSION 6 -#define ZIP_CD_OFF_GPFLAG 8 -#define ZIP_CD_OFF_CMET 10 -#define ZIP_CD_OFF_MODT 12 -#define ZIP_CD_OFF_MODD 14 -#define ZIP_CD_OFF_CRC 16 -#define ZIP_CD_OFF_CSIZE 20 -#define ZIP_CD_OFF_USIZE 24 -#define ZIP_CD_OFF_NAMELEN 28 -#define ZIP_CD_OFF_XLEN 30 -#define ZIP_CD_OFF_COMMLEN 32 -#define ZIP_CD_OFF_DISKSTART 34 -#define ZIP_CD_OFF_IATTR 36 -#define ZIP_CD_OFF_EATTR 38 -#define ZIP_CD_OFF_LHOFF 42 - -// Some offsets inside a EOCD record (including signature) -#define ZIP_EOCD_OFF_DISKNUM 4 -#define ZIP_EOCD_OFF_CDDISKNUM 6 -#define ZIP_EOCD_OFF_ENTRIES 8 -#define ZIP_EOCD_OFF_CDENTRIES 10 -#define ZIP_EOCD_OFF_CDSIZE 12 -#define ZIP_EOCD_OFF_CDOFF 16 -#define ZIP_EOCD_OFF_COMMLEN 20 - -//! PKZip version for archives created by this API -#define ZIP_VERSION 0x14 - -//! Do not store very small files as the compression headers overhead would be to big -#define ZIP_COMPRESSION_THRESHOLD 60 - -/*! - \class Zip zip.h - - \brief Zip file compression. - - Some quick usage examples. - - \verbatim - Suppose you have this directory structure: - - /home/user/dir1/file1.1 - /home/user/dir1/file1.2 - /home/user/dir1/dir1.1/ - /home/user/dir1/dir1.2/file1.2.1 - - EXAMPLE 1: - myZipInstance.addDirectory("/home/user/dir1"); - - RESULT: - Beheaves like any common zip software and creates a zip file with this structure: - - dir1/file1.1 - dir1/file1.2 - dir1/dir1.1/ - dir1/dir1.2/file1.2.1 - - EXAMPLE 2: - myZipInstance.addDirectory("/home/user/dir1", "myRoot/myFolder"); - - RESULT: - Adds a custom root to the paths and creates a zip file with this structure: - - myRoot/myFolder/dir1/file1.1 - myRoot/myFolder/dir1/file1.2 - myRoot/myFolder/dir1/dir1.1/ - myRoot/myFolder/dir1/dir1.2/file1.2.1 - - EXAMPLE 3: - myZipInstance.addDirectory("/home/user/dir1", Zip::AbsolutePaths); - - NOTE: - Same as calling addDirectory(SOME_PATH, PARENT_PATH_of_SOME_PATH). - - RESULT: - Preserves absolute paths and creates a zip file with this structure: - - /home/user/dir1/file1.1 - /home/user/dir1/file1.2 - /home/user/dir1/dir1.1/ - /home/user/dir1/dir1.2/file1.2.1 - - EXAMPLE 4: - myZipInstance.setPassword("hellopass"); - myZipInstance.addDirectory("/home/user/dir1", "/"); - - RESULT: - Adds and encrypts the files in /home/user/dir1, creating the following zip structure: - - /dir1/file1.1 - /dir1/file1.2 - /dir1/dir1.1/ - /dir1/dir1.2/file1.2.1 - - EXAMPLE 5: - myZipInstance.addDirectory("/home/user/dir1", Zip::IgnoreRoot); - - RESULT: - Adds the files in /home/user/dir1 but doesn't create the top level - directory: - - file1.1 - file1.2 - dir1.1/ - dir1.2/file1.2.1 - - EXAMPLE 5: - myZipInstance.addDirectory("/home/user/dir1", "data/backup", Zip::IgnoreRoot); - - RESULT: - Adds the files in /home/user/dir1 but uses "data/backup" as top level - directory instead of "dir1": - - data/backup/file1.1 - data/backup/file1.2 - data/backup/dir1.1/ - data/backup/dir1.2/file1.2.1 - - \endverbatim -*/ - -/*! \enum Zip::ErrorCode The result of a compression operation. - \value Zip::Ok No error occurred. - \value Zip::ZlibInit Failed to init or load the zlib library. - \value Zip::ZlibError The zlib library returned some error. - \value Zip::FileExists The file already exists and will not be overwritten. - \value Zip::OpenFailed Unable to create or open a device. - \value Zip::NoOpenArchive CreateArchive() has not been called yet. - \value Zip::FileNotFound File or directory does not exist. - \value Zip::ReadFailed Reading of a file failed. - \value Zip::WriteFailed Writing of a file failed. - \value Zip::SeekFailed Seek failed. -*/ - -/*! \enum Zip::CompressionLevel Returns the result of a decompression operation. - \value Zip::Store No compression. - \value Zip::Deflate1 Deflate compression level 1(lowest compression). - \value Zip::Deflate1 Deflate compression level 2. - \value Zip::Deflate1 Deflate compression level 3. - \value Zip::Deflate1 Deflate compression level 4. - \value Zip::Deflate1 Deflate compression level 5. - \value Zip::Deflate1 Deflate compression level 6. - \value Zip::Deflate1 Deflate compression level 7. - \value Zip::Deflate1 Deflate compression level 8. - \value Zip::Deflate1 Deflate compression level 9 (maximum compression). - \value Zip::AutoCPU Adapt compression level to CPU speed (faster CPU => better compression). - \value Zip::AutoMIME Adapt compression level to MIME type of the file being compressed. - \value Zip::AutoFull Use both CPU and MIME type detection. -*/ - -namespace { - -struct ZippedDir { - bool init; - QString actualRoot; - int files; - ZippedDir() : init(false), actualRoot(), files(0) {} -}; - -void checkRootPath(QString& path) -{ - const bool isUnixRoot = path.length() == 1 && path.at(0) == QLatin1Char('/'); - if (!path.isEmpty() && !isUnixRoot) { - while (path.endsWith(QLatin1String("\\"))) - path.truncate(path.length() - 1); - - int sepCount = 0; - for (int i = path.length()-1; i >= 0; --i) { - if (path.at(i) == QLatin1Char('/')) - ++sepCount; - else break; - } - - if (sepCount > 1) - path.truncate(path.length() - (sepCount-1)); - else if (sepCount == 0) - path.append(QLatin1String("/")); - } -} - -} - -////////////////////////////////////////////////////////////////////////// - -OSDAB_BEGIN_NAMESPACE(Zip) - -/************************************************************************ - Private interface -*************************************************************************/ - -//! \internal -ZipPrivate::ZipPrivate() : - headers(0), - device(0), - file(0), - uBuffer(0), - crcTable(0), - comment(), - password() -{ - // keep an unsigned pointer so we avoid to over bloat the code with casts - uBuffer = (unsigned char*) buffer1; - crcTable = get_crc_table(); -} - -//! \internal -ZipPrivate::~ZipPrivate() -{ - closeArchive(); -} - -//! \internal -Zip::ErrorCode ZipPrivate::createArchive(QIODevice* dev) -{ - Q_ASSERT(dev); - - if (device) - closeArchive(); - - device = dev; - if (device != file) - connect(device, SIGNAL(destroyed(QObject*)), this, SLOT(deviceDestroyed(QObject*))); - - if (!device->isOpen()) { - if (!device->open(QIODevice::ReadOnly)) { - delete device; - device = 0; - qDebug() << "Unable to open device for writing."; - return Zip::OpenFailed; - } - } - - headers = new QMap; - return Zip::Ok; -} - -//! \internal -void ZipPrivate::deviceDestroyed(QObject*) -{ - qDebug("Unexpected device destruction detected."); - do_closeArchive(); -} - -/*! Returns true if an entry for \p info has already been added. - Uses file size and lower case absolute path to compare entries. -*/ -bool ZipPrivate::containsEntry(const QFileInfo& info) const -{ - if (!headers || headers->isEmpty()) - return false; - - const qint64 sz = info.size(); - const QString path = info.absoluteFilePath().toLower(); - - QMap::ConstIterator b = headers->constBegin(); - const QMap::ConstIterator e = headers->constEnd(); - while (b != e) { - const ZipEntryP* e = b.value(); - if (e->fileSize == sz && e->absolutePath == path) - return true; - ++b; - } - - return false; -} - -//! \internal Actual implementation of the addDirectory* methods. -Zip::ErrorCode ZipPrivate::addDirectory(const QString& path, const QString& root, - Zip::CompressionOptions options, Zip::CompressionLevel level, int hierarchyLevel, - int* addedFiles) -{ - if (addedFiles) - ++(*addedFiles); - - // Bad boy didn't call createArchive() yet :) - if (!device) - return Zip::NoOpenArchive; - - QDir dir(path); - if (!dir.exists()) - return Zip::FileNotFound; - - // Remove any trailing separator - QString actualRoot = root.trimmed(); - - // Preserve Unix root but make sure the path ends only with a single - // unix like separator - ::checkRootPath(actualRoot); - - // QDir::cleanPath() fixes some issues with QDir::dirName() - QFileInfo current(QDir::cleanPath(path)); - - const bool path_absolute = options.testFlag(Zip::AbsolutePaths); - const bool path_ignore = options.testFlag(Zip::IgnorePaths); - const bool path_noroot = options.testFlag(Zip::IgnoreRoot); - - if (path_absolute && !path_ignore && !path_noroot) { - QString absolutePath = extractRoot(path, options); - if (!absolutePath.isEmpty() && absolutePath != QLatin1String("/")) - absolutePath.append(QLatin1String("/")); - actualRoot.append(absolutePath); - } - - const bool skipDirName = !hierarchyLevel && path_noroot; - if (!path_ignore && !skipDirName) { - actualRoot.append(QDir(current.absoluteFilePath()).dirName()); - actualRoot.append(QLatin1String("/")); - } - - // actualRoot now contains the path of the file relative to the zip archive - // with a trailing / - - const bool skipBad = options & Zip::SkipBadFiles; - const bool noDups = options & Zip::CheckForDuplicates; - - const QDir::Filters dir_filter = - QDir::Files | - QDir::Dirs | - QDir::NoDotAndDotDot | - QDir::NoSymLinks; - const QDir::SortFlags dir_sort = - QDir::DirsFirst; - QFileInfoList list = dir.entryInfoList(dir_filter, dir_sort); - - Zip::ErrorCode ec = Zip::Ok; - bool filesAdded = false; - - Zip::CompressionOptions recursionOptions; - if (path_ignore) - recursionOptions |= Zip::IgnorePaths; - else recursionOptions |= Zip::RelativePaths; - - for (int i = 0; i < list.size(); ++i) { - QFileInfo info = list.at(i); - const QString absPath = info.absoluteFilePath(); - if (noDups && containsEntry(info)) - continue; - if (info.isDir()) { - // Recursion - ec = addDirectory(absPath, actualRoot, recursionOptions, - level, hierarchyLevel + 1, addedFiles); - } else { - ec = createEntry(info, actualRoot, level); - if (ec == Zip::Ok) { - filesAdded = true; - if (addedFiles) - ++(*addedFiles); - } - } - - if (ec != Zip::Ok && !skipBad) { - break; - } - } - - // We need an explicit record for this dir - // Non-empty directories don't need it because they have a path component in the filename - if (!filesAdded && !path_ignore) - ec = createEntry(current, actualRoot, level); - - return ec; -} - -//! \internal Actual implementation of the addFile methods. -Zip::ErrorCode ZipPrivate::addFiles(const QStringList& files, const QString& root, - Zip::CompressionOptions options, Zip::CompressionLevel level, - int* addedFiles) -{ - if (addedFiles) - *addedFiles = 0; - - const bool skipBad = options & Zip::SkipBadFiles; - const bool noDups = options & Zip::CheckForDuplicates; - - // Bad boy didn't call createArchive() yet :) - if (!device) - return Zip::NoOpenArchive; - - QFileInfoList paths; - paths.reserve(files.size()); - for (int i = 0; i < files.size(); ++i) { - QFileInfo info(files.at(i)); - if (noDups && (paths.contains(info) || containsEntry(info))) - continue; - if (!info.exists() || !info.isReadable()) { - if (skipBad) { - continue; - } else { - return Zip::FileNotFound; - } - } - paths.append(info); - } - - if (paths.isEmpty()) - return Zip::Ok; - - // Remove any trailing separator - QString actualRoot = root.trimmed(); - - // Preserve Unix root but make sure the path ends only with a single - // unix like separator - ::checkRootPath(actualRoot); - - const bool path_absolute = options.testFlag(Zip::AbsolutePaths); - const bool path_ignore = options.testFlag(Zip::IgnorePaths); - const bool path_noroot = options.testFlag(Zip::IgnoreRoot); - - Zip::ErrorCode ec = Zip::Ok; - QHash dirMap; - - for (int i = 0; i < paths.size(); ++i) { - const QFileInfo& info = paths.at(i); - const QString path = QFileInfo(QDir::cleanPath(info.absolutePath())).absolutePath(); - - ZippedDir& zd = dirMap[path]; - if (!zd.init) { - zd.init = true; - zd.actualRoot = actualRoot; - if (path_absolute && !path_ignore && !path_noroot) { - QString absolutePath = extractRoot(path, options); - if (!absolutePath.isEmpty() && absolutePath != QLatin1String("/")) - absolutePath.append(QLatin1String("/")); - zd.actualRoot.append(absolutePath); - } - - if (!path_ignore && !path_noroot) { - zd.actualRoot.append(QDir(path).dirName()); - zd.actualRoot.append(QLatin1String("/")); - } - } - - // zd.actualRoot now contains the path of the file relative to the zip archive - // with a trailing / - - if (info.isDir()) { - // Recursion - ec = addDirectory(info.absoluteFilePath(), actualRoot, options, - level, 1, addedFiles); - } else { - ec = createEntry(info, actualRoot, level); - if (ec == Zip::Ok) { - ++zd.files; - if (addedFiles) - ++(*addedFiles); - } - } - - if (ec != Zip::Ok && !skipBad) { - break; - } - } - - // Create explicit records for empty directories - if (!path_ignore) { - QHash::ConstIterator b = dirMap.constBegin(); - const QHash::ConstIterator e = dirMap.constEnd(); - while (b != e) { - const ZippedDir& zd = b.value(); - if (zd.files <= 0) { - ec = createEntry(b.key(), zd.actualRoot, level); - } - ++b; - } - } - - return ec; -} - -//! \internal \p file must be a file and not a directory. -Zip::ErrorCode ZipPrivate::deflateFile(const QFileInfo& fileInfo, - quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys) -{ - const QString path = fileInfo.absoluteFilePath(); - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - qDebug() << QString("An error occurred while opening %1").arg(path); - return Zip::OpenFailed; - } - - const Zip::ErrorCode ec = (level == Zip::Store) - ? storeFile(path, file, crc, written, keys) - : compressFile(path, file, crc, written, level, keys); - - file.close(); - return ec; -} - -//! \internal -Zip::ErrorCode ZipPrivate::storeFile(const QString& path, QIODevice& file, - quint32& crc, qint64& totalWritten, quint32** keys) -{ - Q_UNUSED(path); - - qint64 read = 0; - qint64 written = 0; - - const bool encrypt = keys != 0; - - totalWritten = 0; - crc = crc32(0L, Z_NULL, 0); - - while ( (read = file.read(buffer1, ZIP_READ_BUFFER)) > 0 ) { - crc = crc32(crc, uBuffer, read); - if (encrypt) - encryptBytes(*keys, buffer1, read); - written = device->write(buffer1, read); - totalWritten += written; - if (written != read) { - return Zip::WriteFailed; - } - } - - return Zip::Ok; -} - -//! \internal -int ZipPrivate::compressionStrategy(const QString& path, QIODevice& file) const -{ - Q_UNUSED(file); - -#ifndef OSDAB_ZIP_NO_PNG_RLE - return Z_DEFAULT_STRATEGY; -#endif - const bool isPng = path.endsWith(QLatin1String("png"), Qt::CaseInsensitive); - return isPng ? Z_RLE : Z_DEFAULT_STRATEGY; -} - -//! \internal -Zip::ErrorCode ZipPrivate::compressFile(const QString& path, QIODevice& file, - quint32& crc, qint64& totalWritten, const Zip::CompressionLevel& level, quint32** keys) -{ - qint64 read = 0; - qint64 written = 0; - - qint64 totRead = 0; - qint64 toRead = file.size(); - - const bool encrypt = keys != 0; - const int strategy = compressionStrategy(path, file); - - totalWritten = 0; - crc = crc32(0L, Z_NULL, 0); - - z_stream zstr; - - // Initialize zalloc, zfree and opaque before calling the init function - zstr.zalloc = Z_NULL; - zstr.zfree = Z_NULL; - zstr.opaque = Z_NULL; - - int zret; - - // Use deflateInit2 with negative windowBits to get raw compression - if ((zret = deflateInit2_( - &zstr, - (int)level, // compression level - Z_DEFLATED, // method - -MAX_WBITS, // windowBits - 8, // memLevel - strategy, - ZLIB_VERSION, - sizeof(z_stream) - )) != Z_OK ) { - qDebug() << "Could not initialize zlib for compression"; - return Zip::ZlibError; - } - - qint64 compressed; - int flush = Z_NO_FLUSH; - do { - read = file.read(buffer1, ZIP_READ_BUFFER); - totRead += read; - if (!read) - break; - - if (read < 0) { - deflateEnd(&zstr); - qDebug() << QString("Error while reading %1").arg(path); - return Zip::ReadFailed; - } - - crc = crc32(crc, uBuffer, read); - - zstr.next_in = (Bytef*) buffer1; - zstr.avail_in = (uInt)read; - - // Tell zlib if this is the last chunk we want to encode - // by setting the flush parameter to Z_FINISH - flush = (totRead == toRead) ? Z_FINISH : Z_NO_FLUSH; - - // Run deflate() on input until output buffer not full - // finish compression if all of source has been read in - do { - zstr.next_out = (Bytef*) buffer2; - zstr.avail_out = ZIP_READ_BUFFER; - - zret = deflate(&zstr, flush); - // State not clobbered - Q_ASSERT(zret != Z_STREAM_ERROR); - - // Write compressed data to file and empty buffer - compressed = ZIP_READ_BUFFER - zstr.avail_out; - - if (encrypt) - encryptBytes(*keys, buffer2, compressed); - - written = device->write(buffer2, compressed); - totalWritten += written; - - if (written != compressed) { - deflateEnd(&zstr); - qDebug() << QString("Error while writing %1").arg(path); - return Zip::WriteFailed; - } - - } while (zstr.avail_out == 0); - - // All input will be used - Q_ASSERT(zstr.avail_in == 0); - - } while (flush != Z_FINISH); - - // Stream will be complete - Q_ASSERT(zret == Z_STREAM_END); - deflateEnd(&zstr); - - return Zip::Ok; -} - -//! \internal Writes a new entry in the zip file. -Zip::ErrorCode ZipPrivate::createEntry(const QFileInfo& file, const QString& root, - Zip::CompressionLevel level) -{ - const bool dirOnly = file.isDir(); - - // entryName contains the path as it should be written - // in the zip file records - const QString entryName = dirOnly - ? root - : root + file.fileName(); - - // Directory entry - if (dirOnly || file.size() < ZIP_COMPRESSION_THRESHOLD) { - level = Zip::Store; - } else { - switch (level) { - case Zip::AutoCPU: - level = Zip::Deflate5; -#ifndef OSDAB_ZIP_NO_DEBUG - qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); -#endif - break; - case Zip::AutoMIME: - level = detectCompressionByMime(file.completeSuffix().toLower()); -#ifndef OSDAB_ZIP_NO_DEBUG - qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); -#endif - break; - case Zip::AutoFull: - level = detectCompressionByMime(file.completeSuffix().toLower()); -#ifndef OSDAB_ZIP_NO_DEBUG - qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); -#endif - break; - default: ; - } - } - - - - // create header and store it to write a central directory later - QScopedPointer h(new ZipEntryP); - h->absolutePath = file.absoluteFilePath().toLower(); - h->fileSize = file.size(); - - // Set encryption bit and set the data descriptor bit - // so we can use mod time instead of crc for password check - bool encrypt = !dirOnly && !password.isEmpty(); - if (encrypt) - h->gpFlag[0] |= 9; - - QDateTime dt = file.lastModified(); - dt = OSDAB_ZIP_MANGLE(fromFileTimestamp)(dt); - QDate d = dt.date(); - h->modDate[1] = ((d.year() - 1980) << 1) & 254; - h->modDate[1] |= ((d.month() >> 3) & 1); - h->modDate[0] = ((d.month() & 7) << 5) & 224; - h->modDate[0] |= d.day(); - - QTime t = dt.time(); - h->modTime[1] = (t.hour() << 3) & 248; - h->modTime[1] |= ((t.minute() >> 3) & 7); - h->modTime[0] = ((t.minute() & 7) << 5) & 224; - h->modTime[0] |= t.second() / 2; - - h->szUncomp = dirOnly ? 0 : file.size(); - - h->compMethod = (level == Zip::Store) ? 0 : 0x0008; - - // **** Write local file header **** - - // signature - buffer1[0] = 'P'; buffer1[1] = 'K'; - buffer1[2] = 0x3; buffer1[3] = 0x4; - - // version needed to extract - buffer1[ZIP_LH_OFF_VERS] = ZIP_VERSION; - buffer1[ZIP_LH_OFF_VERS + 1] = 0; - - // general purpose flag - buffer1[ZIP_LH_OFF_GPFLAG] = h->gpFlag[0]; - buffer1[ZIP_LH_OFF_GPFLAG + 1] = h->gpFlag[1]; - - // compression method - buffer1[ZIP_LH_OFF_CMET] = h->compMethod & 0xFF; - buffer1[ZIP_LH_OFF_CMET + 1] = (h->compMethod>>8) & 0xFF; - - // last mod file time - buffer1[ZIP_LH_OFF_MODT] = h->modTime[0]; - buffer1[ZIP_LH_OFF_MODT + 1] = h->modTime[1]; - - // last mod file date - buffer1[ZIP_LH_OFF_MODD] = h->modDate[0]; - buffer1[ZIP_LH_OFF_MODD + 1] = h->modDate[1]; - - // skip crc (4bytes) [14,15,16,17] - - // skip compressed size but include evtl. encryption header (4bytes: [18,19,20,21]) - buffer1[ZIP_LH_OFF_CSIZE] = - buffer1[ZIP_LH_OFF_CSIZE + 1] = - buffer1[ZIP_LH_OFF_CSIZE + 2] = - buffer1[ZIP_LH_OFF_CSIZE + 3] = 0; - - h->szComp = encrypt ? ZIP_LOCAL_ENC_HEADER_SIZE : 0; - - // uncompressed size [22,23,24,25] - setULong(h->szUncomp, buffer1, ZIP_LH_OFF_USIZE); - - // filename length - QByteArray entryNameBytes = entryName.toLatin1(); - int sz = entryNameBytes.size(); - - buffer1[ZIP_LH_OFF_NAMELEN] = sz & 0xFF; - buffer1[ZIP_LH_OFF_NAMELEN + 1] = (sz >> 8) & 0xFF; - - // extra field length - buffer1[ZIP_LH_OFF_XLEN] = buffer1[ZIP_LH_OFF_XLEN + 1] = 0; - - // Store offset to write crc and compressed size - h->lhOffset = device->pos(); - quint32 crcOffset = h->lhOffset + ZIP_LH_OFF_CRC; - - if (device->write(buffer1, ZIP_LOCAL_HEADER_SIZE) != ZIP_LOCAL_HEADER_SIZE) { - return Zip::WriteFailed; - } - - // Write out filename - if (device->write(entryNameBytes) != sz) { - return Zip::WriteFailed; - } - - // Encryption keys - quint32 keys[3] = { 0, 0, 0 }; - - if (encrypt) { - // **** encryption header **** - - // XOR with PI to ensure better random numbers - // with poorly implemented rand() as suggested by Info-Zip - srand(time(NULL) ^ 3141592654UL); - int randByte; - - initKeys(keys); - for (int i = 0; i < 10; ++i) { - randByte = (rand() >> 7) & 0xff; - buffer1[i] = decryptByte(keys[2]) ^ randByte; - updateKeys(keys, randByte); - } - - // Encrypt encryption header - initKeys(keys); - for (int i = 0; i < 10; ++i) { - randByte = decryptByte(keys[2]); - updateKeys(keys, buffer1[i]); - buffer1[i] ^= randByte; - } - - // We don't know the CRC at this time, so we use the modification time - // as the last two bytes - randByte = decryptByte(keys[2]); - updateKeys(keys, h->modTime[0]); - buffer1[10] ^= randByte; - - randByte = decryptByte(keys[2]); - updateKeys(keys, h->modTime[1]); - buffer1[11] ^= randByte; - - // Write out encryption header - if (device->write(buffer1, ZIP_LOCAL_ENC_HEADER_SIZE) != ZIP_LOCAL_ENC_HEADER_SIZE) { - return Zip::WriteFailed; - } - } - - quint32 crc = 0; - qint64 written = 0; - - if (!dirOnly) { - quint32* k = keys; - const Zip::ErrorCode ec = deflateFile(file, crc, written, level, encrypt ? &k : 0); - if (ec != Zip::Ok) - return ec; - Q_ASSERT(!h.isNull()); - } - - // Store end of entry offset - quint32 current = device->pos(); - - // Update crc and compressed size in local header - if (!device->seek(crcOffset)) { - return Zip::SeekFailed; - } - - h->crc = dirOnly ? 0 : crc; - h->szComp += written; - - setULong(h->crc, buffer1, 0); - setULong(h->szComp, buffer1, 4); - if ( device->write(buffer1, 8) != 8) { - return Zip::WriteFailed; - } - - // Seek to end of entry - if (!device->seek(current)) { - return Zip::SeekFailed; - } - - if ((h->gpFlag[0] & 8) == 8) { - // Write data descriptor - - // Signature: PK\7\8 - buffer1[0] = 'P'; - buffer1[1] = 'K'; - buffer1[2] = 0x07; - buffer1[3] = 0x08; - - // CRC - setULong(h->crc, buffer1, ZIP_DD_OFF_CRC32); - - // Compressed size - setULong(h->szComp, buffer1, ZIP_DD_OFF_CSIZE); - - // Uncompressed size - setULong(h->szUncomp, buffer1, ZIP_DD_OFF_USIZE); - - if (device->write(buffer1, ZIP_DD_SIZE_WS) != ZIP_DD_SIZE_WS) { - return Zip::WriteFailed; - } - } - - headers->insert(entryName, h.take()); - return Zip::Ok; -} - -//! \internal -int ZipPrivate::decryptByte(quint32 key2) const -{ - quint16 temp = ((quint16)(key2) & 0xffff) | 2; - return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); -} - -//! \internal Writes an quint32 (4 bytes) to a byte array at given offset. -void ZipPrivate::setULong(quint32 v, char* buffer, unsigned int offset) -{ - buffer[offset+3] = ((v >> 24) & 0xFF); - buffer[offset+2] = ((v >> 16) & 0xFF); - buffer[offset+1] = ((v >> 8) & 0xFF); - buffer[offset] = (v & 0xFF); -} - -//! \internal Initializes decryption keys using a password. -void ZipPrivate::initKeys(quint32* keys) const -{ - // Encryption keys initialization constants are taken from the - // PKZip file format specification docs - keys[0] = 305419896L; - keys[1] = 591751049L; - keys[2] = 878082192L; - - QByteArray pwdBytes = password.toLatin1(); - int sz = pwdBytes.size(); - const char* ascii = pwdBytes.data(); - - for (int i = 0; i < sz; ++i) - updateKeys(keys, (int)ascii[i]); -} - -//! Updates a one-char-only CRC; it's the Info-Zip macro re-adapted. -quint32 ZipPrivate::updateChecksum(const quint32& crc, const quint32& val) const -{ - return quint32(crcTable[quint32(crc^val) & 0xff] ^ crc_t(crc >> 8)); -} - -//! \internal Updates encryption keys. -void ZipPrivate::updateKeys(quint32* keys, int c) const -{ - keys[0] = updateChecksum(keys[0], c); - keys[1] += keys[0] & 0xff; - keys[1] = keys[1] * 134775813L + 1; - keys[2] = updateChecksum(keys[2], ((int)keys[1]) >> 24); -} - -//! \internal Encrypts a byte array. -void ZipPrivate::encryptBytes(quint32* keys, char* buffer, qint64 read) -{ - char t; - - for (qint64 i = 0; i < read; ++i) { - t = buffer[i]; - buffer[i] ^= decryptByte(keys[2]); - updateKeys(keys, t); - } -} - -namespace { -struct KeywordHelper { - const QString needle; - inline KeywordHelper(const QString& keyword) : needle(keyword) {} -}; - -bool operator<(const KeywordHelper& helper, const char* keyword) { - return helper.needle.compare(QLatin1String(keyword)) < 0; -} - -bool operator<(const char* keyword, const KeywordHelper& helper) { - return helper.needle.compare(QLatin1String(keyword)) > 0; -} - -bool hasExtension(const QString& ext, const char* const* map, int max) { - const char* const* start = &map[0]; - const char* const* end = &map[max - 1]; - const char* const* kw = qBinaryFind(start, end, KeywordHelper(ext)); - return kw != end; -} -} - -//! \internal Detects the best compression level for a given file extension. -Zip::CompressionLevel ZipPrivate::detectCompressionByMime(const QString& ext) -{ - // NOTE: Keep the MAX_* and the number of strings in the map up to date. - // NOTE: Alphabetically sort the strings in the map -- we use a binary search! - - // Archives or files that will hardly compress - const int MAX_EXT1 = 14; - const char* const ext1[MAX_EXT1] = { - "7z", "bin", "deb", "exe", "gz", "gz2", "jar", "rar", "rpm", "tar", "tgz", "z", "zip", - 0 // # MAX_EXT1 - }; - - // Slow or usually large files that we should not spend to much time with - const int MAX_EXT2 = 24; - const char* const ext2[MAX_EXT2] = { - "asf", - "avi", - "divx", - "doc", - "docx", - "flv", - "gif", - "iso", - "jpg", - "jpeg", - "mka", - "mkv", - "mp3", - "mp4", - "mpeg", - "mpg", - "odt", - "ogg", - "ogm", - "ra", - "rm", - "wma", - "wmv", - 0 // # MAX_EXT2 - }; - - // Files with high compression ratio - const int MAX_EXT3 = 28; - const char* const ext3[MAX_EXT3] = { - "asp", "bat", "c", "conf", "cpp", "cpp", "css", "csv", "cxx", "h", "hpp", "htm", "html", "hxx", - "ini", "js", "php", "pl", "py", "rtf", "sh", "tsv", "txt", "vb", "vbs", "xml", "xst", - 0 // # MAX_EXT3 - }; - - const char* const* map = ext1; - if (hasExtension(ext, map, MAX_EXT1)) - return Zip::Store; - - map = ext2; - if (hasExtension(ext, map, MAX_EXT2)) - return Zip::Deflate2; - - map = ext3; - if (hasExtension(ext, map, MAX_EXT3)) - return Zip::Deflate9; - - return Zip::Deflate5; -} - -/*! - Closes the current archive and writes out pending data. -*/ -Zip::ErrorCode ZipPrivate::closeArchive() -{ - if (!device) { - Q_ASSERT(!file); - return Zip::Ok; - } - - if (device != file) - disconnect(device, 0, this, 0); - - return do_closeArchive(); -} - -//! \internal -Zip::ErrorCode ZipPrivate::do_closeArchive() -{ - // Close current archive by writing out central directory - // and free up resources - - if (!device && !headers) - return Zip::Ok; - - quint32 szCentralDir = 0; - quint32 offCentralDir = device->pos(); - Zip::ErrorCode c = Zip::Ok; - - if (headers && device) { - for (QMap::ConstIterator itr = headers->constBegin(); - itr != headers->constEnd(); ++itr) { - const QString fileName = itr.key(); - const ZipEntryP* h = itr.value(); - c = writeEntry(fileName, h, szCentralDir); - } - } - - if (c == Zip::Ok) - c = writeCentralDir(offCentralDir, szCentralDir); - - if (c != Zip::Ok) { - if (file) { - file->close(); - if (!file->remove()) { - qDebug() << "Failed to delete corrupt archive."; - } - } - } - - return c; -} - -//! \internal -Zip::ErrorCode ZipPrivate::writeEntry(const QString& fileName, const ZipEntryP* h, quint32& szCentralDir) -{ - unsigned int sz; - - Q_ASSERT(h && device && headers); - - // signature - buffer1[0] = 'P'; - buffer1[1] = 'K'; - buffer1[2] = 0x01; - buffer1[3] = 0x02; - - // version made by (currently only MS-DOS/FAT - no symlinks or other stuff supported) - buffer1[ZIP_CD_OFF_MADEBY] = buffer1[ZIP_CD_OFF_MADEBY + 1] = 0; - - // version needed to extract - buffer1[ZIP_CD_OFF_VERSION] = ZIP_VERSION; - buffer1[ZIP_CD_OFF_VERSION + 1] = 0; - - // general purpose flag - buffer1[ZIP_CD_OFF_GPFLAG] = h->gpFlag[0]; - buffer1[ZIP_CD_OFF_GPFLAG + 1] = h->gpFlag[1]; - - // compression method - buffer1[ZIP_CD_OFF_CMET] = h->compMethod & 0xFF; - buffer1[ZIP_CD_OFF_CMET + 1] = (h->compMethod >> 8) & 0xFF; - - // last mod file time - buffer1[ZIP_CD_OFF_MODT] = h->modTime[0]; - buffer1[ZIP_CD_OFF_MODT + 1] = h->modTime[1]; - - // last mod file date - buffer1[ZIP_CD_OFF_MODD] = h->modDate[0]; - buffer1[ZIP_CD_OFF_MODD + 1] = h->modDate[1]; - - // crc (4bytes) [16,17,18,19] - setULong(h->crc, buffer1, ZIP_CD_OFF_CRC); - - // compressed size (4bytes: [20,21,22,23]) - setULong(h->szComp, buffer1, ZIP_CD_OFF_CSIZE); - - // uncompressed size [24,25,26,27] - setULong(h->szUncomp, buffer1, ZIP_CD_OFF_USIZE); - - // filename - QByteArray fileNameBytes = fileName.toLatin1(); - sz = fileNameBytes.size(); - buffer1[ZIP_CD_OFF_NAMELEN] = sz & 0xFF; - buffer1[ZIP_CD_OFF_NAMELEN + 1] = (sz >> 8) & 0xFF; - - // extra field length - buffer1[ZIP_CD_OFF_XLEN] = buffer1[ZIP_CD_OFF_XLEN + 1] = 0; - - // file comment length - buffer1[ZIP_CD_OFF_COMMLEN] = buffer1[ZIP_CD_OFF_COMMLEN + 1] = 0; - - // disk number start - buffer1[ZIP_CD_OFF_DISKSTART] = buffer1[ZIP_CD_OFF_DISKSTART + 1] = 0; - - // internal file attributes - buffer1[ZIP_CD_OFF_IATTR] = buffer1[ZIP_CD_OFF_IATTR + 1] = 0; - - // external file attributes - buffer1[ZIP_CD_OFF_EATTR] = - buffer1[ZIP_CD_OFF_EATTR + 1] = - buffer1[ZIP_CD_OFF_EATTR + 2] = - buffer1[ZIP_CD_OFF_EATTR + 3] = 0; - - // relative offset of local header [42->45] - setULong(h->lhOffset, buffer1, ZIP_CD_OFF_LHOFF); - - if (device->write(buffer1, ZIP_CD_SIZE) != ZIP_CD_SIZE) { - return Zip::WriteFailed; - } - - // Write out filename - if ((unsigned int)device->write(fileNameBytes) != sz) { - return Zip::WriteFailed; - } - - szCentralDir += (ZIP_CD_SIZE + sz); - - return Zip::Ok; -} - -//! \internal -Zip::ErrorCode ZipPrivate::writeCentralDir(quint32 offCentralDir, quint32 szCentralDir) -{ - Q_ASSERT(device && headers); - - unsigned int sz; - - // signature - buffer1[0] = 'P'; - buffer1[1] = 'K'; - buffer1[2] = 0x05; - buffer1[3] = 0x06; - - // number of this disk - buffer1[ZIP_EOCD_OFF_DISKNUM] = buffer1[ZIP_EOCD_OFF_DISKNUM + 1] = 0; - - // number of disk with central directory - buffer1[ZIP_EOCD_OFF_CDDISKNUM] = buffer1[ZIP_EOCD_OFF_CDDISKNUM + 1] = 0; - - // number of entries in this disk - sz = headers->count(); - buffer1[ZIP_EOCD_OFF_ENTRIES] = sz & 0xFF; - buffer1[ZIP_EOCD_OFF_ENTRIES + 1] = (sz >> 8) & 0xFF; - - // total number of entries - buffer1[ZIP_EOCD_OFF_CDENTRIES] = buffer1[ZIP_EOCD_OFF_ENTRIES]; - buffer1[ZIP_EOCD_OFF_CDENTRIES + 1] = buffer1[ZIP_EOCD_OFF_ENTRIES + 1]; - - // size of central directory [12->15] - setULong(szCentralDir, buffer1, ZIP_EOCD_OFF_CDSIZE); - - // central dir offset [16->19] - setULong(offCentralDir, buffer1, ZIP_EOCD_OFF_CDOFF); - - // ZIP file comment length - QByteArray commentBytes = comment.toLatin1(); - quint16 commentLength = commentBytes.size(); - - if (commentLength == 0) { - buffer1[ZIP_EOCD_OFF_COMMLEN] = buffer1[ZIP_EOCD_OFF_COMMLEN + 1] = 0; - } else { - buffer1[ZIP_EOCD_OFF_COMMLEN] = commentLength & 0xFF; - buffer1[ZIP_EOCD_OFF_COMMLEN + 1] = (commentLength >> 8) & 0xFF; - } - - if (device->write(buffer1, ZIP_EOCD_SIZE) != ZIP_EOCD_SIZE) { - return Zip::WriteFailed; - } - - if (commentLength != 0) { - if ((unsigned int)device->write(commentBytes) != commentLength) { - return Zip::WriteFailed; - } - } - - return Zip::Ok; -} - -//! \internal -void ZipPrivate::reset() -{ - comment.clear(); - - if (headers) { - qDeleteAll(*headers); - delete headers; - headers = 0; - } - - device = 0; - - if (file) - delete file; - file = 0; -} - -//! \internal Returns the path of the parent directory -QString ZipPrivate::extractRoot(const QString& p, Zip::CompressionOptions o) -{ - Q_UNUSED(o); - QDir d(QDir::cleanPath(p)); - if (!d.exists()) - return QString(); - - if (!d.cdUp()) - return QString(); - - return d.absolutePath(); -} - - -/************************************************************************ - Public interface -*************************************************************************/ - -/*! - Creates a new Zip file compressor. -*/ -Zip::Zip() : d(new ZipPrivate) -{ -} - -/*! - Closes any open archive and releases used resources. -*/ -Zip::~Zip() -{ - closeArchive(); - delete d; -} - -/*! - Returns true if there is an open archive. -*/ -bool Zip::isOpen() const -{ - return d->device; -} - -/*! - Sets the password to be used for the next files being added! - Files added before calling this method will use the previously - set password (if any). - Closing the archive won't clear the password! -*/ -void Zip::setPassword(const QString& pwd) -{ - d->password = pwd; -} - -//! Convenience method, clears the current password. -void Zip::clearPassword() -{ - d->password.clear(); -} - -//! Returns the currently used password. -QString Zip::password() const -{ - return d->password; -} - -/*! - Attempts to create a new Zip archive. If \p overwrite is true and the file - already exist it will be overwritten. - Any open archive will be closed. - */ -Zip::ErrorCode Zip::createArchive(const QString& filename, bool overwrite) -{ - closeArchive(); - Q_ASSERT(!d->device && !d->file); - - if (filename.isEmpty()) - return Zip::FileNotFound; - - d->file = new QFile(filename); - - if (d->file->exists() && !overwrite) { - delete d->file; - d->file = 0; - return Zip::FileExists; - } - - if (!d->file->open(QIODevice::WriteOnly)) { - delete d->file; - d->file = 0; - return Zip::OpenFailed; - } - - const Zip::ErrorCode ec = createArchive(d->file); - if (ec != Zip::Ok) { - closeArchive(); - } - - return ec; -} - -/*! - Attempts to create a new Zip archive. If there is another open archive this will be closed. - \warning The class takes ownership of the device! - */ -Zip::ErrorCode Zip::createArchive(QIODevice* device) -{ - if (!device) { - qDebug() << "Invalid device."; - return Zip::OpenFailed; - } - - return d->createArchive(device); -} - -/*! - Returns the current archive comment. -*/ -QString Zip::archiveComment() const -{ - return d->comment; -} - -/*! - Sets the comment for this archive. Note: createArchive() should have been - called before. -*/ -void Zip::setArchiveComment(const QString& comment) -{ - d->comment = comment; -} - -/*! - Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) - with the Zip::IgnorePaths flag as compression option and an empty \p root parameter. - - The result is that all files found in \p path (and in subdirectories) are - added to the zip file without a directory entry. -*/ -Zip::ErrorCode Zip::addDirectoryContents(const QString& path, CompressionLevel level) -{ - return addDirectory(path, QString(), IgnorePaths, level); -} - -/*! - Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) - with the Zip::IgnorePaths flag as compression option. - - The result is that all files found in \p path (and in subdirectories) are - added to the zip file without a directory entry (or within a directory - structure specified by \p root). -*/ -Zip::ErrorCode Zip::addDirectoryContents(const QString& path, const QString& root, CompressionLevel level) -{ - return addDirectory(path, root, IgnorePaths, level); -} - -/*! - Convenience method, same as calling - Zip::addDirectory(const QString&,const QString&,CompressionLevel) - with an empty \p root parameter and Zip::RelativePaths flag as compression option. - */ -Zip::ErrorCode Zip::addDirectory(const QString& path, CompressionLevel level) -{ - return addDirectory(path, QString(), Zip::RelativePaths, level); -} - -/*! - Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) - with the Zip::RelativePaths flag as compression option. - */ -Zip::ErrorCode Zip::addDirectory(const QString& path, const QString& root, CompressionLevel level) -{ - return addDirectory(path, root, Zip::RelativePaths, level); -} - -/*! - Recursively adds files contained in \p dir to the archive, using \p root as name for the root folder. - Stops adding files if some error occurs. - - The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. - This means that the last one overwrites the previous one (if some conflict occurs), i.e. - Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. - - The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / - is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). - - If \p addedFiles is not null it is set to the number of successfully added - files. -*/ -Zip::ErrorCode Zip::addDirectory(const QString& path, const QString& root, - CompressionOptions options, CompressionLevel level, int* addedFiles) -{ - const int hierarchyLev = 0; - return d->addDirectory(path, root, options, level, hierarchyLev, addedFiles); -} - -/*! - Convenience method, same as calling Zip::addFile(const QString&,const QString&,CompressionOptions,CompressionLevel) - with an empty \p root parameter and Zip::RelativePaths as compression option. - */ -Zip::ErrorCode Zip::addFile(const QString& path, CompressionLevel level) -{ - return addFile(path, QString(), Zip::RelativePaths, level); -} - -/*! - Convenience method, same as calling Zip::addFile(const QString&,const QString&,CompressionOptions,CompressionLevel) - with the Zip::RelativePaths flag as compression option. - */ -Zip::ErrorCode Zip::addFile(const QString& path, const QString& root, - CompressionLevel level) -{ - return addFile(path, root, Zip::RelativePaths, level); -} - -/*! - Adds the file at \p path to the archive, using \p root as name for the root folder. - If \p path points to a directory the behaviour is basically the same as - addDirectory(). - - The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. - This means that the last one overwrites the previous one (if some conflict occurs), i.e. - Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. - - The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / - is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). -*/ -Zip::ErrorCode Zip::addFile(const QString& path, const QString& root, - CompressionOptions options, CompressionLevel level) -{ - if (path.isEmpty()) - return Zip::Ok; - return addFiles(QStringList() << path, root, options, level); -} - -/*! - Convenience method, same as calling Zip::addFiles(const QStringList&,const QString&,CompressionOptions,CompressionLevel) - with an empty \p root parameter and Zip::RelativePaths as compression option. - */ -Zip::ErrorCode Zip::addFiles(const QStringList& paths, CompressionLevel level) -{ - return addFiles(paths, QString(), Zip::RelativePaths, level); -} - -/*! - Convenience method, same as calling Zip::addFiles(const QStringList&,const QString&,CompressionOptions,CompressionLevel) - with the Zip::RelativePaths flag as compression option. - */ -Zip::ErrorCode Zip::addFiles(const QStringList& paths, const QString& root, - CompressionLevel level) -{ - return addFiles(paths, root, Zip::RelativePaths, level); -} - -/*! - Adds the files or directories in \p paths to the archive, using \p root as - name for the root folder. - This is similar to calling addFile or addDirectory for all the entries in - \p paths, except it is slightly faster. - - The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. - This means that the last one overwrites the previous one (if some conflict occurs), i.e. - Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. - - The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / - is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). - - If \p addedFiles is not null it is set to the number of successfully added - files. -*/ -Zip::ErrorCode Zip::addFiles(const QStringList& paths, const QString& root, - CompressionOptions options, CompressionLevel level, int* addedFiles) -{ - return d->addFiles(paths, root, options, level, addedFiles); -} - -/*! - Closes the archive and writes any pending data. -*/ -Zip::ErrorCode Zip::closeArchive() -{ - Zip::ErrorCode ec = d->closeArchive(); - d->reset(); - return ec; -} - -/*! - Returns a locale translated error string for a given error code. -*/ -QString Zip::formatError(Zip::ErrorCode c) const -{ - switch (c) - { - case Ok: return QCoreApplication::translate("Zip", "ZIP operation completed successfully."); break; - case ZlibInit: return QCoreApplication::translate("Zip", "Failed to initialize or load zlib library."); break; - case ZlibError: return QCoreApplication::translate("Zip", "zlib library error."); break; - case OpenFailed: return QCoreApplication::translate("Zip", "Unable to create or open file."); break; - case NoOpenArchive: return QCoreApplication::translate("Zip", "No archive has been created yet."); break; - case FileNotFound: return QCoreApplication::translate("Zip", "File or directory does not exist."); break; - case ReadFailed: return QCoreApplication::translate("Zip", "File read error."); break; - case WriteFailed: return QCoreApplication::translate("Zip", "File write error."); break; - case SeekFailed: return QCoreApplication::translate("Zip", "File seek error."); break; - default: ; - } - - return QCoreApplication::translate("Zip", "Unknown error."); -} - -OSDAB_END_NAMESPACE +/**************************************************************************** +** Filename: zip.cpp +** Last updated [dd/mm/yyyy]: 01/02/2007 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#include "zip.h" +#include "zip_p.h" +#include "zipentry_p.h" + +// we only use this to seed the random number generator +#include + +#include +#include +#include +#include +#include +#include +#include + +// You can remove this #include if you replace the qDebug() statements. +#include + + +/*! #define OSDAB_ZIP_NO_PNG_RLE to disable the use of Z_RLE compression strategy with + PNG files (achieves slightly better compression levels according to the authors). +*/ +// #define OSDAB_ZIP_NO_PNG_RLE + +#define OSDAB_ZIP_NO_DEBUG + +//! Local header size (including signature, excluding variable length fields) +#define ZIP_LOCAL_HEADER_SIZE 30 +//! Encryption header size +#define ZIP_LOCAL_ENC_HEADER_SIZE 12 +//! Data descriptor size (signature included) +#define ZIP_DD_SIZE_WS 16 +//! Central Directory record size (signature included) +#define ZIP_CD_SIZE 46 +//! End of Central Directory record size (signature included) +#define ZIP_EOCD_SIZE 22 + +// Some offsets inside a local header record (signature included) +#define ZIP_LH_OFF_VERS 4 +#define ZIP_LH_OFF_GPFLAG 6 +#define ZIP_LH_OFF_CMET 8 +#define ZIP_LH_OFF_MODT 10 +#define ZIP_LH_OFF_MODD 12 +#define ZIP_LH_OFF_CRC 14 +#define ZIP_LH_OFF_CSIZE 18 +#define ZIP_LH_OFF_USIZE 22 +#define ZIP_LH_OFF_NAMELEN 26 +#define ZIP_LH_OFF_XLEN 28 + +// Some offsets inside a data descriptor record (including signature) +#define ZIP_DD_OFF_CRC32 4 +#define ZIP_DD_OFF_CSIZE 8 +#define ZIP_DD_OFF_USIZE 12 + +// Some offsets inside a Central Directory record (including signature) +#define ZIP_CD_OFF_MADEBY 4 +#define ZIP_CD_OFF_VERSION 6 +#define ZIP_CD_OFF_GPFLAG 8 +#define ZIP_CD_OFF_CMET 10 +#define ZIP_CD_OFF_MODT 12 +#define ZIP_CD_OFF_MODD 14 +#define ZIP_CD_OFF_CRC 16 +#define ZIP_CD_OFF_CSIZE 20 +#define ZIP_CD_OFF_USIZE 24 +#define ZIP_CD_OFF_NAMELEN 28 +#define ZIP_CD_OFF_XLEN 30 +#define ZIP_CD_OFF_COMMLEN 32 +#define ZIP_CD_OFF_DISKSTART 34 +#define ZIP_CD_OFF_IATTR 36 +#define ZIP_CD_OFF_EATTR 38 +#define ZIP_CD_OFF_LHOFF 42 + +// Some offsets inside a EOCD record (including signature) +#define ZIP_EOCD_OFF_DISKNUM 4 +#define ZIP_EOCD_OFF_CDDISKNUM 6 +#define ZIP_EOCD_OFF_ENTRIES 8 +#define ZIP_EOCD_OFF_CDENTRIES 10 +#define ZIP_EOCD_OFF_CDSIZE 12 +#define ZIP_EOCD_OFF_CDOFF 16 +#define ZIP_EOCD_OFF_COMMLEN 20 + +//! PKZip version for archives created by this API +#define ZIP_VERSION 0x14 + +//! Do not store very small files as the compression headers overhead would be to big +#define ZIP_COMPRESSION_THRESHOLD 60 + +/*! + \class Zip zip.h + + \brief Zip file compression. + + Some quick usage examples. + + \verbatim + Suppose you have this directory structure: + + /home/user/dir1/file1.1 + /home/user/dir1/file1.2 + /home/user/dir1/dir1.1/ + /home/user/dir1/dir1.2/file1.2.1 + + EXAMPLE 1: + myZipInstance.addDirectory("/home/user/dir1"); + + RESULT: + Beheaves like any common zip software and creates a zip file with this structure: + + dir1/file1.1 + dir1/file1.2 + dir1/dir1.1/ + dir1/dir1.2/file1.2.1 + + EXAMPLE 2: + myZipInstance.addDirectory("/home/user/dir1", "myRoot/myFolder"); + + RESULT: + Adds a custom root to the paths and creates a zip file with this structure: + + myRoot/myFolder/dir1/file1.1 + myRoot/myFolder/dir1/file1.2 + myRoot/myFolder/dir1/dir1.1/ + myRoot/myFolder/dir1/dir1.2/file1.2.1 + + EXAMPLE 3: + myZipInstance.addDirectory("/home/user/dir1", Zip::AbsolutePaths); + + NOTE: + Same as calling addDirectory(SOME_PATH, PARENT_PATH_of_SOME_PATH). + + RESULT: + Preserves absolute paths and creates a zip file with this structure: + + /home/user/dir1/file1.1 + /home/user/dir1/file1.2 + /home/user/dir1/dir1.1/ + /home/user/dir1/dir1.2/file1.2.1 + + EXAMPLE 4: + myZipInstance.setPassword("hellopass"); + myZipInstance.addDirectory("/home/user/dir1", "/"); + + RESULT: + Adds and encrypts the files in /home/user/dir1, creating the following zip structure: + + /dir1/file1.1 + /dir1/file1.2 + /dir1/dir1.1/ + /dir1/dir1.2/file1.2.1 + + EXAMPLE 5: + myZipInstance.addDirectory("/home/user/dir1", Zip::IgnoreRoot); + + RESULT: + Adds the files in /home/user/dir1 but doesn't create the top level + directory: + + file1.1 + file1.2 + dir1.1/ + dir1.2/file1.2.1 + + EXAMPLE 5: + myZipInstance.addDirectory("/home/user/dir1", "data/backup", Zip::IgnoreRoot); + + RESULT: + Adds the files in /home/user/dir1 but uses "data/backup" as top level + directory instead of "dir1": + + data/backup/file1.1 + data/backup/file1.2 + data/backup/dir1.1/ + data/backup/dir1.2/file1.2.1 + + \endverbatim +*/ + +/*! \enum Zip::ErrorCode The result of a compression operation. + \value Zip::Ok No error occurred. + \value Zip::ZlibInit Failed to init or load the zlib library. + \value Zip::ZlibError The zlib library returned some error. + \value Zip::FileExists The file already exists and will not be overwritten. + \value Zip::OpenFailed Unable to create or open a device. + \value Zip::NoOpenArchive CreateArchive() has not been called yet. + \value Zip::FileNotFound File or directory does not exist. + \value Zip::ReadFailed Reading of a file failed. + \value Zip::WriteFailed Writing of a file failed. + \value Zip::SeekFailed Seek failed. +*/ + +/*! \enum Zip::CompressionLevel Returns the result of a decompression operation. + \value Zip::Store No compression. + \value Zip::Deflate1 Deflate compression level 1(lowest compression). + \value Zip::Deflate1 Deflate compression level 2. + \value Zip::Deflate1 Deflate compression level 3. + \value Zip::Deflate1 Deflate compression level 4. + \value Zip::Deflate1 Deflate compression level 5. + \value Zip::Deflate1 Deflate compression level 6. + \value Zip::Deflate1 Deflate compression level 7. + \value Zip::Deflate1 Deflate compression level 8. + \value Zip::Deflate1 Deflate compression level 9 (maximum compression). + \value Zip::AutoCPU Adapt compression level to CPU speed (faster CPU => better compression). + \value Zip::AutoMIME Adapt compression level to MIME type of the file being compressed. + \value Zip::AutoFull Use both CPU and MIME type detection. +*/ + +namespace { + +struct ZippedDir { + bool init; + QString actualRoot; + int files; + ZippedDir() : init(false), actualRoot(), files(0) {} +}; + +void checkRootPath(QString& path) +{ + const bool isUnixRoot = path.length() == 1 && path.at(0) == QLatin1Char('/'); + if (!path.isEmpty() && !isUnixRoot) { + while (path.endsWith(QLatin1String("\\"))) + path.truncate(path.length() - 1); + + int sepCount = 0; + for (int i = path.length()-1; i >= 0; --i) { + if (path.at(i) == QLatin1Char('/')) + ++sepCount; + else break; + } + + if (sepCount > 1) + path.truncate(path.length() - (sepCount-1)); + else if (sepCount == 0) + path.append(QLatin1String("/")); + } +} + +} + +////////////////////////////////////////////////////////////////////////// + +OSDAB_BEGIN_NAMESPACE(Zip) + +/************************************************************************ + Private interface +*************************************************************************/ + +//! \internal +ZipPrivate::ZipPrivate() : + headers(0), + device(0), + file(0), + uBuffer(0), + crcTable(0), + comment(), + password() +{ + // keep an unsigned pointer so we avoid to over bloat the code with casts + uBuffer = (unsigned char*) buffer1; + crcTable = get_crc_table(); +} + +//! \internal +ZipPrivate::~ZipPrivate() +{ + closeArchive(); +} + +//! \internal +Zip::ErrorCode ZipPrivate::createArchive(QIODevice* dev) +{ + Q_ASSERT(dev); + + if (device) + closeArchive(); + + device = dev; + if (device != file) + connect(device, SIGNAL(destroyed(QObject*)), this, SLOT(deviceDestroyed(QObject*))); + + if (!device->isOpen()) { + if (!device->open(QIODevice::ReadOnly)) { + delete device; + device = 0; + qDebug() << "Unable to open device for writing."; + return Zip::OpenFailed; + } + } + + headers = new QMap; + return Zip::Ok; +} + +//! \internal +void ZipPrivate::deviceDestroyed(QObject*) +{ + qDebug("Unexpected device destruction detected."); + do_closeArchive(); +} + +/*! Returns true if an entry for \p info has already been added. + Uses file size and lower case absolute path to compare entries. +*/ +bool ZipPrivate::containsEntry(const QFileInfo& info) const +{ + if (!headers || headers->isEmpty()) + return false; + + const qint64 sz = info.size(); + const QString path = info.absoluteFilePath().toLower(); + + QMap::ConstIterator b = headers->constBegin(); + const QMap::ConstIterator e = headers->constEnd(); + while (b != e) { + const ZipEntryP* e = b.value(); + if (e->fileSize == sz && e->absolutePath == path) + return true; + ++b; + } + + return false; +} + +//! \internal Actual implementation of the addDirectory* methods. +Zip::ErrorCode ZipPrivate::addDirectory(const QString& path, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, int hierarchyLevel, + int* addedFiles) +{ + if (addedFiles) + ++(*addedFiles); + + // Bad boy didn't call createArchive() yet :) + if (!device) + return Zip::NoOpenArchive; + + QDir dir(path); + if (!dir.exists()) + return Zip::FileNotFound; + + // Remove any trailing separator + QString actualRoot = root.trimmed(); + + // Preserve Unix root but make sure the path ends only with a single + // unix like separator + ::checkRootPath(actualRoot); + + // QDir::cleanPath() fixes some issues with QDir::dirName() + QFileInfo current(QDir::cleanPath(path)); + + const bool path_absolute = options.testFlag(Zip::AbsolutePaths); + const bool path_ignore = options.testFlag(Zip::IgnorePaths); + const bool path_noroot = options.testFlag(Zip::IgnoreRoot); + + if (path_absolute && !path_ignore && !path_noroot) { + QString absolutePath = extractRoot(path, options); + if (!absolutePath.isEmpty() && absolutePath != QLatin1String("/")) + absolutePath.append(QLatin1String("/")); + actualRoot.append(absolutePath); + } + + const bool skipDirName = !hierarchyLevel && path_noroot; + if (!path_ignore && !skipDirName) { + actualRoot.append(QDir(current.absoluteFilePath()).dirName()); + actualRoot.append(QLatin1String("/")); + } + + // actualRoot now contains the path of the file relative to the zip archive + // with a trailing / + + const bool skipBad = options & Zip::SkipBadFiles; + const bool noDups = options & Zip::CheckForDuplicates; + + const QDir::Filters dir_filter = + QDir::Files | + QDir::Dirs | + QDir::NoDotAndDotDot | + QDir::NoSymLinks; + const QDir::SortFlags dir_sort = + QDir::DirsFirst; + QFileInfoList list = dir.entryInfoList(dir_filter, dir_sort); + + Zip::ErrorCode ec = Zip::Ok; + bool filesAdded = false; + + Zip::CompressionOptions recursionOptions; + if (path_ignore) + recursionOptions |= Zip::IgnorePaths; + else recursionOptions |= Zip::RelativePaths; + + for (int i = 0; i < list.size(); ++i) { + QFileInfo info = list.at(i); + const QString absPath = info.absoluteFilePath(); + if (noDups && containsEntry(info)) + continue; + if (info.isDir()) { + // Recursion + ec = addDirectory(absPath, actualRoot, recursionOptions, + level, hierarchyLevel + 1, addedFiles); + } else { + ec = createEntry(info, actualRoot, level); + if (ec == Zip::Ok) { + filesAdded = true; + if (addedFiles) + ++(*addedFiles); + } + } + + if (ec != Zip::Ok && !skipBad) { + break; + } + } + + // We need an explicit record for this dir + // Non-empty directories don't need it because they have a path component in the filename + if (!filesAdded && !path_ignore) + ec = createEntry(current, actualRoot, level); + + return ec; +} + +//! \internal Actual implementation of the addFile methods. +Zip::ErrorCode ZipPrivate::addFiles(const QStringList& files, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, + int* addedFiles) +{ + if (addedFiles) + *addedFiles = 0; + + const bool skipBad = options & Zip::SkipBadFiles; + const bool noDups = options & Zip::CheckForDuplicates; + + // Bad boy didn't call createArchive() yet :) + if (!device) + return Zip::NoOpenArchive; + + QFileInfoList paths; + paths.reserve(files.size()); + for (int i = 0; i < files.size(); ++i) { + QFileInfo info(files.at(i)); + if (noDups && (paths.contains(info) || containsEntry(info))) + continue; + if (!info.exists() || !info.isReadable()) { + if (skipBad) { + continue; + } else { + return Zip::FileNotFound; + } + } + paths.append(info); + } + + if (paths.isEmpty()) + return Zip::Ok; + + // Remove any trailing separator + QString actualRoot = root.trimmed(); + + // Preserve Unix root but make sure the path ends only with a single + // unix like separator + ::checkRootPath(actualRoot); + + const bool path_absolute = options.testFlag(Zip::AbsolutePaths); + const bool path_ignore = options.testFlag(Zip::IgnorePaths); + const bool path_noroot = options.testFlag(Zip::IgnoreRoot); + + Zip::ErrorCode ec = Zip::Ok; + QHash dirMap; + + for (int i = 0; i < paths.size(); ++i) { + const QFileInfo& info = paths.at(i); + const QString path = QFileInfo(QDir::cleanPath(info.absolutePath())).absolutePath(); + + ZippedDir& zd = dirMap[path]; + if (!zd.init) { + zd.init = true; + zd.actualRoot = actualRoot; + if (path_absolute && !path_ignore && !path_noroot) { + QString absolutePath = extractRoot(path, options); + if (!absolutePath.isEmpty() && absolutePath != QLatin1String("/")) + absolutePath.append(QLatin1String("/")); + zd.actualRoot.append(absolutePath); + } + + if (!path_ignore && !path_noroot) { + zd.actualRoot.append(QDir(path).dirName()); + zd.actualRoot.append(QLatin1String("/")); + } + } + + // zd.actualRoot now contains the path of the file relative to the zip archive + // with a trailing / + + if (info.isDir()) { + // Recursion + ec = addDirectory(info.absoluteFilePath(), actualRoot, options, + level, 1, addedFiles); + } else { + ec = createEntry(info, actualRoot, level); + if (ec == Zip::Ok) { + ++zd.files; + if (addedFiles) + ++(*addedFiles); + } + } + + if (ec != Zip::Ok && !skipBad) { + break; + } + } + + // Create explicit records for empty directories + if (!path_ignore) { + QHash::ConstIterator b = dirMap.constBegin(); + const QHash::ConstIterator e = dirMap.constEnd(); + while (b != e) { + const ZippedDir& zd = b.value(); + if (zd.files <= 0) { + ec = createEntry(b.key(), zd.actualRoot, level); + } + ++b; + } + } + + return ec; +} + +//! \internal \p file must be a file and not a directory. +Zip::ErrorCode ZipPrivate::deflateFile(const QFileInfo& fileInfo, + quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys) +{ + const QString path = fileInfo.absoluteFilePath(); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << QString("An error occurred while opening %1").arg(path); + return Zip::OpenFailed; + } + + const Zip::ErrorCode ec = (level == Zip::Store) + ? storeFile(path, file, crc, written, keys) + : compressFile(path, file, crc, written, level, keys); + + file.close(); + return ec; +} + +//! \internal +Zip::ErrorCode ZipPrivate::storeFile(const QString& path, QIODevice& file, + quint32& crc, qint64& totalWritten, quint32** keys) +{ + Q_UNUSED(path); + + qint64 read = 0; + qint64 written = 0; + + const bool encrypt = keys != 0; + + totalWritten = 0; + crc = crc32(0L, Z_NULL, 0); + + while ( (read = file.read(buffer1, ZIP_READ_BUFFER)) > 0 ) { + crc = crc32(crc, uBuffer, read); + if (encrypt) + encryptBytes(*keys, buffer1, read); + written = device->write(buffer1, read); + totalWritten += written; + if (written != read) { + return Zip::WriteFailed; + } + } + + return Zip::Ok; +} + +//! \internal +int ZipPrivate::compressionStrategy(const QString& path, QIODevice& file) const +{ + Q_UNUSED(file); + +#ifndef OSDAB_ZIP_NO_PNG_RLE + return Z_DEFAULT_STRATEGY; +#endif + const bool isPng = path.endsWith(QLatin1String("png"), Qt::CaseInsensitive); + return isPng ? Z_RLE : Z_DEFAULT_STRATEGY; +} + +//! \internal +Zip::ErrorCode ZipPrivate::compressFile(const QString& path, QIODevice& file, + quint32& crc, qint64& totalWritten, const Zip::CompressionLevel& level, quint32** keys) +{ + qint64 read = 0; + qint64 written = 0; + + qint64 totRead = 0; + qint64 toRead = file.size(); + + const bool encrypt = keys != 0; + const int strategy = compressionStrategy(path, file); + + totalWritten = 0; + crc = crc32(0L, Z_NULL, 0); + + z_stream zstr; + + // Initialize zalloc, zfree and opaque before calling the init function + zstr.zalloc = Z_NULL; + zstr.zfree = Z_NULL; + zstr.opaque = Z_NULL; + + int zret; + + // Use deflateInit2 with negative windowBits to get raw compression + if ((zret = deflateInit2_( + &zstr, + (int)level, // compression level + Z_DEFLATED, // method + -MAX_WBITS, // windowBits + 8, // memLevel + strategy, + ZLIB_VERSION, + sizeof(z_stream) + )) != Z_OK ) { + qDebug() << "Could not initialize zlib for compression"; + return Zip::ZlibError; + } + + qint64 compressed; + int flush = Z_NO_FLUSH; + do { + read = file.read(buffer1, ZIP_READ_BUFFER); + totRead += read; + if (!read) + break; + + if (read < 0) { + deflateEnd(&zstr); + qDebug() << QString("Error while reading %1").arg(path); + return Zip::ReadFailed; + } + + crc = crc32(crc, uBuffer, read); + + zstr.next_in = (Bytef*) buffer1; + zstr.avail_in = (uInt)read; + + // Tell zlib if this is the last chunk we want to encode + // by setting the flush parameter to Z_FINISH + flush = (totRead == toRead) ? Z_FINISH : Z_NO_FLUSH; + + // Run deflate() on input until output buffer not full + // finish compression if all of source has been read in + do { + zstr.next_out = (Bytef*) buffer2; + zstr.avail_out = ZIP_READ_BUFFER; + + zret = deflate(&zstr, flush); + // State not clobbered + Q_ASSERT(zret != Z_STREAM_ERROR); + + // Write compressed data to file and empty buffer + compressed = ZIP_READ_BUFFER - zstr.avail_out; + + if (encrypt) + encryptBytes(*keys, buffer2, compressed); + + written = device->write(buffer2, compressed); + totalWritten += written; + + if (written != compressed) { + deflateEnd(&zstr); + qDebug() << QString("Error while writing %1").arg(path); + return Zip::WriteFailed; + } + + } while (zstr.avail_out == 0); + + // All input will be used + Q_ASSERT(zstr.avail_in == 0); + + } while (flush != Z_FINISH); + + // Stream will be complete + Q_ASSERT(zret == Z_STREAM_END); + deflateEnd(&zstr); + + return Zip::Ok; +} + +//! \internal Writes a new entry in the zip file. +Zip::ErrorCode ZipPrivate::createEntry(const QFileInfo& file, const QString& root, + Zip::CompressionLevel level) +{ + const bool dirOnly = file.isDir(); + + // entryName contains the path as it should be written + // in the zip file records + const QString entryName = dirOnly + ? root + : root + file.fileName(); + + // Directory entry + if (dirOnly || file.size() < ZIP_COMPRESSION_THRESHOLD) { + level = Zip::Store; + } else { + switch (level) { + case Zip::AutoCPU: + level = Zip::Deflate5; +#ifndef OSDAB_ZIP_NO_DEBUG + qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); +#endif + break; + case Zip::AutoMIME: + level = detectCompressionByMime(file.completeSuffix().toLower()); +#ifndef OSDAB_ZIP_NO_DEBUG + qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); +#endif + break; + case Zip::AutoFull: + level = detectCompressionByMime(file.completeSuffix().toLower()); +#ifndef OSDAB_ZIP_NO_DEBUG + qDebug("Compression level for '%s': %d", entryName.toLatin1().constData(), (int)level); +#endif + break; + default: ; + } + } + + + + // create header and store it to write a central directory later + QScopedPointer h(new ZipEntryP); + h->absolutePath = file.absoluteFilePath().toLower(); + h->fileSize = file.size(); + + // Set encryption bit and set the data descriptor bit + // so we can use mod time instead of crc for password check + bool encrypt = !dirOnly && !password.isEmpty(); + if (encrypt) + h->gpFlag[0] |= 9; + + QDateTime dt = file.lastModified(); + dt = OSDAB_ZIP_MANGLE(fromFileTimestamp)(dt); + QDate d = dt.date(); + h->modDate[1] = ((d.year() - 1980) << 1) & 254; + h->modDate[1] |= ((d.month() >> 3) & 1); + h->modDate[0] = ((d.month() & 7) << 5) & 224; + h->modDate[0] |= d.day(); + + QTime t = dt.time(); + h->modTime[1] = (t.hour() << 3) & 248; + h->modTime[1] |= ((t.minute() >> 3) & 7); + h->modTime[0] = ((t.minute() & 7) << 5) & 224; + h->modTime[0] |= t.second() / 2; + + h->szUncomp = dirOnly ? 0 : file.size(); + + h->compMethod = (level == Zip::Store) ? 0 : 0x0008; + + // **** Write local file header **** + + // signature + buffer1[0] = 'P'; buffer1[1] = 'K'; + buffer1[2] = 0x3; buffer1[3] = 0x4; + + // version needed to extract + buffer1[ZIP_LH_OFF_VERS] = ZIP_VERSION; + buffer1[ZIP_LH_OFF_VERS + 1] = 0; + + // general purpose flag + buffer1[ZIP_LH_OFF_GPFLAG] = h->gpFlag[0]; + buffer1[ZIP_LH_OFF_GPFLAG + 1] = h->gpFlag[1]; + + // compression method + buffer1[ZIP_LH_OFF_CMET] = h->compMethod & 0xFF; + buffer1[ZIP_LH_OFF_CMET + 1] = (h->compMethod>>8) & 0xFF; + + // last mod file time + buffer1[ZIP_LH_OFF_MODT] = h->modTime[0]; + buffer1[ZIP_LH_OFF_MODT + 1] = h->modTime[1]; + + // last mod file date + buffer1[ZIP_LH_OFF_MODD] = h->modDate[0]; + buffer1[ZIP_LH_OFF_MODD + 1] = h->modDate[1]; + + // skip crc (4bytes) [14,15,16,17] + + // skip compressed size but include evtl. encryption header (4bytes: [18,19,20,21]) + buffer1[ZIP_LH_OFF_CSIZE] = + buffer1[ZIP_LH_OFF_CSIZE + 1] = + buffer1[ZIP_LH_OFF_CSIZE + 2] = + buffer1[ZIP_LH_OFF_CSIZE + 3] = 0; + + h->szComp = encrypt ? ZIP_LOCAL_ENC_HEADER_SIZE : 0; + + // uncompressed size [22,23,24,25] + setULong(h->szUncomp, buffer1, ZIP_LH_OFF_USIZE); + + // filename length + QByteArray entryNameBytes = entryName.toLatin1(); + int sz = entryNameBytes.size(); + + buffer1[ZIP_LH_OFF_NAMELEN] = sz & 0xFF; + buffer1[ZIP_LH_OFF_NAMELEN + 1] = (sz >> 8) & 0xFF; + + // extra field length + buffer1[ZIP_LH_OFF_XLEN] = buffer1[ZIP_LH_OFF_XLEN + 1] = 0; + + // Store offset to write crc and compressed size + h->lhOffset = device->pos(); + quint32 crcOffset = h->lhOffset + ZIP_LH_OFF_CRC; + + if (device->write(buffer1, ZIP_LOCAL_HEADER_SIZE) != ZIP_LOCAL_HEADER_SIZE) { + return Zip::WriteFailed; + } + + // Write out filename + if (device->write(entryNameBytes) != sz) { + return Zip::WriteFailed; + } + + // Encryption keys + quint32 keys[3] = { 0, 0, 0 }; + + if (encrypt) { + // **** encryption header **** + + // XOR with PI to ensure better random numbers + // with poorly implemented rand() as suggested by Info-Zip + srand(time(NULL) ^ 3141592654UL); + int randByte; + + initKeys(keys); + for (int i = 0; i < 10; ++i) { + randByte = (rand() >> 7) & 0xff; + buffer1[i] = decryptByte(keys[2]) ^ randByte; + updateKeys(keys, randByte); + } + + // Encrypt encryption header + initKeys(keys); + for (int i = 0; i < 10; ++i) { + randByte = decryptByte(keys[2]); + updateKeys(keys, buffer1[i]); + buffer1[i] ^= randByte; + } + + // We don't know the CRC at this time, so we use the modification time + // as the last two bytes + randByte = decryptByte(keys[2]); + updateKeys(keys, h->modTime[0]); + buffer1[10] ^= randByte; + + randByte = decryptByte(keys[2]); + updateKeys(keys, h->modTime[1]); + buffer1[11] ^= randByte; + + // Write out encryption header + if (device->write(buffer1, ZIP_LOCAL_ENC_HEADER_SIZE) != ZIP_LOCAL_ENC_HEADER_SIZE) { + return Zip::WriteFailed; + } + } + + quint32 crc = 0; + qint64 written = 0; + + if (!dirOnly) { + quint32* k = keys; + const Zip::ErrorCode ec = deflateFile(file, crc, written, level, encrypt ? &k : 0); + if (ec != Zip::Ok) + return ec; + Q_ASSERT(!h.isNull()); + } + + // Store end of entry offset + quint32 current = device->pos(); + + // Update crc and compressed size in local header + if (!device->seek(crcOffset)) { + return Zip::SeekFailed; + } + + h->crc = dirOnly ? 0 : crc; + h->szComp += written; + + setULong(h->crc, buffer1, 0); + setULong(h->szComp, buffer1, 4); + if ( device->write(buffer1, 8) != 8) { + return Zip::WriteFailed; + } + + // Seek to end of entry + if (!device->seek(current)) { + return Zip::SeekFailed; + } + + if ((h->gpFlag[0] & 8) == 8) { + // Write data descriptor + + // Signature: PK\7\8 + buffer1[0] = 'P'; + buffer1[1] = 'K'; + buffer1[2] = 0x07; + buffer1[3] = 0x08; + + // CRC + setULong(h->crc, buffer1, ZIP_DD_OFF_CRC32); + + // Compressed size + setULong(h->szComp, buffer1, ZIP_DD_OFF_CSIZE); + + // Uncompressed size + setULong(h->szUncomp, buffer1, ZIP_DD_OFF_USIZE); + + if (device->write(buffer1, ZIP_DD_SIZE_WS) != ZIP_DD_SIZE_WS) { + return Zip::WriteFailed; + } + } + + headers->insert(entryName, h.take()); + return Zip::Ok; +} + +//! \internal +int ZipPrivate::decryptByte(quint32 key2) const +{ + quint16 temp = ((quint16)(key2) & 0xffff) | 2; + return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); +} + +//! \internal Writes an quint32 (4 bytes) to a byte array at given offset. +void ZipPrivate::setULong(quint32 v, char* buffer, unsigned int offset) +{ + buffer[offset+3] = ((v >> 24) & 0xFF); + buffer[offset+2] = ((v >> 16) & 0xFF); + buffer[offset+1] = ((v >> 8) & 0xFF); + buffer[offset] = (v & 0xFF); +} + +//! \internal Initializes decryption keys using a password. +void ZipPrivate::initKeys(quint32* keys) const +{ + // Encryption keys initialization constants are taken from the + // PKZip file format specification docs + keys[0] = 305419896L; + keys[1] = 591751049L; + keys[2] = 878082192L; + + QByteArray pwdBytes = password.toLatin1(); + int sz = pwdBytes.size(); + const char* ascii = pwdBytes.data(); + + for (int i = 0; i < sz; ++i) + updateKeys(keys, (int)ascii[i]); +} + +//! Updates a one-char-only CRC; it's the Info-Zip macro re-adapted. +quint32 ZipPrivate::updateChecksum(const quint32& crc, const quint32& val) const +{ + return quint32(crcTable[quint32(crc^val) & 0xff] ^ crc_t(crc >> 8)); +} + +//! \internal Updates encryption keys. +void ZipPrivate::updateKeys(quint32* keys, int c) const +{ + keys[0] = updateChecksum(keys[0], c); + keys[1] += keys[0] & 0xff; + keys[1] = keys[1] * 134775813L + 1; + keys[2] = updateChecksum(keys[2], ((int)keys[1]) >> 24); +} + +//! \internal Encrypts a byte array. +void ZipPrivate::encryptBytes(quint32* keys, char* buffer, qint64 read) +{ + char t; + + for (qint64 i = 0; i < read; ++i) { + t = buffer[i]; + buffer[i] ^= decryptByte(keys[2]); + updateKeys(keys, t); + } +} + +namespace { +struct KeywordHelper { + const QString needle; + inline KeywordHelper(const QString& keyword) : needle(keyword) {} +}; + +bool operator<(const KeywordHelper& helper, const char* keyword) { + return helper.needle.compare(QLatin1String(keyword)) < 0; +} + +bool operator<(const char* keyword, const KeywordHelper& helper) { + return helper.needle.compare(QLatin1String(keyword)) > 0; +} + +bool hasExtension(const QString& ext, const char* const* map, int max) { + const char* const* start = &map[0]; + const char* const* end = &map[max - 1]; + const char* const* kw = qBinaryFind(start, end, KeywordHelper(ext)); + return kw != end; +} +} + +//! \internal Detects the best compression level for a given file extension. +Zip::CompressionLevel ZipPrivate::detectCompressionByMime(const QString& ext) +{ + // NOTE: Keep the MAX_* and the number of strings in the map up to date. + // NOTE: Alphabetically sort the strings in the map -- we use a binary search! + + // Archives or files that will hardly compress + const int MAX_EXT1 = 14; + const char* const ext1[MAX_EXT1] = { + "7z", "bin", "deb", "exe", "gz", "gz2", "jar", "rar", "rpm", "tar", "tgz", "z", "zip", + 0 // # MAX_EXT1 + }; + + // Slow or usually large files that we should not spend to much time with + const int MAX_EXT2 = 24; + const char* const ext2[MAX_EXT2] = { + "asf", + "avi", + "divx", + "doc", + "docx", + "flv", + "gif", + "iso", + "jpg", + "jpeg", + "mka", + "mkv", + "mp3", + "mp4", + "mpeg", + "mpg", + "odt", + "ogg", + "ogm", + "ra", + "rm", + "wma", + "wmv", + 0 // # MAX_EXT2 + }; + + // Files with high compression ratio + const int MAX_EXT3 = 28; + const char* const ext3[MAX_EXT3] = { + "asp", "bat", "c", "conf", "cpp", "cpp", "css", "csv", "cxx", "h", "hpp", "htm", "html", "hxx", + "ini", "js", "php", "pl", "py", "rtf", "sh", "tsv", "txt", "vb", "vbs", "xml", "xst", + 0 // # MAX_EXT3 + }; + + const char* const* map = ext1; + if (hasExtension(ext, map, MAX_EXT1)) + return Zip::Store; + + map = ext2; + if (hasExtension(ext, map, MAX_EXT2)) + return Zip::Deflate2; + + map = ext3; + if (hasExtension(ext, map, MAX_EXT3)) + return Zip::Deflate9; + + return Zip::Deflate5; +} + +/*! + Closes the current archive and writes out pending data. +*/ +Zip::ErrorCode ZipPrivate::closeArchive() +{ + if (!device) { + Q_ASSERT(!file); + return Zip::Ok; + } + + if (device != file) + disconnect(device, 0, this, 0); + + return do_closeArchive(); +} + +//! \internal +Zip::ErrorCode ZipPrivate::do_closeArchive() +{ + // Close current archive by writing out central directory + // and free up resources + + if (!device && !headers) + return Zip::Ok; + + quint32 szCentralDir = 0; + quint32 offCentralDir = device->pos(); + Zip::ErrorCode c = Zip::Ok; + + if (headers && device) { + for (QMap::ConstIterator itr = headers->constBegin(); + itr != headers->constEnd(); ++itr) { + const QString fileName = itr.key(); + const ZipEntryP* h = itr.value(); + c = writeEntry(fileName, h, szCentralDir); + } + } + + if (c == Zip::Ok) + c = writeCentralDir(offCentralDir, szCentralDir); + + if (c != Zip::Ok) { + if (file) { + file->close(); + if (!file->remove()) { + qDebug() << "Failed to delete corrupt archive."; + } + } + } + + return c; +} + +//! \internal +Zip::ErrorCode ZipPrivate::writeEntry(const QString& fileName, const ZipEntryP* h, quint32& szCentralDir) +{ + unsigned int sz; + + Q_ASSERT(h && device && headers); + + // signature + buffer1[0] = 'P'; + buffer1[1] = 'K'; + buffer1[2] = 0x01; + buffer1[3] = 0x02; + + // version made by (currently only MS-DOS/FAT - no symlinks or other stuff supported) + buffer1[ZIP_CD_OFF_MADEBY] = buffer1[ZIP_CD_OFF_MADEBY + 1] = 0; + + // version needed to extract + buffer1[ZIP_CD_OFF_VERSION] = ZIP_VERSION; + buffer1[ZIP_CD_OFF_VERSION + 1] = 0; + + // general purpose flag + buffer1[ZIP_CD_OFF_GPFLAG] = h->gpFlag[0]; + buffer1[ZIP_CD_OFF_GPFLAG + 1] = h->gpFlag[1]; + + // compression method + buffer1[ZIP_CD_OFF_CMET] = h->compMethod & 0xFF; + buffer1[ZIP_CD_OFF_CMET + 1] = (h->compMethod >> 8) & 0xFF; + + // last mod file time + buffer1[ZIP_CD_OFF_MODT] = h->modTime[0]; + buffer1[ZIP_CD_OFF_MODT + 1] = h->modTime[1]; + + // last mod file date + buffer1[ZIP_CD_OFF_MODD] = h->modDate[0]; + buffer1[ZIP_CD_OFF_MODD + 1] = h->modDate[1]; + + // crc (4bytes) [16,17,18,19] + setULong(h->crc, buffer1, ZIP_CD_OFF_CRC); + + // compressed size (4bytes: [20,21,22,23]) + setULong(h->szComp, buffer1, ZIP_CD_OFF_CSIZE); + + // uncompressed size [24,25,26,27] + setULong(h->szUncomp, buffer1, ZIP_CD_OFF_USIZE); + + // filename + QByteArray fileNameBytes = fileName.toLatin1(); + sz = fileNameBytes.size(); + buffer1[ZIP_CD_OFF_NAMELEN] = sz & 0xFF; + buffer1[ZIP_CD_OFF_NAMELEN + 1] = (sz >> 8) & 0xFF; + + // extra field length + buffer1[ZIP_CD_OFF_XLEN] = buffer1[ZIP_CD_OFF_XLEN + 1] = 0; + + // file comment length + buffer1[ZIP_CD_OFF_COMMLEN] = buffer1[ZIP_CD_OFF_COMMLEN + 1] = 0; + + // disk number start + buffer1[ZIP_CD_OFF_DISKSTART] = buffer1[ZIP_CD_OFF_DISKSTART + 1] = 0; + + // internal file attributes + buffer1[ZIP_CD_OFF_IATTR] = buffer1[ZIP_CD_OFF_IATTR + 1] = 0; + + // external file attributes + buffer1[ZIP_CD_OFF_EATTR] = + buffer1[ZIP_CD_OFF_EATTR + 1] = + buffer1[ZIP_CD_OFF_EATTR + 2] = + buffer1[ZIP_CD_OFF_EATTR + 3] = 0; + + // relative offset of local header [42->45] + setULong(h->lhOffset, buffer1, ZIP_CD_OFF_LHOFF); + + if (device->write(buffer1, ZIP_CD_SIZE) != ZIP_CD_SIZE) { + return Zip::WriteFailed; + } + + // Write out filename + if ((unsigned int)device->write(fileNameBytes) != sz) { + return Zip::WriteFailed; + } + + szCentralDir += (ZIP_CD_SIZE + sz); + + return Zip::Ok; +} + +//! \internal +Zip::ErrorCode ZipPrivate::writeCentralDir(quint32 offCentralDir, quint32 szCentralDir) +{ + Q_ASSERT(device && headers); + + unsigned int sz; + + // signature + buffer1[0] = 'P'; + buffer1[1] = 'K'; + buffer1[2] = 0x05; + buffer1[3] = 0x06; + + // number of this disk + buffer1[ZIP_EOCD_OFF_DISKNUM] = buffer1[ZIP_EOCD_OFF_DISKNUM + 1] = 0; + + // number of disk with central directory + buffer1[ZIP_EOCD_OFF_CDDISKNUM] = buffer1[ZIP_EOCD_OFF_CDDISKNUM + 1] = 0; + + // number of entries in this disk + sz = headers->count(); + buffer1[ZIP_EOCD_OFF_ENTRIES] = sz & 0xFF; + buffer1[ZIP_EOCD_OFF_ENTRIES + 1] = (sz >> 8) & 0xFF; + + // total number of entries + buffer1[ZIP_EOCD_OFF_CDENTRIES] = buffer1[ZIP_EOCD_OFF_ENTRIES]; + buffer1[ZIP_EOCD_OFF_CDENTRIES + 1] = buffer1[ZIP_EOCD_OFF_ENTRIES + 1]; + + // size of central directory [12->15] + setULong(szCentralDir, buffer1, ZIP_EOCD_OFF_CDSIZE); + + // central dir offset [16->19] + setULong(offCentralDir, buffer1, ZIP_EOCD_OFF_CDOFF); + + // ZIP file comment length + QByteArray commentBytes = comment.toLatin1(); + quint16 commentLength = commentBytes.size(); + + if (commentLength == 0) { + buffer1[ZIP_EOCD_OFF_COMMLEN] = buffer1[ZIP_EOCD_OFF_COMMLEN + 1] = 0; + } else { + buffer1[ZIP_EOCD_OFF_COMMLEN] = commentLength & 0xFF; + buffer1[ZIP_EOCD_OFF_COMMLEN + 1] = (commentLength >> 8) & 0xFF; + } + + if (device->write(buffer1, ZIP_EOCD_SIZE) != ZIP_EOCD_SIZE) { + return Zip::WriteFailed; + } + + if (commentLength != 0) { + if ((unsigned int)device->write(commentBytes) != commentLength) { + return Zip::WriteFailed; + } + } + + return Zip::Ok; +} + +//! \internal +void ZipPrivate::reset() +{ + comment.clear(); + + if (headers) { + qDeleteAll(*headers); + delete headers; + headers = 0; + } + + device = 0; + + if (file) + delete file; + file = 0; +} + +//! \internal Returns the path of the parent directory +QString ZipPrivate::extractRoot(const QString& p, Zip::CompressionOptions o) +{ + Q_UNUSED(o); + QDir d(QDir::cleanPath(p)); + if (!d.exists()) + return QString(); + + if (!d.cdUp()) + return QString(); + + return d.absolutePath(); +} + + +/************************************************************************ + Public interface +*************************************************************************/ + +/*! + Creates a new Zip file compressor. +*/ +Zip::Zip() : d(new ZipPrivate) +{ +} + +/*! + Closes any open archive and releases used resources. +*/ +Zip::~Zip() +{ + closeArchive(); + delete d; +} + +/*! + Returns true if there is an open archive. +*/ +bool Zip::isOpen() const +{ + return d->device; +} + +/*! + Sets the password to be used for the next files being added! + Files added before calling this method will use the previously + set password (if any). + Closing the archive won't clear the password! +*/ +void Zip::setPassword(const QString& pwd) +{ + d->password = pwd; +} + +//! Convenience method, clears the current password. +void Zip::clearPassword() +{ + d->password.clear(); +} + +//! Returns the currently used password. +QString Zip::password() const +{ + return d->password; +} + +/*! + Attempts to create a new Zip archive. If \p overwrite is true and the file + already exist it will be overwritten. + Any open archive will be closed. + */ +Zip::ErrorCode Zip::createArchive(const QString& filename, bool overwrite) +{ + closeArchive(); + Q_ASSERT(!d->device && !d->file); + + if (filename.isEmpty()) + return Zip::FileNotFound; + + d->file = new QFile(filename); + + if (d->file->exists() && !overwrite) { + delete d->file; + d->file = 0; + return Zip::FileExists; + } + + if (!d->file->open(QIODevice::WriteOnly)) { + delete d->file; + d->file = 0; + return Zip::OpenFailed; + } + + const Zip::ErrorCode ec = createArchive(d->file); + if (ec != Zip::Ok) { + closeArchive(); + } + + return ec; +} + +/*! + Attempts to create a new Zip archive. If there is another open archive this will be closed. + \warning The class takes ownership of the device! + */ +Zip::ErrorCode Zip::createArchive(QIODevice* device) +{ + if (!device) { + qDebug() << "Invalid device."; + return Zip::OpenFailed; + } + + return d->createArchive(device); +} + +/*! + Returns the current archive comment. +*/ +QString Zip::archiveComment() const +{ + return d->comment; +} + +/*! + Sets the comment for this archive. Note: createArchive() should have been + called before. +*/ +void Zip::setArchiveComment(const QString& comment) +{ + d->comment = comment; +} + +/*! + Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::IgnorePaths flag as compression option and an empty \p root parameter. + + The result is that all files found in \p path (and in subdirectories) are + added to the zip file without a directory entry. +*/ +Zip::ErrorCode Zip::addDirectoryContents(const QString& path, CompressionLevel level) +{ + return addDirectory(path, QString(), IgnorePaths, level); +} + +/*! + Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::IgnorePaths flag as compression option. + + The result is that all files found in \p path (and in subdirectories) are + added to the zip file without a directory entry (or within a directory + structure specified by \p root). +*/ +Zip::ErrorCode Zip::addDirectoryContents(const QString& path, const QString& root, CompressionLevel level) +{ + return addDirectory(path, root, IgnorePaths, level); +} + +/*! + Convenience method, same as calling + Zip::addDirectory(const QString&,const QString&,CompressionLevel) + with an empty \p root parameter and Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addDirectory(const QString& path, CompressionLevel level) +{ + return addDirectory(path, QString(), Zip::RelativePaths, level); +} + +/*! + Convenience method, same as calling Zip::addDirectory(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addDirectory(const QString& path, const QString& root, CompressionLevel level) +{ + return addDirectory(path, root, Zip::RelativePaths, level); +} + +/*! + Recursively adds files contained in \p dir to the archive, using \p root as name for the root folder. + Stops adding files if some error occurs. + + The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. + This means that the last one overwrites the previous one (if some conflict occurs), i.e. + Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. + + The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / + is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). + + If \p addedFiles is not null it is set to the number of successfully added + files. +*/ +Zip::ErrorCode Zip::addDirectory(const QString& path, const QString& root, + CompressionOptions options, CompressionLevel level, int* addedFiles) +{ + const int hierarchyLev = 0; + return d->addDirectory(path, root, options, level, hierarchyLev, addedFiles); +} + +/*! + Convenience method, same as calling Zip::addFile(const QString&,const QString&,CompressionOptions,CompressionLevel) + with an empty \p root parameter and Zip::RelativePaths as compression option. + */ +Zip::ErrorCode Zip::addFile(const QString& path, CompressionLevel level) +{ + return addFile(path, QString(), Zip::RelativePaths, level); +} + +/*! + Convenience method, same as calling Zip::addFile(const QString&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addFile(const QString& path, const QString& root, + CompressionLevel level) +{ + return addFile(path, root, Zip::RelativePaths, level); +} + +/*! + Adds the file at \p path to the archive, using \p root as name for the root folder. + If \p path points to a directory the behaviour is basically the same as + addDirectory(). + + The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. + This means that the last one overwrites the previous one (if some conflict occurs), i.e. + Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. + + The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / + is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). +*/ +Zip::ErrorCode Zip::addFile(const QString& path, const QString& root, + CompressionOptions options, CompressionLevel level) +{ + if (path.isEmpty()) + return Zip::Ok; + return addFiles(QStringList() << path, root, options, level); +} + +/*! + Convenience method, same as calling Zip::addFiles(const QStringList&,const QString&,CompressionOptions,CompressionLevel) + with an empty \p root parameter and Zip::RelativePaths as compression option. + */ +Zip::ErrorCode Zip::addFiles(const QStringList& paths, CompressionLevel level) +{ + return addFiles(paths, QString(), Zip::RelativePaths, level); +} + +/*! + Convenience method, same as calling Zip::addFiles(const QStringList&,const QString&,CompressionOptions,CompressionLevel) + with the Zip::RelativePaths flag as compression option. + */ +Zip::ErrorCode Zip::addFiles(const QStringList& paths, const QString& root, + CompressionLevel level) +{ + return addFiles(paths, root, Zip::RelativePaths, level); +} + +/*! + Adds the files or directories in \p paths to the archive, using \p root as + name for the root folder. + This is similar to calling addFile or addDirectory for all the entries in + \p paths, except it is slightly faster. + + The ExtractionOptions are checked in the order they are defined in the zip.h heaser file. + This means that the last one overwrites the previous one (if some conflict occurs), i.e. + Zip::IgnorePaths | Zip::AbsolutePaths would be interpreted as Zip::IgnorePaths. + + The \p root parameter is ignored with the Zip::IgnorePaths parameter and used as path prefix (a trailing / + is always added as directory separator!) otherwise (even with Zip::AbsolutePaths set!). + + If \p addedFiles is not null it is set to the number of successfully added + files. +*/ +Zip::ErrorCode Zip::addFiles(const QStringList& paths, const QString& root, + CompressionOptions options, CompressionLevel level, int* addedFiles) +{ + return d->addFiles(paths, root, options, level, addedFiles); +} + +/*! + Closes the archive and writes any pending data. +*/ +Zip::ErrorCode Zip::closeArchive() +{ + Zip::ErrorCode ec = d->closeArchive(); + d->reset(); + return ec; +} + +/*! + Returns a locale translated error string for a given error code. +*/ +QString Zip::formatError(Zip::ErrorCode c) const +{ + switch (c) + { + case Ok: return QCoreApplication::translate("Zip", "ZIP operation completed successfully."); break; + case ZlibInit: return QCoreApplication::translate("Zip", "Failed to initialize or load zlib library."); break; + case ZlibError: return QCoreApplication::translate("Zip", "zlib library error."); break; + case OpenFailed: return QCoreApplication::translate("Zip", "Unable to create or open file."); break; + case NoOpenArchive: return QCoreApplication::translate("Zip", "No archive has been created yet."); break; + case FileNotFound: return QCoreApplication::translate("Zip", "File or directory does not exist."); break; + case ReadFailed: return QCoreApplication::translate("Zip", "File read error."); break; + case WriteFailed: return QCoreApplication::translate("Zip", "File write error."); break; + case SeekFailed: return QCoreApplication::translate("Zip", "File seek error."); break; + default: ; + } + + return QCoreApplication::translate("Zip", "Unknown error."); +} + +OSDAB_END_NAMESPACE diff --git a/oracle/src/zip/zip.h b/oracle/src/zip/zip.h index 79c512ce4..79f32ada5 100755 --- a/oracle/src/zip/zip.h +++ b/oracle/src/zip/zip.h @@ -1,158 +1,158 @@ -/**************************************************************************** -** Filename: zip.h -** Last updated [dd/mm/yyyy]: 27/03/2011 -** -** pkzip 2.0 file compression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -#ifndef OSDAB_ZIP__H -#define OSDAB_ZIP__H - -#include "zipglobal.h" - -#include -#include - -#include - -class QIODevice; -class QFile; -class QDir; -class QStringList; -class QString; - -OSDAB_BEGIN_NAMESPACE(Zip) - -class ZipPrivate; - -class OSDAB_ZIP_EXPORT Zip -{ -public: - enum ErrorCode - { - Ok, - ZlibInit, - ZlibError, - FileExists, - OpenFailed, - NoOpenArchive, - FileNotFound, - ReadFailed, - WriteFailed, - SeekFailed, - InternalError - }; - - enum CompressionLevel - { - Store, - Deflate1 = 1, Deflate2, Deflate3, Deflate4, - Deflate5, Deflate6, Deflate7, Deflate8, Deflate9, - AutoCPU, AutoMIME, AutoFull - }; - - enum CompressionOption - { - /*! Does not preserve absolute paths in the zip file when adding a - file or directory (default) */ - RelativePaths = 0x0001, - /*! Preserve absolute paths */ - AbsolutePaths = 0x0002, - /*! Do not store paths. All the files are put in the (evtl. user defined) - root of the zip file */ - IgnorePaths = 0x0004, - /*! Works only with addDirectory(). Adds the directory's contents, - including subdirectories, but does not add an entry for the root - directory itself. */ - IgnoreRoot = 0x0008, - /*! Used only when compressing a directory or multiple files. - If set invalid or unreadable files are simply skipped. - */ - SkipBadFiles = 0x0020, - /*! Makes sure a file is never added twice to the same zip archive. - This check is only necessary in certain usage scenarios and given - that it slows down processing you need to enable it explicitly with - this flag. - */ - CheckForDuplicates = 0x0040 - }; - Q_DECLARE_FLAGS(CompressionOptions, CompressionOption) - - Zip(); - virtual ~Zip(); - - bool isOpen() const; - - void setPassword(const QString& pwd); - void clearPassword(); - QString password() const; - - ErrorCode createArchive(const QString& file, bool overwrite = true); - ErrorCode createArchive(QIODevice* device); - - QString archiveComment() const; - void setArchiveComment(const QString& comment); - - ErrorCode addDirectoryContents(const QString& path, - CompressionLevel level = AutoFull); - ErrorCode addDirectoryContents(const QString& path, const QString& root, - CompressionLevel level = AutoFull); - - ErrorCode addDirectory(const QString& path, - CompressionLevel level = AutoFull); - ErrorCode addDirectory(const QString& path, const QString& root, - CompressionLevel level = AutoFull); - ErrorCode addDirectory(const QString& path, const QString& root, - CompressionOptions options, CompressionLevel level = AutoFull, - int* addedFiles = 0); - - ErrorCode addFile(const QString& path, - CompressionLevel level = AutoFull); - ErrorCode addFile(const QString& path, const QString& root, - CompressionLevel level = AutoFull); - ErrorCode addFile(const QString& path, const QString& root, - CompressionOptions options, - CompressionLevel level = AutoFull); - - ErrorCode addFiles(const QStringList& paths, - CompressionLevel level = AutoFull); - ErrorCode addFiles(const QStringList& paths, const QString& root, - CompressionLevel level = AutoFull); - ErrorCode addFiles(const QStringList& paths, const QString& root, - CompressionOptions options, - CompressionLevel level = AutoFull, - int* addedFiles = 0); - - ErrorCode closeArchive(); - - QString formatError(ErrorCode c) const; - -private: - ZipPrivate* d; -}; - -Q_DECLARE_OPERATORS_FOR_FLAGS(Zip::CompressionOptions) - -OSDAB_END_NAMESPACE - -#endif // OSDAB_ZIP__H +/**************************************************************************** +** Filename: zip.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#ifndef OSDAB_ZIP__H +#define OSDAB_ZIP__H + +#include "zipglobal.h" + +#include +#include + +#include + +class QIODevice; +class QFile; +class QDir; +class QStringList; +class QString; + +OSDAB_BEGIN_NAMESPACE(Zip) + +class ZipPrivate; + +class OSDAB_ZIP_EXPORT Zip +{ +public: + enum ErrorCode + { + Ok, + ZlibInit, + ZlibError, + FileExists, + OpenFailed, + NoOpenArchive, + FileNotFound, + ReadFailed, + WriteFailed, + SeekFailed, + InternalError + }; + + enum CompressionLevel + { + Store, + Deflate1 = 1, Deflate2, Deflate3, Deflate4, + Deflate5, Deflate6, Deflate7, Deflate8, Deflate9, + AutoCPU, AutoMIME, AutoFull + }; + + enum CompressionOption + { + /*! Does not preserve absolute paths in the zip file when adding a + file or directory (default) */ + RelativePaths = 0x0001, + /*! Preserve absolute paths */ + AbsolutePaths = 0x0002, + /*! Do not store paths. All the files are put in the (evtl. user defined) + root of the zip file */ + IgnorePaths = 0x0004, + /*! Works only with addDirectory(). Adds the directory's contents, + including subdirectories, but does not add an entry for the root + directory itself. */ + IgnoreRoot = 0x0008, + /*! Used only when compressing a directory or multiple files. + If set invalid or unreadable files are simply skipped. + */ + SkipBadFiles = 0x0020, + /*! Makes sure a file is never added twice to the same zip archive. + This check is only necessary in certain usage scenarios and given + that it slows down processing you need to enable it explicitly with + this flag. + */ + CheckForDuplicates = 0x0040 + }; + Q_DECLARE_FLAGS(CompressionOptions, CompressionOption) + + Zip(); + virtual ~Zip(); + + bool isOpen() const; + + void setPassword(const QString& pwd); + void clearPassword(); + QString password() const; + + ErrorCode createArchive(const QString& file, bool overwrite = true); + ErrorCode createArchive(QIODevice* device); + + QString archiveComment() const; + void setArchiveComment(const QString& comment); + + ErrorCode addDirectoryContents(const QString& path, + CompressionLevel level = AutoFull); + ErrorCode addDirectoryContents(const QString& path, const QString& root, + CompressionLevel level = AutoFull); + + ErrorCode addDirectory(const QString& path, + CompressionLevel level = AutoFull); + ErrorCode addDirectory(const QString& path, const QString& root, + CompressionLevel level = AutoFull); + ErrorCode addDirectory(const QString& path, const QString& root, + CompressionOptions options, CompressionLevel level = AutoFull, + int* addedFiles = 0); + + ErrorCode addFile(const QString& path, + CompressionLevel level = AutoFull); + ErrorCode addFile(const QString& path, const QString& root, + CompressionLevel level = AutoFull); + ErrorCode addFile(const QString& path, const QString& root, + CompressionOptions options, + CompressionLevel level = AutoFull); + + ErrorCode addFiles(const QStringList& paths, + CompressionLevel level = AutoFull); + ErrorCode addFiles(const QStringList& paths, const QString& root, + CompressionLevel level = AutoFull); + ErrorCode addFiles(const QStringList& paths, const QString& root, + CompressionOptions options, + CompressionLevel level = AutoFull, + int* addedFiles = 0); + + ErrorCode closeArchive(); + + QString formatError(ErrorCode c) const; + +private: + ZipPrivate* d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(Zip::CompressionOptions) + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIP__H diff --git a/oracle/src/zip/zip_p.h b/oracle/src/zip/zip_p.h index 81c2dacd0..b8ec6564b 100755 --- a/oracle/src/zip/zip_p.h +++ b/oracle/src/zip/zip_p.h @@ -1,133 +1,133 @@ -/**************************************************************************** -** Filename: zip_p.h -** Last updated [dd/mm/yyyy]: 27/03/2011 -** -** pkzip 2.0 file compression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Zip/UnZip API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -#ifndef OSDAB_ZIP_P__H -#define OSDAB_ZIP_P__H - -#include "zip.h" -#include "zipentry_p.h" - -#include -#include -#include - -#include - -/*! - zLib authors suggest using larger buffers (128K or 256K) for (de)compression (especially for inflate()) - we use a 256K buffer here - if you want to use this code on a pre-iceage mainframe please change it ;) -*/ -#define ZIP_READ_BUFFER (256*1024) - -OSDAB_BEGIN_NAMESPACE(Zip) - -class ZipPrivate : public QObject -{ - Q_OBJECT - -public: - // uLongf from zconf.h - typedef uLongf crc_t; - - ZipPrivate(); - virtual ~ZipPrivate(); - - QMap* headers; - - QIODevice* device; - QFile* file; - - char buffer1[ZIP_READ_BUFFER]; - char buffer2[ZIP_READ_BUFFER]; - - unsigned char* uBuffer; - - const crc_t* crcTable; - - QString comment; - QString password; - - Zip::ErrorCode createArchive(QIODevice* device); - Zip::ErrorCode closeArchive(); - void reset(); - - bool zLibInit(); - - bool containsEntry(const QFileInfo& info) const; - - Zip::ErrorCode addDirectory(const QString& path, const QString& root, - Zip::CompressionOptions options, Zip::CompressionLevel level, - int hierarchyLevel, int* addedFiles = 0); - Zip::ErrorCode addFiles(const QStringList& paths, const QString& root, - Zip::CompressionOptions options, Zip::CompressionLevel level, - int* addedFiles); - - Zip::ErrorCode createEntry(const QFileInfo& file, const QString& root, - Zip::CompressionLevel level); - Zip::CompressionLevel detectCompressionByMime(const QString& ext); - - inline quint32 updateChecksum(const quint32& crc, const quint32& val) const; - - inline void encryptBytes(quint32* keys, char* buffer, qint64 read); - - inline void setULong(quint32 v, char* buffer, unsigned int offset); - inline void updateKeys(quint32* keys, int c) const; - inline void initKeys(quint32* keys) const; - inline int decryptByte(quint32 key2) const; - - inline QString extractRoot(const QString& p, Zip::CompressionOptions o); - -private slots: - void deviceDestroyed(QObject*); - -private: - int compressionStrategy(const QString& path, QIODevice& file) const; - Zip::ErrorCode deflateFile(const QFileInfo& fileInfo, - quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys); - Zip::ErrorCode storeFile(const QString& path, QIODevice& file, - quint32& crc, qint64& written, quint32** keys); - Zip::ErrorCode compressFile(const QString& path, QIODevice& file, - quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys); - Zip::ErrorCode do_closeArchive(); - Zip::ErrorCode writeEntry(const QString& fileName, const ZipEntryP* h, quint32& szCentralDir); - Zip::ErrorCode writeCentralDir(quint32 offCentralDir, quint32 szCentralDir); -}; - -OSDAB_END_NAMESPACE - -#endif // OSDAB_ZIP_P__H +/**************************************************************************** +** Filename: zip_p.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Zip/UnZip API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef OSDAB_ZIP_P__H +#define OSDAB_ZIP_P__H + +#include "zip.h" +#include "zipentry_p.h" + +#include +#include +#include + +#include + +/*! + zLib authors suggest using larger buffers (128K or 256K) for (de)compression (especially for inflate()) + we use a 256K buffer here - if you want to use this code on a pre-iceage mainframe please change it ;) +*/ +#define ZIP_READ_BUFFER (256*1024) + +OSDAB_BEGIN_NAMESPACE(Zip) + +class ZipPrivate : public QObject +{ + Q_OBJECT + +public: + // uLongf from zconf.h + typedef uLongf crc_t; + + ZipPrivate(); + virtual ~ZipPrivate(); + + QMap* headers; + + QIODevice* device; + QFile* file; + + char buffer1[ZIP_READ_BUFFER]; + char buffer2[ZIP_READ_BUFFER]; + + unsigned char* uBuffer; + + const crc_t* crcTable; + + QString comment; + QString password; + + Zip::ErrorCode createArchive(QIODevice* device); + Zip::ErrorCode closeArchive(); + void reset(); + + bool zLibInit(); + + bool containsEntry(const QFileInfo& info) const; + + Zip::ErrorCode addDirectory(const QString& path, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, + int hierarchyLevel, int* addedFiles = 0); + Zip::ErrorCode addFiles(const QStringList& paths, const QString& root, + Zip::CompressionOptions options, Zip::CompressionLevel level, + int* addedFiles); + + Zip::ErrorCode createEntry(const QFileInfo& file, const QString& root, + Zip::CompressionLevel level); + Zip::CompressionLevel detectCompressionByMime(const QString& ext); + + inline quint32 updateChecksum(const quint32& crc, const quint32& val) const; + + inline void encryptBytes(quint32* keys, char* buffer, qint64 read); + + inline void setULong(quint32 v, char* buffer, unsigned int offset); + inline void updateKeys(quint32* keys, int c) const; + inline void initKeys(quint32* keys) const; + inline int decryptByte(quint32 key2) const; + + inline QString extractRoot(const QString& p, Zip::CompressionOptions o); + +private slots: + void deviceDestroyed(QObject*); + +private: + int compressionStrategy(const QString& path, QIODevice& file) const; + Zip::ErrorCode deflateFile(const QFileInfo& fileInfo, + quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys); + Zip::ErrorCode storeFile(const QString& path, QIODevice& file, + quint32& crc, qint64& written, quint32** keys); + Zip::ErrorCode compressFile(const QString& path, QIODevice& file, + quint32& crc, qint64& written, const Zip::CompressionLevel& level, quint32** keys); + Zip::ErrorCode do_closeArchive(); + Zip::ErrorCode writeEntry(const QString& fileName, const ZipEntryP* h, quint32& szCentralDir); + Zip::ErrorCode writeCentralDir(quint32 offCentralDir, quint32 szCentralDir); +}; + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIP_P__H diff --git a/oracle/src/zip/zipentry_p.h b/oracle/src/zip/zipentry_p.h index 5c355f590..f0675b6b6 100755 --- a/oracle/src/zip/zipentry_p.h +++ b/oracle/src/zip/zipentry_p.h @@ -1,91 +1,91 @@ -/**************************************************************************** -** Filename: ZipEntryP.h -** Last updated [dd/mm/yyyy]: 27/03/2011 -** -** Wrapper for a ZIP local header. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -// -// W A R N I N G -// ------------- -// -// This file is not part of the Zip/UnZip API. It exists purely as an -// implementation detail. This header file may change from version to -// version without notice, or even be removed. -// -// We mean it. -// - -#ifndef OSDAB_ZIPENTRY_P__H -#define OSDAB_ZIPENTRY_P__H - -#include -#include - -OSDAB_BEGIN_NAMESPACE(Zip) - -class ZipEntryP -{ -public: - ZipEntryP() : - lhOffset(0), - dataOffset(0), - gpFlag(), - compMethod(0), - modTime(), - modDate(), - crc(0), - szComp(0), - szUncomp(0), - absolutePath(), - fileSize(0), - lhEntryChecked(false) - { - gpFlag[0] = gpFlag[1] = 0; - modTime[0] = modTime[1] = 0; - modDate[0] = modDate[1] = 0; - } - - quint32 lhOffset; // Offset of the local header record for this entry - mutable quint32 dataOffset; // Offset of the file data for this entry - unsigned char gpFlag[2]; // General purpose flag - quint16 compMethod; // Compression method - unsigned char modTime[2]; // Last modified time - unsigned char modDate[2]; // Last modified date - quint32 crc; // CRC32 - quint32 szComp; // Compressed file size - quint32 szUncomp; // Uncompressed file size - QString comment; // File comment - - QString absolutePath; // Internal use - qint64 fileSize; // Internal use - - mutable bool lhEntryChecked; // Is true if the local header record for this entry has been parsed - - inline bool isEncrypted() const { return gpFlag[0] & 0x01; } - inline bool hasDataDescriptor() const { return gpFlag[0] & 0x08; } -}; - -OSDAB_END_NAMESPACE - -#endif // OSDAB_ZIPENTRY_P__H +/**************************************************************************** +** Filename: ZipEntryP.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** Wrapper for a ZIP local header. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Zip/UnZip API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef OSDAB_ZIPENTRY_P__H +#define OSDAB_ZIPENTRY_P__H + +#include +#include + +OSDAB_BEGIN_NAMESPACE(Zip) + +class ZipEntryP +{ +public: + ZipEntryP() : + lhOffset(0), + dataOffset(0), + gpFlag(), + compMethod(0), + modTime(), + modDate(), + crc(0), + szComp(0), + szUncomp(0), + absolutePath(), + fileSize(0), + lhEntryChecked(false) + { + gpFlag[0] = gpFlag[1] = 0; + modTime[0] = modTime[1] = 0; + modDate[0] = modDate[1] = 0; + } + + quint32 lhOffset; // Offset of the local header record for this entry + mutable quint32 dataOffset; // Offset of the file data for this entry + unsigned char gpFlag[2]; // General purpose flag + quint16 compMethod; // Compression method + unsigned char modTime[2]; // Last modified time + unsigned char modDate[2]; // Last modified date + quint32 crc; // CRC32 + quint32 szComp; // Compressed file size + quint32 szUncomp; // Uncompressed file size + QString comment; // File comment + + QString absolutePath; // Internal use + qint64 fileSize; // Internal use + + mutable bool lhEntryChecked; // Is true if the local header record for this entry has been parsed + + inline bool isEncrypted() const { return gpFlag[0] & 0x01; } + inline bool hasDataDescriptor() const { return gpFlag[0] & 0x08; } +}; + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIPENTRY_P__H diff --git a/oracle/src/zip/zipglobal.cpp b/oracle/src/zip/zipglobal.cpp index 5abf6a723..aed1ee0e7 100755 --- a/oracle/src/zip/zipglobal.cpp +++ b/oracle/src/zip/zipglobal.cpp @@ -1,152 +1,152 @@ -/**************************************************************************** -** Filename: zipglobal.cpp -** Last updated [dd/mm/yyyy]: 06/02/2011 -** -** pkzip 2.0 file compression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -#include "zipglobal.h" - -#if defined(Q_OS_WIN) || defined(Q_OS_WINCE) || defined(Q_OS_LINUX) || defined (Q_OS_MACX) -#define OSDAB_ZIP_HAS_UTC -#include -#else -#undef OSDAB_ZIP_HAS_UTC -#endif - -#if defined(Q_OS_WIN) -#include -#elif defined(Q_OS_LINUX) || defined(Q_OS_MACX) -#include -#endif - -OSDAB_BEGIN_NAMESPACE(Zip) - -/*! Returns the current UTC offset in seconds unless OSDAB_ZIP_NO_UTC is defined - and method is implemented for the current platform and 0 otherwise. -*/ -int OSDAB_ZIP_MANGLE(currentUtcOffset)() -{ -#if !(!defined OSDAB_ZIP_NO_UTC && defined OSDAB_ZIP_HAS_UTC) - return 0; -#else - time_t curr_time_t; - time(&curr_time_t); - -#if defined Q_OS_WIN - struct tm _tm_struct; - struct tm* tm_struct = &_tm_struct; -#else - struct tm* tm_struct = 0; -#endif - -#if !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) - // use the reentrant version of localtime() where available - tzset(); - tm res; - tm_struct = gmtime_r(&curr_time_t, &res); -#elif defined Q_OS_WIN && !defined Q_CC_MINGW - if (gmtime_s(tm_struct, &curr_time_t)) - return 0; -#else - tm_struct = gmtime(&curr_time_t); -#endif - - if (!tm_struct) - return 0; - - const time_t global_time_t = mktime(tm_struct); - -#if !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) - // use the reentrant version of localtime() where available - tm_struct = localtime_r(&curr_time_t, &res); -#elif defined Q_OS_WIN && !defined Q_CC_MINGW - if (localtime_s(tm_struct, &curr_time_t)) - return 0; -#else - tm_struct = localtime(&curr_time_t); -#endif - - if (!tm_struct) - return 0; - - const time_t local_time_t = mktime(tm_struct); - - const int utcOffset = - qRound(difftime(global_time_t, local_time_t)); - return tm_struct->tm_isdst > 0 ? utcOffset + 3600 : utcOffset; -#endif // No UTC - - return 0; -} - -QDateTime OSDAB_ZIP_MANGLE(fromFileTimestamp)(const QDateTime& dateTime) -{ -#if !defined OSDAB_ZIP_NO_UTC && defined OSDAB_ZIP_HAS_UTC - const int utc = OSDAB_ZIP_MANGLE(currentUtcOffset)(); - return dateTime.toUTC().addSecs(utc); -#else - return dateTime; -#endif // OSDAB_ZIP_NO_UTC -} - -bool OSDAB_ZIP_MANGLE(setFileTimestamp)(const QString& fileName, const QDateTime& dateTime) -{ - if (fileName.isEmpty()) - return true; - -#ifdef Q_OS_WIN - HANDLE hFile = CreateFileW(fileName.toStdWString().c_str(), - GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); - if (hFile == INVALID_HANDLE_VALUE) { - return false; - } - - SYSTEMTIME st; - FILETIME ft, ftLastMod; - const QDate date = dateTime.date(); - const QTime time = dateTime.time(); - st.wYear = date.year(); - st.wMonth = date.month(); - st.wDay = date.day(); - st.wHour = time.hour(); - st.wMinute = time.minute(); - st.wSecond = time.second(); - st.wMilliseconds = time.msec(); - - SystemTimeToFileTime(&st, &ft); - LocalFileTimeToFileTime(&ft, &ftLastMod); - - const bool success = SetFileTime(hFile, NULL, NULL, &ftLastMod); - CloseHandle(hFile); - return success; - -#elif defined(Q_OS_LINUX) || defined(Q_OS_MACX) - - struct utimbuf t_buffer; - t_buffer.actime = t_buffer.modtime = dateTime.toTime_t(); - return utime(fileName.toLocal8Bit().constData(), &t_buffer) == 0; -#endif - - return true; -} -OSDAB_END_NAMESPACE +/**************************************************************************** +** Filename: zipglobal.cpp +** Last updated [dd/mm/yyyy]: 06/02/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#include "zipglobal.h" + +#if defined(Q_OS_WIN) || defined(Q_OS_WINCE) || defined(Q_OS_LINUX) || defined (Q_OS_MACX) +#define OSDAB_ZIP_HAS_UTC +#include +#else +#undef OSDAB_ZIP_HAS_UTC +#endif + +#if defined(Q_OS_WIN) +#include +#elif defined(Q_OS_LINUX) || defined(Q_OS_MACX) +#include +#endif + +OSDAB_BEGIN_NAMESPACE(Zip) + +/*! Returns the current UTC offset in seconds unless OSDAB_ZIP_NO_UTC is defined + and method is implemented for the current platform and 0 otherwise. +*/ +int OSDAB_ZIP_MANGLE(currentUtcOffset)() +{ +#if !(!defined OSDAB_ZIP_NO_UTC && defined OSDAB_ZIP_HAS_UTC) + return 0; +#else + time_t curr_time_t; + time(&curr_time_t); + +#if defined Q_OS_WIN + struct tm _tm_struct; + struct tm* tm_struct = &_tm_struct; +#else + struct tm* tm_struct = 0; +#endif + +#if !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) + // use the reentrant version of localtime() where available + tzset(); + tm res; + tm_struct = gmtime_r(&curr_time_t, &res); +#elif defined Q_OS_WIN && !defined Q_CC_MINGW + if (gmtime_s(tm_struct, &curr_time_t)) + return 0; +#else + tm_struct = gmtime(&curr_time_t); +#endif + + if (!tm_struct) + return 0; + + const time_t global_time_t = mktime(tm_struct); + +#if !defined(QT_NO_THREAD) && defined(_POSIX_THREAD_SAFE_FUNCTIONS) + // use the reentrant version of localtime() where available + tm_struct = localtime_r(&curr_time_t, &res); +#elif defined Q_OS_WIN && !defined Q_CC_MINGW + if (localtime_s(tm_struct, &curr_time_t)) + return 0; +#else + tm_struct = localtime(&curr_time_t); +#endif + + if (!tm_struct) + return 0; + + const time_t local_time_t = mktime(tm_struct); + + const int utcOffset = - qRound(difftime(global_time_t, local_time_t)); + return tm_struct->tm_isdst > 0 ? utcOffset + 3600 : utcOffset; +#endif // No UTC + + return 0; +} + +QDateTime OSDAB_ZIP_MANGLE(fromFileTimestamp)(const QDateTime& dateTime) +{ +#if !defined OSDAB_ZIP_NO_UTC && defined OSDAB_ZIP_HAS_UTC + const int utc = OSDAB_ZIP_MANGLE(currentUtcOffset)(); + return dateTime.toUTC().addSecs(utc); +#else + return dateTime; +#endif // OSDAB_ZIP_NO_UTC +} + +bool OSDAB_ZIP_MANGLE(setFileTimestamp)(const QString& fileName, const QDateTime& dateTime) +{ + if (fileName.isEmpty()) + return true; + +#ifdef Q_OS_WIN + HANDLE hFile = CreateFileW(fileName.toStdWString().c_str(), + GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); + if (hFile == INVALID_HANDLE_VALUE) { + return false; + } + + SYSTEMTIME st; + FILETIME ft, ftLastMod; + const QDate date = dateTime.date(); + const QTime time = dateTime.time(); + st.wYear = date.year(); + st.wMonth = date.month(); + st.wDay = date.day(); + st.wHour = time.hour(); + st.wMinute = time.minute(); + st.wSecond = time.second(); + st.wMilliseconds = time.msec(); + + SystemTimeToFileTime(&st, &ft); + LocalFileTimeToFileTime(&ft, &ftLastMod); + + const bool success = SetFileTime(hFile, NULL, NULL, &ftLastMod); + CloseHandle(hFile); + return success; + +#elif defined(Q_OS_LINUX) || defined(Q_OS_MACX) + + struct utimbuf t_buffer; + t_buffer.actime = t_buffer.modtime = dateTime.toTime_t(); + return utime(fileName.toLocal8Bit().constData(), &t_buffer) == 0; +#endif + + return true; +} +OSDAB_END_NAMESPACE diff --git a/oracle/src/zip/zipglobal.h b/oracle/src/zip/zipglobal.h index 0b4c94f66..e7ff33105 100755 --- a/oracle/src/zip/zipglobal.h +++ b/oracle/src/zip/zipglobal.h @@ -1,77 +1,77 @@ -/**************************************************************************** -** Filename: zipglobal.h -** Last updated [dd/mm/yyyy]: 27/03/2011 -** -** pkzip 2.0 file compression. -** -** Some of the code has been inspired by other open source projects, -** (mainly Info-Zip and Gilles Vollant's minizip). -** Compression and decompression actually uses the zlib library. -** -** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. -** -** This file is part of the OSDaB project (http://osdab.42cows.org/). -** -** This file may be distributed and/or modified under the terms of the -** GNU General Public License version 2 as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL included in the -** packaging of this file. -** -** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE -** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. -** -** See the file LICENSE.GPL that came with this software distribution or -** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. -** -**********************************************************************/ - -#ifndef OSDAB_ZIPGLOBAL__H -#define OSDAB_ZIPGLOBAL__H - -#include -#include - -/* If you want to build the OSDaB Zip code as - a library, define OSDAB_ZIP_LIB in the library's .pro file and - in the libraries using it OR remove the #ifndef OSDAB_ZIP_LIB - define below and leave the #else body. Also remember to define - OSDAB_ZIP_BUILD_LIB in the library's project). -*/ - -#ifndef OSDAB_ZIP_LIB -# define OSDAB_ZIP_EXPORT -#else -# if defined(OSDAB_ZIP_BUILD_LIB) -# define OSDAB_ZIP_EXPORT Q_DECL_EXPORT -# else -# define OSDAB_ZIP_EXPORT Q_DECL_IMPORT -# endif -#endif - -#ifdef OSDAB_NAMESPACE -#define OSDAB_BEGIN_NAMESPACE(ModuleName) namespace Osdab { namespace ModuleName { -#else -#define OSDAB_BEGIN_NAMESPACE(ModuleName) -#endif - -#ifdef OSDAB_NAMESPACE -#define OSDAB_END_NAMESPACE } } -#else -#define OSDAB_END_NAMESPACE -#endif - -#ifndef OSDAB_NAMESPACE -#define OSDAB_ZIP_MANGLE(x) zip_##x -#else -#define OSDAB_ZIP_MANGLE(x) x -#endif - -OSDAB_BEGIN_NAMESPACE(Zip) - -OSDAB_ZIP_EXPORT int OSDAB_ZIP_MANGLE(currentUtcOffset)(); -OSDAB_ZIP_EXPORT QDateTime OSDAB_ZIP_MANGLE(fromFileTimestamp)(const QDateTime& dateTime); -OSDAB_ZIP_EXPORT bool OSDAB_ZIP_MANGLE(setFileTimestamp)(const QString& fileName, const QDateTime& dateTime); - -OSDAB_END_NAMESPACE - -#endif // OSDAB_ZIPGLOBAL__H +/**************************************************************************** +** Filename: zipglobal.h +** Last updated [dd/mm/yyyy]: 27/03/2011 +** +** pkzip 2.0 file compression. +** +** Some of the code has been inspired by other open source projects, +** (mainly Info-Zip and Gilles Vollant's minizip). +** Compression and decompression actually uses the zlib library. +** +** Copyright (C) 2007-2012 Angius Fabrizio. All rights reserved. +** +** This file is part of the OSDaB project (http://osdab.42cows.org/). +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See the file LICENSE.GPL that came with this software distribution or +** visit http://www.gnu.org/copyleft/gpl.html for GPL licensing information. +** +**********************************************************************/ + +#ifndef OSDAB_ZIPGLOBAL__H +#define OSDAB_ZIPGLOBAL__H + +#include +#include + +/* If you want to build the OSDaB Zip code as + a library, define OSDAB_ZIP_LIB in the library's .pro file and + in the libraries using it OR remove the #ifndef OSDAB_ZIP_LIB + define below and leave the #else body. Also remember to define + OSDAB_ZIP_BUILD_LIB in the library's project). +*/ + +#ifndef OSDAB_ZIP_LIB +# define OSDAB_ZIP_EXPORT +#else +# if defined(OSDAB_ZIP_BUILD_LIB) +# define OSDAB_ZIP_EXPORT Q_DECL_EXPORT +# else +# define OSDAB_ZIP_EXPORT Q_DECL_IMPORT +# endif +#endif + +#ifdef OSDAB_NAMESPACE +#define OSDAB_BEGIN_NAMESPACE(ModuleName) namespace Osdab { namespace ModuleName { +#else +#define OSDAB_BEGIN_NAMESPACE(ModuleName) +#endif + +#ifdef OSDAB_NAMESPACE +#define OSDAB_END_NAMESPACE } } +#else +#define OSDAB_END_NAMESPACE +#endif + +#ifndef OSDAB_NAMESPACE +#define OSDAB_ZIP_MANGLE(x) zip_##x +#else +#define OSDAB_ZIP_MANGLE(x) x +#endif + +OSDAB_BEGIN_NAMESPACE(Zip) + +OSDAB_ZIP_EXPORT int OSDAB_ZIP_MANGLE(currentUtcOffset)(); +OSDAB_ZIP_EXPORT QDateTime OSDAB_ZIP_MANGLE(fromFileTimestamp)(const QDateTime& dateTime); +OSDAB_ZIP_EXPORT bool OSDAB_ZIP_MANGLE(setFileTimestamp)(const QString& fileName, const QDateTime& dateTime); + +OSDAB_END_NAMESPACE + +#endif // OSDAB_ZIPGLOBAL__H From ce140041e353b92fd6b91dc18f21eba4b124d8ef Mon Sep 17 00:00:00 2001 From: Caledor Date: Sun, 6 Jan 2019 20:48:20 +0100 Subject: [PATCH 35/62] Switch from (s) to plural form whenever possible in message log (#3487) * Use plural form whenever possible Change for "%1 draws %2 card(s)" is self explanatory Change for "look top X" switches var %2 and %3 so i can skip %3 for singular and translate as "top card", as explained in tr comment For "counter(s)" i just moved it into %3 (each color) since plural form is already used there. * clangify * manual clangify attempt * Add missing tr * More missing tr --- cockatrice/src/dlg_connect.cpp | 6 +++--- cockatrice/src/dlg_settings.cpp | 2 +- cockatrice/src/messagelogwidget.cpp | 23 ++++++++++++----------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/cockatrice/src/dlg_connect.cpp b/cockatrice/src/dlg_connect.cpp index 84eb1dec4..4d0c8be4b 100644 --- a/cockatrice/src/dlg_connect.cpp +++ b/cockatrice/src/dlg_connect.cpp @@ -273,15 +273,15 @@ void DlgConnect::newHostSelected(bool state) previousHosts->setDisabled(true); btnRefreshServers->setDisabled(true); hostEdit->clear(); - hostEdit->setPlaceholderText("Server URL"); + hostEdit->setPlaceholderText(tr("Server URL")); hostEdit->setDisabled(false); portEdit->clear(); - portEdit->setPlaceholderText("Communication Port"); + portEdit->setPlaceholderText(tr("Communication Port")); portEdit->setDisabled(false); playernameEdit->clear(); passwordEdit->clear(); saveEdit->clear(); - saveEdit->setPlaceholderText("Unique Server Name"); + saveEdit->setPlaceholderText(tr("Unique Server Name")); saveEdit->setDisabled(false); serverContactLabel->setText(""); serverContactLink->setText(""); diff --git a/cockatrice/src/dlg_settings.cpp b/cockatrice/src/dlg_settings.cpp index 723e1128f..62473fc41 100644 --- a/cockatrice/src/dlg_settings.cpp +++ b/cockatrice/src/dlg_settings.cpp @@ -649,7 +649,7 @@ MessagesSettingsPage::MessagesSettingsPage() connect(&roomHistory, SIGNAL(stateChanged(int)), settingsCache, SLOT(setRoomHistory(int))); customAlertString = new QLineEdit(); - customAlertString->setPlaceholderText("Word1 Word2 Word3"); + customAlertString->setPlaceholderText(tr("Word1 Word2 Word3")); customAlertString->setText(settingsCache->getHighlightWords()); connect(customAlertString, SIGNAL(textChanged(QString)), settingsCache, SLOT(setHighlightWords(QString))); diff --git a/cockatrice/src/messagelogwidget.cpp b/cockatrice/src/messagelogwidget.cpp index 97b917f00..2305970a6 100644 --- a/cockatrice/src/messagelogwidget.cpp +++ b/cockatrice/src/messagelogwidget.cpp @@ -156,7 +156,7 @@ void MessageLogWidget::logAlwaysRevealTopCard(Player *player, CardZone *zone, bo void MessageLogWidget::logAttachCard(Player *player, QString cardName, Player *targetPlayer, QString targetCardName) { - appendHtmlServerMessage(QString("%1 attaches %2 to %3's %4.") + appendHtmlServerMessage(tr("%1 attaches %2 to %3's %4.") .arg(sanitizeHtml(player->getName())) .arg(cardLink(cardName)) .arg(sanitizeHtml(targetPlayer->getName())) @@ -341,7 +341,7 @@ void MessageLogWidget::logDrawCards(Player *player, int number) mulliganPlayer = player; else { soundEngine->playSound("draw_card"); - appendHtmlServerMessage(tr("%1 draws %2 card(s).") + appendHtmlServerMessage(tr("%1 draws %2 card(s).", "", number) .arg(sanitizeHtml(player->getName())) .arg("" + QString::number(number) + "")); } @@ -354,10 +354,11 @@ void MessageLogWidget::logDumpZone(Player *player, CardZone *zone, int numberCar .arg(sanitizeHtml(player->getName())) .arg(zone->getTranslatedName(zone->getPlayer() == player, CaseLookAtZone))); else - appendHtmlServerMessage(tr("%1 is looking at the top %2 card(s) %3.") - .arg(sanitizeHtml(player->getName())) - .arg("" + QString::number(numberCards) + "") - .arg(zone->getTranslatedName(zone->getPlayer() == player, CaseTopCardsOfZone))); + appendHtmlServerMessage( + tr("%1 is looking at the top %3 card(s) %2.", "top card for singular, top %3 cards for plural", numberCards) + .arg(sanitizeHtml(player->getName())) + .arg(zone->getTranslatedName(zone->getPlayer() == player, CaseTopCardsOfZone)) + .arg("" + QString::number(numberCards) + "")); } void MessageLogWidget::logFlipCard(Player *player, QString cardName, bool faceDown) @@ -626,20 +627,20 @@ void MessageLogWidget::logSetCardCounter(Player *player, QString cardName, int c QString finalStr; int delta = abs(oldValue - value); if (value > oldValue) - finalStr = tr("%1 places %2 %3 counter(s) on %4 (now %5)."); + finalStr = tr("%1 places %2 %3 on %4 (now %5)."); else - finalStr = tr("%1 removes %2 %3 counter(s) from %4 (now %5)."); + finalStr = tr("%1 removes %2 %3 from %4 (now %5)."); QString colorStr; switch (counterId) { case 0: - colorStr = tr("red", "", delta); + colorStr = tr("red counter(s)", "", delta); break; case 1: - colorStr = tr("yellow", "", delta); + colorStr = tr("yellow counter(s)", "", delta); break; case 2: - colorStr = tr("green", "", delta); + colorStr = tr("green counter(s)", "", delta); break; default:; } From 463ef13fe0f73f9b28bab6ca5bfc032a6a17ee6f Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Mon, 7 Jan 2019 15:12:27 +0100 Subject: [PATCH 36/62] Update version to 2.6.2 (#3492) ## Short roundup of the initial problem We released 2.6.2, but CMake still thinks it's 2.6.1 ## What will change with this Pull Request? Update CMake version to 2.6.2 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f55835061..48f1d7fc9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,7 @@ endif() # A project name is needed for CPack # Version can be overriden by git tags, see cmake/getversion.cmake -PROJECT("Cockatrice" VERSION 2.6.1) +PROJECT("Cockatrice" VERSION 2.6.2) # Use c++11 for all targets set(CMAKE_CXX_STANDARD 11 CACHE STRING "C++ ISO Standard") From 4eda7cda9e213a9ab9447a49bcb454da7e47d5dd Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Tue, 8 Jan 2019 03:50:36 +0100 Subject: [PATCH 37/62] Make deck editor filter translatable ; extracted new translations; Fix #3488 (#3490) * Fix #3488 ; extracted new translations * Make operators translatable, too --- cockatrice/src/cardfilter.cpp | 38 +- cockatrice/src/cardfilter.h | 9 +- cockatrice/src/filterbuilder.cpp | 2 +- cockatrice/src/filtertree.h | 22 +- cockatrice/src/filtertreemodel.cpp | 5 +- cockatrice/translations/cockatrice_en.ts | 1974 ++++++++++++---------- oracle/translations/oracle_en.ts | 185 +- 7 files changed, 1216 insertions(+), 1019 deletions(-) diff --git a/cockatrice/src/cardfilter.cpp b/cockatrice/src/cardfilter.cpp index 0ccb1086a..ce72d7946 100644 --- a/cockatrice/src/cardfilter.cpp +++ b/cockatrice/src/cardfilter.cpp @@ -1,47 +1,47 @@ #include "cardfilter.h" -const char *CardFilter::typeName(Type t) +const QString CardFilter::typeName(Type t) { switch (t) { case TypeAnd: - return "AND"; + return tr("AND", "Logical conjunction operator used in card filter"); case TypeOr: - return "OR"; + return tr("OR", "Logical disjunction operator used in card filter"); case TypeAndNot: - return "AND NOT"; + return tr("AND NOT", "Negated logical conjunction operator used in card filter"); case TypeOrNot: - return "OR NOT"; + return tr("OR NOT", "Negated logical disjunction operator used in card filter"); default: - return ""; + return QString(""); } } -const char *CardFilter::attrName(Attr a) +const QString CardFilter::attrName(Attr a) { switch (a) { case AttrName: - return "Name"; + return tr("Name"); case AttrType: - return "Type"; + return tr("Type"); case AttrColor: - return "Color"; + return tr("Color"); case AttrText: - return "Text"; + return tr("Text"); case AttrSet: - return "Set"; + return tr("Set"); case AttrManaCost: - return "Mana Cost"; + return tr("Mana Cost"); case AttrCmc: - return "CMC"; + return tr("CMC"); case AttrRarity: - return "Rarity"; + return tr("Rarity"); case AttrPow: - return "Power"; + return tr("Power"); case AttrTough: - return "Toughness"; + return tr("Toughness"); case AttrLoyalty: - return "Loyalty"; + return tr("Loyalty"); default: - return ""; + return QString(""); } } diff --git a/cockatrice/src/cardfilter.h b/cockatrice/src/cardfilter.h index b9b18b920..514ebd3e1 100644 --- a/cockatrice/src/cardfilter.h +++ b/cockatrice/src/cardfilter.h @@ -1,10 +1,13 @@ #ifndef CARDFILTER_H #define CARDFILTER_H +#include #include -class CardFilter +class CardFilter : public QObject { + Q_OBJECT + public: enum Type { @@ -54,8 +57,8 @@ public: return a; } - static const char *typeName(Type t); - static const char *attrName(Attr a); + static const QString typeName(Type t); + static const QString attrName(Attr a); }; #endif diff --git a/cockatrice/src/filterbuilder.cpp b/cockatrice/src/filterbuilder.cpp index b3275b90b..03a4632c3 100644 --- a/cockatrice/src/filterbuilder.cpp +++ b/cockatrice/src/filterbuilder.cpp @@ -12,7 +12,7 @@ FilterBuilder::FilterBuilder(QWidget *parent) : QWidget(parent) filterCombo = new QComboBox; filterCombo->setObjectName("filterCombo"); for (int i = 0; i < CardFilter::AttrEnd; i++) - filterCombo->addItem(tr(CardFilter::attrName(static_cast(i))), QVariant(i)); + filterCombo->addItem(CardFilter::attrName(static_cast(i)), QVariant(i)); typeCombo = new QComboBox; typeCombo->setObjectName("typeCombo"); diff --git a/cockatrice/src/filtertree.h b/cockatrice/src/filtertree.h index 3045d4e14..043bc95c5 100644 --- a/cockatrice/src/filtertree.h +++ b/cockatrice/src/filtertree.h @@ -54,18 +54,14 @@ public: { return (parent() != NULL) ? parent()->childIndex(this) : -1; } - virtual QString text() const + virtual const QString text() const { - return QString(textCStr()); + return QString(""); } virtual bool isLeaf() const { return false; } - virtual const char *textCStr() const - { - return ""; - } virtual void nodeChanged() const { if (parent() != NULL) @@ -126,7 +122,7 @@ public: const FilterItemList *findTypeList(CardFilter::Type type) const; FilterItemList *typeList(CardFilter::Type type); FilterTreeNode *parent() const; - const char *textCStr() const + const QString text() const { return CardFilter::attrName(attr); } @@ -154,7 +150,7 @@ public: } int termIndex(const QString &term) const; FilterTreeNode *termNode(const QString &term); - const char *textCStr() const + const QString text() const { return CardFilter::typeName(type); } @@ -190,14 +186,10 @@ public: { return p; } - QString text() const + const QString text() const { return term; } - const char *textCStr() const - { - return term.toStdString().c_str(); - } bool isLeaf() const { return true; @@ -263,9 +255,9 @@ public: FilterTreeNode *termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term); FilterTreeNode *termNode(const CardFilter *f); FilterTreeNode *attrTypeNode(CardFilter::Attr attr, CardFilter::Type type); - const char *textCStr() const + const QString text() const { - return "root"; + return QString("root"); } int index() const { diff --git a/cockatrice/src/filtertreemodel.cpp b/cockatrice/src/filtertreemodel.cpp index 29bf6b58a..f01f7b4a1 100644 --- a/cockatrice/src/filtertreemodel.cpp +++ b/cockatrice/src/filtertreemodel.cpp @@ -128,10 +128,7 @@ QVariant FilterTreeModel::data(const QModelIndex &index, int role) const case Qt::ToolTipRole: case Qt::StatusTipRole: case Qt::WhatsThisRole: - if (!node->isLeaf()) - return tr(node->textCStr()); - else - return node->text(); + return node->text(); case Qt::CheckStateRole: if (node->isEnabled()) return Qt::Checked; diff --git a/cockatrice/translations/cockatrice_en.ts b/cockatrice/translations/cockatrice_en.ts index be13c84bb..8c02bad73 100644 --- a/cockatrice/translations/cockatrice_en.ts +++ b/cockatrice/translations/cockatrice_en.ts @@ -192,22 +192,22 @@ This is only saved for moderators and cannot be seen by the banned person. BetaReleaseChannel - + Beta Releases - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the file update server. @@ -215,36 +215,118 @@ This is only saved for moderators and cannot be seen by the banned person. CardDatabaseModel - + Name - + Sets - + Mana cost - + Card type - + P/T - + Color(s)
+ + CardFilter + + + AND + Logical conjunction operator used in card filter + + + + + OR + Logical disjunction operator used in card filter + + + + + AND NOT + Negated logical conjunction operator used in card filter + + + + + OR NOT + Negated logical disjunction operator used in card filter + + + + + Name + + + + + Type + + + + + Color + + + + + Text + + + + + Set + + + + + Mana Cost + + + + + CMC + + + + + Rarity + + + + + Power + + + + + Toughness + + + + + Loyalty + + + CardFrame @@ -664,6 +746,21 @@ This is only saved for moderators and cannot be seen by the banned person.Login + + + Server URL + + + + + Communication Port + + + + + Unique Server Name + + Connection Warning @@ -708,77 +805,82 @@ This is only saved for moderators and cannot be seen by the banned person. - + + General + + + + Game type - + &Password: - + Only &buddies can join - + Only &registered users can join - + Joining restrictions - + &Spectators can watch - + Spectators &need a password to watch - + Spectators can &chat - + Spectators can see &hands - + Spectators - + &Clear - + Create game - + Game information - + Error - + Server error. @@ -1095,37 +1197,42 @@ Make sure to enable the 'Token' set in the "Manage sets" dia
- + &Creator name: - + + General + + + + &Game types - + at &least: - + at &most: - + Maximum player count - + Restrictions - + Filter games @@ -1391,12 +1498,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -1407,7 +1514,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 @@ -1418,7 +1525,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -1427,21 +1534,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 http://github.com/Cockatrice/Cockatrice/issues @@ -1450,59 +1557,59 @@ 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 - + Deck Editor - + Chat - + Sound - + Shortcuts @@ -1529,9 +1636,9 @@ Would you like to change your database location setting? DlgUpdate - - - + + + Error @@ -1567,98 +1674,98 @@ Please visit the download page to update manually. - + No Update Available - + Cockatrice is up to date! - + You are already running the latest version available in the chosen release channel. - + Current version - + Selected release channel - - - - Update Available - - - - - - A new version of Cockatrice is available! - - - - - - New version - - - Released + Update Available - Changelog + A new version of Cockatrice is available! + + + + + + New version + + Released + + + + + + Changelog + + + + Do you want to update now? - + Unfortunately there are no download packages available for your operating system. You may have to build from source yourself. - + Please check the download page manually and visit the wiki for instructions on compiling. - + An error occurred while checking for updates: - + An error occurred while downloading an update: - + Cockatrice is unable to open the installer. - + Try to update manually by closing Cockatrice and running the installer. - + Download location @@ -1673,14 +1780,14 @@ You may have to build from source yourself. - - - + + + Update Error - + Installing... @@ -2023,7 +2130,7 @@ You may have to build from source yourself. MainWindow - + The server has reached its maximum user capacity, please check back later. @@ -2054,7 +2161,7 @@ You may have to build from source yourself. - + Invalid username. @@ -2194,184 +2301,184 @@ Will now login. - - + - + - - - - + + + + - - - - - - - + + + + + + + + Error - + Server timeout - + Failed Login - + 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 ID's. 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 try closing and reopening your client and try again. If the error persists try updating your client to the most recent build and if need be contact your software provider. - + Account activation - + 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 - + 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. - + Your client seems to be missing features this server requires for connection. @@ -2386,355 +2493,360 @@ This usually means that your client version is out of date, and the server sent - + + To update your client, go to 'Help -> Check for Client Updates'. + + + + Your account has not been activated yet. You need to provide the activation token received in the activation email. - + The email address provider used during registration has been blacklisted for use on this server. - + 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. - + 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 forgot password to %1 as %2... - + &Connect... - + &Disconnect - + Start &local game... - + &Watch replay... - + &Deck editor - + &Full screen - + &Register to server... - + &Settings... - - + + &Exit - + A&ctions - + &Cockatrice - + C&ard Database - + Open custom image folder - + Open custom sets folder - + Add custom sets/cards - + Edit &tokens... - + &About Cockatrice - + &Tip of the Day - + Check for Client Updates - + View &debug log - + &Help - + Check for card updates... - + 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 - + %1 new set(s) found in the card database Set code(s): %2 Do you want to enable it/them? - + View sets - + Welcome - - - + + + Information - + A card database update is already running. - + Unable to run the card database updater: - + failed to start. - + crashed. - + timed out. - + write error. - + read error. - + unknown error. - + The card database updater exited with an error: %1 - + Update completed successfully. Cockatrice will now reload the card database. - + You can only import XML databases at this time. - - - + + + Forgot Password - + Your password has been reset successfully, you now may 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. - - - - - + + + + + Load sets/cards - + &Manage sets... - + 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. - + 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. @@ -2742,18 +2854,18 @@ To update your client, go to Help -> Check for Updates. - + Selected file cannot be found. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. @@ -2761,17 +2873,17 @@ Cockatrice will now reload the card database. MessageLogWidget - + The game has been closed. - + You have been kicked out of the game. - + %1 is now watching the game. @@ -2786,7 +2898,7 @@ Cockatrice will now reload the card database. - + The game has started. @@ -2835,9 +2947,14 @@ Cockatrice will now reload the card database. from the stack + + + %1 attaches %2 to %3's %4. + + - + a card @@ -2876,115 +2993,100 @@ Cockatrice will now reload the card database. %1 plays %2%3. + + + %1 is looking at the top %3 card(s) %2. + top card for singular, top %3 cards for plural + + + + + - + %1 turns %2 face-down. - + %1 turns %2 face-up. - + %1 has left the game (%2). - + %1 is not watching the game any more (%2). - + Untap - + Upkeep - + Draw - + First Main - + Beginning of Combat - + Declare Attackers - + Declare Blockers - + Combat Damage - + End of Combat - + Second Main - + End/Cleanup - + Unknown Phase - + %1's turn. - - - red - - - - - - - - yellow - - - - - - - - green - - - - - %1 is now keeping the top card %2 revealed. @@ -2996,32 +3098,32 @@ Cockatrice will now reload the card database. - + You are watching a replay of game #%1. - + %1 has joined the game. - + %1 is ready to start the game. - + %1 is not ready to start the game any more. - + %1 has locked their sideboard. - + %1 has unlocked their sideboard. @@ -3041,42 +3143,79 @@ Cockatrice will now reload the card database. - + + %1 places %2 %3 on %4 (now %5). + + + + + %1 removes %2 %3 from %4 (now %5). + + + + + red counter(s) + + + + + + + + yellow counter(s) + + + + + + + + green counter(s) + + + + + + + %1 shuffles %2. - + Heads - + Tails - + %1 flipped a coin. It landed as %2. - + %1 rolls a %2 with a %3-sided die. - + %1 draws %2 card(s). - + + + + - + %1 undoes their last draw. - + %1 undoes their last draw (%2). @@ -3141,12 +3280,12 @@ Cockatrice will now reload the card database. - + %1 takes a mulligan to %2. - + %1 draws their initial hand. @@ -3156,7 +3295,7 @@ Cockatrice will now reload the card database. - + %1 unattaches %2. @@ -3206,57 +3345,47 @@ Cockatrice will now reload the card database. - - %1 places %2 %3 counter(s) on %4 (now %5). - - - - - %1 removes %2 %3 counter(s) from %4 (now %5). - - - - + %1 taps their permanents. - + %1 untaps their permanents. - + %1 taps %2. - + %1 untaps %2. - + %1 sets counter %2 to %3 (%4%5). - + %1 sets %2 to not untap normally. - + %1 sets %2 to untap normally. - + %1 sets PT of %2 to %3. - + %1 sets annotation of %2 to %3. @@ -3266,52 +3395,47 @@ Cockatrice will now reload the card database. - - %1 is looking at the top %2 card(s) %3. - - - - + %1 stops looking at %2. - + %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. @@ -3319,79 +3443,90 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + + Word1 Word2 Word3 + + + + Add message - + + Message: + Edit message + + + + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered 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 @@ -3457,485 +3592,496 @@ Cockatrice will now reload the card database. Player - + Reveal top cards of library - + Number of cards: (max. %1) - + &View graveyard - + &View exile - + Player "%1" - - - - + + + + &Graveyard - - - - + + + + &Exile - + &Move hand to... - - - - + + + + &Top of library - - - - + + + + &Bottom of library - + &Move graveyard to... - - - - + + + + &Hand - + &Move exile to... - + &View library - + View &top cards of library... - + Reveal &library to... - + Reveal t&op cards to... - + &Always reveal top card - + O&pen deck in deck editor - + &View sideboard - + &Draw card - + D&raw cards... - + &Undo last draw - + Take &mulligan - + &Shuffle - + Play top card &face down - - - Move top cards to &graveyard... - - - - - Move top cards to &exile... - - - - - Put top card on &bottom - - - - - Put bottom card &in graveyard - - - - - &Reveal hand to... - - - - - Reveal r&andom card to... - - - - - Reveal random card to... - - - - - &Sideboard - - - - - &Library - - - - - &Counters - - - - - &Untap all permanents - - - R&oll die... + Move top card to grave&yard - &Create token... + Move top card to e&xile - C&reate another token + Move top cards to &graveyard... - Cr&eate predefined token + Move top cards to &exile... - S&ay + Put top card on &bottom + + + + + Put bottom card &in graveyard + + + + + &Reveal hand to... + + + + + Reveal r&andom card to... - C&ard + Reveal random card to... + + + + + &Sideboard + + + + + &Library - &All players + &Counters + + + + + &Untap all permanents - &Play + R&oll die... - &Hide + &Create token... - Play &Face Down + C&reate another token - - Toggle &normal untapping - - - - - &Peek at card face - - - - - &Clone - - - - - Attac&h to card... - - - - - Unattac&h - - - - - &Draw arrow... - - - - - &Increase power - - - - - &Decrease power - - - - - I&ncrease toughness - - - - - D&ecrease toughness - - - - - In&crease power and toughness - - - - - Dec&rease power and toughness - - - - - Set &power and toughness... - - - - - &Set annotation... - - - - - Red - - - - - Yellow - - - - - Green - - - - - X cards from the top of library... - - - - - - C&reate another %1 token - - - - - Create tokens - - - - - - - - - - Token: - - - - - Place card X cards from top of library - - - - - How many cards from the top of the deck should this card be placed: - - - - - View related cards - - - - - - Attach to - - - - - All tokens - - - - - View top cards of library + + Cr&eate predefined token + S&ay + + + + + C&ard + + + + + &All players + + + + + &Play + + + + + &Hide + + + + + Play &Face Down + + + + + Toggle &normal untapping + + + + + &Peek at card face + + + + + &Clone + + + + + Attac&h to card... + + + + + Unattac&h + + + + + &Draw arrow... + + + + + &Increase power + + + + + &Decrease power + + + + + I&ncrease toughness + + + + + D&ecrease toughness + + + + + In&crease power and toughness + + + + + Dec&rease power and toughness + + + + + Set &power and toughness... + + + + + Reset p&ower and toughness + + + + + &Set annotation... + + + + + Red + + + + + Yellow + + + + + Green + + + + + X cards from the top of library... + + + + + + C&reate another %1 token + + + + + Create tokens + + + + + + + Token: + + + + + Place card X cards from top of library + + + + + How many cards from the top of the deck should this card be placed: + + + + + View related cards + + + + + Attach to + + + + + All tokens + + + + + View top cards of library + + + + &Tap / Untap Turn sideways or back again - + T&urn Over Turn face up/face down - + &Add counter (%1) - + &Remove counter (%1) - + &Set counters (%1)... - + Number of cards: - + Draw cards - - - - - + + + + + Number: - + Move top cards to grave - + Move top cards to exile - + Roll die - + Number of sides: - + Set power/toughness - + Please enter the new PT: - + Set annotation - + Please enter the new annotation: - + Set counters @@ -3943,37 +4089,37 @@ Cockatrice will now reload the card database. QMenuBar - + Services - + Hide %1 - + Hide Others - + Show All - + Preferences... - + Quit %1 - + About %1 @@ -4002,17 +4148,17 @@ Cockatrice will now reload the card database. - + Maindeck - + Sideboard - + Tokens @@ -4109,12 +4255,12 @@ Cockatrice will now reload the card database. SequenceEdit - + Shortcut already in use - + Invalid key @@ -4150,13 +4296,13 @@ Cockatrice will now reload the card database. ShortcutsSettings - + Your configuration file contained invalid shortcuts. Please check your shortcut settings! - + The following shortcuts have been set to default: @@ -4206,27 +4352,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -4283,27 +4429,27 @@ Please check your shortcut settings! StableReleaseChannel - + Stable Releases - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the tag update server. - + Invalid reply received from the tag update server. @@ -4349,194 +4495,194 @@ Please check your shortcut settings! TabDeckEditor - + &Clear all filters - + Delete selected - + Deck &name: - + &Comments: - + Hash: - + &New deck - + &Load deck... - + &Save deck - + Save deck &as... - + Load deck from cl&ipboard... - + &Print deck... - + Search by card name - + Add to Deck - + Add to Sideboard - + Show Related cards - + Save deck to clipboard - + Annotated - + Not Annotated - + &Send deck to online service - + Create decklist (decklist.org) - + Analyze deck (deckstats.net) - + Analyze deck (tappedout.net) - + &Close - + Add card to &maindeck - + Add card to &sideboard - + &Remove row - + &Increment number - + &Decrement number - + &Deck Editor + + + + Card Info + + + + + + Deck + + - Card Info - - - - - - Deck - - - - - Filters - + &View - - - + + + Visible - - - + + + Floating - + Reset layout - + Deck: %1 @@ -4552,43 +4698,43 @@ Do you want to save the changes? - + Load deck - - - - - + + + + + Error - + The deck could not be saved. - - + + The deck could not be saved. Please check that the directory is writable and try again. - + Save deck - + There are no cards in your deck to be exported - + No deck was selected to be saved. @@ -5747,127 +5893,142 @@ 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 - + Only cards in enabled sets will appear in the deck editor card list - + Card Art - + Image priority is decided in the following order - + The - + CUSTOM Folder - + Enabled Sets (Top to Bottom) - + Disabled Sets (Top to Bottom) - Warning: + Hints - - While the set list is sorted by any of the columns, custom art priority setting is disabled. + + Note - - To disable sorting click on the same column header again until this message disappears. + + 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 - + Success - + The sets database has been saved successfully. @@ -5906,635 +6067,674 @@ Please refrain from engaging in this activity or further actions may be taken ag shortcutsTab - + Main Window - + Deck editor - + Local gameplay - + Watch replay - + Connect - + Register - + Full screen - + Settings - + Check for card updates - + Disconnect - + Exit - + Deck Editor - + Analyze deck - + Load deck (clipboard) - + Clear all filters - + New deck - + Clear selected filter - + Open custom pic folder - + Close - + Print deck - + Delete card - + Edit tokens - + Reset layout - + Add card - + Save deck - + Remove card - + Save deck as - + Load deck - - + + Counters - + Life - - - - - - - - - - - - - + + + + + + + + + + + + + Set - - - - - - - - - - - - + + + + + + + + + + + + Add - - - - - - - - - - - - + + + + + + + + + + + + Remove - + Red - + Green - + Yellow - + Storm - + W - + U - + B - + R - + G - + X - + Main Window | Deck Editor - + Power / Toughness - + Power and Toughness - + Add (+1/+1) - + Remove (-1/-1) - + + Reset + + + + Toughness - + Remove (-0/-1) - + Add (+0/+1) - + Power - + Remove (-1/-0) - + Add (+1/+0) - + Game Phases - + Untap - + Upkeep - - + Draw - + Main 1 - + Start combat - + Attack - + Block - + Damage - + End combat - + Main 2 - + End - + Next phase - + Next turn - + Playing Area - + + Move selected card to + + + + + Top cards of library + + + + + Gameplay | Draw | Move | View + + + + Manage sets - + Export deck - + Save deck (clip) - + Save deck (clip; no annotations) - + Tap / Untap Card - + Untap all - + Toggle untap - + Flip card - + Peek card - + Play card - + Attach card - + Unattach card - + Clone card - + Create token - + Create all related tokens - + Create another token - + Set annotation - + Phases | P/T | Playing Area - - Move card to - - - - + Bottom library - + Top library - - + + Graveyard - - + + Exile - + Hand - + + Play face down + + + + View - + Library - - Tops card of library - - - - + Sideboard - + Close recent view - + + Move top card to + + + + + Graveyard Once + + + + + Graveyard Multiple + + + + + Exile Once + + + + + Exile Multiple + + + + Game Lobby - + Load remote deck - + Load local deck - + Gameplay - + Draw arrow - + Leave game - + Remove local arrows - + Concede - + Roll dice - + Rotate view CW - + Shuffle library - + Rotate view CCW - + + Drawing + + + + Mulligan - + Draw card - + Draw cards - + Undo draw - + Always reveal top card - - Draw | Move | View | Gameplay - - - - + How to set custom shortcuts - + Restore all default shortcuts - + Clear all shortcuts diff --git a/oracle/translations/oracle_en.ts b/oracle/translations/oracle_en.ts index 883f95992..a65be73ad 100644 --- a/oracle/translations/oracle_en.ts +++ b/oracle/translations/oracle_en.ts @@ -4,22 +4,22 @@ IntroPage - + Introduction - + This wizard will import the list of sets, cards, and tokens that will be used by Cockatrice. - + Language: - + Version: @@ -27,122 +27,127 @@ LoadSetsPage - + Source selection - + Please specify a source for the list of sets and cards. You can specify a URL address that will be downloaded or use an existing file from your computer. - + Download URL: - + Local file: - + Restore default URL - + Choose file... - + Load sets file - - Sets JSON file (*.json *.zip) + + Sets JSON file (%1) - - Sets JSON file (*.json) - - - - - - - - - + + + + + + Error - + The provided URL is not valid. - + Downloading (0MB) - + Please choose a file. - + Cannot open file '%1'. - + Downloading (%1MB) - + Network error: %1. - + Parsing file - + + Xz extraction failed. + + + + + Sorry, this version of Oracle does not support xz compressed files. + + + + Failed to open Zip archive: %1. - + Zip extraction failed: the Zip archive doesn't contain exactly one file. - + Zip extraction failed: %1. - + Sorry, this version of Oracle does not support zipped files. - + Do you want to try to download a fresh copy of the uncompressed file instead? - + The file was retrieved successfully, but it does not contain any sets data. @@ -150,48 +155,48 @@ LoadSpoilersPage - + Downloading (%1MB) - - + + Error - + Network error: %1. - + The provided URL is not valid. - + Downloading (0MB) - + Spoilers source selection - + Please specify a spoiler source. - + Download URL: - + Restore default URL @@ -199,48 +204,48 @@ LoadTokensPage - + Tokens source selection - + Please specify a source for the list of tokens. - + Download URL: - + Restore default URL - - + + Error - + The provided URL is not valid. - + Downloading (0MB) - + Downloading (%1MB) - + Network error: %1. @@ -248,7 +253,7 @@ OracleImporter - + Dummy set containing tokens @@ -256,12 +261,12 @@ OracleWizard - + Oracle Importer - + Save @@ -269,69 +274,69 @@ SaveSetsPage - - + + Error - + No set has been imported. - + Sets imported - + The following sets has been imported. Press "Save" to save the imported cards to the Cockatrice database. - + Save to the default path (recommended) - + &Save - + Import finished: %1 cards. - + %1: %2 cards imported - + Save card database - + XML; card database (*.xml) - + Success - + The card database has been saved successfully to %1 - + The file could not be saved to %1 @@ -339,37 +344,37 @@ SaveSpoilersPage - + Spoilers imported - + The spoilers file has been imported. Press "Save" to save the imported spoilers to the Cockatrice card database. - + Save to the default path (recommended) - + Save spoiler database - + XML; card database (*.xml) - + Error - + The file could not be saved to %1 @@ -377,48 +382,48 @@ SaveTokensPage - + Tokens imported - + The tokens has been imported. Press "Save" to save the imported tokens to the Cockatrice tokens database. - + Save to the default path (recommended) - + Save token database - + XML; token database (*.xml) - + Success - + The token database has been saved successfully to %1 - + Error - + The file could not be saved to %1 From b0e643ecc0044b94a2ff2d391ed2cc92de486f3b Mon Sep 17 00:00:00 2001 From: Zach H Date: Tue, 8 Jan 2019 15:18:06 -0500 Subject: [PATCH 38/62] Several download URLs have arrived! (#3494) * Cockatrice Picture loader uses better defined URLs now URLs are defined on the Card Management tab Instead of Primary/Backup, you can now define a list of URLs List of URLs can be drag/dropped for priority ordering Oracle now uses scryfallId > mtgjsonUUID for !uuid! Signed-off-by: Zach Halpern * Simplify to QStringList and remove metacall Signed-off-by: Zach Halpern * fix issues brought up by Dae. Also fix how the defaults load to account for first time users. Signed-off-by: Zach Halpern * clangify * Fix save settings on row moved (#3495) * merge model fix, and reclangify Signed-off-by: Zach Halpern * Sources > Resources Signed-off-by: Zach Halpern --- cockatrice/CMakeLists.txt | 3 +- cockatrice/src/dlg_settings.cpp | 234 ++++++++++++------- cockatrice/src/dlg_settings.h | 26 +-- cockatrice/src/pictureloader.cpp | 104 ++++----- cockatrice/src/pictureloader.h | 35 ++- cockatrice/src/settings/downloadsettings.cpp | 56 +++++ cockatrice/src/settings/downloadsettings.h | 28 +++ cockatrice/src/settingscache.cpp | 16 +- cockatrice/src/settingscache.h | 20 +- oracle/CMakeLists.txt | 1 + oracle/src/main.cpp | 2 +- oracle/src/oracleimporter.cpp | 59 ++--- 12 files changed, 354 insertions(+), 230 deletions(-) create mode 100644 cockatrice/src/settings/downloadsettings.cpp create mode 100644 cockatrice/src/settings/downloadsettings.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 3c62074d9..38b4a3314 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -114,6 +114,7 @@ SET(cockatrice_SOURCES src/settings/messagesettings.cpp src/settings/gamefilterssettings.cpp src/settings/layoutssettings.cpp + src/settings/downloadsettings.cpp src/update_downloader.cpp src/logger.cpp src/releasechannel.cpp @@ -122,7 +123,7 @@ SET(cockatrice_SOURCES src/handle_public_servers.cpp src/carddbparser/cockatricexml3.cpp ${VERSION_STRING_CPP} -) + ) add_subdirectory(sounds) add_subdirectory(themes) diff --git a/cockatrice/src/dlg_settings.cpp b/cockatrice/src/dlg_settings.cpp index 62473fc41..f184da699 100644 --- a/cockatrice/src/dlg_settings.cpp +++ b/cockatrice/src/dlg_settings.cpp @@ -45,8 +45,6 @@ GeneralSettingsPage::GeneralSettingsPage() languageBox.setCurrentIndex(i); } - picDownloadCheckBox.setChecked(settingsCache->getPicDownload()); - // updates QList channels = settingsCache->getUpdateReleaseChannels(); foreach (ReleaseChannel *chan, channels) { @@ -64,27 +62,15 @@ GeneralSettingsPage::GeneralSettingsPage() pixmapCacheEdit.setValue(settingsCache->getPixmapCacheSize()); pixmapCacheEdit.setSuffix(" MB"); - defaultUrlEdit = new QLineEdit(settingsCache->getPicUrl()); - fallbackUrlEdit = new QLineEdit(settingsCache->getPicUrlFallback()); - showTipsOnStartup.setChecked(settingsCache->getShowTipsOnStartup()); - connect(&clearDownloadedPicsButton, SIGNAL(clicked()), this, SLOT(clearDownloadedPicsButtonClicked())); connect(&languageBox, SIGNAL(currentIndexChanged(int)), this, SLOT(languageBoxChanged(int))); - connect(&picDownloadCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setPicDownload(int))); connect(&pixmapCacheEdit, SIGNAL(valueChanged(int)), settingsCache, SLOT(setPixmapCacheSize(int))); connect(&updateReleaseChannelBox, SIGNAL(currentIndexChanged(int)), settingsCache, SLOT(setUpdateReleaseChannel(int))); connect(&updateNotificationCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setNotifyAboutUpdate(int))); - connect(&picDownloadCheckBox, SIGNAL(clicked(bool)), this, SLOT(setEnabledStatus(bool))); - connect(defaultUrlEdit, SIGNAL(textChanged(QString)), settingsCache, SLOT(setPicUrl(QString))); - connect(fallbackUrlEdit, SIGNAL(textChanged(QString)), settingsCache, SLOT(setPicUrlFallback(QString))); - connect(&defaultUrlRestoreButton, SIGNAL(clicked()), this, SLOT(defaultUrlRestoreButtonClicked())); - connect(&fallbackUrlRestoreButton, SIGNAL(clicked()), this, SLOT(fallbackUrlRestoreButtonClicked())); connect(&showTipsOnStartup, SIGNAL(clicked(bool)), settingsCache, SLOT(setShowTipsOnStartup(bool))); - setEnabledStatus(settingsCache->getPicDownload()); - auto *personalGrid = new QGridLayout; personalGrid->addWidget(&languageLabel, 0, 0); personalGrid->addWidget(&languageBox, 0, 1); @@ -94,18 +80,6 @@ GeneralSettingsPage::GeneralSettingsPage() personalGrid->addWidget(&pixmapCacheEdit, 2, 1); personalGrid->addWidget(&updateNotificationCheckBox, 3, 0); personalGrid->addWidget(&showTipsOnStartup, 4, 0); - personalGrid->addWidget(&picDownloadCheckBox, 5, 0); - personalGrid->addWidget(&urlLinkLabel, 5, 1); - personalGrid->addWidget(&defaultUrlLabel, 6, 0, 1, 1); - personalGrid->addWidget(defaultUrlEdit, 6, 1, 1, 1); - personalGrid->addWidget(&defaultUrlRestoreButton, 6, 2, 1, 1); - personalGrid->addWidget(&fallbackUrlLabel, 7, 0, 1, 1); - personalGrid->addWidget(fallbackUrlEdit, 7, 1, 1, 1); - personalGrid->addWidget(&fallbackUrlRestoreButton, 7, 2, 1, 1); - personalGrid->addWidget(&clearDownloadedPicsButton, 8, 1); - - urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse); - urlLinkLabel.setOpenExternalLinks(true); personalGroupBox = new QGroupBox; personalGroupBox->setLayout(personalGrid); @@ -194,20 +168,6 @@ QString GeneralSettingsPage::languageName(const QString &qmFile) return translator.translate("i18n", DEFAULT_LANG_NAME); } -void GeneralSettingsPage::defaultUrlRestoreButtonClicked() -{ - QString path = PIC_URL_DEFAULT; - defaultUrlEdit->setText(path); - settingsCache->setPicUrl(path); -} - -void GeneralSettingsPage::fallbackUrlRestoreButtonClicked() -{ - QString path = PIC_URL_FALLBACK; - fallbackUrlEdit->setText(path); - settingsCache->setPicUrlFallback(path); -} - void GeneralSettingsPage::deckPathButtonClicked() { QString path = QFileDialog::getExistingDirectory(this, tr("Choose path")); @@ -238,30 +198,6 @@ void GeneralSettingsPage::picsPathButtonClicked() settingsCache->setPicsPath(path); } -void GeneralSettingsPage::clearDownloadedPicsButtonClicked() -{ - QString picsPath = settingsCache->getPicsPath() + "/downloadedPics/"; - QStringList dirs = QDir(picsPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot); - bool outerSuccessRemove = true; - for (int i = 0; i < dirs.length(); i++) { - QString currentPath = picsPath + dirs.at(i) + "/"; - QStringList files = QDir(currentPath).entryList(QDir::Files); - bool innerSuccessRemove = true; - for (int j = 0; j < files.length(); j++) - if (!QDir(currentPath).remove(files.at(j))) { - qDebug() << "Failed to remove " + currentPath.toUtf8() + files.at(j).toUtf8(); - outerSuccessRemove = false; - innerSuccessRemove = false; - } - if (innerSuccessRemove) - QDir(picsPath).rmdir(dirs.at(i)); - } - if (outerSuccessRemove) - QMessageBox::information(this, tr("Success"), tr("Downloaded card pictures have been reset.")); - else - QMessageBox::critical(this, tr("Error"), tr("One or more downloaded card pictures could not be cleared.")); -} - void GeneralSettingsPage::cardDatabasePathButtonClicked() { QString path = QFileDialog::getOpenFileName(this, tr("Choose path")); @@ -291,7 +227,6 @@ void GeneralSettingsPage::retranslateUi() { personalGroupBox->setTitle(tr("Personal settings")); languageLabel.setText(tr("Language:")); - picDownloadCheckBox.setText(tr("Download card pictures on the fly")); if (settingsCache->getIsPortableBuild()) { pathsGroupBox->setTitle(tr("Paths (editing disabled in portable mode)")); @@ -305,26 +240,11 @@ void GeneralSettingsPage::retranslateUi() cardDatabasePathLabel.setText(tr("Card database:")); tokenDatabasePathLabel.setText(tr("Token database:")); pixmapCacheLabel.setText(tr("Picture cache size:")); - defaultUrlLabel.setText(tr("Primary download URL:")); - fallbackUrlLabel.setText(tr("Fallback download URL:")); - urlLinkLabel.setText( - QString("%2").arg(WIKI_CUSTOM_PIC_URL).arg(tr("How to set a custom picture url"))); - clearDownloadedPicsButton.setText(tr("Reset/clear downloaded pictures")); updateReleaseChannelLabel.setText(tr("Update channel")); updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client")); - defaultUrlRestoreButton.setText(tr("Reset")); - fallbackUrlRestoreButton.setText(tr("Reset")); showTipsOnStartup.setText(tr("Show tips on startup")); } -void GeneralSettingsPage::setEnabledStatus(bool status) -{ - defaultUrlEdit->setEnabled(status); - fallbackUrlEdit->setEnabled(status); - defaultUrlRestoreButton.setEnabled(status); - fallbackUrlRestoreButton.setEnabled(status); -} - AppearanceSettingsPage::AppearanceSettingsPage() { QString themeName = settingsCache->getThemeName(); @@ -498,6 +418,15 @@ void UserInterfaceSettingsPage::retranslateUi() DeckEditorSettingsPage::DeckEditorSettingsPage() { + picDownloadCheckBox.setChecked(settingsCache->getPicDownload()); + connect(&picDownloadCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setPicDownload(int))); + + urlLinkLabel.setTextInteractionFlags(Qt::LinksAccessibleByMouse); + urlLinkLabel.setOpenExternalLinks(true); + + connect(&clearDownloadedPicsButton, SIGNAL(clicked()), this, SLOT(clearDownloadedPicsButtonClicked())); + connect(&resetDownloadURLs, SIGNAL(clicked()), this, SLOT(resetDownloadedURLsButtonClicked())); + auto *lpGeneralGrid = new QGridLayout; auto *lpSpoilerGrid = new QGridLayout; @@ -515,9 +444,46 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() // Update the GUI depending on if the box is ticked or not setSpoilersEnabled(mcDownloadSpoilersCheckBox.isChecked()); - // Create the layout - lpGeneralGrid->addWidget(&mcGeneralMessageLabel, 0, 0); + urlList = new QListWidget; + urlList->setSelectionMode(QAbstractItemView::SingleSelection); + urlList->setAlternatingRowColors(true); + urlList->setDragEnabled(true); + urlList->setDragDropMode(QAbstractItemView::InternalMove); + connect(urlList->model(), SIGNAL(rowsMoved(const QModelIndex, int, int, const QModelIndex, int)), this, + SLOT(urlListChanged(const QModelIndex, int, int, const QModelIndex, int))); + for (int i = 0; i < settingsCache->downloads().getCount(); i++) + urlList->addItem(settingsCache->downloads().getDownloadUrlAt(i)); + + auto aAdd = new QAction(this); + aAdd->setIcon(QPixmap("theme:icons/increment")); + connect(aAdd, SIGNAL(triggered()), this, SLOT(actAddURL())); + auto aEdit = new QAction(this); + aEdit->setIcon(QPixmap("theme:icons/pencil")); + connect(aEdit, SIGNAL(triggered()), this, SLOT(actEditURL())); + auto aRemove = new QAction(this); + aRemove->setIcon(QPixmap("theme:icons/decrement")); + connect(aRemove, SIGNAL(triggered()), this, SLOT(actRemoveURL())); + + auto *messageToolBar = new QToolBar; + messageToolBar->setOrientation(Qt::Vertical); + messageToolBar->addAction(aAdd); + messageToolBar->addAction(aRemove); + messageToolBar->addAction(aEdit); + messageToolBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); + + auto *messageListLayout = new QHBoxLayout; + messageListLayout->addWidget(messageToolBar); + messageListLayout->addWidget(urlList); + + // Top Layout + lpGeneralGrid->addWidget(&picDownloadCheckBox, 0, 0); + lpGeneralGrid->addWidget(&resetDownloadURLs, 0, 1); + lpGeneralGrid->addLayout(messageListLayout, 1, 0, 1, 2); + lpGeneralGrid->addWidget(&urlLinkLabel, 2, 0); + lpGeneralGrid->addWidget(&clearDownloadedPicsButton, 2, 1); + + // Spoiler Layout lpSpoilerGrid->addWidget(&mcDownloadSpoilersCheckBox, 0, 0); lpSpoilerGrid->addWidget(&mcSpoilerSaveLabel, 1, 0); lpSpoilerGrid->addWidget(mpSpoilerSavePathLineEdit, 1, 1); @@ -543,6 +509,94 @@ DeckEditorSettingsPage::DeckEditorSettingsPage() setLayout(lpMainLayout); } +void DeckEditorSettingsPage::resetDownloadedURLsButtonClicked() +{ + settingsCache->downloads().clear(); + urlList->clear(); + urlList->addItems(settingsCache->downloads().getAllURLs()); + QMessageBox::information(this, tr("Success"), tr("Download URLs have been reset.")); +} + +void DeckEditorSettingsPage::clearDownloadedPicsButtonClicked() +{ + QString picsPath = settingsCache->getPicsPath() + "/downloadedPics/"; + QStringList dirs = QDir(picsPath).entryList(QDir::AllDirs | QDir::NoDotAndDotDot); + bool outerSuccessRemove = true; + for (const auto &dir : dirs) { + QString currentPath = picsPath + dir + "/"; + QStringList files = QDir(currentPath).entryList(QDir::Files); + bool innerSuccessRemove = true; + for (int j = 0; j < files.length(); j++) { + if (!QDir(currentPath).remove(files.at(j))) { + qInfo() << "Failed to remove " + currentPath.toUtf8() + files.at(j).toUtf8(); + outerSuccessRemove = false; + innerSuccessRemove = false; + } + qInfo() << "Removed " << currentPath << files.at(j); + } + + if (innerSuccessRemove) { + bool success = QDir(picsPath).rmdir(dir); + if (!success) { + qInfo() << "Failed to remove inner directory" << picsPath; + } else { + qInfo() << "Removed" << currentPath; + } + } + } + if (outerSuccessRemove) { + QMessageBox::information(this, tr("Success"), tr("Downloaded card pictures have been reset.")); + } else { + QMessageBox::critical(this, tr("Error"), tr("One or more downloaded card pictures could not be cleared.")); + } +} + +void DeckEditorSettingsPage::actAddURL() +{ + bool ok; + QString msg = QInputDialog::getText(this, tr("Add URL"), tr("URL:"), QLineEdit::Normal, QString(), &ok); + if (ok) { + urlList->addItem(msg); + storeSettings(); + } +} + +void DeckEditorSettingsPage::actRemoveURL() +{ + if (urlList->currentItem() != nullptr) { + delete urlList->takeItem(urlList->currentRow()); + storeSettings(); + } +} + +void DeckEditorSettingsPage::actEditURL() +{ + if (urlList->currentItem()) { + QString oldText = urlList->currentItem()->text(); + bool ok; + QString msg = QInputDialog::getText(this, tr("Edit URL"), tr("URL:"), QLineEdit::Normal, oldText, &ok); + if (ok) { + urlList->currentItem()->setText(msg); + storeSettings(); + } + } +} + +void DeckEditorSettingsPage::storeSettings() +{ + qInfo() << "URL Priority Reset"; + settingsCache->downloads().clear(); + for (int i = 0; i < urlList->count(); i++) { + qInfo() << "Priority" << i << ":" << urlList->item(i)->text(); + settingsCache->downloads().setDownloadUrlAt(i, urlList->item(i)->text()); + } +} + +void DeckEditorSettingsPage::urlListChanged(const QModelIndex &, int, int, const QModelIndex &, int) +{ + storeSettings(); +} + void DeckEditorSettingsPage::updateSpoilers() { // Disable the button so the user can only press it once at a time @@ -603,14 +657,18 @@ void DeckEditorSettingsPage::setSpoilersEnabled(bool anInput) void DeckEditorSettingsPage::retranslateUi() { + mpGeneralGroupBox->setTitle(tr("URL Download Priority")); mpSpoilerGroupBox->setTitle(tr("Spoilers")); mcDownloadSpoilersCheckBox.setText(tr("Download Spoilers Automatically")); mcSpoilerSaveLabel.setText(tr("Spoiler Location:")); - mcGeneralMessageLabel.setText(tr("Hey, something's here finally!")); lastUpdatedLabel.setText(tr("Last Updated") + ": " + getLastUpdateTime()); infoOnSpoilersLabel.setText(tr("Spoilers download automatically on launch") + "\n" + tr("Press the button to manually update without relaunching") + "\n\n" + tr("Do not close settings until manual update complete")); + picDownloadCheckBox.setText(tr("Download card pictures on the fly")); + urlLinkLabel.setText(QString("%2").arg(WIKI_CUSTOM_PIC_URL).arg(tr("How to add a custom URL"))); + clearDownloadedPicsButton.setText(tr("Delete Downloaded Images")); + resetDownloadURLs.setText(tr("Reset Download URLs")); } MessagesSettingsPage::MessagesSettingsPage() @@ -689,12 +747,16 @@ MessagesSettingsPage::MessagesSettingsPage() aAdd = new QAction(this); aAdd->setIcon(QPixmap("theme:icons/increment")); + aAdd->setStatusTip(tr("Add New URL")); + connect(aAdd, SIGNAL(triggered()), this, SLOT(actAdd())); aEdit = new QAction(this); aEdit->setIcon(QPixmap("theme:icons/pencil")); + aEdit->setStatusTip(tr("Edit URL")); connect(aEdit, SIGNAL(triggered()), this, SLOT(actEdit())); aRemove = new QAction(this); aRemove->setIcon(QPixmap("theme:icons/decrement")); + aRemove->setStatusTip(tr("Remove URL")); connect(aRemove, SIGNAL(triggered()), this, SLOT(actRemove())); auto *messageToolBar = new QToolBar; @@ -798,7 +860,7 @@ void MessagesSettingsPage::actEdit() void MessagesSettingsPage::actRemove() { - if (messageList->currentItem()) { + if (messageList->currentItem() != nullptr) { delete messageList->takeItem(messageList->currentRow()); storeSettings(); } @@ -1000,7 +1062,7 @@ void DlgSettings::setTab(int index) void DlgSettings::updateLanguage() { - qApp->removeTranslator(translator); + qApp->removeTranslator(translator); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast) installNewTranslator(); } @@ -1094,7 +1156,7 @@ void DlgSettings::retranslateUi() generalButton->setText(tr("General")); appearanceButton->setText(tr("Appearance")); userInterfaceButton->setText(tr("User Interface")); - deckEditorButton->setText(tr("Deck Editor")); + deckEditorButton->setText(tr("Card Sources")); messagesButton->setText(tr("Chat")); soundButton->setText(tr("Sound")); shortcutsButton->setText(tr("Shortcuts")); diff --git a/cockatrice/src/dlg_settings.h b/cockatrice/src/dlg_settings.h index d35add0c9..6dc44e122 100644 --- a/cockatrice/src/dlg_settings.h +++ b/cockatrice/src/dlg_settings.h @@ -43,13 +43,9 @@ private slots: void deckPathButtonClicked(); void replaysPathButtonClicked(); void picsPathButtonClicked(); - void clearDownloadedPicsButtonClicked(); void cardDatabasePathButtonClicked(); void tokenDatabasePathButtonClicked(); void languageBoxChanged(int index); - void setEnabledStatus(bool); - void defaultUrlRestoreButtonClicked(); - void fallbackUrlRestoreButtonClicked(); private: QStringList findQmFiles(); @@ -59,13 +55,10 @@ private: QLineEdit *picsPathEdit; QLineEdit *cardDatabasePathEdit; QLineEdit *tokenDatabasePathEdit; - QLineEdit *defaultUrlEdit; - QLineEdit *fallbackUrlEdit; QSpinBox pixmapCacheEdit; QGroupBox *personalGroupBox; QGroupBox *pathsGroupBox; QComboBox languageBox; - QCheckBox picDownloadCheckBox; QCheckBox updateNotificationCheckBox; QComboBox updateReleaseChannelBox; QLabel languageLabel; @@ -75,13 +68,7 @@ private: QLabel picsPathLabel; QLabel cardDatabasePathLabel; QLabel tokenDatabasePathLabel; - QLabel defaultUrlLabel; - QLabel fallbackUrlLabel; - QLabel urlLinkLabel; QLabel updateReleaseChannelLabel; - QPushButton clearDownloadedPicsButton; - QPushButton defaultUrlRestoreButton; - QPushButton fallbackUrlRestoreButton; QCheckBox showTipsOnStartup; }; @@ -143,19 +130,30 @@ public: QString getLastUpdateTime(); private slots: + void storeSettings(); + void urlListChanged(const QModelIndex &, int, int, const QModelIndex &, int); void setSpoilersEnabled(bool); void spoilerPathButtonClicked(); void updateSpoilers(); void unlockSettings(); + void actAddURL(); + void actRemoveURL(); + void actEditURL(); + void clearDownloadedPicsButtonClicked(); + void resetDownloadedURLsButtonClicked(); private: + QPushButton clearDownloadedPicsButton; + QPushButton resetDownloadURLs; + QLabel urlLinkLabel; + QCheckBox picDownloadCheckBox; + QListWidget *urlList; QCheckBox mcDownloadSpoilersCheckBox; QLabel msDownloadSpoilersLabel; QGroupBox *mpGeneralGroupBox; QGroupBox *mpSpoilerGroupBox; QLineEdit *mpSpoilerSavePathLineEdit; QLabel mcSpoilerSaveLabel; - QLabel mcGeneralMessageLabel; QLabel lastUpdatedLabel; QLabel infoOnSpoilersLabel; QPushButton *mpSpoilerPathButton; diff --git a/cockatrice/src/pictureloader.cpp b/cockatrice/src/pictureloader.cpp index 8d2c4b2c6..252fab180 100644 --- a/cockatrice/src/pictureloader.cpp +++ b/cockatrice/src/pictureloader.cpp @@ -25,35 +25,9 @@ // never cache more than 300 cards at once for a single deck #define CACHED_CARD_PER_DECK_MAX 300 -// Other URLs we can use (TODO: Make this less messy) -#define GATHERER_DEFAULT "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card" -#define GATHERER_FALLBACK "http://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card" - -class PictureToLoad::SetDownloadPriorityComparator -{ -public: - /* - * Returns true if a has higher download priority than b - * Enabled sets have priority over disabled sets - * Both groups follows the user-defined order - */ - inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const - { - if (a->getEnabled()) { - return !b->getEnabled() || a->getSortKey() < b->getSortKey(); - } else { - return !b->getEnabled() && a->getSortKey() < b->getSortKey(); - } - } -}; - PictureToLoad::PictureToLoad(CardInfoPtr _card) : card(std::move(_card)) { - /* #2479 will expand this into a list of Urls */ - urlTemplates.append(settingsCache->getPicUrl()); - urlTemplates.append(settingsCache->getPicUrlFallback()); - urlTemplates.append(GATHERER_DEFAULT); - urlTemplates.append(GATHERER_FALLBACK); + urlTemplates = settingsCache->downloads().getAllURLs(); if (card) { sortedSets = card->getSets(); @@ -78,7 +52,7 @@ void PictureToLoad::populateSetUrls() } } - foreach (QString urlTemplate, urlTemplates) { + for (const QString &urlTemplate : urlTemplates) { QString transformedUrl = transformUrl(urlTemplate); if (!transformedUrl.isEmpty()) { @@ -121,10 +95,8 @@ QString PictureToLoad::getSetName() const } } -QStringList PictureLoaderWorker::md5Blacklist = QStringList() - << "db0c48db407a907c16ade38de048a441"; // card back returned - // by gatherer when - // card is not found +// Card back returned by gatherer when card is not found +QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441"; PictureLoaderWorker::PictureLoaderWorker() : QObject(nullptr), downloadRunning(false), loadQueueRunning(false) { @@ -151,12 +123,12 @@ PictureLoaderWorker::~PictureLoaderWorker() void PictureLoaderWorker::processLoadQueue() { - if (loadQueueRunning) + if (loadQueueRunning) { return; + } loadQueueRunning = true; - forever - { + while (true) { mutex.lock(); if (loadQueue.isEmpty()) { mutex.unlock(); @@ -169,23 +141,26 @@ void PictureLoaderWorker::processLoadQueue() QString setName = cardBeingLoaded.getSetName(); QString cardName = cardBeingLoaded.getCard()->getName(); QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName(); + qDebug() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Trying to load picture"; - if (cardImageExistsOnDisk(setName, correctedCardName)) + if (cardImageExistsOnDisk(setName, correctedCardName)) { continue; + } if (picDownload) { qDebug() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Picture not found on disk, trying to download"; cardsToDownload.append(cardBeingLoaded); cardBeingLoaded.clear(); - if (!downloadRunning) + if (!downloadRunning) { startNextPicDownload(); + } } else { if (cardBeingLoaded.nextSet()) { qDebug() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Picture NOT found and download disabled, moving to next " - "set (newset: " + "set (new set: " << setName << " card: " << cardName << ")"; mutex.lock(); loadQueue.prepend(cardBeingLoaded); @@ -194,7 +169,7 @@ void PictureLoaderWorker::processLoadQueue() } else { qDebug() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Picture NOT found, download disabled, no more sets to " - "try: BAILING OUT (oldset: " + "try: BAILING OUT (old set: " << setName << " card: " << cardName << ")"; imageLoaded(cardBeingLoaded.getCard(), QImage()); } @@ -227,22 +202,22 @@ bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &corre // Iterates through the list of paths, searching for images with the desired // name with any QImageReader-supported // extension - for (int i = 0; i < picsPaths.length(); i++) { - imgReader.setFileName(picsPaths.at(i)); + for (const auto &picsPath : picsPaths) { + imgReader.setFileName(picsPath); if (imgReader.read(&image)) { qDebug() << "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture found on disk."; imageLoaded(cardBeingLoaded.getCard(), image); return true; } - imgReader.setFileName(picsPaths.at(i) + ".full"); + imgReader.setFileName(picsPath + ".full"); if (imgReader.read(&image)) { qDebug() << "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture.full found on disk."; imageLoaded(cardBeingLoaded.getCard(), image); return true; } - imgReader.setFileName(picsPaths.at(i) + ".xlhq"); + imgReader.setFileName(picsPath + ".xlhq"); if (imgReader.read(&image)) { qDebug() << "PictureLoader: [card: " << correctedCardname << " set: " << setName << "]: Picture.xlhq found on disk."; @@ -254,7 +229,7 @@ bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &corre return false; } -QString PictureToLoad::transformUrl(QString urlTemplate) const +QString PictureToLoad::transformUrl(const QString &urlTemplate) const { /* This function takes Url templates and substitutes actual card details into the url. This is used for making Urls with follow a predictable format @@ -289,7 +264,7 @@ QString PictureToLoad::transformUrl(QString urlTemplate) const transformMap["!setname_lower!"] = QString(); } - foreach (QString prop, transformMap.keys()) { + for (const QString &prop : transformMap.keys()) { if (transformedUrl.contains(prop)) { if (!transformMap[prop].isEmpty()) { transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop])); @@ -380,8 +355,8 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply) return; } - const QByteArray &picData = reply->peek(reply->size()); // peek is used to keep the data in the buffer - // for use by QImageReader + // peek is used to keep the data in the buffer for use by QImageReader + const QByteArray &picData = reply->peek(reply->size()); if (imageIsBlackListed(picData)) { qDebug() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName() @@ -403,8 +378,9 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply) // prior to reading the // QImageReader data // into a QImage object, as that wipes the QImageReader buffer - if (extension == ".jpeg") + if (extension == ".jpeg") { extension = ".jpg"; + } if (imgReader.read(&testImage)) { QString setName = cardBeingDownloaded.getSetName(); @@ -418,8 +394,9 @@ void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply) QFile newPic(picsPath + "/downloadedPics/" + setName + "/" + cardBeingDownloaded.getCard()->getCorrectedName() + extension); - if (!newPic.open(QIODevice::WriteOnly)) + if (!newPic.open(QIODevice::WriteOnly)) { return; + } newPic.write(picData); newPic.close(); } @@ -444,15 +421,16 @@ void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card) QMutexLocker locker(&mutex); // avoid queueing the same card more than once - if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) + if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) { return; + } - foreach (PictureToLoad pic, loadQueue) { + for (const PictureToLoad &pic : loadQueue) { if (pic.getCard() == card) return; } - foreach (PictureToLoad pic, cardsToDownload) { + for (const PictureToLoad &pic : cardsToDownload) { if (pic.getCard() == card) return; } @@ -474,7 +452,7 @@ void PictureLoaderWorker::picsPathChanged() customPicsPath = settingsCache->getCustomPicsPath(); } -PictureLoader::PictureLoader() : QObject(0) +PictureLoader::PictureLoader() : QObject(nullptr) { worker = new PictureLoaderWorker; connect(settingsCache, SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged())); @@ -501,20 +479,21 @@ void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size) void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size) { - if (card == nullptr) + if (card == nullptr) { return; + } // search for an exact size copy of the picure in cache QString key = card->getPixmapCacheKey(); - QString sizekey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height()); - if (QPixmapCache::find(sizekey, &pixmap)) + QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height()); + if (QPixmapCache::find(sizeKey, &pixmap)) return; // load the image and create a copy of the correct size QPixmap bigPixmap; if (QPixmapCache::find(key, &bigPixmap)) { pixmap = bigPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); - QPixmapCache::insert(sizekey, pixmap); + QPixmapCache::insert(sizeKey, pixmap); return; } @@ -540,8 +519,9 @@ void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image) void PictureLoader::clearPixmapCache(CardInfoPtr card) { - if (card) + if (card) { QPixmapCache::remove(card->getPixmapCacheKey()); + } } void PictureLoader::clearPixmapCache() @@ -554,13 +534,15 @@ void PictureLoader::cacheCardPixmaps(QList cards) QPixmap tmp; int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX); for (int i = 0; i < max; ++i) { - CardInfoPtr card = cards.at(i); - if (!card) + const CardInfoPtr &card = cards.at(i); + if (!card) { continue; + } QString key = card->getPixmapCacheKey(); - if (QPixmapCache::find(key, &tmp)) + if (QPixmapCache::find(key, &tmp)) { continue; + } getInstance().worker->enqueueImageLoad(card); } diff --git a/cockatrice/src/pictureloader.h b/cockatrice/src/pictureloader.h index b6c7a2c0a..db5757e89 100644 --- a/cockatrice/src/pictureloader.h +++ b/cockatrice/src/pictureloader.h @@ -14,7 +14,23 @@ class QThread; class PictureToLoad { private: - class SetDownloadPriorityComparator; + class SetDownloadPriorityComparator + { + public: + /* + * Returns true if a has higher download priority than b + * Enabled sets have priority over disabled sets + * Both groups follows the user-defined order + */ + inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const + { + if (a->getEnabled()) { + return !b->getEnabled() || a->getSortKey() < b->getSortKey(); + } else { + return !b->getEnabled() && a->getSortKey() < b->getSortKey(); + } + } + }; CardInfoPtr card; QList sortedSets; @@ -24,7 +40,8 @@ private: CardSetPtr currentSet; public: - PictureToLoad(CardInfoPtr _card = CardInfoPtr()); + explicit PictureToLoad(CardInfoPtr _card = CardInfoPtr()); + CardInfoPtr getCard() const { return card; @@ -42,7 +59,7 @@ public: return currentSet; } QString getSetName() const; - QString transformUrl(QString urlTemplate) const; + QString transformUrl(const QString &urlTemplate) const; bool nextSet(); bool nextUrl(); void populateSetUrls(); @@ -52,8 +69,8 @@ class PictureLoaderWorker : public QObject { Q_OBJECT public: - PictureLoaderWorker(); - ~PictureLoaderWorker(); + explicit PictureLoaderWorker(); + ~PictureLoaderWorker() override; void enqueueImageLoad(CardInfoPtr card); @@ -70,8 +87,8 @@ private: PictureToLoad cardBeingDownloaded; bool picDownload, downloadRunning, loadQueueRunning; void startNextPicDownload(); - bool cardImageExistsOnDisk(QString &setName, QString &correctedCardname); - bool imageIsBlackListed(const QByteArray &picData); + bool cardImageExistsOnDisk(QString &, QString &); + bool imageIsBlackListed(const QByteArray &); private slots: void picDownloadFinished(QNetworkReply *reply); void picDownloadFailed(); @@ -96,8 +113,8 @@ public: } private: - PictureLoader(); - ~PictureLoader(); + explicit PictureLoader(); + ~PictureLoader() override; // Singleton - Don't implement copy constructor and assign operator PictureLoader(PictureLoader const &); void operator=(PictureLoader const &); diff --git a/cockatrice/src/settings/downloadsettings.cpp b/cockatrice/src/settings/downloadsettings.cpp new file mode 100644 index 000000000..4591b680d --- /dev/null +++ b/cockatrice/src/settings/downloadsettings.cpp @@ -0,0 +1,56 @@ +#include "downloadsettings.h" +#include "settingsmanager.h" + +DownloadSettings::DownloadSettings(const QString &settingPath, QObject *parent = nullptr) + : SettingsManager(settingPath + "downloads.ini", parent) +{ + downloadURLs = getValue("urls", "downloads").value(); +} + +void DownloadSettings::setDownloadUrlAt(int index, const QString &url) +{ + downloadURLs.insert(index, url); + setValue(QVariant::fromValue(downloadURLs), "urls", "downloads"); +} + +/** + * If reset or first run, this method contains the default URLs we will populate + */ +QStringList DownloadSettings::getAllURLs() +{ + // First run, these will be empty + if (downloadURLs.count() == 0) { + populateDefaultURLs(); + } + + return downloadURLs; +} + +void DownloadSettings::populateDefaultURLs() +{ + downloadURLs.clear(); + downloadURLs.append("https://api.scryfall.com/cards/!uuid!?format=image"); + downloadURLs.append("https://api.scryfall.com/cards/multiverse/!cardid!?format=image"); + downloadURLs.append("http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card"); + downloadURLs.append("http://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card"); + setValue(QVariant::fromValue(downloadURLs), "urls", "downloads"); +} + +QString DownloadSettings::getDownloadUrlAt(int index) +{ + if (0 <= index && index < downloadURLs.size()) { + return downloadURLs[index]; + } + + return ""; +} + +int DownloadSettings::getCount() +{ + return downloadURLs.size(); +} + +void DownloadSettings::clear() +{ + downloadURLs.clear(); +} \ No newline at end of file diff --git a/cockatrice/src/settings/downloadsettings.h b/cockatrice/src/settings/downloadsettings.h new file mode 100644 index 000000000..f3c3e4d12 --- /dev/null +++ b/cockatrice/src/settings/downloadsettings.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_DOWNLOADSETTINGS_H +#define COCKATRICE_DOWNLOADSETTINGS_H + +#include "settingsmanager.h" +#include + +class DownloadSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + explicit DownloadSettings(const QString &, QObject *); + + QStringList getAllURLs(); + QString getDownloadUrlAt(int); + void setDownloadUrlAt(int, const QString &); + int getCount(); + void clear(); + +private: + QStringList downloadURLs; + +private: + void populateDefaultURLs(); +}; + +#endif // COCKATRICE_DOWNLOADSETTINGS_H diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index 76c573f1b..8586da971 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -164,6 +164,7 @@ SettingsCache::SettingsCache() messageSettings = new MessageSettings(settingsPath, this); gameFiltersSettings = new GameFiltersSettings(settingsPath, this); layoutsSettings = new LayoutsSettings(settingsPath, this); + downloadSettings = new DownloadSettings(settingsPath, this); if (!QFile(settingsPath + "global.ini").exists()) translateLegacySettings(); @@ -220,9 +221,6 @@ SettingsCache::SettingsCache() picDownload = settings->value("personal/picturedownload", true).toBool(); - picUrl = settings->value("personal/picUrl", PIC_URL_DEFAULT).toString(); - picUrlFallback = settings->value("personal/picUrlFallback", PIC_URL_FALLBACK).toString(); - mainWindowGeometry = settings->value("interface/main_window_geometry").toByteArray(); tokenDialogGeometry = settings->value("interface/token_dialog_geometry").toByteArray(); notificationsEnabled = settings->value("interface/notificationsenabled", true).toBool(); @@ -415,18 +413,6 @@ void SettingsCache::setPicDownload(int _picDownload) emit picDownloadChanged(); } -void SettingsCache::setPicUrl(const QString &_picUrl) -{ - picUrl = _picUrl; - settings->setValue("personal/picUrl", picUrl); -} - -void SettingsCache::setPicUrlFallback(const QString &_picUrlFallback) -{ - picUrlFallback = _picUrlFallback; - settings->setValue("personal/picUrlFallback", picUrlFallback); -} - void SettingsCache::setNotificationsEnabled(int _notificationsEnabled) { notificationsEnabled = static_cast(_notificationsEnabled); diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index b3d82889d..34421fcfb 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -2,6 +2,7 @@ #define SETTINGSCACHE_H #include "settings/carddatabasesettings.h" +#include "settings/downloadsettings.h" #include "settings/gamefilterssettings.h" #include "settings/layoutssettings.h" #include "settings/messagesettings.h" @@ -13,10 +14,6 @@ class ReleaseChannel; -// Fallbacks used for cards w/o MultiverseId -#define PIC_URL_DEFAULT "https://api.scryfall.com/cards/multiverse/!cardid!?format=image" -#define PIC_URL_FALLBACK "https://api.scryfall.com/cards/named?fuzzy=!name!&format=image" - // size should be a multiple of 64 #define PIXMAPCACHE_SIZE_DEFAULT 2047 #define PIXMAPCACHE_SIZE_MIN 64 @@ -61,6 +58,7 @@ private: MessageSettings *messageSettings; GameFiltersSettings *gameFiltersSettings; LayoutsSettings *layoutsSettings; + DownloadSettings *downloadSettings; QByteArray mainWindowGeometry; QByteArray tokenDialogGeometry; @@ -303,14 +301,6 @@ public: { return ignoreUnregisteredUserMessages; } - QString getPicUrl() const - { - return picUrl; - } - QString getPicUrlFallback() const - { - return picUrlFallback; - } int getPixmapCacheSize() const { return pixmapCacheSize; @@ -430,6 +420,10 @@ public: { return *layoutsSettings; } + DownloadSettings &downloads() const + { + return *downloadSettings; + } bool getIsPortableBuild() const { return isPortableBuild; @@ -478,8 +472,6 @@ public slots: void setSoundThemeName(const QString &_soundThemeName); void setIgnoreUnregisteredUsers(int _ignoreUnregisteredUsers); void setIgnoreUnregisteredUserMessages(int _ignoreUnregisteredUserMessages); - void setPicUrl(const QString &_picUrl); - void setPicUrlFallback(const QString &_picUrlFallback); void setPixmapCacheSize(const int _pixmapCacheSize); void setCardScaling(const int _scaleCards); void setShowMessagePopups(const int _showMessagePopups); diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index ec7b5d171..82df556f5 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -22,6 +22,7 @@ SET(oracle_SOURCES ../cockatrice/src/settings/messagesettings.cpp ../cockatrice/src/settings/gamefilterssettings.cpp ../cockatrice/src/settings/layoutssettings.cpp + ../cockatrice/src/settings/downloadsettings.cpp ../cockatrice/src/thememanager.cpp ../cockatrice/src/qt-json/json.cpp ../cockatrice/src/releasechannel.cpp diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index 0005e077c..523a53b79 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -34,7 +34,7 @@ int main(int argc, char *argv[]) QCoreApplication::setOrganizationName("Cockatrice"); QCoreApplication::setOrganizationDomain("cockatrice"); - // this can't be changed, as it influences the default savepath for cards.xml + // this can't be changed, as it influences the default save path for cards.xml QCoreApplication::setApplicationName("Cockatrice"); // If the program is opened with the -s flag, it will only do spoilers. Otherwise it will do MTGJSON/Tokens diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 4ac937cc3..ec096f5c3 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -97,7 +97,7 @@ CardInfoPtr OracleImporter::addCard(const QString &setName, bool mArtifact = false; if (cardType.endsWith("Artifact")) { for (int i = 0; i < cardTextRows.size(); ++i) { - cardTextRows[i].remove(QRegularExpression("\\\".*?\\\"")); + cardTextRows[i].remove(QRegularExpression(R"(\".*?\")")); if (cardTextRows[i].contains("{T}") && cardTextRows[i].contains("to your mana pool")) { mArtifact = true; } @@ -124,6 +124,7 @@ CardInfoPtr OracleImporter::addCard(const QString &setName, cards.insert(cardName, card); } + card->setMuId(setName, cardId); card->setUuId(setName, cardUuId); card->setSetNumber(setName, setNumber); @@ -152,7 +153,7 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) QString setNumber; QString rarity; QString cardLoyalty; - bool upsideDown = false; + bool upsideDown; QMap splitCards; while (it.hasNext()) { @@ -187,14 +188,14 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) : QString(""); cardText = map.contains("text") ? map.value("text").toString() : QString(""); cardId = map.contains("multiverseId") ? map.value("multiverseId").toInt() : 0; - cardUuId = map.contains("uuid") ? map.value("uuid").toString() : QString(""); + cardUuId = map.contains("scryfallId") ? map.value("scryfallId").toString() : QString(""); setNumber = map.contains("number") ? map.value("number").toString() : QString(""); rarity = map.contains("rarity") ? map.value("rarity").toString() : QString(""); cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toString() : QString(""); colors = map.contains("colors") ? map.value("colors").toStringList() : QStringList(); relatedCards = QList(); if (map.contains("names")) - foreach (const QString &name, map.value("names").toStringList()) { + for (const QString &name : map.value("names").toStringList()) { if (name != cardName) relatedCards.append(new CardRelation(name, true)); } @@ -218,18 +219,18 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) // split cards handling - get all unique card muids QList muids = splitCards.uniqueKeys(); - foreach (int muid, muids) { + for (int muid : muids) { // get all cards for this specific muid QList maps = splitCards.values(muid); QStringList names; // now, reorder the cards using the ordered list of names QMap orderedMaps; - foreach (QVariantMap map, maps) { + for (const QVariantMap &inner_map : maps) { if (names.isEmpty()) - names = map.contains("names") ? map.value("names").toStringList() : QStringList(); - QString name = map.value("name").toString(); + names = inner_map.contains("names") ? inner_map.value("names").toStringList() : QStringList(); + QString name = inner_map.value("name").toString(); int index = names.indexOf(name); - orderedMaps.insertMulti(index, map); + orderedMaps.insertMulti(index, inner_map); } // clean variables @@ -248,51 +249,51 @@ int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) // loop cards and merge their contents QString prefix = QString(" // "); QString prefix2 = QString("\n\n---\n\n"); - foreach (QVariantMap map, orderedMaps.values()) { - if (map.contains("name")) { + for (const QVariantMap &inner_map : orderedMaps.values()) { + if (inner_map.contains("name")) { if (!cardName.isEmpty()) cardName += (orderedMaps.count() > 2) ? QString("/") : prefix; - cardName += map.value("name").toString(); + cardName += inner_map.value("name").toString(); } - if (map.contains("manaCost")) { + if (inner_map.contains("manaCost")) { if (!cardCost.isEmpty()) cardCost += prefix; - cardCost += map.value("manaCost").toString(); + cardCost += inner_map.value("manaCost").toString(); } - if (map.contains("convertedManaCost")) { + if (inner_map.contains("convertedManaCost")) { if (!cmc.isEmpty()) cmc += prefix; - cmc += map.value("convertedManaCost").toString(); + cmc += inner_map.value("convertedManaCost").toString(); } - if (map.contains("type")) { + if (inner_map.contains("type")) { if (!cardType.isEmpty()) cardType += prefix; - cardType += map.value("type").toString(); + cardType += inner_map.value("type").toString(); } - if (map.contains("power") || map.contains("toughness")) { + if (inner_map.contains("power") || inner_map.contains("toughness")) { if (!cardPT.isEmpty()) cardPT += prefix; - cardPT += map.value("power").toString() + QString('/') + map.value("toughness").toString(); + cardPT += inner_map.value("power").toString() + QString('/') + inner_map.value("toughness").toString(); } - if (map.contains("text")) { + if (inner_map.contains("text")) { if (!cardText.isEmpty()) cardText += prefix2; - cardText += map.value("text").toString(); + cardText += inner_map.value("text").toString(); } - if (map.contains("uuid")) { + if (inner_map.contains("uuid")) { if (cardUuId.isEmpty()) - cardUuId = map.value("uuid").toString(); + cardUuId = inner_map.value("uuid").toString(); } - if (map.contains("number")) { + if (inner_map.contains("number")) { if (setNumber.isEmpty()) - setNumber = map.value("number").toString(); + setNumber = inner_map.value("number").toString(); } - if (map.contains("rarity")) { + if (inner_map.contains("rarity")) { if (rarity.isEmpty()) - rarity = map.value("rarity").toString(); + rarity = inner_map.value("rarity").toString(); } - colors << map.value("colors").toStringList(); + colors << inner_map.value("colors").toStringList(); } colors.removeDuplicates(); From 273d5d89b74afd2ad5cb88e7a14a6f0edf276fdb Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Tue, 8 Jan 2019 21:20:29 +0100 Subject: [PATCH 39/62] Fix shortcuts on mac; fix #3482 (#3491) * hopefully fix #3482 * Travis:uUse the most recent osx_image; use homebrew support in travis --- .travis.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index b20c35a97..ba664c7c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,24 +84,32 @@ matrix: - name: macOS (Debug) if: tag IS NOT present os: osx - osx_image: xcode9.2 - env: HOMEBREW_NO_AUTO_UPDATE=1 + osx_image: xcode10.1 cache: ccache - before_install: - - brew install ccache protobuf qt xz + addons: + homebrew: + packages: + - ccache + - protobuf + - qt + - xz script: bash ./.ci/travis-compile.sh --server --install --debug - name: macOS (Release) if: (branch = master AND NOT type = pull_request) OR tag IS present os: osx osx_image: xcode9.2 - env: HOMEBREW_NO_AUTO_UPDATE=1 cache: ccache - before_install: - - brew install ccache protobuf qt xz + addons: + homebrew: + packages: + - ccache + - protobuf + - qt + - xz + update: true script: bash ./.ci/travis-compile.sh --server --package "$TRAVIS_OS_NAME" --release - # Builds for pull requests skip the deployment step altogether deploy: # Deploy configuration for "beta" releases From 41bfbf2e83d2932617e4e967bce4b5e8b847e095 Mon Sep 17 00:00:00 2001 From: Zach H Date: Mon, 14 Jan 2019 01:11:05 -0500 Subject: [PATCH 40/62] Force Oracle run on new install/update (#3497) * Force Oracle run on new install/update Signed-off-by: Zach Halpern * Add settings option to disable such a check Signed-off-by: Zach Halpern --- cockatrice/src/dlg_settings.cpp | 6 +++++- cockatrice/src/dlg_settings.h | 1 + cockatrice/src/settingscache.cpp | 14 ++++++++++++++ cockatrice/src/settingscache.h | 12 ++++++++++++ cockatrice/src/window_main.cpp | 32 ++++++++++++++++++++++++++++---- cockatrice/src/window_main.h | 15 +++++++++++++++ 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/cockatrice/src/dlg_settings.cpp b/cockatrice/src/dlg_settings.cpp index f184da699..6b0e8497c 100644 --- a/cockatrice/src/dlg_settings.cpp +++ b/cockatrice/src/dlg_settings.cpp @@ -53,6 +53,7 @@ GeneralSettingsPage::GeneralSettingsPage() updateReleaseChannelBox.setCurrentIndex(settingsCache->getUpdateReleaseChannel()->getIndex()); updateNotificationCheckBox.setChecked(settingsCache->getNotifyAboutUpdates()); + newVersionOracleCheckBox.setChecked(settingsCache->getNotifyAboutNewVersion()); // pixmap cache pixmapCacheEdit.setMinimum(PIXMAPCACHE_SIZE_MIN); @@ -69,6 +70,7 @@ GeneralSettingsPage::GeneralSettingsPage() connect(&updateReleaseChannelBox, SIGNAL(currentIndexChanged(int)), settingsCache, SLOT(setUpdateReleaseChannel(int))); connect(&updateNotificationCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setNotifyAboutUpdate(int))); + connect(&newVersionOracleCheckBox, SIGNAL(stateChanged(int)), settingsCache, SLOT(setNotifyAboutNewVersion(int))); connect(&showTipsOnStartup, SIGNAL(clicked(bool)), settingsCache, SLOT(setShowTipsOnStartup(bool))); auto *personalGrid = new QGridLayout; @@ -79,7 +81,8 @@ GeneralSettingsPage::GeneralSettingsPage() personalGrid->addWidget(&pixmapCacheLabel, 2, 0); personalGrid->addWidget(&pixmapCacheEdit, 2, 1); personalGrid->addWidget(&updateNotificationCheckBox, 3, 0); - personalGrid->addWidget(&showTipsOnStartup, 4, 0); + personalGrid->addWidget(&newVersionOracleCheckBox, 4, 0); + personalGrid->addWidget(&showTipsOnStartup, 5, 0); personalGroupBox = new QGroupBox; personalGroupBox->setLayout(personalGrid); @@ -242,6 +245,7 @@ void GeneralSettingsPage::retranslateUi() pixmapCacheLabel.setText(tr("Picture cache size:")); updateReleaseChannelLabel.setText(tr("Update channel")); updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client")); + newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice")); showTipsOnStartup.setText(tr("Show tips on startup")); } diff --git a/cockatrice/src/dlg_settings.h b/cockatrice/src/dlg_settings.h index 6dc44e122..479e66771 100644 --- a/cockatrice/src/dlg_settings.h +++ b/cockatrice/src/dlg_settings.h @@ -60,6 +60,7 @@ private: QGroupBox *pathsGroupBox; QComboBox languageBox; QCheckBox updateNotificationCheckBox; + QCheckBox newVersionOracleCheckBox; QComboBox updateReleaseChannelBox; QLabel languageLabel; QLabel pixmapCacheLabel; diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index 8586da971..393b014de 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -177,6 +177,7 @@ SettingsCache::SettingsCache() mbDownloadSpoilers = settings->value("personal/downloadspoilers", false).toBool(); notifyAboutUpdates = settings->value("personal/updatenotification", true).toBool(); + notifyAboutNewVersion = settings->value("personal/newversionnotification", true).toBool(); updateReleaseChannel = settings->value("personal/updatereleasechannel", 0).toInt(); lang = settings->value("personal/lang").toString(); @@ -275,6 +276,7 @@ SettingsCache::SettingsCache() spectatorsCanSeeEverything = settings->value("game/spectatorscanseeeverything", false).toBool(); rememberGameSettings = settings->value("game/remembergamesettings", true).toBool(); clientID = settings->value("personal/clientid", "notset").toString(); + clientVersion = settings->value("personal/clientversion", "notset").toString(); knownMissingFeatures = settings->value("interface/knownmissingfeatures", "").toString(); } @@ -589,6 +591,12 @@ void SettingsCache::setClientID(QString _clientID) settings->setValue("personal/clientid", clientID); } +void SettingsCache::setClientVersion(QString _clientVersion) +{ + clientVersion = std::move(_clientVersion); + settings->setValue("personal/clientversion", clientVersion); +} + QStringList SettingsCache::getCountries() const { static QStringList countries = QStringList() << "ad" @@ -910,6 +918,12 @@ void SettingsCache::setNotifyAboutUpdate(int _notifyaboutupdate) settings->setValue("personal/updatenotification", notifyAboutUpdates); } +void SettingsCache::setNotifyAboutNewVersion(int _notifyaboutnewversion) +{ + notifyAboutNewVersion = static_cast(_notifyaboutnewversion); + settings->setValue("personal/newversionnotification", notifyAboutNewVersion); +} + void SettingsCache::setDownloadSpoilerStatus(bool _spoilerStatus) { mbDownloadSpoilers = _spoilerStatus; diff --git a/cockatrice/src/settingscache.h b/cockatrice/src/settingscache.h index 34421fcfb..0dca177f5 100644 --- a/cockatrice/src/settingscache.h +++ b/cockatrice/src/settingscache.h @@ -66,6 +66,7 @@ private: QString deckPath, replaysPath, picsPath, customPicsPath, cardDatabasePath, customCardDatabasePath, spoilerDatabasePath, tokenDatabasePath, themeName; bool notifyAboutUpdates; + bool notifyAboutNewVersion; bool showTipsOnStartup; QList seenTips; bool mbDownloadSpoilers; @@ -97,6 +98,7 @@ private: QString picUrl; QString picUrlFallback; QString clientID; + QString clientVersion; QString knownMissingFeatures; int pixmapCacheSize; bool scaleCards; @@ -200,6 +202,10 @@ public: { return notifyAboutUpdates; } + bool getNotifyAboutNewVersion() const + { + return notifyAboutNewVersion; + } bool getShowTipsOnStartup() const { return showTipsOnStartup; @@ -387,11 +393,16 @@ public: return maxFontSize; } void setClientID(QString clientID); + void setClientVersion(QString clientVersion); void setKnownMissingFeatures(QString _knownMissingFeatures); QString getClientID() { return clientID; } + QString getClientVersion() + { + return clientVersion; + } QString getKnownMissingFeatures() { return knownMissingFeatures; @@ -492,6 +503,7 @@ public slots: void setSpectatorsCanSeeEverything(const bool _spectatorsCanSeeEverything); void setRememberGameSettings(const bool _rememberGameSettings); void setNotifyAboutUpdate(int _notifyaboutupdate); + void setNotifyAboutNewVersion(int _notifyaboutnewversion); void setUpdateReleaseChannel(int _updateReleaseChannel); void setMaxFontSize(int _max); }; diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index bdca76c9f..657ddc61b 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -317,9 +317,9 @@ void MainWindow::actAbout() void MainWindow::actTips() { - if (tip != NULL) { + if (tip != nullptr) { delete tip; - tip = NULL; + tip = nullptr; } tip = new DlgTipOfTheDay(); if (tip->successfulInit) { @@ -828,13 +828,30 @@ MainWindow::MainWindow(QWidget *parent) if (tip->successfulInit && settingsCache->getShowTipsOnStartup() && tip->newTipsAvailable) { tip->show(); } + + // Only run the check updater if the user wants it (defaults to on) + if (settingsCache->getNotifyAboutNewVersion()) { + auto versionUpdater = new MainUpdateHelper(); + connect(versionUpdater, SIGNAL(newVersionDetected(QString)), this, SLOT(alertForcedOracleRun(QString))); + QtConcurrent::run(versionUpdater, &MainUpdateHelper::testForNewVersion); + } +} + +void MainWindow::alertForcedOracleRun(const QString &newVersion) +{ + settingsCache->setClientVersion(newVersion); + QMessageBox::information(this, tr("New Version"), + tr("Congratulations on updating to Cockatrice %1!\n" + "Oracle will now launch to update your card database.") + .arg(newVersion)); + actCheckCardUpdates(); } MainWindow::~MainWindow() { - if (tip != NULL) { + if (tip != nullptr) { delete tip; - tip = NULL; + tip = nullptr; } if (trayIcon) { trayIcon->hide(); @@ -1271,3 +1288,10 @@ void MainWindow::promptForgotPasswordReset() dlg.getPlayerName(), dlg.getToken(), dlg.getPassword()); } } + +void MainUpdateHelper::testForNewVersion() +{ + if (settingsCache->getClientVersion() != VERSION_STRING) { + emit newVersionDetected(VERSION_STRING); + } +} diff --git a/cockatrice/src/window_main.h b/cockatrice/src/window_main.h index d6c7238b7..68067924f 100644 --- a/cockatrice/src/window_main.h +++ b/cockatrice/src/window_main.h @@ -101,6 +101,8 @@ private slots: void actManageSets(); void actEditTokens(); + void alertForcedOracleRun(const QString &); + private: static const QString appName; static const QStringList fileNameFilters; @@ -146,4 +148,17 @@ protected: QString extractInvalidUsernameMessage(QString &in); }; +class MainUpdateHelper : public QObject +{ + Q_OBJECT + +signals: + void newVersionDetected(QString); + +public: + explicit MainUpdateHelper() = default; + ~MainUpdateHelper() override = default; + void testForNewVersion(); +}; + #endif From 1c6b43c545edffd8d6d0036573955b31096af528 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Sat, 19 Jan 2019 11:15:22 +0100 Subject: [PATCH 41/62] fix typo in shortcut name (#3509) In #3439 the shortcuts got some overhaul but this shortcut wasn't correctly renamed. This fix will clear users' original shortcut for increasing toughness. An improvement over the current state of it not working at all and being unable to set to the default shortcut. Fixes #3506 --- cockatrice/src/player.cpp | 2 +- cockatrice/src/sequenceEdit/ui_shortcutstab.h | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index 95a55152d..1b1386b35 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -763,7 +763,7 @@ void Player::setShortcutsActive() aAttach->setShortcuts(shortcuts.getShortcut("Player/aAttach")); aUnattach->setShortcuts(shortcuts.getShortcut("Player/aUnattach")); aDrawArrow->setShortcuts(shortcuts.getShortcut("Player/aDrawArrow")); - aIncP->setShortcuts(shortcuts.getShortcut("Player/IncP")); + aIncP->setShortcuts(shortcuts.getShortcut("Player/aIncP")); aDecP->setShortcuts(shortcuts.getShortcut("Player/aDecP")); aIncT->setShortcuts(shortcuts.getShortcut("Player/aIncT")); aDecT->setShortcuts(shortcuts.getShortcut("Player/aDecT")); diff --git a/cockatrice/src/sequenceEdit/ui_shortcutstab.h b/cockatrice/src/sequenceEdit/ui_shortcutstab.h index 69b1a0afe..3a826e943 100644 --- a/cockatrice/src/sequenceEdit/ui_shortcutstab.h +++ b/cockatrice/src/sequenceEdit/ui_shortcutstab.h @@ -217,8 +217,8 @@ public: QGridLayout *gridLayout_10; QLabel *lbl_Player_aDecP; SequenceEdit *Player_aDecP; - SequenceEdit *Player_IncP; - QLabel *lbl_Player_IncP; + SequenceEdit *Player_aIncP; + QLabel *lbl_Player_aIncP; QGroupBox *groupBox_8; QGridLayout *gridLayout_5; QLabel *lbl_TabGame_phase0; @@ -1121,15 +1121,15 @@ public: gridLayout_10->addWidget(Player_aDecP, 1, 1, 1, 1); - Player_IncP = new SequenceEdit("Player/IncP", groupBox_10); - Player_IncP->setObjectName("Player_IncP"); + Player_aIncP = new SequenceEdit("Player/aIncP", groupBox_10); + Player_aIncP->setObjectName("Player_aIncP"); - gridLayout_10->addWidget(Player_IncP, 0, 1, 1, 1); + gridLayout_10->addWidget(Player_aIncP, 0, 1, 1, 1); - lbl_Player_IncP = new QLabel(groupBox_10); - lbl_Player_IncP->setObjectName("lbl_Player_IncP"); + lbl_Player_aIncP = new QLabel(groupBox_10); + lbl_Player_aIncP->setObjectName("lbl_Player_aIncP"); - gridLayout_10->addWidget(lbl_Player_IncP, 0, 0, 1, 1); + gridLayout_10->addWidget(lbl_Player_aIncP, 0, 0, 1, 1); verticalLayout->addWidget(groupBox_10); @@ -1920,7 +1920,7 @@ public: lbl_Player_aIncT->setText(QApplication::translate("shortcutsTab", "Add (+0/+1)", 0)); groupBox_10->setTitle(QApplication::translate("shortcutsTab", "Power", 0)); lbl_Player_aDecP->setText(QApplication::translate("shortcutsTab", "Remove (-1/-0)", 0)); - lbl_Player_IncP->setText(QApplication::translate("shortcutsTab", "Add (+1/+0)", 0)); + lbl_Player_aIncP->setText(QApplication::translate("shortcutsTab", "Add (+1/+0)", 0)); groupBox_8->setTitle(QApplication::translate("shortcutsTab", "Game Phases", 0)); lbl_TabGame_phase0->setText(QApplication::translate("shortcutsTab", "Untap", 0)); lbl_TabGame_phase1->setText(QApplication::translate("shortcutsTab", "Upkeep", 0)); From a08cf9379b975212ecb5a516b570aa8952d216d3 Mon Sep 17 00:00:00 2001 From: Rob Blanckaert Date: Tue, 22 Jan 2019 01:28:06 -0800 Subject: [PATCH 42/62] Disable darkmode on MacOS (#3513) --- cmake/Info.plist | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmake/Info.plist b/cmake/Info.plist index eecb35bab..a4916941b 100644 --- a/cmake/Info.plist +++ b/cmake/Info.plist @@ -34,5 +34,7 @@ ${MACOSX_BUNDLE_COPYRIGHT} NSHighResolutionCapable + NSRequiresAquaSystemAppearance + - \ No newline at end of file + From 19180243aaf24f7a8e792bc37c2d06aac17a0eba Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Tue, 22 Jan 2019 10:28:20 +0100 Subject: [PATCH 43/62] Workaround fedora compilation problem on Travis; fix #3508 (#3512) * attempt fix fedora by downgrading qt * fix --- .ci/Fedora29/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/Fedora29/Dockerfile b/.ci/Fedora29/Dockerfile index 7b0e9cf68..0107d5980 100644 --- a/.ci/Fedora29/Dockerfile +++ b/.ci/Fedora29/Dockerfile @@ -10,7 +10,7 @@ RUN dnf install -y \ hicolor-icon-theme \ libappstream-glib \ protobuf-devel \ - qt5-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ + qt5-{qttools,qtsvg,qtmultimedia,qtwebsockets}-devel-5.11.1-2.fc29 \ rpm-build \ sqlite-devel \ wget \ From ed70099e36c9ebfb0f2de5e8aa87ac711a3ff228 Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Thu, 24 Jan 2019 00:17:10 +0100 Subject: [PATCH 44/62] Rework of the card database, xml format and oracle parser (#3511) * CardDB: merge all card properties in a new structure * Pre Json parser changes * Cockatrice: use qt's builtin json support * Move qt-json src dir from cockatrice to oracle * Add dummy cockatricexml4 parser (yet to be implemented) * Implement a new parser and xml format * cockatricexml4: new xml parser following the "generic properties hash" pattern; * oracleimporter: refactor the parsing code to better adapt to cockatricexml4; rewrote split cards parsing * carddb: change "colors" from a stringlist to a string * carddb: move the getMainCardType() method to the cockatricexml3 parser * * CardInfo: show all properties (stil missing: nice name + translation) * Rework the "add related card" feature so that it doesn't change the card name in the carddb Also, fix token count display * Picture loader: Added support for transform cards * Fix side information for flip cards Mtgjson uses side a/b for flip cards, while scryfall doesn't * Pictureloader: dynamic tag resolution from card properties Examples old => new * !cardid! => !set:muid! * !uuid! => !set:uuid! * !collectornumber! => !set:num! New examples: * !prop:type! * !prop:manacost! * Start moving mtg-related property names to a specific file * Clangify * Fix tests * Make gcc an happy puppy * Revert "Make gcc an happy puppy" This reverts commit 446ec5f27516c4d3b32dbfc79557f4827c5c5bdf. * Some gcc fixes * Share set list between different db parsers, so they won't overwrite one each other * All glory to the hypnoclangifier! * Fix test compilation * Cleanup edited files in the prior PR. (#3519) * Cleanup edited files in the prior PR. Signed-off-by: Zach Halpern * Fix includes Signed-off-by: Zach Halpern * Update carddatabase.h --- clangify.sh | 6 +- cockatrice/CMakeLists.txt | 3 +- cockatrice/src/carddatabase.cpp | 208 +++----- cockatrice/src/carddatabase.h | 285 +++++----- cockatrice/src/carddatabasemodel.cpp | 10 +- cockatrice/src/carddatabasemodel.h | 30 +- .../src/carddbparser/carddatabaseparser.cpp | 27 + .../src/carddbparser/carddatabaseparser.h | 20 +- .../src/carddbparser/cockatricexml3.cpp | 217 ++++---- cockatrice/src/carddbparser/cockatricexml3.h | 23 +- .../src/carddbparser/cockatricexml4.cpp | 362 +++++++++++++ cockatrice/src/carddbparser/cockatricexml4.h | 28 + cockatrice/src/cardframe.cpp | 9 +- cockatrice/src/cardframe.h | 2 +- cockatrice/src/cardinfotext.cpp | 160 ++---- cockatrice/src/cardinfotext.h | 12 +- cockatrice/src/cardinfowidget.cpp | 11 +- cockatrice/src/cardinfowidget.h | 2 +- cockatrice/src/decklistmodel.cpp | 4 +- cockatrice/src/decklistmodel.h | 33 +- cockatrice/src/dlg_edit_tokens.cpp | 19 +- cockatrice/src/dlg_edit_tokens.h | 2 +- cockatrice/src/filtertree.cpp | 19 +- cockatrice/src/filtertree.h | 100 ++-- cockatrice/src/game_specific_terms.h | 49 ++ cockatrice/src/handle_public_servers.cpp | 15 +- cockatrice/src/pictureloader.cpp | 52 +- cockatrice/src/player.cpp | 302 ++++++----- cockatrice/src/player.h | 19 +- cockatrice/src/releasechannel.cpp | 53 +- cockatrice/src/releasechannel.h | 51 +- cockatrice/src/settings/downloadsettings.cpp | 6 +- cockatrice/translations/cockatrice_en.ts | 452 +++++++++------- oracle/CMakeLists.txt | 4 +- oracle/src/oracleimporter.cpp | 498 +++++++++--------- oracle/src/oracleimporter.h | 73 ++- {cockatrice => oracle}/src/qt-json/AUTHORS | 0 {cockatrice => oracle}/src/qt-json/LICENSE | 0 {cockatrice => oracle}/src/qt-json/README | 0 {cockatrice => oracle}/src/qt-json/json.cpp | 0 {cockatrice => oracle}/src/qt-json/json.h | 0 oracle/translations/oracle_en.ts | 2 +- tests/carddatabase/CMakeLists.txt | 3 +- tests/carddatabase/carddatabase_test.cpp | 3 - 44 files changed, 1814 insertions(+), 1360 deletions(-) create mode 100644 cockatrice/src/carddbparser/carddatabaseparser.cpp create mode 100644 cockatrice/src/carddbparser/cockatricexml4.cpp create mode 100644 cockatrice/src/carddbparser/cockatricexml4.h create mode 100644 cockatrice/src/game_specific_terms.h rename {cockatrice => oracle}/src/qt-json/AUTHORS (100%) rename {cockatrice => oracle}/src/qt-json/LICENSE (100%) rename {cockatrice => oracle}/src/qt-json/README (100%) rename {cockatrice => oracle}/src/qt-json/json.cpp (100%) rename {cockatrice => oracle}/src/qt-json/json.h (100%) diff --git a/clangify.sh b/clangify.sh index ce81a1140..9113952e1 100755 --- a/clangify.sh +++ b/clangify.sh @@ -12,11 +12,11 @@ include=("common" \ "cockatrice/src" \ "oracle/src" \ "servatrice/src") -exclude=("cockatrice/src/qt-json" \ -"servatrice/src/smtp" \ +exclude=("servatrice/src/smtp" \ "common/sfmt" \ "oracle/src/zip" \ -"oracle/src/lzma") +"oracle/src/lzma" \ +"oracle/src/qt-json") exts=("cpp" "h") cf_cmd="clang-format" branch="origin/master" diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 38b4a3314..3e7defbe7 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -100,7 +100,6 @@ SET(cockatrice_SOURCES src/localserver.cpp src/localserverinterface.cpp src/localclient.cpp - src/qt-json/json.cpp src/soundengine.cpp src/pending_command.cpp src/pictureloader.cpp @@ -121,7 +120,9 @@ SET(cockatrice_SOURCES src/userconnection_information.cpp src/spoilerbackgroundupdater.cpp src/handle_public_servers.cpp + src/carddbparser/carddatabaseparser.cpp src/carddbparser/cockatricexml3.cpp + src/carddbparser/cockatricexml4.cpp ${VERSION_STRING_CPP} ) diff --git a/cockatrice/src/carddatabase.cpp b/cockatrice/src/carddatabase.cpp index f4995db5f..a75f150c4 100644 --- a/cockatrice/src/carddatabase.cpp +++ b/cockatrice/src/carddatabase.cpp @@ -1,5 +1,7 @@ #include "carddatabase.h" #include "carddbparser/cockatricexml3.h" +#include "carddbparser/cockatricexml4.h" +#include "game_specific_terms.h" #include "pictureloader.h" #include "settingscache.h" #include "spoilerbackgroundupdater.h" @@ -207,31 +209,23 @@ void SetList::guessSortKeys() } } +CardInfoPerSet::CardInfoPerSet(const CardSetPtr &_set) : set(_set) +{ +} + CardInfo::CardInfo(const QString &_name, - bool _isToken, - const QString &_manacost, - const QString &_cmc, - const QString &_cardtype, - const QString &_powtough, const QString &_text, - const QStringList &_colors, + bool _isToken, + QVariantHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, - bool _upsideDownArt, - const QString &_loyalty, + CardInfoPerSetMap _sets, bool _cipt, int _tableRow, - const SetList &_sets, - const QStringMap &_customPicURLs, - MuidMap _muIds, - QStringMap _uuIds, - QStringMap _collectorNumbers, - QStringMap _rarities) - : name(_name), isToken(_isToken), sets(_sets), manacost(_manacost), cmc(_cmc), cardtype(_cardtype), - powtough(_powtough), text(_text), colors(_colors), relatedCards(_relatedCards), - reverseRelatedCards(_reverseRelatedCards), setsNames(), upsideDownArt(_upsideDownArt), loyalty(_loyalty), - customPicURLs(_customPicURLs), muIds(std::move(_muIds)), uuIds(std::move(_uuIds)), - collectorNumbers(std::move(_collectorNumbers)), rarities(std::move(_rarities)), cipt(_cipt), tableRow(_tableRow) + bool _upsideDownArt) + : name(_name), text(_text), isToken(_isToken), properties(std::move(_properties)), relatedCards(_relatedCards), + reverseRelatedCards(_reverseRelatedCards), sets(std::move(_sets)), cipt(_cipt), tableRow(_tableRow), + upsideDownArt(_upsideDownArt) { pixmapCacheKey = QLatin1String("card_") + name; simpleName = CardInfo::simplifyName(name); @@ -245,77 +239,27 @@ CardInfo::~CardInfo() } CardInfoPtr CardInfo::newInstance(const QString &_name, - bool _isToken, - const QString &_manacost, - const QString &_cmc, - const QString &_cardtype, - const QString &_powtough, const QString &_text, - const QStringList &_colors, + bool _isToken, + QVariantHash _properties, const QList &_relatedCards, const QList &_reverseRelatedCards, - bool _upsideDownArt, - const QString &_loyalty, + CardInfoPerSetMap _sets, bool _cipt, int _tableRow, - const SetList &_sets, - const QStringMap &_customPicURLs, - MuidMap _muIds, - QStringMap _uuIds, - QStringMap _collectorNumbers, - QStringMap _rarities) + bool _upsideDownArt) { - CardInfoPtr ptr(new CardInfo(_name, _isToken, _manacost, _cmc, _cardtype, _powtough, _text, _colors, _relatedCards, - _reverseRelatedCards, _upsideDownArt, _loyalty, _cipt, _tableRow, _sets, - _customPicURLs, std::move(_muIds), std::move(_uuIds), std::move(_collectorNumbers), - std::move(_rarities))); + CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards, + _sets, _cipt, _tableRow, _upsideDownArt)); ptr->setSmartPointer(ptr); - for (int i = 0; i < _sets.size(); i++) { - _sets[i]->append(ptr); + for (const CardInfoPerSet &set : _sets) { + set.getPtr()->append(ptr); } return ptr; } -QString CardInfo::getMainCardType() const -{ - QString result = getCardType(); - /* - Legendary Artifact Creature - Golem - Instant // Instant - */ - - int pos; - if ((pos = result.indexOf('-')) != -1) { - result.remove(pos, result.length()); - } - - if ((pos = result.indexOf("—")) != -1) { - result.remove(pos, result.length()); - } - - if ((pos = result.indexOf("//")) != -1) { - result.remove(pos, result.length()); - } - - result = result.simplified(); - /* - Legendary Artifact Creature - Instant - */ - - if ((pos = result.lastIndexOf(' ')) != -1) { - result = result.mid(pos + 1); - } - /* - Creature - Instant - */ - - return result; -} - QString CardInfo::getCorrectedName() const { QString result = name; @@ -323,26 +267,21 @@ QString CardInfo::getCorrectedName() const return result.remove(" // ").remove(':').remove('"').remove('?').replace('/', ' '); } -void CardInfo::addToSet(CardSetPtr set) +void CardInfo::addToSet(const CardSetPtr &_set, const CardInfoPerSet _info) { - if (set.isNull()) { - qDebug() << "addToSet(nullptr)"; - return; - } - - set->append(smartThis); - sets << set; + _set->append(smartThis); + sets.insert(_set->getShortName(), _info); refreshCachedSetNames(); } void CardInfo::refreshCachedSetNames() { - // update the cached list of set names QStringList setList; - for (int i = 0; i < sets.size(); i++) { - if (sets[i]->getEnabled()) { - setList << sets[i]->getShortName(); + // update the cached list of set names + for (const auto &set : sets) { + if (set.getPtr()->getEnabled()) { + setList << set.getPtr()->getShortName(); } } setsNames = setList.join(", "); @@ -371,11 +310,12 @@ QString CardInfo::simplifyName(const QString &name) const QChar CardInfo::getColorChar() const { + QString colors = getColors(); switch (colors.size()) { case 0: return QChar(); case 1: - return colors.first().isEmpty() ? QChar() : colors.first().at(0); + return colors.at(0); default: return QChar('m'); } @@ -388,6 +328,7 @@ CardDatabase::CardDatabase(QObject *parent) : QObject(parent), loadStatus(NotLoa // add new parsers here availableParsers << new CockatriceXml3Parser; + availableParsers << new CockatriceXml4Parser; for (auto &parser : availableParsers) { connect(parser, SIGNAL(addCard(CardInfoPtr)), this, SLOT(addCard(CardInfoPtr)), Qt::DirectConnection); @@ -419,9 +360,7 @@ void CardDatabase::clear() simpleNameCards.clear(); sets.clear(); - for (auto parser : availableParsers) { - parser->clearSetlist(); - } + ICardDatabaseParser::clearSetlist(); loadStatus = NotLoaded; @@ -438,13 +377,8 @@ void CardDatabase::addCard(CardInfoPtr card) // if card already exists just add the new set property if (cards.contains(card->getName())) { CardInfoPtr sameCard = cards[card->getName()]; - for (auto set : card->getSets()) { - QString setName = set->getCorrectedShortName(); - sameCard->setSet(set); - sameCard->setMuId(setName, card->getMuId(setName)); - sameCard->setUuId(setName, card->getUuId(setName)); - sameCard->setRarity(setName, card->getRarity(setName)); - sameCard->setSetNumber(setName, card->getCollectorNumber(setName)); + for (const CardInfoPerSet &set : card->getSets()) { + sameCard->addToSet(set.getPtr(), set); } return; } @@ -585,7 +519,7 @@ LoadStatus CardDatabase::loadCardDatabases() // load custom card databases QDir dir(settingsCache->getCustomCardDatabasePath()); - for (QString fileName : + for (const QString &fileName : dir.entryList(QStringList("*.xml"), QDir::Files | QDir::Readable, QDir::Name | QDir::IgnoreCase)) { loadCardDatabase(dir.absoluteFilePath(fileName)); } @@ -617,20 +551,13 @@ void CardDatabase::refreshCachedReverseRelatedCards() continue; } - QString relatedCardName; - if (card->getPowTough().size() > 0) { - relatedCardName = card->getPowTough() + " " + card->getName(); // "n/n name" - } else { - relatedCardName = card->getName(); // "name" - } - foreach (CardRelation *cardRelation, card->getReverseRelatedCards()) { const QString &targetCard = cardRelation->getName(); if (!cards.contains(targetCard)) { continue; } - auto *newCardRelation = new CardRelation(relatedCardName, cardRelation->getDoesAttach(), + auto *newCardRelation = new CardRelation(card->getName(), cardRelation->getDoesAttach(), cardRelation->getIsCreateAllExclusion(), cardRelation->getIsVariable(), cardRelation->getDefaultCount()); cards.value(targetCard)->addReverseRelatedCards2Me(newCardRelation); @@ -638,23 +565,6 @@ void CardDatabase::refreshCachedReverseRelatedCards() } } -QStringList CardDatabase::getAllColors() const -{ - QSet colors; - QHashIterator cardIterator(cards); - while (cardIterator.hasNext()) { - const QStringList &cardColors = cardIterator.next().value()->getColors(); - if (cardColors.isEmpty()) { - colors.insert("X"); - } else { - for (int i = 0; i < cardColors.size(); ++i) { - colors.insert(cardColors[i]); - } - } - } - return colors.toList(); -} - QStringList CardDatabase::getAllMainCardTypes() const { QSet types; @@ -720,8 +630,8 @@ bool CardDatabase::saveCustomTokensToFile() tmpSets.insert(CardDatabase::TOKENS_SETNAME, customTokensSet); CardNameMap tmpCards; - for (CardInfoPtr card : cards) { - if (card->getSets().contains(customTokensSet)) { + for (const CardInfoPtr &card : cards) { + if (card->getSets().contains(CardDatabase::TOKENS_SETNAME)) { tmpCards.insert(card->getName(), card); } } @@ -746,4 +656,46 @@ void CardInfo::resetReverseRelatedCards2Me() cardRelation->deleteLater(); } reverseRelatedCardsToMe = QList(); +} + +// Back-compatibility methods. Remove ASAP +const QString CardInfo::getCardType() const +{ + return getProperty(Mtg::CardType); +} +void CardInfo::setCardType(const QString &value) +{ + setProperty(Mtg::CardType, value); +} +const QString CardInfo::getCmc() const +{ + return getProperty(Mtg::ConvertedManaCost); +} +const QString CardInfo::getColors() const +{ + return getProperty(Mtg::Colors); +} +void CardInfo::setColors(const QString &value) +{ + setProperty(Mtg::Colors, value); +} +const QString CardInfo::getLoyalty() const +{ + return getProperty(Mtg::Loyalty); +} +const QString CardInfo::getMainCardType() const +{ + return getProperty(Mtg::MainCardType); +} +const QString CardInfo::getManaCost() const +{ + return getProperty(Mtg::ManaCost); +} +const QString CardInfo::getPowTough() const +{ + return getProperty(Mtg::PowTough); +} +void CardInfo::setPowTough(const QString &value) +{ + setProperty(Mtg::PowTough, value); } \ No newline at end of file diff --git a/cockatrice/src/carddatabase.h b/cockatrice/src/carddatabase.h index 8aa89e0b2..c5cf0ea23 100644 --- a/cockatrice/src/carddatabase.h +++ b/cockatrice/src/carddatabase.h @@ -9,10 +9,13 @@ #include #include #include +#include #include +#include class CardDatabase; class CardInfo; +class CardInfoPerSet; class CardSet; class CardRelation; class ICardDatabaseParser; @@ -21,6 +24,7 @@ typedef QMap QStringMap; typedef QMap MuidMap; typedef QSharedPointer CardInfoPtr; typedef QSharedPointer CardSetPtr; +typedef QMap CardInfoPerSetMap; Q_DECLARE_METATYPE(CardInfoPtr) @@ -112,178 +116,162 @@ public: QStringList getUnknownSetsNames(); }; +class CardInfoPerSet +{ +public: + explicit CardInfoPerSet(const CardSetPtr &_set = QSharedPointer(nullptr)); + ~CardInfoPerSet() = default; + +private: + CardSetPtr set; + // per-set card properties; + QVariantHash properties; + +public: + const CardSetPtr getPtr() const + { + return set; + } + const QStringList getProperties() const + { + return properties.keys(); + } + const QString getProperty(const QString &propertyName) const + { + return properties.value(propertyName).toString(); + } + void setProperty(const QString &_name, const QString &_value) + { + properties.insert(_name, _value); + } +}; + class CardInfo : public QObject { Q_OBJECT private: CardInfoPtr smartThis; + // The card name QString name; - - /* - * The name without punctuation or capitalization, for better card tag name - * recognition. - */ + // The name without punctuation or capitalization, for better card name recognition. QString simpleName; - - bool isToken; - SetList sets; - QString manacost; - QString cmc; - QString cardtype; - QString powtough; + // The key used to identify this card in the cache + QString pixmapCacheKey; + // card text QString text; - QStringList colors; - + // whether this is not a "real" card but a token + bool isToken; + // basic card properties; common for all the sets + QVariantHash properties; // the cards i'm related to QList relatedCards; - // the card i'm reverse-related to QList reverseRelatedCards; - // the cards thare are reverse-related to me QList reverseRelatedCardsToMe; - + // card sets + CardInfoPerSetMap sets; + // cached set names QString setsNames; - - bool upsideDownArt; - QString loyalty; - QStringMap customPicURLs; - MuidMap muIds; - QStringMap uuIds; - QStringMap collectorNumbers; - QStringMap rarities; + // positioning properties; used by UI bool cipt; int tableRow; - QString pixmapCacheKey; + bool upsideDownArt; public: explicit CardInfo(const QString &_name = QString(), - bool _isToken = false, - const QString &_manacost = QString(), - const QString &_cmc = QString(), - const QString &_cardtype = QString(), - const QString &_powtough = QString(), const QString &_text = QString(), - const QStringList &_colors = QStringList(), + bool _isToken = false, + QVariantHash _properties = QVariantHash(), const QList &_relatedCards = QList(), const QList &_reverseRelatedCards = QList(), - bool _upsideDownArt = false, - const QString &_loyalty = QString(), + CardInfoPerSetMap _sets = CardInfoPerSetMap(), bool _cipt = false, int _tableRow = 0, - const SetList &_sets = SetList(), - const QStringMap &_customPicURLs = QStringMap(), - MuidMap _muids = MuidMap(), - QStringMap _uuIds = QStringMap(), - QStringMap _collectorNumbers = QStringMap(), - QStringMap _rarities = QStringMap()); + bool _upsideDownArt = false); ~CardInfo() override; static CardInfoPtr newInstance(const QString &_name = QString(), - bool _isToken = false, - const QString &_manacost = QString(), - const QString &_cmc = QString(), - const QString &_cardtype = QString(), - const QString &_powtough = QString(), const QString &_text = QString(), - const QStringList &_colors = QStringList(), + bool _isToken = false, + QVariantHash _properties = QVariantHash(), const QList &_relatedCards = QList(), const QList &_reverseRelatedCards = QList(), - bool _upsideDownArt = false, - const QString &_loyalty = QString(), + CardInfoPerSetMap _sets = CardInfoPerSetMap(), bool _cipt = false, int _tableRow = 0, - const SetList &_sets = SetList(), - const QStringMap &_customPicURLs = QStringMap(), - MuidMap _muids = MuidMap(), - QStringMap _uuIds = QStringMap(), - QStringMap _collectorNumbers = QStringMap(), - QStringMap _rarities = QStringMap()); + bool _upsideDownArt = false); void setSmartPointer(CardInfoPtr _ptr) { - smartThis = _ptr; + smartThis = std::move(_ptr); } + // basic properties inline const QString &getName() const { return name; } - inline const QString &getSetsNames() const - { - return setsNames; - } const QString &getSimpleName() const { return simpleName; } - bool getIsToken() const - { - return isToken; - } - const SetList &getSets() const - { - return sets; - } - inline const QString &getManaCost() const - { - return manacost; - } - inline const QString &getCmc() const - { - return cmc; - } - inline const QString &getCardType() const - { - return cardtype; - } - inline const QString &getPowTough() const - { - return powtough; - } - const QString &getText() const - { - return text; - } const QString &getPixmapCacheKey() const { return pixmapCacheKey; } - const QString &getLoyalty() const + + const QString &getText() const { - return loyalty; - } - bool getCipt() const - { - return cipt; - } - // void setManaCost(const QString &_manaCost) { manacost = _manaCost; emit cardInfoChanged(smartThis); } - // void setCmc(const QString &_cmc) { cmc = _cmc; emit cardInfoChanged(smartThis); } - void setCardType(const QString &_cardType) - { - cardtype = _cardType; - emit cardInfoChanged(smartThis); - } - void setPowTough(const QString &_powTough) - { - powtough = _powTough; - emit cardInfoChanged(smartThis); + return text; } void setText(const QString &_text) { text = _text; emit cardInfoChanged(smartThis); } - void setColors(const QStringList &_colors) + + bool getIsToken() const { - colors = _colors; + return isToken; + } + const QStringList getProperties() const + { + return properties.keys(); + } + const QString getProperty(const QString &propertyName) const + { + return properties.value(propertyName).toString(); + } + void setProperty(const QString &_name, const QString &_value) + { + properties.insert(_name, _value); emit cardInfoChanged(smartThis); } - const QChar getColorChar() const; - const QStringList &getColors() const + const CardInfoPerSetMap &getSets() const { - return colors; + return sets; } + const QString &getSetsNames() const + { + return setsNames; + } + const QString getSetProperty(const QString &setName, const QString &propertyName) const + { + if (!sets.contains(setName)) + return ""; + return sets[setName].getProperty(propertyName); + } + void setSetProperty(const QString &setName, const QString &_name, const QString &_value) + { + if (!sets.contains(setName)) + return; + + sets[setName].setProperty(_name, _value); + emit cardInfoChanged(smartThis); + } + + // related cards const QList &getRelatedCards() const { return relatedCards; @@ -301,36 +289,12 @@ public: { reverseRelatedCardsToMe.append(cardRelation); } - bool getUpsideDownArt() const + + // positioning + bool getCipt() const { - return upsideDownArt; + return cipt; } - QString getCustomPicURL(const QString &set) const - { - return customPicURLs.value(set); - } - int getMuId(const QString &set) const - { - return muIds.value(set); - } - QString getUuId(const QString &set) const - { - return uuIds.value(set); - } - QString getCollectorNumber(const QString &set) const - { - return collectorNumbers.value(set); - } - QString getRarity(const QString &set) const - { - return rarities.value(set); - } - QStringMap getRarities() const - { - return rarities; - } - QString getMainCardType() const; - QString getCorrectedName() const; int getTableRow() const { return tableRow; @@ -339,31 +303,31 @@ public: { tableRow = _tableRow; } - // void setLoyalty(int _loyalty) { loyalty = _loyalty; emit cardInfoChanged(smartThis); } - // void setCustomPicURL(const QString &_set, const QString &_customPicURL) { customPicURLs.insert(_set, - // _customPicURL); } - void setSet(const CardSetPtr &_set) + bool getUpsideDownArt() const { - sets.append(_set); - refreshCachedSetNames(); + return upsideDownArt; } - void setMuId(const QString &_set, const int &_muId) + const QChar getColorChar() const; + + // Back-compatibility methods. Remove ASAP + const QString getCardType() const; + void setCardType(const QString &value); + const QString getCmc() const; + const QString getColors() const; + void setColors(const QString &value); + const QString getLoyalty() const; + const QString getMainCardType() const; + const QString getManaCost() const; + const QString getPowTough() const; + void setPowTough(const QString &value); + + // methods using per-set properties + QString getCustomPicURL(const QString &set) const { - muIds.insert(_set, _muId); + return getSetProperty(set, "picurl"); } - void setUuId(const QString &_set, const QString &_uuId) - { - uuIds.insert(_set, _uuId); - } - void setSetNumber(const QString &_set, const QString &_setNumber) - { - collectorNumbers.insert(_set, _setNumber); - } - void setRarity(const QString &_set, const QString &_setNumber) - { - rarities.insert(_set, _setNumber); - } - void addToSet(CardSetPtr set); + QString getCorrectedName() const; + void addToSet(const CardSetPtr &_set, CardInfoPerSet _info = CardInfoPerSet()); void emitPixmapUpdated() { emit pixmapUpdated(); @@ -450,7 +414,6 @@ public: SetList getSetList() const; LoadStatus loadFromFile(const QString &fileName); bool saveCustomTokensToFile(); - QStringList getAllColors() const; QStringList getAllMainCardTypes() const; LoadStatus getLoadStatus() const { @@ -517,4 +480,4 @@ public: return defaultCount; } }; -#endif \ No newline at end of file +#endif diff --git a/cockatrice/src/carddatabasemodel.cpp b/cockatrice/src/carddatabasemodel.cpp index faa8c3a85..9ae855786 100644 --- a/cockatrice/src/carddatabasemodel.cpp +++ b/cockatrice/src/carddatabasemodel.cpp @@ -14,9 +14,7 @@ CardDatabaseModel::CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromE cardDatabaseEnabledSetsChanged(); } -CardDatabaseModel::~CardDatabaseModel() -{ -} +CardDatabaseModel::~CardDatabaseModel() = default; QMap CardDatabaseDisplayModel::characterTranslation = {{L'“', L'\"'}, {L'”', L'\"'}, @@ -53,7 +51,7 @@ QVariant CardDatabaseModel::data(const QModelIndex &index, int role) const case PTColumn: return card->getPowTough(); case ColorColumn: - return card->getColors().join(""); + return card->getColors(); default: return QVariant(); } @@ -97,8 +95,8 @@ bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(CardInfoPtr card) if (!showOnlyCardsFromEnabledSets) return true; - for (CardSetPtr set : card->getSets()) { - if (set->getEnabled()) + for (const auto &set : card->getSets()) { + if (set.getPtr()->getEnabled()) return true; } diff --git a/cockatrice/src/carddatabasemodel.h b/cockatrice/src/carddatabasemodel.h index 5400c1f4a..8e8cb67c4 100644 --- a/cockatrice/src/carddatabasemodel.h +++ b/cockatrice/src/carddatabasemodel.h @@ -26,12 +26,12 @@ public: { SortRole = Qt::UserRole }; - CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent = 0); - ~CardDatabaseModel(); - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex &parent = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + CardDatabaseModel(CardDatabase *_db, bool _showOnlyCardsFromEnabledSets, QObject *parent = nullptr); + ~CardDatabaseModel() override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; CardDatabase *getDatabase() const { return db; @@ -77,7 +77,7 @@ private: static QMap characterTranslation; public: - CardDatabaseDisplayModel(QObject *parent = 0); + explicit CardDatabaseDisplayModel(QObject *parent = nullptr); void setFilterTree(FilterTree *filterTree); void setIsToken(FilterBool _isToken) { @@ -119,15 +119,15 @@ public: invalidate(); } void clearFilterAll(); - int rowCount(const QModelIndex &parent = QModelIndex()) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; protected: - bool lessThan(const QModelIndex &left, const QModelIndex &right) const; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; static int lessThanNumerically(const QString &left, const QString &right); - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; bool rowMatchesCardName(CardInfoPtr info) const; - bool canFetchMore(const QModelIndex &parent) const; - void fetchMore(const QModelIndex &parent); + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; private slots: void filterTreeChanged(); /** Will translate all undesirable characters in DIRTYNAME according to the TABLE. */ @@ -138,11 +138,11 @@ class TokenDisplayModel : public CardDatabaseDisplayModel { Q_OBJECT public: - TokenDisplayModel(QObject *parent = 0); - int rowCount(const QModelIndex &parent = QModelIndex()) const; + explicit TokenDisplayModel(QObject *parent = nullptr); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; protected: - bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; }; #endif diff --git a/cockatrice/src/carddbparser/carddatabaseparser.cpp b/cockatrice/src/carddbparser/carddatabaseparser.cpp new file mode 100644 index 000000000..0048c9584 --- /dev/null +++ b/cockatrice/src/carddbparser/carddatabaseparser.cpp @@ -0,0 +1,27 @@ +#include "carddatabaseparser.h" + +SetNameMap ICardDatabaseParser::sets; + +void ICardDatabaseParser::clearSetlist() +{ + sets.clear(); +} + +CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName, + const QString &longName, + const QString &setType, + const QDate &releaseDate) +{ + if (sets.contains(setName)) { + return sets.value(setName); + } + + CardSetPtr newSet = CardSet::newInstance(setName); + newSet->setLongName(longName); + newSet->setSetType(setType); + newSet->setReleaseDate(releaseDate); + + sets.insert(setName, newSet); + emit addSet(newSet); + return newSet; +} \ No newline at end of file diff --git a/cockatrice/src/carddbparser/carddatabaseparser.h b/cockatrice/src/carddbparser/carddatabaseparser.h index d094b6d79..e8b46f95d 100644 --- a/cockatrice/src/carddbparser/carddatabaseparser.h +++ b/cockatrice/src/carddbparser/carddatabaseparser.h @@ -9,15 +9,27 @@ class ICardDatabaseParser : public QObject { public: - virtual ~ICardDatabaseParser() - { - } + ~ICardDatabaseParser() override = default; + virtual bool getCanParseFile(const QString &name, QIODevice &device) = 0; virtual void parseFile(QIODevice &device) = 0; virtual bool saveToFile(SetNameMap sets, CardNameMap cards, const QString &fileName) = 0; - virtual void clearSetlist() = 0; + static void clearSetlist(); + +protected: + /* + * A cached list of the available sets, needed to cross-reference sets from cards. + * Shared between all parsers + */ + static SetNameMap sets; + + CardSetPtr internalAddSet(const QString &setName, + const QString &longName = "", + const QString &setType = "", + const QDate &releaseDate = QDate()); signals: virtual void addCard(CardInfoPtr card) = 0; + virtual void addSet(CardSetPtr set) = 0; }; Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser") diff --git a/cockatrice/src/carddbparser/cockatricexml3.cpp b/cockatrice/src/carddbparser/cockatricexml3.cpp index 6a6300cf4..b63352772 100644 --- a/cockatrice/src/carddbparser/cockatricexml3.cpp +++ b/cockatrice/src/carddbparser/cockatricexml3.cpp @@ -61,30 +61,6 @@ void CockatriceXml3Parser::parseFile(QIODevice &device) } } -CardSetPtr CockatriceXml3Parser::internalAddSet(const QString &setName, - const QString &longName, - const QString &setType, - const QDate &releaseDate) -{ - if (sets.contains(setName)) { - return sets.value(setName); - } - - CardSetPtr newSet = CardSet::newInstance(setName); - newSet->setLongName(longName); - newSet->setSetType(setType); - newSet->setReleaseDate(releaseDate); - - sets.insert(setName, newSet); - emit addSet(newSet); - return newSet; -} - -void CockatriceXml3Parser::clearSetlist() -{ - sets.clear(); -} - void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml) { while (!xml.atEnd()) { @@ -120,6 +96,44 @@ void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml) } } +QString CockatriceXml3Parser::getMainCardType(QString &type) +{ + QString result = type; + /* + Legendary Artifact Creature - Golem + Instant // Instant + */ + + int pos; + if ((pos = result.indexOf('-')) != -1) { + result.remove(pos, result.length()); + } + + if ((pos = result.indexOf("—")) != -1) { + result.remove(pos, result.length()); + } + + if ((pos = result.indexOf("//")) != -1) { + result.remove(pos, result.length()); + } + + result = result.simplified(); + /* + Legendary Artifact Creature + Instant + */ + + if ((pos = result.lastIndexOf(' ')) != -1) { + result = result.mid(pos + 1); + } + /* + Creature + Instant + */ + + return result; +} + void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) { while (!xml.atEnd()) { @@ -128,59 +142,77 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) } if (xml.name() == "card") { - QString name, manacost, cmc, type, pt, text, loyalty; - QStringList colors; + QString name = QString(""); + QString text = QString(""); + QVariantHash properties = QVariantHash(); + QString colors = QString(""); QList relatedCards, reverseRelatedCards; - QStringMap customPicURLs; - MuidMap muids; - QStringMap uuids, collectorNumbers, rarities; - SetList sets; + CardInfoPerSetMap sets = CardInfoPerSetMap(); int tableRow = 0; bool cipt = false; bool isToken = false; bool upsideDown = false; + while (!xml.atEnd()) { if (xml.readNext() == QXmlStreamReader::EndElement) { break; } - + // variable - assigned properties if (xml.name() == "name") { name = xml.readElementText(); - } else if (xml.name() == "manacost") { - manacost = xml.readElementText(); - } else if (xml.name() == "cmc") { - cmc = xml.readElementText(); - } else if (xml.name() == "type") { - type = xml.readElementText(); - } else if (xml.name() == "pt") { - pt = xml.readElementText(); } else if (xml.name() == "text") { text = xml.readElementText(); + } else if (xml.name() == "color") { + colors.append(xml.readElementText()); + } else if (xml.name() == "token") { + isToken = static_cast(xml.readElementText().toInt()); + // generic properties + } else if (xml.name() == "manacost") { + properties.insert("manacost", xml.readElementText()); + } else if (xml.name() == "cmc") { + properties.insert("cmc", xml.readElementText()); + } else if (xml.name() == "type") { + QString type = xml.readElementText(); + properties.insert("type", type); + properties.insert("maintype", getMainCardType(type)); + } else if (xml.name() == "pt") { + properties.insert("pt", xml.readElementText()); + } else if (xml.name() == "loyalty") { + properties.insert("loyalty", xml.readElementText()); + // positioning info + } else if (xml.name() == "tablerow") { + tableRow = xml.readElementText().toInt(); + } else if (xml.name() == "cipt") { + cipt = (xml.readElementText() == "1"); + } else if (xml.name() == "upsidedown") { + upsideDown = (xml.readElementText() == "1"); + // sets } else if (xml.name() == "set") { + // NOTE: attributes must be read before readElementText() QXmlStreamAttributes attrs = xml.attributes(); QString setName = xml.readElementText(); - sets.append(internalAddSet(setName)); + CardInfoPerSet setInfo(internalAddSet(setName)); if (attrs.hasAttribute("muId")) { - muids[setName] = attrs.value("muId").toString().toInt(); + setInfo.setProperty("muid", attrs.value("muId").toString()); } if (attrs.hasAttribute("muId")) { - uuids[setName] = attrs.value("uuId").toString(); + setInfo.setProperty("uuid", attrs.value("uuId").toString()); } if (attrs.hasAttribute("picURL")) { - customPicURLs[setName] = attrs.value("picURL").toString(); + setInfo.setProperty("picurl", attrs.value("picURL").toString()); } if (attrs.hasAttribute("num")) { - collectorNumbers[setName] = attrs.value("num").toString(); + setInfo.setProperty("num", attrs.value("num").toString()); } if (attrs.hasAttribute("rarity")) { - rarities[setName] = attrs.value("rarity").toString(); + setInfo.setProperty("rarity", attrs.value("rarity").toString()); } - } else if (xml.name() == "color") { - colors << xml.readElementText(); + sets.insert(setName, setInfo); + // relatd cards } else if (xml.name() == "related" || xml.name() == "reverse-related") { bool attach = false; bool exclude = false; @@ -217,16 +249,6 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) } else { relatedCards << relation; } - } else if (xml.name() == "tablerow") { - tableRow = xml.readElementText().toInt(); - } else if (xml.name() == "cipt") { - cipt = (xml.readElementText() == "1"); - } else if (xml.name() == "upsidedown") { - upsideDown = (xml.readElementText() == "1"); - } else if (xml.name() == "loyalty") { - loyalty = xml.readElementText(); - } else if (xml.name() == "token") { - isToken = static_cast(xml.readElementText().toInt()); } else if (xml.name() != "") { qDebug() << "[CockatriceXml3Parser] Unknown card property" << xml.name() << ", trying to continue anyway"; @@ -234,9 +256,9 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml) } } - CardInfoPtr newCard = CardInfo::newInstance( - name, isToken, manacost, cmc, type, pt, text, colors, relatedCards, reverseRelatedCards, upsideDown, - loyalty, cipt, tableRow, sets, customPicURLs, muids, uuids, collectorNumbers, rarities); + properties.insert("colors", colors); + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, sets, cipt, tableRow, upsideDown); emit addCard(newCard); } } @@ -266,38 +288,60 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in return xml; } - xml.writeStartElement("card"); - xml.writeTextElement("name", info->getName()); - - const SetList &sets = info->getSets(); QString tmpString; - QString tmpSet; - for (int i = 0; i < sets.size(); i++) { + + xml.writeStartElement("card"); + + // variable - assigned properties + xml.writeTextElement("name", info->getName()); + xml.writeTextElement("text", info->getText()); + if (info->getIsToken()) { + xml.writeTextElement("token", "1"); + } + + // generic properties + xml.writeTextElement("manacost", info->getProperty("manacost")); + xml.writeTextElement("cmc", info->getProperty("cmc")); + xml.writeTextElement("type", info->getProperty("type")); + + int colorSize = info->getColors().size(); + for (int i = 0; i < colorSize; ++i) { + xml.writeTextElement("color", info->getColors().at(i)); + } + + tmpString = info->getProperty("pt"); + if (!tmpString.isEmpty()) { + xml.writeTextElement("pt", tmpString); + } + + tmpString = info->getProperty("loyalty"); + if (!tmpString.isEmpty()) { + xml.writeTextElement("loyalty", tmpString); + } + + // sets + const CardInfoPerSetMap sets = info->getSets(); + for (CardInfoPerSet set : sets) { xml.writeStartElement("set"); + xml.writeAttribute("rarity", set.getProperty("rarity")); + xml.writeAttribute("muId", set.getProperty("muid")); + xml.writeAttribute("uuId", set.getProperty("uuid")); - tmpSet = sets[i]->getShortName(); - xml.writeAttribute("rarity", info->getRarity(tmpSet)); - xml.writeAttribute("muId", QString::number(info->getMuId(tmpSet))); - xml.writeAttribute("uuId", info->getUuId(tmpSet)); - - tmpString = info->getCollectorNumber(tmpSet); + tmpString = set.getProperty("num"); if (!tmpString.isEmpty()) { - xml.writeAttribute("num", info->getCollectorNumber(tmpSet)); + xml.writeAttribute("num", tmpString); } - tmpString = info->getCustomPicURL(tmpSet); + tmpString = set.getProperty("picurl"); if (!tmpString.isEmpty()) { xml.writeAttribute("picURL", tmpString); } - xml.writeCharacters(tmpSet); + xml.writeCharacters(set.getPtr()->getShortName()); xml.writeEndElement(); } - const QStringList &colors = info->getColors(); - for (int i = 0; i < colors.size(); i++) { - xml.writeTextElement("color", colors[i]); - } + // related cards const QList related = info->getRelatedCards(); for (auto i : related) { xml.writeStartElement("related"); @@ -343,23 +387,12 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in xml.writeCharacters(i->getName()); xml.writeEndElement(); } - xml.writeTextElement("manacost", info->getManaCost()); - xml.writeTextElement("cmc", info->getCmc()); - xml.writeTextElement("type", info->getCardType()); - if (!info->getPowTough().isEmpty()) { - xml.writeTextElement("pt", info->getPowTough()); - } + + // positioning xml.writeTextElement("tablerow", QString::number(info->getTableRow())); - xml.writeTextElement("text", info->getText()); - if (info->getMainCardType() == "Planeswalker") { - xml.writeTextElement("loyalty", info->getLoyalty()); - } if (info->getCipt()) { xml.writeTextElement("cipt", "1"); } - if (info->getIsToken()) { - xml.writeTextElement("token", "1"); - } if (info->getUpsideDownArt()) { xml.writeTextElement("upsidedown", "1"); } diff --git a/cockatrice/src/carddbparser/cockatricexml3.h b/cockatrice/src/carddbparser/cockatricexml3.h index 286ee3af1..109832cf4 100644 --- a/cockatrice/src/carddbparser/cockatricexml3.h +++ b/cockatrice/src/carddbparser/cockatricexml3.h @@ -11,27 +11,18 @@ class CockatriceXml3Parser : public ICardDatabaseParser Q_INTERFACES(ICardDatabaseParser) public: CockatriceXml3Parser() = default; - ~CockatriceXml3Parser() = default; - bool getCanParseFile(const QString &name, QIODevice &device); - void parseFile(QIODevice &device); - bool saveToFile(SetNameMap sets, CardNameMap cards, const QString &fileName); - void clearSetlist(); + ~CockatriceXml3Parser() override = default; + bool getCanParseFile(const QString &name, QIODevice &device) override; + void parseFile(QIODevice &device) override; + bool saveToFile(SetNameMap sets, CardNameMap cards, const QString &fileName) override; private: - /* - * A cached list of the available sets, needed to cross-reference sets from cards. - */ - SetNameMap sets; - - CardSetPtr internalAddSet(const QString &setName, - const QString &longName = "", - const QString &setType = "", - const QDate &releaseDate = QDate()); void loadCardsFromXml(QXmlStreamReader &xml); void loadSetsFromXml(QXmlStreamReader &xml); + QString getMainCardType(QString &type); signals: - void addCard(CardInfoPtr card); - void addSet(CardSetPtr set); + void addCard(CardInfoPtr card) override; + void addSet(CardSetPtr set) override; }; #endif \ No newline at end of file diff --git a/cockatrice/src/carddbparser/cockatricexml4.cpp b/cockatrice/src/carddbparser/cockatricexml4.cpp new file mode 100644 index 000000000..a3b2adf00 --- /dev/null +++ b/cockatrice/src/carddbparser/cockatricexml4.cpp @@ -0,0 +1,362 @@ +#include "cockatricexml4.h" + +#include +#include +#include + +#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase" +#define COCKATRICE_XML4_TAGVER 4 + +bool CockatriceXml4Parser::getCanParseFile(const QString &fileName, QIODevice &device) +{ + qDebug() << "[CockatriceXml4Parser] Trying to parse: " << fileName; + + if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) { + qDebug() << "[CockatriceXml4Parser] Parsing failed: wrong extension"; + return false; + } + + QXmlStreamReader xml(&device); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::StartElement) { + if (xml.name() == COCKATRICE_XML4_TAGNAME) { + int version = xml.attributes().value("version").toString().toInt(); + if (version == COCKATRICE_XML4_TAGVER) { + return true; + } else { + qDebug() << "[CockatriceXml4Parser] Parsing failed: wrong version" << version; + return false; + } + + } else { + qDebug() << "[CockatriceXml4Parser] Parsing failed: wrong element tag" << xml.name(); + return false; + } + } + } + + return true; +} + +void CockatriceXml4Parser::parseFile(QIODevice &device) +{ + QXmlStreamReader xml(&device); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::StartElement) { + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name() == "sets") { + loadSetsFromXml(xml); + } else if (xml.name() == "cards") { + loadCardsFromXml(xml); + } else if (xml.name() != "") { + qDebug() << "[CockatriceXml4Parser] Unknown item" << xml.name() << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + } + } +} + +void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name() == "set") { + QString shortName, longName, setType; + QDate releaseDate; + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name() == "name") { + shortName = xml.readElementText(); + } else if (xml.name() == "longname") { + longName = xml.readElementText(); + } else if (xml.name() == "settype") { + setType = xml.readElementText(); + } else if (xml.name() == "releasedate") { + releaseDate = QDate::fromString(xml.readElementText(), Qt::ISODate); + } else if (xml.name() != "") { + qDebug() << "[CockatriceXml4Parser] Unknown set property" << xml.name() + << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + + internalAddSet(shortName, longName, setType, releaseDate); + } + } +} + +QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml) +{ + QVariantHash properties = QVariantHash(); + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name() != "") { + properties.insert(xml.name().toString(), xml.readElementText()); + } + } + return properties; +} + +void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name() == "card") { + QString name = QString(""); + QString text = QString(""); + QVariantHash properties = QVariantHash(); + QList relatedCards, reverseRelatedCards; + CardInfoPerSetMap sets = CardInfoPerSetMap(); + int tableRow = 0; + bool cipt = false; + bool isToken = false; + bool upsideDown = false; + + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + // variable - assigned properties + if (xml.name() == "name") { + name = xml.readElementText(); + } else if (xml.name() == "text") { + text = xml.readElementText(); + } else if (xml.name() == "token") { + isToken = static_cast(xml.readElementText().toInt()); + // generic properties + } else if (xml.name() == "prop") { + properties = loadCardPropertiesFromXml(xml); + // positioning info + } else if (xml.name() == "tablerow") { + tableRow = xml.readElementText().toInt(); + } else if (xml.name() == "cipt") { + cipt = (xml.readElementText() == "1"); + } else if (xml.name() == "upsidedown") { + upsideDown = (xml.readElementText() == "1"); + // sets + } else if (xml.name() == "set") { + // NOTE: attributes but be read before readElementText() + QXmlStreamAttributes attrs = xml.attributes(); + QString setName = xml.readElementText(); + CardInfoPerSet setInfo(internalAddSet(setName)); + for (QXmlStreamAttribute attr : attrs) { + setInfo.setProperty(attr.name().toString(), attr.value().toString()); + } + sets.insert(setName, setInfo); + // relatd cards + } else if (xml.name() == "related" || xml.name() == "reverse-related") { + bool attach = false; + bool exclude = false; + bool variable = false; + int count = 1; + QXmlStreamAttributes attrs = xml.attributes(); + QString cardName = xml.readElementText(); + if (attrs.hasAttribute("count")) { + if (attrs.value("count").toString().indexOf("x=") == 0) { + variable = true; + count = attrs.value("count").toString().remove(0, 2).toInt(); + } else if (attrs.value("count").toString().indexOf("x") == 0) { + variable = true; + } else { + count = attrs.value("count").toString().toInt(); + } + + if (count < 1) { + count = 1; + } + } + + if (attrs.hasAttribute("attach")) { + attach = true; + } + + if (attrs.hasAttribute("exclude")) { + exclude = true; + } + + auto *relation = new CardRelation(cardName, attach, exclude, variable, count); + if (xml.name() == "reverse-related") { + reverseRelatedCards << relation; + } else { + relatedCards << relation; + } + } else if (xml.name() != "") { + qDebug() << "[CockatriceXml4Parser] Unknown card property" << xml.name() + << ", trying to continue anyway"; + xml.skipCurrentElement(); + } + } + + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, + reverseRelatedCards, sets, cipt, tableRow, upsideDown); + emit addCard(newCard); + } + } +} + +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set) +{ + if (set.isNull()) { + qDebug() << "&operator<< set is nullptr"; + return xml; + } + + xml.writeStartElement("set"); + xml.writeTextElement("name", set->getShortName()); + xml.writeTextElement("longname", set->getLongName()); + xml.writeTextElement("settype", set->getSetType()); + xml.writeTextElement("releasedate", set->getReleaseDate().toString(Qt::ISODate)); + xml.writeEndElement(); + + return xml; +} + +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &info) +{ + if (info.isNull()) { + qDebug() << "operator<< info is nullptr"; + return xml; + } + + QString tmpString; + + xml.writeStartElement("card"); + + // variable - assigned properties + xml.writeTextElement("name", info->getName()); + xml.writeTextElement("text", info->getText()); + if (info->getIsToken()) { + xml.writeTextElement("token", "1"); + } + + // generic properties + xml.writeStartElement("prop"); + for (QString propName : info->getProperties()) { + xml.writeTextElement(propName, info->getProperty(propName)); + } + xml.writeEndElement(); + + // sets + for (CardInfoPerSet set : info->getSets()) { + xml.writeStartElement("set"); + for (QString propName : set.getProperties()) { + xml.writeAttribute(propName, set.getProperty(propName)); + } + + xml.writeCharacters(set.getPtr()->getShortName()); + xml.writeEndElement(); + } + + // related cards + const QList related = info->getRelatedCards(); + for (auto i : related) { + xml.writeStartElement("related"); + if (i->getDoesAttach()) { + xml.writeAttribute("attach", "attach"); + } + if (i->getIsCreateAllExclusion()) { + xml.writeAttribute("exclude", "exclude"); + } + + if (i->getIsVariable()) { + if (1 == i->getDefaultCount()) { + xml.writeAttribute("count", "x"); + } else { + xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount())); + } + } else if (1 != i->getDefaultCount()) { + xml.writeAttribute("count", QString::number(i->getDefaultCount())); + } + xml.writeCharacters(i->getName()); + xml.writeEndElement(); + } + const QList reverseRelated = info->getReverseRelatedCards(); + for (auto i : reverseRelated) { + xml.writeStartElement("reverse-related"); + if (i->getDoesAttach()) { + xml.writeAttribute("attach", "attach"); + } + + if (i->getIsCreateAllExclusion()) { + xml.writeAttribute("exclude", "exclude"); + } + + if (i->getIsVariable()) { + if (1 == i->getDefaultCount()) { + xml.writeAttribute("count", "x"); + } else { + xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount())); + } + } else if (1 != i->getDefaultCount()) { + xml.writeAttribute("count", QString::number(i->getDefaultCount())); + } + xml.writeCharacters(i->getName()); + xml.writeEndElement(); + } + + // positioning + xml.writeTextElement("tablerow", QString::number(info->getTableRow())); + if (info->getCipt()) { + xml.writeTextElement("cipt", "1"); + } + if (info->getUpsideDownArt()) { + xml.writeTextElement("upsidedown", "1"); + } + + xml.writeEndElement(); // card + + return xml; +} + +bool CockatriceXml4Parser::saveToFile(SetNameMap sets, CardNameMap cards, const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + return false; + } + + QXmlStreamWriter xml(&file); + + xml.setAutoFormatting(true); + xml.writeStartDocument(); + xml.writeStartElement(COCKATRICE_XML4_TAGNAME); + xml.writeAttribute("version", QString::number(COCKATRICE_XML4_TAGVER)); + + if (sets.count() > 0) { + xml.writeStartElement("sets"); + for (CardSetPtr set : sets) { + xml << set; + } + xml.writeEndElement(); + } + + if (cards.count() > 0) { + xml.writeStartElement("cards"); + for (CardInfoPtr card : cards) { + xml << card; + } + xml.writeEndElement(); + } + + xml.writeEndElement(); // cockatrice_carddatabase + xml.writeEndDocument(); + + return true; +} \ No newline at end of file diff --git a/cockatrice/src/carddbparser/cockatricexml4.h b/cockatrice/src/carddbparser/cockatricexml4.h new file mode 100644 index 000000000..cc37c4f37 --- /dev/null +++ b/cockatrice/src/carddbparser/cockatricexml4.h @@ -0,0 +1,28 @@ +#ifndef COCKATRICE_XML4_H +#define COCKATRICE_XML4_H + +#include + +#include "carddatabaseparser.h" + +class CockatriceXml4Parser : public ICardDatabaseParser +{ + Q_OBJECT + Q_INTERFACES(ICardDatabaseParser) +public: + CockatriceXml4Parser() = default; + ~CockatriceXml4Parser() override = default; + bool getCanParseFile(const QString &name, QIODevice &device) override; + void parseFile(QIODevice &device) override; + bool saveToFile(SetNameMap sets, CardNameMap cards, const QString &fileName) override; + +private: + QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml); + void loadCardsFromXml(QXmlStreamReader &xml); + void loadSetsFromXml(QXmlStreamReader &xml); +signals: + void addCard(CardInfoPtr card) override; + void addSet(CardSetPtr set) override; +}; + +#endif \ No newline at end of file diff --git a/cockatrice/src/cardframe.cpp b/cockatrice/src/cardframe.cpp index eecbc3286..fd1d08730 100644 --- a/cockatrice/src/cardframe.cpp +++ b/cockatrice/src/cardframe.cpp @@ -1,3 +1,5 @@ +#include + #include "cardframe.h" #include "cardinfopicture.h" @@ -16,6 +18,7 @@ CardFrame::CardFrame(const QString &cardName, QWidget *parent) : QTabWidget(pare pic->setObjectName("pic"); text = new CardInfoText(); text->setObjectName("text"); + connect(text, SIGNAL(linkActivated(const QString &)), this, SLOT(setCard(const QString &))); tab1 = new QWidget(this); tab2 = new QWidget(this); @@ -93,10 +96,10 @@ void CardFrame::setCard(CardInfoPtr card) disconnect(info.data(), nullptr, this, nullptr); } - info = card; + info = std::move(card); if (info) { - connect(info.data(), SIGNAL(destroyed()), this, SLOT(clear())); + connect(info.data(), SIGNAL(destroyed()), this, SLOT(clearCard())); } text->setCard(info); @@ -115,7 +118,7 @@ void CardFrame::setCard(AbstractCardItem *card) } } -void CardFrame::clear() +void CardFrame::clearCard() { setCard((CardInfoPtr) nullptr); } diff --git a/cockatrice/src/cardframe.h b/cockatrice/src/cardframe.h index cf8b2ad00..5023fd70c 100644 --- a/cockatrice/src/cardframe.h +++ b/cockatrice/src/cardframe.h @@ -37,7 +37,7 @@ public slots: void setCard(CardInfoPtr card); void setCard(const QString &cardName); void setCard(AbstractCardItem *card); - void clear(); + void clearCard(); void setViewMode(int mode); }; diff --git a/cockatrice/src/cardinfotext.cpp b/cockatrice/src/cardinfotext.cpp index 9f3157513..448b276b1 100644 --- a/cockatrice/src/cardinfotext.cpp +++ b/cockatrice/src/cardinfotext.cpp @@ -1,137 +1,83 @@ #include "cardinfotext.h" - #include "carditem.h" +#include "game_specific_terms.h" #include "main.h" + #include #include #include CardInfoText::CardInfoText(QWidget *parent) : QFrame(parent), info(nullptr) { - nameLabel1 = new QLabel; - nameLabel2 = new QLabel; - nameLabel2->setWordWrap(true); - nameLabel2->setTextInteractionFlags(Qt::TextBrowserInteraction); - manacostLabel1 = new QLabel; - manacostLabel2 = new QLabel; - manacostLabel2->setWordWrap(true); - manacostLabel2->setTextInteractionFlags(Qt::TextBrowserInteraction); - colorLabel1 = new QLabel; - colorLabel2 = new QLabel; - colorLabel2->setWordWrap(true); - colorLabel2->setTextInteractionFlags(Qt::TextBrowserInteraction); - cardtypeLabel1 = new QLabel; - cardtypeLabel2 = new QLabel; - cardtypeLabel2->setWordWrap(true); - cardtypeLabel2->setTextInteractionFlags(Qt::TextBrowserInteraction); - powtoughLabel1 = new QLabel; - powtoughLabel2 = new QLabel; - powtoughLabel2->setTextInteractionFlags(Qt::TextBrowserInteraction); - loyaltyLabel1 = new QLabel; - loyaltyLabel2 = new QLabel; - loyaltyLabel1->setTextInteractionFlags(Qt::TextBrowserInteraction); + nameLabel = new QLabel; + nameLabel->setOpenExternalLinks(false); + connect(nameLabel, SIGNAL(linkActivated(const QString &)), this, SIGNAL(linkActivated(const QString &))); textLabel = new QTextEdit(); textLabel->setReadOnly(true); QGridLayout *grid = new QGridLayout(this); - int row = 0; - grid->addWidget(nameLabel1, row, 0); - grid->addWidget(nameLabel2, row++, 1); - grid->addWidget(manacostLabel1, row, 0); - grid->addWidget(manacostLabel2, row++, 1); - grid->addWidget(colorLabel1, row, 0); - grid->addWidget(colorLabel2, row++, 1); - grid->addWidget(cardtypeLabel1, row, 0); - grid->addWidget(cardtypeLabel2, row++, 1); - grid->addWidget(powtoughLabel1, row, 0); - grid->addWidget(powtoughLabel2, row++, 1); - grid->addWidget(loyaltyLabel1, row, 0); - grid->addWidget(loyaltyLabel2, row++, 1); - grid->addWidget(textLabel, row, 0, -1, 2); - grid->setRowStretch(row, 1); + grid->addWidget(nameLabel, 0, 0); + grid->addWidget(textLabel, 1, 0, -1, 2); + grid->setRowStretch(1, 1); grid->setColumnStretch(1, 1); retranslateUi(); } -// Reset every label which is optionally hidden -void CardInfoText::resetLabels() -{ - nameLabel1->show(); - nameLabel2->show(); - manacostLabel1->show(); - manacostLabel2->show(); - colorLabel1->show(); - colorLabel2->show(); - cardtypeLabel1->show(); - cardtypeLabel2->show(); - powtoughLabel1->show(); - powtoughLabel2->show(); - loyaltyLabel1->show(); - loyaltyLabel2->show(); - textLabel->show(); -} + void CardInfoText::setCard(CardInfoPtr card) { - if (card) { - resetLabels(); - nameLabel2->setText(card->getName()); - if (!card->getManaCost().isEmpty()) { - manacostLabel2->setText(card->getManaCost()); - } else { - manacostLabel1->hide(); - manacostLabel2->hide(); - } - if (!card->getColors().isEmpty()) { - colorLabel2->setText(card->getColors().join("")); - } else { - colorLabel2->setText("Colorless"); - } - cardtypeLabel2->setText(card->getCardType()); - if (!card->getPowTough().isEmpty()) { - powtoughLabel2->setText(card->getPowTough()); - } else { - powtoughLabel1->hide(); - powtoughLabel2->hide(); - } - if (!card->getLoyalty().isEmpty()) { - loyaltyLabel2->setText(card->getLoyalty()); - } else { - loyaltyLabel1->hide(); - loyaltyLabel2->hide(); - } - textLabel->setText(card->getText()); - } else { - nameLabel1->hide(); - nameLabel2->hide(); - manacostLabel1->hide(); - manacostLabel2->hide(); - colorLabel1->hide(); - colorLabel2->hide(); - cardtypeLabel1->hide(); - cardtypeLabel2->hide(); - powtoughLabel1->hide(); - powtoughLabel2->hide(); - loyaltyLabel1->hide(); - loyaltyLabel2->hide(); - textLabel->hide(); + if (card == nullptr) { + nameLabel->setText(""); + textLabel->setText(""); + return; } + + QString text = ""; + text += QString("") + .arg(tr("Name:"), card->getName().toHtmlEscaped()); + + QStringList cardProps = card->getProperties(); + foreach (QString key, cardProps) { + QString keyText = Mtg::getNicePropertyName(key).toHtmlEscaped() + ":"; + text += + QString("").arg(keyText, card->getProperty(key).toHtmlEscaped()); + } + + auto relatedCards = card->getRelatedCards(); + auto reverserelatedCards2Me = card->getReverseRelatedCards2Me(); + if (relatedCards.size() || reverserelatedCards2Me.size()) { + text += QString(""; + } + + text += "
%1%2
%1%2
%1").arg(tr("Related cards:")); + + for (int i = 0; i < relatedCards.size(); ++i) { + QString tmp = relatedCards.at(i)->getName().toHtmlEscaped(); + text += "" + tmp + "
"; + } + + for (int i = 0; i < reverserelatedCards2Me.size(); ++i) { + QString tmp = reverserelatedCards2Me.at(i)->getName().toHtmlEscaped(); + text += "" + tmp + "
"; + } + + text += "
"; + nameLabel->setText(text); + textLabel->setText(card->getText()); } void CardInfoText::setInvalidCardName(const QString &cardName) { - nameLabel1->setText(tr("Unknown card:")); - nameLabel1->show(); - nameLabel2->setText(cardName); - nameLabel2->show(); + nameLabel->setText(tr("Unknown card:") + " " + cardName); + textLabel->setText(""); } void CardInfoText::retranslateUi() { - nameLabel1->setText(tr("Name:")); - manacostLabel1->setText(tr("Mana cost:")); - colorLabel1->setText(tr("Color(s):")); - cardtypeLabel1->setText(tr("Card type:")); - powtoughLabel1->setText(tr("P / T:")); - loyaltyLabel1->setText(tr("Loyalty:")); + /* + * There's no way we can really translate the text currently being rendered. + * The best we can do is invalidate the current text. + */ + setInvalidCardName(""); } diff --git a/cockatrice/src/cardinfotext.h b/cockatrice/src/cardinfotext.h index 49445c09f..7ee0a890a 100644 --- a/cockatrice/src/cardinfotext.h +++ b/cockatrice/src/cardinfotext.h @@ -12,23 +12,17 @@ class CardInfoText : public QFrame Q_OBJECT private: - QLabel *nameLabel1, *nameLabel2; - QLabel *manacostLabel1, *manacostLabel2; - QLabel *colorLabel1, *colorLabel2; - QLabel *cardtypeLabel1, *cardtypeLabel2; - QLabel *powtoughLabel1, *powtoughLabel2; - QLabel *loyaltyLabel1, *loyaltyLabel2; + QLabel *nameLabel; QTextEdit *textLabel; - CardInfoPtr info; - void resetLabels(); - public: CardInfoText(QWidget *parent = 0); void retranslateUi(); void setInvalidCardName(const QString &cardName); +signals: + void linkActivated(const QString &link); public slots: void setCard(CardInfoPtr card); }; diff --git a/cockatrice/src/cardinfowidget.cpp b/cockatrice/src/cardinfowidget.cpp index 170540df9..4bf8e94e7 100644 --- a/cockatrice/src/cardinfowidget.cpp +++ b/cockatrice/src/cardinfowidget.cpp @@ -1,6 +1,8 @@ -#include "cardinfowidget.h" +#include + #include "cardinfopicture.h" #include "cardinfotext.h" +#include "cardinfowidget.h" #include "carditem.h" #include "main.h" #include @@ -14,8 +16,9 @@ CardInfoWidget::CardInfoWidget(const QString &cardName, QWidget *parent, Qt::Win pic->setObjectName("pic"); text = new CardInfoText(); text->setObjectName("text"); + connect(text, SIGNAL(linkActivated(const QString &)), this, SLOT(setCard(const QString &))); - QVBoxLayout *layout = new QVBoxLayout(); + auto *layout = new QVBoxLayout(); layout->setObjectName("layout"); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); @@ -26,7 +29,7 @@ CardInfoWidget::CardInfoWidget(const QString &cardName, QWidget *parent, Qt::Win setFrameStyle(QFrame::Panel | QFrame::Raised); QDesktopWidget desktopWidget; int pixmapHeight = desktopWidget.screenGeometry().height() / 3; - int pixmapWidth = pixmapHeight / aspectRatio; + int pixmapWidth = static_cast(pixmapHeight / aspectRatio); pic->setFixedWidth(pixmapWidth); pic->setFixedHeight(pixmapHeight); setFixedWidth(pixmapWidth + 150); @@ -41,7 +44,7 @@ void CardInfoWidget::setCard(CardInfoPtr card) { if (info) disconnect(info.data(), nullptr, this, nullptr); - info = card; + info = std::move(card); if (info) connect(info.data(), SIGNAL(destroyed()), this, SLOT(clear())); diff --git a/cockatrice/src/cardinfowidget.h b/cockatrice/src/cardinfowidget.h index f89ca504e..7e986b75c 100644 --- a/cockatrice/src/cardinfowidget.h +++ b/cockatrice/src/cardinfowidget.h @@ -22,7 +22,7 @@ private: CardInfoText *text; public: - CardInfoWidget(const QString &cardName, QWidget *parent = 0, Qt::WindowFlags f = 0); + explicit CardInfoWidget(const QString &cardName, QWidget *parent = nullptr, Qt::WindowFlags f = nullptr); public slots: void setCard(CardInfoPtr card); diff --git a/cockatrice/src/decklistmodel.cpp b/cockatrice/src/decklistmodel.cpp index 8b6e7bf54..7b66a927c 100644 --- a/cockatrice/src/decklistmodel.cpp +++ b/cockatrice/src/decklistmodel.cpp @@ -322,9 +322,7 @@ QModelIndex DeckListModel::addCard(const QString &cardName, const QString &zoneN // This is usually called from tab_deck_editor // So we'll create a new CardInfo with the name // and default values for all fields - info = CardInfo::newInstance(cardName, false, nullptr, nullptr, "unknown", nullptr, nullptr, QStringList(), - QList(), QList(), false, 0, false, 0, - SetList(), QStringMap(), MuidMap(), QStringMap(), QStringMap(), QStringMap()); + info = CardInfo::newInstance(cardName); } else { return {}; } diff --git a/cockatrice/src/decklistmodel.h b/cockatrice/src/decklistmodel.h index 5e4f4dd30..19a0aecad 100644 --- a/cockatrice/src/decklistmodel.h +++ b/cockatrice/src/decklistmodel.h @@ -7,7 +7,6 @@ class DeckLoader; class CardDatabase; -class QProgressDialog; class QPrinter; class QTextCursor; @@ -21,19 +20,19 @@ public: : AbstractDecklistCardNode(_parent), dataNode(_dataNode) { } - int getNumber() const + int getNumber() const override { return dataNode->getNumber(); } - void setNumber(int _number) + void setNumber(int _number) override { dataNode->setNumber(_number); } - QString getName() const + QString getName() const override { return dataNode->getName(); } - void setName(const QString &_name) + void setName(const QString &_name) override { dataNode->setName(_name); } @@ -54,20 +53,20 @@ signals: void deckHashChanged(); public: - DeckListModel(QObject *parent = 0); - ~DeckListModel(); - int rowCount(const QModelIndex &parent) const; - int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const; - QVariant data(const QModelIndex &index, int role) const; - QVariant headerData(int section, Qt::Orientation orientation, int role) const; - QModelIndex index(int row, int column, const QModelIndex &parent) const; - QModelIndex parent(const QModelIndex &index) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - bool setData(const QModelIndex &index, const QVariant &value, int role); - bool removeRows(int row, int count, const QModelIndex &parent); + explicit DeckListModel(QObject *parent = nullptr); + ~DeckListModel() override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex & /*parent*/ = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &index) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + bool removeRows(int row, int count, const QModelIndex &parent) override; QModelIndex findCard(const QString &cardName, const QString &zoneName) const; QModelIndex addCard(const QString &cardName, const QString &zoneName, bool abAddAnyway = false); - void sort(int column, Qt::SortOrder order); + void sort(int column, Qt::SortOrder order) override; void cleanList(); DeckLoader *getDeckList() const { diff --git a/cockatrice/src/dlg_edit_tokens.cpp b/cockatrice/src/dlg_edit_tokens.cpp index be910ff97..81ce5875f 100644 --- a/cockatrice/src/dlg_edit_tokens.cpp +++ b/cockatrice/src/dlg_edit_tokens.cpp @@ -17,7 +17,7 @@ #include #include -DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(0) +DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(nullptr) { nameLabel = new QLabel(tr("&Name:")); nameEdit = new QLineEdit; @@ -46,7 +46,7 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(0) annotationLabel->setBuddy(annotationEdit); connect(annotationEdit, SIGNAL(textChanged(QString)), this, SLOT(annotationChanged(QString))); - QGridLayout *grid = new QGridLayout; + auto *grid = new QGridLayout; grid->addWidget(nameLabel, 0, 0); grid->addWidget(nameEdit, 0, 1); grid->addWidget(colorLabel, 1, 0); @@ -89,15 +89,15 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(0) aRemoveToken->setIcon(QPixmap("theme:icons/decrement")); connect(aRemoveToken, SIGNAL(triggered()), this, SLOT(actRemoveToken())); - QToolBar *databaseToolBar = new QToolBar; + auto *databaseToolBar = new QToolBar; databaseToolBar->addAction(aAddToken); databaseToolBar->addAction(aRemoveToken); - QVBoxLayout *leftVBox = new QVBoxLayout; + auto *leftVBox = new QVBoxLayout; leftVBox->addWidget(chooseTokenView); leftVBox->addWidget(databaseToolBar); - QHBoxLayout *hbox = new QHBoxLayout; + auto *hbox = new QHBoxLayout; hbox->addLayout(leftVBox); hbox->addWidget(tokenDataGroupBox); @@ -105,7 +105,7 @@ DlgEditTokens::DlgEditTokens(QWidget *parent) : QDialog(parent), currentCard(0) connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); - QVBoxLayout *mainLayout = new QVBoxLayout; + auto *mainLayout = new QVBoxLayout; mainLayout->addLayout(hbox); mainLayout->addWidget(buttonBox); @@ -154,9 +154,10 @@ void DlgEditTokens::actAddToken() } } while (askAgain); - CardInfoPtr card = CardInfo::newInstance(name, true); - card->addToSet(databaseModel->getDatabase()->getSet(CardDatabase::TOKENS_SETNAME)); + CardInfoPtr card = CardInfo::newInstance(name, "", true); card->setCardType("Token"); + card->addToSet(databaseModel->getDatabase()->getSet(CardDatabase::TOKENS_SETNAME)); + databaseModel->getDatabase()->addCard(card); } @@ -172,7 +173,7 @@ void DlgEditTokens::actRemoveToken() void DlgEditTokens::colorChanged(int colorIndex) { if (currentCard) - currentCard->setColors(QStringList() << QString(colorEdit->itemData(colorIndex).toChar())); + currentCard->setColors(QString(colorEdit->itemData(colorIndex).toChar())); } void DlgEditTokens::ptChanged(const QString &_pt) diff --git a/cockatrice/src/dlg_edit_tokens.h b/cockatrice/src/dlg_edit_tokens.h index 6e9b36660..4f3f5eaf4 100644 --- a/cockatrice/src/dlg_edit_tokens.h +++ b/cockatrice/src/dlg_edit_tokens.h @@ -35,7 +35,7 @@ private: QTreeView *chooseTokenView; public: - DlgEditTokens(QWidget *parent = nullptr); + explicit DlgEditTokens(QWidget *parent = nullptr); }; #endif diff --git a/cockatrice/src/filtertree.cpp b/cockatrice/src/filtertree.cpp index 93abf774c..1dcb37cf8 100644 --- a/cockatrice/src/filtertree.cpp +++ b/cockatrice/src/filtertree.cpp @@ -187,11 +187,8 @@ bool FilterItem::acceptColor(const CardInfoPtr info) const */ int match_count = 0; for (auto &it : converted_term) { - for (auto i = info->getColors().constBegin(); i != info->getColors().constEnd(); i++) { - if ((*i).contains(it, Qt::CaseInsensitive)) { - match_count++; - } - } + if (info->getColors().contains(it, Qt::CaseInsensitive)) + match_count++; } return match_count == converted_term.length(); @@ -205,9 +202,9 @@ bool FilterItem::acceptText(const CardInfoPtr info) const bool FilterItem::acceptSet(const CardInfoPtr info) const { bool status = false; - for (auto i = info->getSets().constBegin(); i != info->getSets().constEnd(); i++) { - if ((*i)->getShortName().compare(term, Qt::CaseInsensitive) == 0 || - (*i)->getLongName().compare(term, Qt::CaseInsensitive) == 0) { + for (const auto &set : info->getSets()) { + if (set.getPtr()->getShortName().compare(term, Qt::CaseInsensitive) == 0 || + set.getPtr()->getLongName().compare(term, Qt::CaseInsensitive) == 0) { status = true; break; } @@ -299,7 +296,7 @@ bool FilterItem::acceptRarity(const CardInfoPtr info) const /* * The purpose of this loop is to only apply one of the replacement - * policies and then escape. If we attempt to layer them ontop of + * policies and then escape. If we attempt to layer them on top of * each other, we will get awkward results (i.e. comythic rare mythic rareon) * Conditional statement will exit once a case is successful in * replacement OR we go through all possible cases. @@ -334,8 +331,8 @@ bool FilterItem::acceptRarity(const CardInfoPtr info) const } } - for (const QString &rareLevel : info->getRarities()) { - if (rareLevel.compare(converted_term, Qt::CaseInsensitive) == 0) { + for (const auto &set : info->getSets()) { + if (set.getProperty("rarity").compare(converted_term, Qt::CaseInsensitive) == 0) { return true; } } diff --git a/cockatrice/src/filtertree.h b/cockatrice/src/filtertree.h index 043bc95c5..7227f9442 100644 --- a/cockatrice/src/filtertree.h +++ b/cockatrice/src/filtertree.h @@ -1,12 +1,14 @@ + + #ifndef FILTERTREE_H #define FILTERTREE_H +#include "carddatabase.h" +#include "cardfilter.h" #include #include #include - -#include "carddatabase.h" -#include "cardfilter.h" +#include class FilterTreeNode { @@ -33,11 +35,11 @@ public: } virtual FilterTreeNode *parent() const { - return NULL; + return nullptr; } virtual FilterTreeNode *nodeAt(int /* i */) const { - return NULL; + return nullptr; } virtual void deleteAt(int /* i */) { @@ -52,7 +54,7 @@ public: } virtual int index() const { - return (parent() != NULL) ? parent()->childIndex(this) : -1; + return (parent() != nullptr) ? parent()->childIndex(this) : -1; } virtual const QString text() const { @@ -64,27 +66,27 @@ public: } virtual void nodeChanged() const { - if (parent() != NULL) + if (parent() != nullptr) parent()->nodeChanged(); } virtual void preInsertChild(const FilterTreeNode *p, int i) const { - if (parent() != NULL) + if (parent() != nullptr) parent()->preInsertChild(p, i); } virtual void postInsertChild(const FilterTreeNode *p, int i) const { - if (parent() != NULL) + if (parent() != nullptr) parent()->postInsertChild(p, i); } virtual void preRemoveChild(const FilterTreeNode *p, int i) const { - if (parent() != NULL) + if (parent() != nullptr) parent()->preRemoveChild(p, i); } virtual void postRemoveChild(const FilterTreeNode *p, int i) const { - if (parent() != NULL) + if (parent() != nullptr) parent()->postRemoveChild(p, i); } }; @@ -96,13 +98,13 @@ protected: public: virtual ~FilterTreeBranch(); - FilterTreeNode *nodeAt(int i) const; - void deleteAt(int i); - int childCount() const + FilterTreeNode *nodeAt(int i) const override; + void deleteAt(int i) override; + int childCount() const override { return childNodes.size(); } - int childIndex(const FilterTreeNode *node) const; + int childIndex(const FilterTreeNode *node) const override; }; class FilterItemList; @@ -121,8 +123,8 @@ public: } const FilterItemList *findTypeList(CardFilter::Type type) const; FilterItemList *typeList(CardFilter::Type type); - FilterTreeNode *parent() const; - const QString text() const + FilterTreeNode *parent() const override; + const QString text() const override { return CardFilter::attrName(attr); } @@ -144,21 +146,21 @@ public: { return p->attr; } - FilterTreeNode *parent() const + FilterTreeNode *parent() const override { return p; } int termIndex(const QString &term) const; FilterTreeNode *termNode(const QString &term); - const QString text() const + const QString text() const override { return CardFilter::typeName(type); } - bool testTypeAnd(const CardInfoPtr info, CardFilter::Attr attr) const; - bool testTypeAndNot(const CardInfoPtr info, CardFilter::Attr attr) const; - bool testTypeOr(const CardInfoPtr info, CardFilter::Attr attr) const; - bool testTypeOrNot(const CardInfoPtr info, CardFilter::Attr attr) const; + bool testTypeAnd(CardInfoPtr info, CardFilter::Attr attr) const; + bool testTypeAndNot(CardInfoPtr info, CardFilter::Attr attr) const; + bool testTypeOr(CardInfoPtr info, CardFilter::Attr attr) const; + bool testTypeOrNot(CardInfoPtr info, CardFilter::Attr attr) const; }; class FilterItem : public FilterTreeNode @@ -169,10 +171,10 @@ private: public: const QString term; - FilterItem(QString trm, FilterItemList *parent) : p(parent), term(trm) + FilterItem(QString trm, FilterItemList *parent) : p(parent), term(std::move(trm)) { } - virtual ~FilterItem(){}; + virtual ~FilterItem() = default; CardFilter::Attr attr() const { @@ -182,30 +184,30 @@ public: { return p->type; } - FilterTreeNode *parent() const + FilterTreeNode *parent() const override { return p; } - const QString text() const + const QString text() const override { return term; } - bool isLeaf() const + bool isLeaf() const override { return true; } - bool acceptName(const CardInfoPtr info) const; - bool acceptType(const CardInfoPtr info) const; - bool acceptColor(const CardInfoPtr info) const; - bool acceptText(const CardInfoPtr info) const; - bool acceptSet(const CardInfoPtr info) const; - bool acceptManaCost(const CardInfoPtr info) const; - bool acceptCmc(const CardInfoPtr info) const; - bool acceptPowerToughness(const CardInfoPtr info, CardFilter::Attr attr) const; - bool acceptLoyalty(const CardInfoPtr info) const; - bool acceptRarity(const CardInfoPtr info) const; - bool acceptCardAttr(const CardInfoPtr info, CardFilter::Attr attr) const; + bool acceptName(CardInfoPtr info) const; + bool acceptType(CardInfoPtr info) const; + bool acceptColor(CardInfoPtr info) const; + bool acceptText(CardInfoPtr info) const; + bool acceptSet(CardInfoPtr info) const; + bool acceptManaCost(CardInfoPtr info) const; + bool acceptCmc(CardInfoPtr info) const; + bool acceptPowerToughness(CardInfoPtr info, CardFilter::Attr attr) const; + bool acceptLoyalty(CardInfoPtr info) const; + bool acceptRarity(CardInfoPtr info) const; + bool acceptCardAttr(CardInfoPtr info, CardFilter::Attr attr) const; bool relationCheck(int cardInfo) const; }; @@ -224,47 +226,47 @@ private: LogicMap *attrLogicMap(CardFilter::Attr attr); FilterItemList *attrTypeList(CardFilter::Attr attr, CardFilter::Type type); - bool testAttr(const CardInfoPtr info, const LogicMap *lm) const; + bool testAttr(CardInfoPtr info, const LogicMap *lm) const; - void nodeChanged() const + void nodeChanged() const override { emit changed(); } - void preInsertChild(const FilterTreeNode *p, int i) const + void preInsertChild(const FilterTreeNode *p, int i) const override { emit preInsertRow(p, i); } - void postInsertChild(const FilterTreeNode *p, int i) const + void postInsertChild(const FilterTreeNode *p, int i) const override { emit postInsertRow(p, i); } - void preRemoveChild(const FilterTreeNode *p, int i) const + void preRemoveChild(const FilterTreeNode *p, int i) const override { emit preRemoveRow(p, i); } - void postRemoveChild(const FilterTreeNode *p, int i) const + void postRemoveChild(const FilterTreeNode *p, int i) const override { emit postRemoveRow(p, i); } public: FilterTree(); - ~FilterTree(); + ~FilterTree() override; int findTermIndex(CardFilter::Attr attr, CardFilter::Type type, const QString &term); int findTermIndex(const CardFilter *f); FilterTreeNode *termNode(CardFilter::Attr attr, CardFilter::Type type, const QString &term); FilterTreeNode *termNode(const CardFilter *f); FilterTreeNode *attrTypeNode(CardFilter::Attr attr, CardFilter::Type type); - const QString text() const + const QString text() const override { return QString("root"); } - int index() const + int index() const override { return 0; } - bool acceptsCard(const CardInfoPtr info) const; + bool acceptsCard(CardInfoPtr info) const; void clear(); }; diff --git a/cockatrice/src/game_specific_terms.h b/cockatrice/src/game_specific_terms.h new file mode 100644 index 000000000..9e3d03868 --- /dev/null +++ b/cockatrice/src/game_specific_terms.h @@ -0,0 +1,49 @@ +#ifndef GAME_SPECIFIC_TERMS_H +#define GAME_SPECIFIC_TERMS_H + +#include +#include + +/* + * Collection of traslatable property names used in games, + * so we can use Game::Property instead of hardcoding strings. + * Note: Mtg = "Maybe that game" + */ + +namespace Mtg +{ +QString const CardType("type"); +QString const ConvertedManaCost("cmc"); +QString const Colors("colors"); +QString const Loyalty("loyalty"); +QString const MainCardType("maintype"); +QString const ManaCost("manacost"); +QString const PowTough("pt"); +QString const Side("side"); +QString const Layout("layout"); + +inline static const QString getNicePropertyName(QString key) +{ + if (key == CardType) + return QCoreApplication::translate("Mtg", "Card type"); + if (key == ConvertedManaCost) + return QCoreApplication::translate("Mtg", "Converted mana cost"); + if (key == Colors) + return QCoreApplication::translate("Mtg", "Color(s)"); + if (key == Loyalty) + return QCoreApplication::translate("Mtg", "Loyalty"); + if (key == MainCardType) + return QCoreApplication::translate("Mtg", "Main card type"); + if (key == ManaCost) + return QCoreApplication::translate("Mtg", "Mana cost"); + if (key == PowTough) + return QCoreApplication::translate("Mtg", "P / T"); + if (key == Side) + return QCoreApplication::translate("Mtg", "Side"); + if (key == Layout) + return QCoreApplication::translate("Mtg", "Layout"); + return key; +} +}; // namespace Mtg + +#endif \ No newline at end of file diff --git a/cockatrice/src/handle_public_servers.cpp b/cockatrice/src/handle_public_servers.cpp index 11411ccdc..7bbbbeb2e 100644 --- a/cockatrice/src/handle_public_servers.cpp +++ b/cockatrice/src/handle_public_servers.cpp @@ -1,6 +1,6 @@ #include "handle_public_servers.h" -#include "qt-json/json.h" #include "settingscache.h" +#include #include #include #include @@ -31,19 +31,16 @@ void HandlePublicServers::actFinishParsingDownloadedData() savedHostList = uci.getServerInfo(); // Downloaded data from GitHub - bool jsonSuccessful; - QString jsonData = QString(reply->readAll()); - - auto jsonMap = QtJson::Json::parse(jsonData, jsonSuccessful).toMap(); - - if (jsonSuccessful) { + QJsonParseError parseError{}; + QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError); + if (parseError.error == QJsonParseError::NoError) { + QVariantMap jsonMap = jsonResponse.toVariant().toMap(); updateServerINISettings(jsonMap); } else { qDebug() << "[PUBLIC SERVER HANDLER]" - << "JSON Parsing Error"; + << "JSON Parsing Error:" << parseError.errorString(); emit sigPublicServersDownloadedUnsuccessfully(errorCode); } - } else { qDebug() << "[PUBLIC SERVER HANDLER]" << "Error Downloading Public Servers" << errorCode; diff --git a/cockatrice/src/pictureloader.cpp b/cockatrice/src/pictureloader.cpp index 252fab180..cecca1de0 100644 --- a/cockatrice/src/pictureloader.cpp +++ b/cockatrice/src/pictureloader.cpp @@ -30,7 +30,9 @@ PictureToLoad::PictureToLoad(CardInfoPtr _card) : card(std::move(_card)) urlTemplates = settingsCache->downloads().getAllURLs(); if (card) { - sortedSets = card->getSets(); + for (const auto &set : card->getSets()) { + sortedSets << set.getPtr(); + } qSort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator()); // The first time called, nextSet will also populate the Urls for the first set. nextSet(); @@ -240,28 +242,50 @@ QString PictureToLoad::transformUrl(const QString &urlTemplate) const CardSetPtr set = getCurrentSet(); QMap transformMap = QMap(); - + // name transformMap["!name!"] = card->getName(); transformMap["!name_lower!"] = card->getName().toLower(); transformMap["!corrected_name!"] = card->getCorrectedName(); transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower(); + // card properties + QRegExp rxCardProp("!prop:([^!]+)!"); + int pos = 0; + while ((pos = rxCardProp.indexIn(transformedUrl, pos)) != -1) { + QString propertyName = rxCardProp.cap(1); + pos += rxCardProp.matchedLength(); + QString propertyValue = card->getProperty(propertyName); + if (propertyValue.isEmpty()) { + qDebug() << "PictureLoader: [card: " << card->getName() << " set: " << getSetName() + << "]: Requested property (" << propertyName << ") for Url template (" << urlTemplate + << ") is not available"; + return QString(); + } else { + transformMap["!prop:" + propertyName + "!"] = propertyValue; + } + } + if (set) { - transformMap["!cardid!"] = QString::number(card->getMuId(set->getShortName())); - transformMap["!uuid!"] = card->getUuId(set->getShortName()); - transformMap["!collectornumber!"] = card->getCollectorNumber(set->getShortName()); transformMap["!setcode!"] = set->getShortName(); transformMap["!setcode_lower!"] = set->getShortName().toLower(); transformMap["!setname!"] = set->getLongName(); transformMap["!setname_lower!"] = set->getLongName().toLower(); - } else { - transformMap["!cardid!"] = QString(); - transformMap["!uuid!"] = QString(); - transformMap["!collectornumber!"] = QString(); - transformMap["!setcode!"] = QString(); - transformMap["!setcode_lower!"] = QString(); - transformMap["!setname!"] = QString(); - transformMap["!setname_lower!"] = QString(); + + QRegExp rxSetProp("!set:([^!]+)!"); + pos = 0; // Defined above + while ((pos = rxSetProp.indexIn(transformedUrl, pos)) != -1) { + QString propertyName = rxSetProp.cap(1); + pos += rxSetProp.matchedLength(); + QString propertyValue = card->getSetProperty(set->getShortName(), propertyName); + if (propertyValue.isEmpty()) { + qDebug() << "PictureLoader: [card: " << card->getName() << " set: " << getSetName() + << "]: Requested set property (" << propertyName << ") for Url template (" << urlTemplate + << ") is not available"; + return QString(); + } else { + transformMap["!set:" + propertyName + "!"] = propertyValue; + } + } } for (const QString &prop : transformMap.keys()) { @@ -483,7 +507,7 @@ void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size) return; } - // search for an exact size copy of the picure in cache + // search for an exact size copy of the picture in cache QString key = card->getPixmapCacheKey(); QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height()); if (QPixmapCache::find(sizeKey, &pixmap)) diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index 1b1386b35..65cdda5f2 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -94,7 +94,7 @@ void PlayerArea::setSize(qreal width, qreal height) Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_parent) : QObject(_parent), game(_parent), shortcutsActive(false), defaultNumberTopCards(1), defaultNumberTopCardsToPlaceBelow(1), lastTokenDestroy(true), lastTokenTableRow(0), id(_id), active(false), - local(_local), mirrored(false), handVisible(false), conceded(false), dialogSemaphore(false), deck(0) + local(_local), mirrored(false), handVisible(false), conceded(false), dialogSemaphore(false), deck(nullptr) { userInfo = new ServerInfo_User; userInfo->CopyFrom(info); @@ -115,7 +115,7 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare qreal h = deck->boundingRect().width() + 5; - HandCounter *handCounter = new HandCounter(playerArea); + auto *handCounter = new HandCounter(playerArea); handCounter->setPos(base + QPointF(0, h + 10)); qreal h2 = handCounter->boundingRect().height(); @@ -279,8 +279,8 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare libraryMenu->addAction(aOpenDeckInDeckEditor); deck->setMenu(libraryMenu, aDrawCard); } else { - handMenu = 0; - libraryMenu = 0; + handMenu = nullptr; + libraryMenu = nullptr; } graveMenu = playerMenu->addMenu(QString()); @@ -356,19 +356,19 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare playerMenu->addSeparator(); playerMenu->addAction(aCardMenu); - for (int i = 0; i < playerLists.size(); ++i) { - QAction *newAction = playerLists[i]->addAction(QString()); + for (auto &playerList : playerLists) { + QAction *newAction = playerList->addAction(QString()); newAction->setData(-1); connect(newAction, SIGNAL(triggered()), this, SLOT(playerListActionTriggered())); allPlayersActions.append(newAction); - playerLists[i]->addSeparator(); + playerList->addSeparator(); } } else { - countersMenu = 0; - sbMenu = 0; - aCreateAnotherToken = 0; - createPredefinedTokenMenu = 0; - aCardMenu = 0; + countersMenu = nullptr; + sbMenu = nullptr; + aCreateAnotherToken = nullptr; + createPredefinedTokenMenu = nullptr; + aCardMenu = nullptr; } aTap = new QAction(this); @@ -436,11 +436,11 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare connect(aPlayFacedown, SIGNAL(triggered()), this, SLOT(actPlayFacedown())); for (int i = 0; i < 3; ++i) { - QAction *tempAddCounter = new QAction(this); + auto *tempAddCounter = new QAction(this); tempAddCounter->setData(9 + i * 1000); - QAction *tempRemoveCounter = new QAction(this); + auto *tempRemoveCounter = new QAction(this); tempRemoveCounter->setData(10 + i * 1000); - QAction *tempSetCounter = new QAction(this); + auto *tempSetCounter = new QAction(this); tempSetCounter->setData(11 + i * 1000); aAddCounter.append(tempAddCounter); aRemoveCounter.append(tempRemoveCounter); @@ -451,8 +451,8 @@ Player::Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_pare } const QList &players = game->getPlayers().values(); - for (int i = 0; i < players.size(); ++i) - addPlayer(players[i]); + for (auto player : players) + addPlayer(player); rearrangeZones(); retranslateUi(); @@ -494,8 +494,8 @@ void Player::addPlayer(Player *player) return; } - for (int i = 0; i < playerLists.size(); ++i) { - QAction *newAction = playerLists[i]->addAction(player->getName()); + for (auto &playerList : playerLists) { + QAction *newAction = playerList->addAction(player->getName()); newAction->setData(player->getId()); connect(newAction, SIGNAL(triggered()), this, SLOT(playerListActionTriggered())); } @@ -507,20 +507,20 @@ void Player::removePlayer(Player *player) return; } - for (int i = 0; i < playerLists.size(); ++i) { - QList actionList = playerLists[i]->actions(); - for (int j = 0; j < actionList.size(); ++j) - if (actionList[j]->data().toInt() == player->getId()) { - playerLists[i]->removeAction(actionList[j]); - actionList[j]->deleteLater(); + for (auto &playerList : playerLists) { + QList actionList = playerList->actions(); + for (auto &j : actionList) + if (j->data().toInt() == player->getId()) { + playerList->removeAction(j); + j->deleteLater(); } } } void Player::playerListActionTriggered() { - QAction *action = static_cast(sender()); - QMenu *menu = static_cast(action->parentWidget()); + auto *action = static_cast(sender()); + auto *menu = static_cast(action->parentWidget()); Command_RevealCards cmd; const int otherPlayerId = action->data().toInt(); @@ -533,9 +533,9 @@ void Player::playerListActionTriggered() } else if (menu == mRevealTopCard) { int decksize = zones.value("deck")->getCards().size(); bool ok; - int number = - QInputDialog::getInt(0, tr("Reveal top cards of library"), tr("Number of cards: (max. %1)").arg(decksize), - defaultNumberTopCards, 1, decksize, 1, &ok); + int number = QInputDialog::getInt(nullptr, tr("Reveal top cards of library"), + tr("Number of cards: (max. %1)").arg(decksize), defaultNumberTopCards, 1, + decksize, 1, &ok); if (ok) { cmd.set_zone_name("deck"); cmd.set_top_cards(number); @@ -692,8 +692,8 @@ void Player::retranslateUi() aCardMenu->setText(tr("C&ard")); - for (int i = 0; i < allPlayersActions.size(); ++i) - allPlayersActions[i]->setText(tr("&All players")); + for (auto &allPlayersAction : allPlayersActions) + allPlayersAction->setText(tr("&All players")); } aPlay->setText(tr("&Play")); @@ -904,8 +904,8 @@ void Player::actViewLibrary() void Player::actViewTopCards() { bool ok; - int number = QInputDialog::getInt(0, tr("View top cards of library"), tr("Number of cards:"), defaultNumberTopCards, - 1, 2000000000, 1, &ok); + int number = QInputDialog::getInt(nullptr, tr("View top cards of library"), tr("Number of cards:"), + defaultNumberTopCards, 1, 2000000000, 1, &ok); if (ok) { defaultNumberTopCards = number; static_cast(scene())->toggleZoneView(this, "deck", number); @@ -934,7 +934,7 @@ void Player::actViewGraveyard() void Player::actRevealRandomGraveyardCard() { Command_RevealCards cmd; - QAction *action = dynamic_cast(sender()); + auto *action = dynamic_cast(sender()); const int otherPlayerId = action->data().toInt(); if (otherPlayerId != -1) { cmd.set_player_id(otherPlayerId); @@ -973,10 +973,10 @@ void Player::actMulligan() void Player::actDrawCards() { - int number = QInputDialog::getInt(0, tr("Draw cards"), tr("Number:")); + int number = QInputDialog::getInt(nullptr, tr("Draw cards"), tr("Number:")); if (number) { Command_DrawCards cmd; - cmd.set_number(number); + cmd.set_number(static_cast(number)); sendGameCommand(cmd); } } @@ -988,7 +988,7 @@ void Player::actUndoDraw() void Player::actMoveTopCardToGrave() { - if (zones.value("deck")->getCards().size() == 0) { + if (zones.value("deck")->getCards().empty()) { return; } @@ -1005,7 +1005,7 @@ void Player::actMoveTopCardToGrave() void Player::actMoveTopCardToExile() { - if (zones.value("deck")->getCards().size() == 0) { + if (zones.value("deck")->getCards().empty()) { return; } @@ -1022,7 +1022,7 @@ void Player::actMoveTopCardToExile() void Player::actMoveTopCardsToGrave() { - int number = QInputDialog::getInt(0, tr("Move top cards to grave"), tr("Number:")); + int number = QInputDialog::getInt(nullptr, tr("Move top cards to grave"), tr("Number:")); if (!number) { return; } @@ -1048,7 +1048,7 @@ void Player::actMoveTopCardsToGrave() void Player::actMoveTopCardsToExile() { - int number = QInputDialog::getInt(0, tr("Move top cards to exile"), tr("Number:")); + int number = QInputDialog::getInt(nullptr, tr("Move top cards to exile"), tr("Number:")); if (!number) { return; } @@ -1131,7 +1131,7 @@ void Player::actRollDie() 1000, 1, &ok); if (ok) { Command_RollDie cmd; - cmd.set_sides(sides); + cmd.set_sides(static_cast(sides)); sendGameCommand(cmd); } } @@ -1148,7 +1148,7 @@ void Player::actCreateToken() CardInfoPtr correctedCard = db->getCardBySimpleName(lastTokenName); if (correctedCard) { lastTokenName = correctedCard->getName(); - lastTokenTableRow = table->clampValidTableRow(2 - correctedCard->getTableRow()); + lastTokenTableRow = TableZone::clampValidTableRow(2 - correctedCard->getTableRow()); if (lastTokenPT.isEmpty()) { lastTokenPT = correctedCard->getPowTough(); } @@ -1182,7 +1182,7 @@ void Player::actCreateAnotherToken() void Player::actCreatePredefinedToken() { - QAction *action = static_cast(sender()); + auto *action = static_cast(sender()); CardInfoPtr cardInfo = db->getCard(action->text()); if (!cardInfo) { return; @@ -1199,7 +1199,7 @@ void Player::actCreateRelatedCard() if (!sourceCard) { return; } - QAction *action = static_cast(sender()); + auto *action = static_cast(sender()); // If there is a better way of passing a CardRelation through a QAction, please add it here. QList relatedCards = QList(); relatedCards.append(sourceCard->getInfo()->getRelatedCards()); @@ -1211,7 +1211,7 @@ void Player::actCreateRelatedCard() * then let's allow it to be created via "create another token" */ if (createRelatedFromRelation(sourceCard, cardRelation) && cardRelation->getCanCreateAnother()) { - CardInfoPtr cardInfo = db->getCard(dbNameFromTokenDisplayName(cardRelation->getName())); + CardInfoPtr cardInfo = db->getCard(cardRelation->getName()); setLastToken(cardInfo); } } @@ -1257,7 +1257,7 @@ void Player::actCreateAllRelatedCards() case 0: // else if nonExcludedRelatedCards == 0 for (CardRelation *cardRelationAll : relatedCards) { if (!cardRelationAll->getDoesAttach() && !cardRelationAll->getIsVariable()) { - dbName = dbNameFromTokenDisplayName(cardRelationAll->getName()); + dbName = cardRelationAll->getName(); for (int i = 0; i < cardRelationAll->getDefaultCount(); ++i) { createCard(sourceCard, dbName); } @@ -1271,7 +1271,7 @@ void Player::actCreateAllRelatedCards() default: // else for (CardRelation *cardRelationNotExcluded : nonExcludedRelatedCards) { if (!cardRelationNotExcluded->getDoesAttach() && !cardRelationNotExcluded->getIsVariable()) { - dbName = dbNameFromTokenDisplayName(cardRelationNotExcluded->getName()); + dbName = cardRelationNotExcluded->getName(); for (int i = 0; i < cardRelationNotExcluded->getDefaultCount(); ++i) { createCard(sourceCard, dbName); } @@ -1290,7 +1290,7 @@ void Player::actCreateAllRelatedCards() * then assign the first to the "Create another" shortcut. */ if (cardRelation != nullptr && cardRelation->getCanCreateAnother()) { - CardInfoPtr cardInfo = db->getCard(dbNameFromTokenDisplayName(cardRelation->getName())); + CardInfoPtr cardInfo = db->getCard(cardRelation->getName()); setLastToken(cardInfo); } } @@ -1300,12 +1300,12 @@ bool Player::createRelatedFromRelation(const CardItem *sourceCard, const CardRel if (sourceCard == nullptr || cardRelation == nullptr) { return false; } - QString dbName = dbNameFromTokenDisplayName(cardRelation->getName()); + QString dbName = cardRelation->getName(); if (cardRelation->getIsVariable()) { bool ok; dialogSemaphore = true; - int count = QInputDialog::getInt(0, tr("Create tokens"), tr("Number:"), cardRelation->getDefaultCount(), 1, - MAX_TOKENS_PER_DIALOG, 1, &ok); + int count = QInputDialog::getInt(nullptr, tr("Create tokens"), tr("Number:"), cardRelation->getDefaultCount(), + 1, MAX_TOKENS_PER_DIALOG, 1, &ok); dialogSemaphore = false; if (!ok) { return false; @@ -1337,19 +1337,22 @@ void Player::createCard(const CardItem *sourceCard, const QString &dbCardName, b // get the target token's location // TODO: Define this QPoint into its own function along with the one below - QPoint gridPoint = QPoint(-1, table->clampValidTableRow(2 - cardInfo->getTableRow())); + QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - cardInfo->getTableRow())); // create the token for the related card Command_CreateToken cmd; cmd.set_zone("table"); cmd.set_card_name(cardInfo->getName().toStdString()); - if (cardInfo->getColors().length() > 1) // Multicoloured - { - cmd.set_color("m"); - } else if (cardInfo->getColors().isEmpty()) { - cmd.set_color(""); - } else { - cmd.set_color(cardInfo->getColors().first().toLower().toStdString()); + switch (cardInfo->getColors().size()) { + case 0: + cmd.set_color(""); + break; + case 1: + cmd.set_color("m"); + break; + default: + cmd.set_color(cardInfo->getColors().left(1).toLower().toStdString()); + break; } cmd.set_pt(cardInfo->getPowTough().toStdString()); @@ -1377,7 +1380,7 @@ void Player::createAttachedCard(const CardItem *sourceCard, const QString &dbCar void Player::actSayMessage() { - QAction *a = qobject_cast(sender()); + auto *a = qobject_cast(sender()); Command_GameSay cmd; cmd.set_message(a->text().toStdString()); sendGameCommand(cmd); @@ -1436,22 +1439,6 @@ void Player::setCardAttrHelper(const GameEventContext &context, } } -// token names take the form of " / " or " ". -// dbName for tokens should take the form of " ". -// trailing whitespace is significant; it is hacked on at the end as an additional identifier in our single key database -QString Player::dbNameFromTokenDisplayName(const QString &tokenName) -{ - QRegularExpression tokenNamePattern(".*/\\S+\\s+(.*)"); - QRegularExpressionMatch match = tokenNamePattern.match(tokenName); - if (match.hasMatch()) { - return match.captured(1); - } else if (tokenName.indexOf(tr("Token: ")) != -1) { - return tokenName.mid(tr("Token: ").length()); - } else { - return tokenName; - } -} - void Player::eventGameSay(const Event_GameSay &event) { emit logSay(this, QString::fromStdString(event.message())); @@ -1481,8 +1468,8 @@ void Player::eventCreateArrow(const Event_CreateArrow &event) return; } - CardItem *startCard = static_cast(arrow->getStartItem()); - CardItem *targetCard = qgraphicsitem_cast(arrow->getTargetItem()); + auto *startCard = static_cast(arrow->getStartItem()); + auto *targetCard = qgraphicsitem_cast(arrow->getTargetItem()); if (targetCard) { emit logCreateArrow(this, startCard->getOwner(), startCard->getName(), targetCard->getOwner(), targetCard->getName(), false); @@ -1536,7 +1523,7 @@ void Player::eventSetCardAttr(const Event_SetCardAttr &event, const GameEventCon true); } if (event.attribute() == AttrTapped) { - emit logSetTapped(this, 0, event.attr_value() == "1"); + emit logSetTapped(this, nullptr, event.attr_value() == "1"); } } else { CardItem *card = zone->getCard(event.card_id(), QString()); @@ -1654,7 +1641,7 @@ void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext & if (card->getAttachedTo() && (startZone != targetZone)) { CardItem *parentCard = card->getAttachedTo(); - card->setAttachedTo(0); + card->setAttachedTo(nullptr); parentCard->getZone()->reorganizeCards(); } @@ -1667,8 +1654,8 @@ void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext & card->setHovered(false); const QList &attachedCards = card->getAttachedCards(); - for (int i = 0; i < attachedCards.size(); ++i) { - attachedCards[i]->setParentItem(targetZone); + for (auto attachedCard : attachedCards) { + attachedCard->setParentItem(targetZone); } if (startZone->getPlayer() != targetZone->getPlayer()) { @@ -1704,8 +1691,8 @@ void Player::eventMoveCard(const Event_MoveCard &event, const GameEventContext & } } } - for (int i = 0; i < arrowsToDelete.size(); ++i) { - arrowsToDelete[i]->delArrow(); + for (auto &i : arrowsToDelete) { + i->delArrow(); } } } @@ -1738,8 +1725,8 @@ void Player::eventDestroyCard(const Event_DestroyCard &event) QList attachedCards = card->getAttachedCards(); // This list is always empty except for buggy server implementations. - for (int i = 0; i < attachedCards.size(); ++i) { - attachedCards[i]->setAttachedTo(0); + for (auto &attachedCard : attachedCards) { + attachedCard->setAttachedTo(0); } emit logDestroyCard(this, card->getName()); @@ -1750,9 +1737,9 @@ void Player::eventDestroyCard(const Event_DestroyCard &event) void Player::eventAttachCard(const Event_AttachCard &event) { const QMap &playerList = game->getPlayers(); - Player *targetPlayer = 0; - CardZone *targetZone = 0; - CardItem *targetCard = 0; + Player *targetPlayer = nullptr; + CardZone *targetZone = nullptr; + CardItem *targetCard = nullptr; if (event.has_target_player_id()) { targetPlayer = playerList.value(event.target_player_id(), 0); if (targetPlayer) { @@ -1823,7 +1810,7 @@ void Player::eventRevealCards(const Event_RevealCards &event) if (!zone) { return; } - Player *otherPlayer = 0; + Player *otherPlayer = nullptr; if (event.has_other_player_id()) { otherPlayer = game->getPlayers().value(event.other_player_id()); if (!otherPlayer) { @@ -1843,14 +1830,14 @@ void Player::eventRevealCards(const Event_RevealCards &event) } if (peeking) { - for (int i = 0; i < cardList.size(); ++i) { - QString cardName = QString::fromStdString(cardList.at(i)->name()); - CardItem *card = zone->getCard(cardList.at(i)->id(), QString()); + for (auto i : cardList) { + QString cardName = QString::fromStdString(i->name()); + CardItem *card = zone->getCard(i->id(), QString()); if (!card) { continue; } card->setName(cardName); - emit logRevealCards(this, zone, cardList.at(i)->id(), cardName, this, true); + emit logRevealCards(this, zone, i->id(), cardName, this, true); } } else { bool showZoneView = true; @@ -1953,7 +1940,7 @@ void Player::processGameEvent(GameEvent::GameEventType type, const GameEvent &ev } } -void Player::setActive(bool _active) +void Player::setActivePlayer(bool _active) { active = _active; table->setActive(active); @@ -2073,7 +2060,7 @@ void Player::playCard(CardItem *card, bool faceDown, bool tapped) cmd.set_y(0); } else { int tableRow = faceDown ? 2 : info->getTableRow(); - QPoint gridPoint = QPoint(-1, table->clampValidTableRow(2 - tableRow)); + QPoint gridPoint = QPoint(-1, TableZone::clampValidTableRow(2 - tableRow)); cardToMove->set_face_down(faceDown); cardToMove->set_pt(info->getPowTough().toStdString()); cardToMove->set_tapped(faceDown ? false : tapped); @@ -2116,7 +2103,7 @@ AbstractCounter *Player::addCounter(int counterId, const QString &name, QColor c { qDebug() << "addCounter:" << getName() << counterId << name; if (counters.contains(counterId)) { - return 0; + return nullptr; } AbstractCounter *ctr; @@ -2163,25 +2150,25 @@ ArrowItem *Player::addArrow(const ServerInfo_Arrow &arrow) Player *startPlayer = playerList.value(arrow.start_player_id(), 0); Player *targetPlayer = playerList.value(arrow.target_player_id(), 0); if (!startPlayer || !targetPlayer) { - return 0; + return nullptr; } CardZone *startZone = startPlayer->getZones().value(QString::fromStdString(arrow.start_zone()), 0); - CardZone *targetZone = 0; + CardZone *targetZone = nullptr; if (arrow.has_target_zone()) { targetZone = targetPlayer->getZones().value(QString::fromStdString(arrow.target_zone()), 0); } if (!startZone || (!targetZone && arrow.has_target_zone())) { - return 0; + return nullptr; } CardItem *startCard = startZone->getCard(arrow.start_card_id(), QString()); - CardItem *targetCard = 0; + CardItem *targetCard = nullptr; if (targetZone) { targetCard = targetZone->getCard(arrow.target_card_id(), QString()); } if (!startCard || (!targetCard && arrow.has_target_card_id())) { - return 0; + return nullptr; } if (targetCard) { @@ -2194,7 +2181,7 @@ ArrowItem *Player::addArrow(const ServerInfo_Arrow &arrow) ArrowItem *Player::addArrow(int arrowId, CardItem *startCard, ArrowTarget *targetItem, const QColor &color) { - ArrowItem *arrow = new ArrowItem(this, arrowId, startCard, targetItem, color); + auto *arrow = new ArrowItem(this, arrowId, startCard, targetItem, color); arrows.insert(arrowId, arrow); scene()->addItem(arrow); return arrow; @@ -2295,7 +2282,7 @@ bool Player::clearCardsToDelete() void Player::actMoveCardXCardsFromTop() { bool ok; - int number = QInputDialog::getInt(0, tr("Place card X cards from top of library"), + int number = QInputDialog::getInt(nullptr, tr("Place card X cards from top of library"), tr("How many cards from the top of the deck should this card be placed:"), defaultNumberTopCardsToPlaceBelow, 1, 2000000000, 1, &ok); number--; @@ -2325,7 +2312,7 @@ void Player::actMoveCardXCardsFromTop() int startPlayerId = cardList[0]->getZone()->getPlayer()->getId(); QString startZone = cardList[0]->getZone()->getName(); - Command_MoveCard *cmd = new Command_MoveCard; + auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); @@ -2344,7 +2331,7 @@ void Player::actMoveCardXCardsFromTop() void Player::cardMenuAction() { - QAction *a = dynamic_cast(sender()); + auto *a = dynamic_cast(sender()); QList sel = scene()->selectedItems(); QList cardList; while (!sel.isEmpty()) { @@ -2353,14 +2340,13 @@ void Player::cardMenuAction() QList commandList; if (a->data().toInt() <= (int)cmClone) { - for (int i = 0; i < cardList.size(); ++i) { - CardItem *card = cardList[i]; + for (auto card : cardList) { switch (static_cast(a->data().toInt())) { // Leaving both for compatibility with server case cmUntap: // fallthrough case cmTap: { - Command_SetCardAttr *cmd = new Command_SetCardAttr; + auto *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_attribute(AttrTapped); @@ -2369,7 +2355,7 @@ void Player::cardMenuAction() break; } case cmDoesntUntap: { - Command_SetCardAttr *cmd = new Command_SetCardAttr; + auto *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_attribute(AttrDoesntUntap); @@ -2378,7 +2364,7 @@ void Player::cardMenuAction() break; } case cmFlip: { - Command_FlipCard *cmd = new Command_FlipCard; + auto *cmd = new Command_FlipCard; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_face_down(!card->getFaceDown()); @@ -2392,7 +2378,7 @@ void Player::cardMenuAction() break; } case cmPeek: { - Command_RevealCards *cmd = new Command_RevealCards; + auto *cmd = new Command_RevealCards; cmd->set_zone_name(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_player_id(id); @@ -2400,7 +2386,7 @@ void Player::cardMenuAction() break; } case cmClone: { - Command_CreateToken *cmd = new Command_CreateToken; + auto *cmd = new Command_CreateToken; cmd->set_zone("table"); cmd->set_card_name(card->getName().toStdString()); cmd->set_color(card->getColor().toStdString()); @@ -2418,15 +2404,15 @@ void Player::cardMenuAction() } } else { ListOfCardsToMove idList; - for (int i = 0; i < cardList.size(); ++i) { - idList.add_card()->set_card_id(cardList[i]->getId()); + for (auto &i : cardList) { + idList.add_card()->set_card_id(i->getId()); } int startPlayerId = cardList[0]->getZone()->getPlayer()->getId(); QString startZone = cardList[0]->getZone()->getName(); switch (static_cast(a->data().toInt())) { case cmMoveToTopLibrary: { - Command_MoveCard *cmd = new Command_MoveCard; + auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); @@ -2438,7 +2424,7 @@ void Player::cardMenuAction() break; } case cmMoveToBottomLibrary: { - Command_MoveCard *cmd = new Command_MoveCard; + auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); @@ -2450,7 +2436,7 @@ void Player::cardMenuAction() break; } case cmMoveToHand: { - Command_MoveCard *cmd = new Command_MoveCard; + auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); @@ -2462,7 +2448,7 @@ void Player::cardMenuAction() break; } case cmMoveToGraveyard: { - Command_MoveCard *cmd = new Command_MoveCard; + auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); @@ -2474,7 +2460,7 @@ void Player::cardMenuAction() break; } case cmMoveToExile: { - Command_MoveCard *cmd = new Command_MoveCard; + auto *cmd = new Command_MoveCard; cmd->set_start_player_id(startPlayerId); cmd->set_start_zone(startZone.toStdString()); cmd->mutable_cards_to_move()->CopyFrom(idList); @@ -2505,8 +2491,8 @@ void Player::actIncPT(int deltaP, int deltaT) QList commandList; QListIterator j(scene()->selectedItems()); while (j.hasNext()) { - CardItem *card = static_cast(j.next()); - Command_SetCardAttr *cmd = new Command_SetCardAttr; + auto *card = static_cast(j.next()); + auto *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_attribute(AttrPT); @@ -2527,12 +2513,12 @@ void Player::actResetPT() QList commandList; QListIterator selected(scene()->selectedItems()); while (selected.hasNext()) { - CardItem *card = static_cast(selected.next()); + auto *card = static_cast(selected.next()); CardInfoPtr info = card->getInfo(); if (!info) { continue; } - Command_SetCardAttr *cmd = new Command_SetCardAttr; + auto *cmd = new Command_SetCardAttr; QString zoneName = card->getZone()->getName(); cmd->set_zone(zoneName.toStdString()); cmd->set_card_id(card->getId()); @@ -2556,15 +2542,15 @@ void Player::actSetPT() QListIterator i(scene()->selectedItems()); while (i.hasNext()) { - CardItem *card = static_cast(i.next()); + auto *card = static_cast(i.next()); if (!card->getPT().isEmpty()) { oldPT = card->getPT(); } } bool ok; dialogSemaphore = true; - QString pt = QInputDialog::getText(0, tr("Set power/toughness"), tr("Please enter the new PT:"), QLineEdit::Normal, - oldPT, &ok); + QString pt = QInputDialog::getText(nullptr, tr("Set power/toughness"), tr("Please enter the new PT:"), + QLineEdit::Normal, oldPT, &ok); dialogSemaphore = false; if (clearCardsToDelete()) { return; @@ -2576,8 +2562,8 @@ void Player::actSetPT() QList commandList; QListIterator j(scene()->selectedItems()); while (j.hasNext()) { - CardItem *card = static_cast(j.next()); - Command_SetCardAttr *cmd = new Command_SetCardAttr; + auto *card = static_cast(j.next()); + auto *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_attribute(AttrPT); @@ -2636,7 +2622,7 @@ void Player::actSetAnnotation() QString oldAnnotation; QListIterator i(scene()->selectedItems()); while (i.hasNext()) { - CardItem *card = static_cast(i.next()); + auto *card = static_cast(i.next()); if (!card->getAnnotation().isEmpty()) { oldAnnotation = card->getAnnotation(); } @@ -2644,7 +2630,7 @@ void Player::actSetAnnotation() bool ok; dialogSemaphore = true; - QString annotation = QInputDialog::getText(0, tr("Set annotation"), tr("Please enter the new annotation:"), + QString annotation = QInputDialog::getText(nullptr, tr("Set annotation"), tr("Please enter the new annotation:"), QLineEdit::Normal, oldAnnotation, &ok); dialogSemaphore = false; if (clearCardsToDelete()) { @@ -2657,8 +2643,8 @@ void Player::actSetAnnotation() QList commandList; i.toFront(); while (i.hasNext()) { - CardItem *card = static_cast(i.next()); - Command_SetCardAttr *cmd = new Command_SetCardAttr; + auto *card = static_cast(i.next()); + auto *cmd = new Command_SetCardAttr; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_attribute(AttrAnnotation); @@ -2674,7 +2660,7 @@ void Player::actAttach() return; } - ArrowAttachItem *arrow = new ArrowAttachItem(game->getActiveCard()); + auto *arrow = new ArrowAttachItem(game->getActiveCard()); scene()->addItem(arrow); arrow->grabMouse(); } @@ -2693,16 +2679,16 @@ void Player::actUnattach() void Player::actCardCounterTrigger() { - QAction *action = static_cast(sender()); + auto *action = static_cast(sender()); int counterId = action->data().toInt() / 1000; QList commandList; switch (action->data().toInt() % 1000) { // TODO: define case numbers case 9: { QListIterator i(scene()->selectedItems()); while (i.hasNext()) { - CardItem *card = static_cast(i.next()); + auto *card = static_cast(i.next()); if (card->getCounters().value(counterId, 0) < MAX_COUNTERS_ON_CARD) { - Command_SetCardCounter *cmd = new Command_SetCardCounter; + auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_counter_id(counterId); @@ -2715,9 +2701,9 @@ void Player::actCardCounterTrigger() case 10: { QListIterator i(scene()->selectedItems()); while (i.hasNext()) { - CardItem *card = static_cast(i.next()); + auto *card = static_cast(i.next()); if (card->getCounters().value(counterId, 0)) { - Command_SetCardCounter *cmd = new Command_SetCardCounter; + auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_counter_id(counterId); @@ -2730,7 +2716,8 @@ void Player::actCardCounterTrigger() case 11: { bool ok; dialogSemaphore = true; - int number = QInputDialog::getInt(0, tr("Set counters"), tr("Number:"), 0, 0, MAX_COUNTERS_ON_CARD, 1, &ok); + int number = + QInputDialog::getInt(nullptr, tr("Set counters"), tr("Number:"), 0, 0, MAX_COUNTERS_ON_CARD, 1, &ok); dialogSemaphore = false; if (clearCardsToDelete() || !ok) { return; @@ -2738,8 +2725,8 @@ void Player::actCardCounterTrigger() QListIterator i(scene()->selectedItems()); while (i.hasNext()) { - CardItem *card = static_cast(i.next()); - Command_SetCardCounter *cmd = new Command_SetCardCounter; + auto *card = static_cast(i.next()); + auto *cmd = new Command_SetCardCounter; cmd->set_zone(card->getZone()->getName().toStdString()); cmd->set_card_id(card->getId()); cmd->set_counter_id(counterId); @@ -2964,16 +2951,25 @@ void Player::addRelatedCardActions(const CardItem *card, QMenu *cardMenu) int index = 0; QAction *createRelatedCards = nullptr; for (const CardRelation *cardRelation : relatedCards) { - QString cardName = cardRelation->getName(); + CardInfoPtr relatedCard = db->getCard(cardRelation->getName()); + if (relatedCard == nullptr) + continue; + QString relatedCardName; + if (relatedCard->getPowTough().size() > 0) { + relatedCardName = relatedCard->getPowTough() + " " + relatedCard->getName(); // "n/n name" + } else { + relatedCardName = relatedCard->getName(); // "name" + } + QString text = tr("Token: "); if (cardRelation->getDoesAttach()) { - text += tr("Attach to ") + "\"" + cardName + "\""; + text += tr("Attach to ") + "\"" + relatedCardName + "\""; } else if (cardRelation->getIsVariable()) { - text += "X " + cardName; + text += "X " + relatedCardName; } else if (cardRelation->getDefaultCount() != 1) { - text += QString(cardRelation->getDefaultCount()) + "x " + cardName; + text += QString::number(cardRelation->getDefaultCount()) + "x " + relatedCardName; } else { - text += cardName; + text += relatedCardName; } if (createRelatedCards == nullptr) { @@ -2985,7 +2981,7 @@ void Player::addRelatedCardActions(const CardItem *card, QMenu *cardMenu) } } - QAction *createRelated = new QAction(text, this); + auto *createRelated = new QAction(text, this); createRelated->setData(QVariant(index++)); connect(createRelated, SIGNAL(triggered()), this, SLOT(actCreateRelatedCard())); cardMenu->addAction(createRelated); @@ -3010,7 +3006,7 @@ QMenu *Player::getCardMenu() const if (aCardMenu) { return aCardMenu->menu(); } - return 0; + return nullptr; } QString Player::getName() const @@ -3055,7 +3051,7 @@ void Player::setMirrored(bool _mirrored) void Player::processSceneSizeChange(int newPlayerWidth) { - // Extend table (and hand, if horizontal) to accomodate the new player width. + // Extend table (and hand, if horizontal) to accommodate the new player width. qreal tableWidth = newPlayerWidth - CARD_HEIGHT - 15 - counterAreaWidth - stack->boundingRect().width(); if (!settingsCache->getHorizontalHand()) { tableWidth -= hand->boundingRect().width(); @@ -3072,10 +3068,10 @@ void Player::setLastToken(CardInfoPtr cardInfo) } lastTokenName = cardInfo->getName(); - lastTokenColor = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().first().toLower(); + lastTokenColor = cardInfo->getColors().isEmpty() ? QString() : cardInfo->getColors().left(1).toLower(); lastTokenPT = cardInfo->getPowTough(); lastTokenAnnotation = settingsCache->getAnnotateTokens() ? cardInfo->getText() : ""; - lastTokenTableRow = table->clampValidTableRow(2 - cardInfo->getTableRow()); + lastTokenTableRow = TableZone::clampValidTableRow(2 - cardInfo->getTableRow()); lastTokenDestroy = true; aCreateAnotherToken->setText(tr("C&reate another %1 token").arg(lastTokenName)); aCreateAnotherToken->setEnabled(true); diff --git a/cockatrice/src/player.h b/cockatrice/src/player.h index c4f6ed008..4f1dc96d4 100644 --- a/cockatrice/src/player.h +++ b/cockatrice/src/player.h @@ -79,17 +79,17 @@ public: { Type = typeOther }; - int type() const + int type() const override { return Type; } - PlayerArea(QGraphicsItem *parent = 0); - QRectF boundingRect() const + explicit PlayerArea(QGraphicsItem *parent = nullptr); + QRectF boundingRect() const override { return bRect; } - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void setSize(qreal width, qreal height); }; @@ -251,7 +251,6 @@ private: void createCard(const CardItem *sourceCard, const QString &dbCardName, bool attach = false); void createAttachedCard(const CardItem *sourceCard, const QString &dbCardName); bool createRelatedFromRelation(const CardItem *sourceCard, const CardRelation *cardRelation); - QString dbNameFromTokenDisplayName(const QString &tokenName); QRectF bRect; @@ -308,12 +307,12 @@ public: { Type = typeOther }; - int type() const + int type() const override { return Type; } - QRectF boundingRect() const; - void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + QRectF boundingRect() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void playCard(CardItem *c, bool faceDown, bool tapped); void addCard(CardItem *c); @@ -336,7 +335,7 @@ public: } Player(const ServerInfo_User &info, int _id, bool _local, TabGame *_parent); - ~Player(); + ~Player() override; void retranslateUi(); void clear(); TabGame *getGame() const @@ -380,7 +379,7 @@ public: { return active; } - void setActive(bool _active); + void setActivePlayer(bool _active); void setShortcutsActive(); void setShortcutsInactive(); void updateZones(); diff --git a/cockatrice/src/releasechannel.cpp b/cockatrice/src/releasechannel.cpp index 841f801a6..aab2cd804 100644 --- a/cockatrice/src/releasechannel.cpp +++ b/cockatrice/src/releasechannel.cpp @@ -1,5 +1,4 @@ #include "releasechannel.h" -#include "qt-json/json.h" #include "version_string.h" #include @@ -93,21 +92,20 @@ QString StableReleaseChannel::getReleaseChannelUrl() const void StableReleaseChannel::releaseListFinished() { - QNetworkReply *reply = static_cast(sender()); - bool ok; - QString tmp = QString(reply->readAll()); + auto *reply = static_cast(sender()); + QJsonParseError parseError{}; + QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError); reply->deleteLater(); - - QVariantMap resultMap = QtJson::Json::parse(tmp, ok).toMap(); - if (!ok) { - qWarning() << "No reply received from the release update server:" << tmp; + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "No reply received from the release update server."; emit error(tr("No reply received from the release update server.")); return; } + QVariantMap resultMap = jsonResponse.toVariant().toMap(); if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") && resultMap.contains("published_at"))) { - qWarning() << "Invalid received from the release update server:" << tmp; + qWarning() << "Invalid received from the release update server."; emit error(tr("Invalid reply received from the release update server.")); return; } @@ -145,7 +143,7 @@ void StableReleaseChannel::releaseListFinished() QString myHash = QString(VERSION_COMMIT); qDebug() << "Current hash=" << myHash << "update hash=" << shortHash; - qDebug() << "Got reply from release server, size=" << tmp.size() << "name=" << lastRelease->getName() + qDebug() << "Got reply from release server, name=" << lastRelease->getName() << "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate() << "url=" << lastRelease->getDownloadUrl(); @@ -158,26 +156,25 @@ void StableReleaseChannel::releaseListFinished() void StableReleaseChannel::tagListFinished() { - QNetworkReply *reply = static_cast(sender()); - bool ok; - QString tmp = QString(reply->readAll()); + auto *reply = static_cast(sender()); + QJsonParseError parseError{}; + QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError); reply->deleteLater(); - - QVariantMap resultMap = QtJson::Json::parse(tmp, ok).toMap(); - if (!ok) { - qWarning() << "No reply received from the tag update server:" << tmp; + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "No reply received from the tag update server."; emit error(tr("No reply received from the tag update server.")); return; } + QVariantMap resultMap = jsonResponse.toVariant().toMap(); if (!(resultMap.contains("object") && resultMap["object"].toMap().contains("sha"))) { - qWarning() << "Invalid received from the tag update server:" << tmp; + qWarning() << "Invalid received from the tag update server."; emit error(tr("Invalid reply received from the tag update server.")); return; } lastRelease->setCommitHash(resultMap["object"].toMap()["sha"].toString()); - qDebug() << "Got reply from tag server, size=" << tmp.size() << "commit=" << lastRelease->getCommitHash(); + qDebug() << "Got reply from tag server, commit=" << lastRelease->getCommitHash(); QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN); QString myHash = QString(VERSION_COMMIT); @@ -190,7 +187,6 @@ void StableReleaseChannel::tagListFinished() void StableReleaseChannel::fileListFinished() { // Only implemented to satisfy interface - return; } QString BetaReleaseChannel::getManualDownloadUrl() const @@ -210,7 +206,7 @@ QString BetaReleaseChannel::getReleaseChannelUrl() const void BetaReleaseChannel::releaseListFinished() { - QNetworkReply *reply = static_cast(sender()); + auto *reply = static_cast(sender()); QByteArray jsonData = reply->readAll(); reply->deleteLater(); @@ -224,7 +220,7 @@ void BetaReleaseChannel::releaseListFinished() */ QVariantMap resultMap = array.at(0).toObject().toVariantMap(); - if (array.size() == 0 || resultMap.size() == 0) { + if (array.empty() || resultMap.empty()) { qWarning() << "No reply received from the release update server:" << QString(jsonData); emit error(tr("No reply received from the release update server.")); return; @@ -262,18 +258,17 @@ void BetaReleaseChannel::releaseListFinished() void BetaReleaseChannel::fileListFinished() { - QNetworkReply *reply = static_cast(sender()); - QByteArray jsonData = reply->readAll(); + auto *reply = static_cast(sender()); + QJsonParseError parseError{}; + QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError); reply->deleteLater(); - bool ok; - - QVariantList resultList = QtJson::Json::parse(jsonData, ok).toList(); - if (!ok) { - qWarning() << "No reply received from the file update server:" << QString(jsonData); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "No reply received from the file update server."; emit error(tr("No reply received from the file update server.")); return; } + QVariantList resultList = jsonResponse.toVariant().toList(); QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN); QString myHash = QString(VERSION_COMMIT); qDebug() << "Current hash=" << myHash << "update hash=" << shortHash; diff --git a/cockatrice/src/releasechannel.h b/cockatrice/src/releasechannel.h index c2b020dc4..8ec4d04c2 100644 --- a/cockatrice/src/releasechannel.h +++ b/cockatrice/src/releasechannel.h @@ -5,6 +5,7 @@ #include #include #include +#include class QNetworkReply; class QNetworkAccessManager; @@ -15,8 +16,8 @@ class Release friend class BetaReleaseChannel; public: - Release(){}; - ~Release(){}; + Release() = default; + ~Release() = default; private: QString name, descriptionUrl, downloadUrl, commitHash; @@ -26,20 +27,20 @@ private: protected: void setName(QString _name) { - name = _name; + name = std::move(_name); } void setDescriptionUrl(QString _descriptionUrl) { - descriptionUrl = _descriptionUrl; + descriptionUrl = std::move(_descriptionUrl); } void setDownloadUrl(QString _downloadUrl) { - downloadUrl = _downloadUrl; + downloadUrl = std::move(_downloadUrl); compatibleVersionFound = true; } void setCommitHash(QString _commitHash) { - commitHash = _commitHash; + commitHash = std::move(_commitHash); } void setPublishDate(QDate _publishDate) { @@ -78,7 +79,7 @@ class ReleaseChannel : public QObject Q_OBJECT public: ReleaseChannel(); - ~ReleaseChannel(); + ~ReleaseChannel() override; protected: // shared by all instances @@ -116,33 +117,41 @@ class StableReleaseChannel : public ReleaseChannel { Q_OBJECT public: - StableReleaseChannel(){}; - ~StableReleaseChannel(){}; - virtual QString getManualDownloadUrl() const; - virtual QString getName() const; + StableReleaseChannel() = default; + ~StableReleaseChannel() override = default; + + QString getManualDownloadUrl() const override; + + QString getName() const override; protected: - virtual QString getReleaseChannelUrl() const; + QString getReleaseChannelUrl() const override; protected slots: - virtual void releaseListFinished(); + + void releaseListFinished() override; void tagListFinished(); - virtual void fileListFinished(); + + void fileListFinished() override; }; class BetaReleaseChannel : public ReleaseChannel { Q_OBJECT public: - BetaReleaseChannel(){}; - ~BetaReleaseChannel(){}; - virtual QString getManualDownloadUrl() const; - virtual QString getName() const; + BetaReleaseChannel() = default; + ~BetaReleaseChannel() override = default; + + QString getManualDownloadUrl() const override; + + QString getName() const override; protected: - virtual QString getReleaseChannelUrl() const; + QString getReleaseChannelUrl() const override; protected slots: - virtual void releaseListFinished(); - virtual void fileListFinished(); + + void releaseListFinished() override; + + void fileListFinished() override; }; #endif \ No newline at end of file diff --git a/cockatrice/src/settings/downloadsettings.cpp b/cockatrice/src/settings/downloadsettings.cpp index 4591b680d..239b3d6cc 100644 --- a/cockatrice/src/settings/downloadsettings.cpp +++ b/cockatrice/src/settings/downloadsettings.cpp @@ -29,9 +29,9 @@ QStringList DownloadSettings::getAllURLs() void DownloadSettings::populateDefaultURLs() { downloadURLs.clear(); - downloadURLs.append("https://api.scryfall.com/cards/!uuid!?format=image"); - downloadURLs.append("https://api.scryfall.com/cards/multiverse/!cardid!?format=image"); - downloadURLs.append("http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!cardid!&type=card"); + downloadURLs.append("https://api.scryfall.com/cards/!set:uuid!?format=image&face=!prop:side!"); + downloadURLs.append("https://api.scryfall.com/cards/multiverse/!set:muid!?format=image"); + downloadURLs.append("http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=!set:muid!&type=card"); downloadURLs.append("http://gatherer.wizards.com/Handlers/Image.ashx?name=!name!&type=card"); setValue(QVariant::fromValue(downloadURLs), "urls", "downloads"); } diff --git a/cockatrice/translations/cockatrice_en.ts b/cockatrice/translations/cockatrice_en.ts index 8c02bad73..bc74d9a10 100644 --- a/cockatrice/translations/cockatrice_en.ts +++ b/cockatrice/translations/cockatrice_en.ts @@ -22,62 +22,62 @@ AppearanceSettingsPage - + Theme settings - + Current theme: - + Card rendering - + Display card names on cards having a picture - + Scale cards on mouse over - + 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: @@ -192,22 +192,22 @@ This is only saved for moderators and cannot be seen by the banned person. BetaReleaseChannel - + Beta Releases - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the file update server. @@ -330,17 +330,17 @@ This is only saved for moderators and cannot be seen by the banned person. CardFrame - + Image - + Description - + Both @@ -348,40 +348,20 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoText - + + Related cards: + + + + Unknown card: - + Name: - - - Mana cost: - - - - - Color(s): - - - - - Card type: - - - - - P / T: - - - - - Loyalty: - - CardItem @@ -522,58 +502,120 @@ 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 + + + + Updating... - + Choose path - + + URL Download Priority + + + + Spoilers - + Download Spoilers Automatically - + Spoiler Location: - - Hey, something's here finally! + + Download card pictures on the fly - + + How to add a custom URL + + + + + Delete Downloaded Images + + + + + Reset Download URLs + + + + Last Updated - + Spoilers download automatically on launch - + Press the button to manually update without relaunching - + Do not close settings until manual update complete @@ -1498,12 +1540,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -1514,7 +1556,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 @@ -1525,7 +1567,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -1534,21 +1576,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 http://github.com/Cockatrice/Cockatrice/issues @@ -1557,59 +1599,59 @@ 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 - - Deck Editor + + Card Sources - + Chat - + Sound - + Shortcuts @@ -2001,127 +2043,76 @@ You may have to build from source yourself. GeneralSettingsPage + + + + - - - - Choose path - - Success - - - - - Downloaded card pictures have been reset. - - - - - Error - - - - - One or more downloaded card pictures could not be cleared. - - - - + Personal settings - + Language: - - Download card pictures on the fly - - - - + Paths (editing disabled in portable mode) - + Paths - + Decks directory: - + Replays directory: - + Pictures directory: - + Card database: - + Token database: - + Picture cache size: - - Primary download URL: - - - - - Fallback download URL: - - - - - How to set a custom picture url - - - - - Reset/clear downloaded pictures - - - - + Update channel - + Notify if a feature supported by the server is missing in my client - - - Reset - - - - + Show tips on startup @@ -3443,94 +3434,157 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Word1 Word2 Word3 - + + Add New URL + + + + + Edit URL + + + + + Remove URL + + + + Add message - - + + Message: - + Edit message - + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered 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 + + Mtg + + + Card type + + + + + Converted mana cost + + + + + Color(s) + + + + + Loyalty + + + + + Main card type + + + + + Mana cost + + + + + P / T + + + + + Side + + + + + Layout + + + PhasesToolbar @@ -3948,7 +4002,7 @@ Cockatrice will now reload the card database. - + C&reate another %1 token @@ -3958,34 +4012,32 @@ Cockatrice will now reload the card database. - - - + Token: - + Place card X cards from top of library - + How many cards from the top of the deck should this card be placed: - + View related cards - + Attach to - + All tokens @@ -4036,7 +4088,7 @@ Cockatrice will now reload the card database. - + Number: @@ -4061,27 +4113,27 @@ Cockatrice will now reload the card database. - + Set power/toughness - + Please enter the new PT: - + Set annotation - + Please enter the new annotation: - + Set counters @@ -4352,27 +4404,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -4429,27 +4481,27 @@ Please check your shortcut settings! StableReleaseChannel - + Stable Releases - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the tag update server. - + Invalid reply received from the tag update server. @@ -5788,42 +5840,42 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings - + Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating - + &Double-click cards to play them (instead of single-click) - + &Play all nonlands onto the stack (not the battlefield) by default - + Annotate card text on tokens - + Animation settings - + &Tap/untap animation diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 82df556f5..13156ed43 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -11,9 +11,12 @@ SET(oracle_SOURCES src/main.cpp src/oraclewizard.cpp src/oracleimporter.cpp + src/qt-json/json.cpp ../cockatrice/src/carddatabase.cpp ../cockatrice/src/pictureloader.cpp + ../cockatrice/src/carddbparser/carddatabaseparser.cpp ../cockatrice/src/carddbparser/cockatricexml3.cpp + ../cockatrice/src/carddbparser/cockatricexml4.cpp ../cockatrice/src/settingscache.cpp ../cockatrice/src/shortcutssettings.cpp ../cockatrice/src/settings/carddatabasesettings.cpp @@ -24,7 +27,6 @@ SET(oracle_SOURCES ../cockatrice/src/settings/layoutssettings.cpp ../cockatrice/src/settings/downloadsettings.cpp ../cockatrice/src/thememanager.cpp - ../cockatrice/src/qt-json/json.cpp ../cockatrice/src/releasechannel.cpp ${VERSION_STRING_CPP} ) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index ec096f5c3..1b652b006 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -1,5 +1,5 @@ #include "oracleimporter.h" -#include "carddbparser/cockatricexml3.h" +#include "carddbparser/cockatricexml4.h" #include #include @@ -7,6 +7,14 @@ #include "qt-json/json.h" +SplitCardPart::SplitCardPart(const int _index, + const QString &_text, + const QVariantHash &_properties, + const CardInfoPerSet _setInfo) + : index(_index), text(_text), properties(_properties), setInfo(_setInfo) +{ +} + OracleImporter::OracleImporter(const QString &_dataDir, QObject *parent) : CardDatabase(parent), dataDir(_dataDir) { } @@ -25,24 +33,23 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data) QListIterator it(setsMap.values()); QVariantMap map; - QString edition; - QString editionLong; - QVariant editionCards; + QString shortName; + QString longName; + QList setCards; QString setType; QDate releaseDate; while (it.hasNext()) { map = it.next().toMap(); - edition = map.value("code").toString().toUpper(); - editionLong = map.value("name").toString(); - editionCards = map.value("cards"); + shortName = map.value("code").toString().toUpper(); + longName = map.value("name").toString(); + setCards = map.value("cards").toList(); setType = map.value("type").toString(); // capitalize set type if (setType.length() > 0) setType[0] = setType[0].toUpper(); releaseDate = map.value("releaseDate").toDate(); - - newSetList.append(SetToDownload(edition, editionLong, editionCards, setType, releaseDate)); + newSetList.append(SetToDownload(shortName, longName, setCards, setType, releaseDate)); } qSort(newSetList); @@ -53,37 +60,27 @@ bool OracleImporter::readSetsFromByteArray(const QByteArray &data) return true; } -CardInfoPtr OracleImporter::addCard(const QString &setName, - QString cardName, +CardInfoPtr OracleImporter::addCard(QString name, + QString text, bool isToken, - int cardId, - QString &cardUuId, - QString &setNumber, - QString &cardCost, - QString &cmc, - const QString &cardType, - const QString &cardPT, - const QString &cardLoyalty, - const QString &cardText, - const QStringList &colors, - const QList &relatedCards, - const QList &reverseRelatedCards, - bool upsideDown, - QString &rarity) + QVariantHash properties, + QList &relatedCards, + CardInfoPerSet setInfo) { - QStringList cardTextRows = cardText.split("\n"); - // Workaround for card name weirdness - cardName = cardName.replace("Æ", "AE"); - cardName = cardName.replace("’", "'"); + name = name.replace("Æ", "AE"); + name = name.replace("’", "'"); + if (cards.contains(name)) { + CardInfoPtr card = cards.value(name); + card->addToSet(setInfo.getPtr(), setInfo); + return card; + } - CardInfoPtr card; - if (cards.contains(cardName)) { - card = cards.value(cardName); - } else { - // Remove {} around mana costs, except if it's split cost - QStringList symbols = cardCost.split("}"); - QString formattedCardCost = QString(); + // Remove {} around mana costs, except if it's split cost + QString manacost = properties.value("manacost").toString(); + if (!manacost.isEmpty()) { + QStringList symbols = manacost.split("}"); + QString formattedCardCost; for (QString symbol : symbols) { if (symbol.contains(QRegExp("[0-9WUBGRP]/[0-9WUBGRP]"))) { symbol.append("}"); @@ -92,240 +89,251 @@ CardInfoPtr OracleImporter::addCard(const QString &setName, } formattedCardCost.append(symbol); } - - // detect mana generator artifacts - bool mArtifact = false; - if (cardType.endsWith("Artifact")) { - for (int i = 0; i < cardTextRows.size(); ++i) { - cardTextRows[i].remove(QRegularExpression(R"(\".*?\")")); - if (cardTextRows[i].contains("{T}") && cardTextRows[i].contains("to your mana pool")) { - mArtifact = true; - } - } - } - - // detect cards that enter the field tapped - bool cipt = - cardText.contains("Hideaway") || (cardText.contains(cardName + " enters the battlefield tapped") && - !cardText.contains(cardName + " enters the battlefield tapped unless")); - - // insert the card and its properties - card = CardInfo::newInstance(cardName, isToken, formattedCardCost, cmc, cardType, cardPT, cardText, colors, - relatedCards, reverseRelatedCards, upsideDown, cardLoyalty, cipt); - int tableRow = 1; - QString mainCardType = card->getMainCardType(); - if ((mainCardType == "Land") || mArtifact) - tableRow = 0; - else if ((mainCardType == "Sorcery") || (mainCardType == "Instant")) - tableRow = 3; - else if (mainCardType == "Creature") - tableRow = 2; - card->setTableRow(tableRow); - - cards.insert(cardName, card); + properties.insert("manacost", formattedCardCost); } - card->setMuId(setName, cardId); - card->setUuId(setName, cardUuId); - card->setSetNumber(setName, setNumber); - card->setRarity(setName, rarity); + // fix colors + QString allColors = properties.value("colors").toString(); + if (allColors.size() > 1) { + sortAndReduceColors(allColors); + properties.insert("colors", allColors); + } - return card; + // DETECT CARD POSITIONING INFO + + // cards that enter the field tapped + bool cipt = text.contains("Hideaway") || (text.contains(name + " enters the battlefield tapped") && + !text.contains(name + " enters the battlefield tapped unless")); + + // detect mana generator artifacts + QStringList cardTextRows = text.split("\n"); + bool mArtifact = false; + QString cardType = properties.value("type").toString(); + if (cardType.endsWith("Artifact")) { + for (int i = 0; i < cardTextRows.size(); ++i) { + cardTextRows[i].remove(QRegularExpression(R"(\".*?\")")); + if (cardTextRows[i].contains("{T}") && cardTextRows[i].contains("to your mana pool")) { + mArtifact = true; + } + } + } + + // table row + int tableRow = 1; + QString mainCardType = properties.value("maintype").toString(); + if ((mainCardType == "Land") || mArtifact) + tableRow = 0; + else if ((mainCardType == "Sorcery") || (mainCardType == "Instant")) + tableRow = 3; + else if (mainCardType == "Creature") + tableRow = 2; + + // card side + QString side = properties.value("side").toString() == "b" ? "back" : "front"; + properties.insert("side", side); + + // upsideDown (flip cards) + bool upsideDown = false; + QStringList additionalNames = properties.value("names").toStringList(); + QString layout = properties.value("layout").toString(); + if (layout == "flip") { + if (properties.value("side").toString() != "front") { + upsideDown = true; + } + // reset the side property, since the card has no back image + properties.insert("side", "front"); + } + + // insert the card and its properties + QList reverseRelatedCards; + CardInfoPerSetMap setsInfo; + setsInfo.insert(setInfo.getPtr()->getShortName(), setInfo); + CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards, reverseRelatedCards, + setsInfo, cipt, tableRow, upsideDown); + + cards.insert(name, newCard); + return newCard; } -int OracleImporter::importTextSpoiler(CardSetPtr set, const QVariant &data) +QString OracleImporter::getStringPropertyFromMap(QVariantMap card, QString propertyName) { - int cards = 0; + return card.contains(propertyName) ? card.value(propertyName).toString() : QString(""); +} - QListIterator it(data.toList()); - QVariantMap map; - QString cardName; - QString cardCost; - QString cmc; - QString cardType; - QString cardPT; - QString cardText; - QStringList colors; +int OracleImporter::importCardsFromSet(CardSetPtr currentSet, const QList &cardsList) +{ + static const QMap cardProperties{ + // mtgjson name => xml name + {"manaCost", "manacost"}, {"convertedManaCost", "cmc"}, {"type", "type"}, + {"loyalty", "loyalty"}, {"layout", "layout"}, {"side", "side"}, + }; + + static const QMap setInfoProperties{// mtgjson name => xml name + {"multiverseId", "muid"}, + {"scryfallId", "uuid"}, + {"number", "num"}, + {"rarity", "rarity"}}; + + int numCards = 0; + QMap splitCards; + QString ptSeparator("/"); + QVariantMap card; + QString layout, name, text, colors, maintype, power, toughness; + bool isToken; + QStringList additionalNames; + QVariantHash properties; + CardInfoPerSet setInfo; QList relatedCards; - QList reverseRelatedCards; // dummy - int cardId; - QString cardUuId; - QString setNumber; - QString rarity; - QString cardLoyalty; - bool upsideDown; - QMap splitCards; - while (it.hasNext()) { - map = it.next().toMap(); + for (const QVariant &cardVar : cardsList) { + card = cardVar.toMap(); /* Currently used layouts are: * augment, double_faced_token, flip, host, leveler, meld, normal, planar, * saga, scheme, split, token, transform, vanguard */ - QString layout = map.value("layout").toString(); + layout = getStringPropertyFromMap(card, "layout"); // don't import tokens from the json file + isToken = false; if (layout == "token") continue; - // Aftermath card layout seems to have been integrated in "split" - if (layout == "split") { - // Enqueue split card for later handling - cardId = map.contains("multiverseId") ? map.value("multiverseId").toInt() : 0; - if (cardId) - splitCards.insertMulti(cardId, map); - continue; - } - // normal cards handling - cardName = map.contains("name") ? map.value("name").toString() : QString(""); - cardCost = map.contains("manaCost") ? map.value("manaCost").toString() : QString(""); - cmc = map.contains("convertedManaCost") ? map.value("convertedManaCost").toString() : QString("0"); - cardType = map.contains("type") ? map.value("type").toString() : QString(""); - cardPT = map.contains("power") || map.contains("toughness") - ? map.value("power").toString() + QString('/') + map.value("toughness").toString() - : QString(""); - cardText = map.contains("text") ? map.value("text").toString() : QString(""); - cardId = map.contains("multiverseId") ? map.value("multiverseId").toInt() : 0; - cardUuId = map.contains("scryfallId") ? map.value("scryfallId").toString() : QString(""); - setNumber = map.contains("number") ? map.value("number").toString() : QString(""); - rarity = map.contains("rarity") ? map.value("rarity").toString() : QString(""); - cardLoyalty = map.contains("loyalty") ? map.value("loyalty").toString() : QString(""); - colors = map.contains("colors") ? map.value("colors").toStringList() : QStringList(); - relatedCards = QList(); - if (map.contains("names")) - for (const QString &name : map.value("names").toStringList()) { - if (name != cardName) - relatedCards.append(new CardRelation(name, true)); - } + name = getStringPropertyFromMap(card, "name"); + text = getStringPropertyFromMap(card, "text"); - if (0 == QString::compare(map.value("layout").toString(), QString("flip"), Qt::CaseInsensitive)) { - QStringList cardNames = map.contains("names") ? map.value("names").toStringList() : QStringList(); - upsideDown = (cardNames.indexOf(cardName) > 0); + // card properties + properties.clear(); + QMapIterator it(cardProperties); + while (it.hasNext()) { + it.next(); + QString mtgjsonProperty = it.key(); + QString xmlPropertyName = it.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); + if (!propertyValue.isEmpty()) + properties.insert(xmlPropertyName, propertyValue); + } + + // per-set properties + setInfo = CardInfoPerSet(currentSet); + QMapIterator it2(setInfoProperties); + while (it2.hasNext()) { + it2.next(); + QString mtgjsonProperty = it2.key(); + QString xmlPropertyName = it2.value(); + QString propertyValue = getStringPropertyFromMap(card, mtgjsonProperty); + if (!propertyValue.isEmpty()) + setInfo.setProperty(xmlPropertyName, propertyValue); + } + + // special handling properties + colors = card.value("colors").toStringList().join(""); + if (!colors.isEmpty()) + properties.insert("colors", colors); + + maintype = card.value("types").toStringList().first(); + if (!maintype.isEmpty()) + properties.insert("maintype", maintype); + + power = getStringPropertyFromMap(card, "power"); + toughness = getStringPropertyFromMap(card, "toughness"); + if (!(power.isEmpty() && toughness.isEmpty())) + properties.insert("pt", power + ptSeparator + toughness); + + additionalNames = card.value("names").toStringList(); + // split cards are considered a single card, enqueue for later merging + if (layout == "split") { + // get the position of this card part + int index = additionalNames.indexOf(name); + // construct full card name + name = additionalNames.join(QString(" // ")); + SplitCardPart split(index, text, properties, setInfo); + splitCards.insertMulti(name, split); } else { - upsideDown = false; - } + // relations + relatedCards.clear(); + if (additionalNames.size() > 1) { + for (const QString &additionalName : additionalNames) { + if (additionalName != name) + relatedCards.append(new CardRelation(additionalName, true)); + } + } - CardInfoPtr card = - addCard(set->getShortName(), cardName, false, cardId, cardUuId, setNumber, cardCost, cmc, cardType, cardPT, - cardLoyalty, cardText, colors, relatedCards, reverseRelatedCards, upsideDown, rarity); - - if (!set->contains(card)) { - card->addToSet(set); - cards++; + CardInfoPtr newCard = addCard(name, text, isToken, properties, relatedCards, setInfo); + numCards++; } } - // split cards handling - get all unique card muids - QList muids = splitCards.uniqueKeys(); - for (int muid : muids) { - // get all cards for this specific muid - QList maps = splitCards.values(muid); - QStringList names; - // now, reorder the cards using the ordered list of names - QMap orderedMaps; - for (const QVariantMap &inner_map : maps) { - if (names.isEmpty()) - names = inner_map.contains("names") ? inner_map.value("names").toStringList() : QStringList(); - QString name = inner_map.value("name").toString(); - int index = names.indexOf(name); - orderedMaps.insertMulti(index, inner_map); + // split cards handling + QString splitCardPropSeparator = QString(" // "); + QString splitCardTextSeparator = QString("\n\n---\n\n"); + for (const QString &nameSplit : splitCards.uniqueKeys()) { + // get all parts for this specific card + QList splitCardParts = splitCards.values(nameSplit); + // sort them by index (aka position) + qSort(splitCardParts.begin(), splitCardParts.end(), + [](const SplitCardPart &a, const SplitCardPart &b) -> bool { return a.getIndex() < b.getIndex(); }); + + text = QString(""); + isToken = false; + properties.clear(); + relatedCards.clear(); + + int lastIndex = -1; + for (const SplitCardPart &tmp : splitCardParts) { + // some sets have 2 different variations of the same split card, + // eg. Fire // Ice in WC02. Avoid adding duplicates. + if (lastIndex == tmp.getIndex()) + continue; + lastIndex = tmp.getIndex(); + + if (!text.isEmpty()) + text.append(splitCardTextSeparator); + text.append(tmp.getText()); + + if (properties.isEmpty()) { + properties = tmp.getProperties(); + setInfo = tmp.getSetInfo(); + } else { + const QVariantHash &props = tmp.getProperties(); + for (const QString &prop : props.keys()) { + QString originalPropertyValue = properties.value(prop).toString(); + QString thisCardPropertyValue = props.value(prop).toString(); + if (originalPropertyValue != thisCardPropertyValue) { + if (prop == "colors") { + properties.insert(prop, originalPropertyValue + thisCardPropertyValue); + } else { + properties.insert(prop, + originalPropertyValue + splitCardPropSeparator + thisCardPropertyValue); + } + } + } + } } - // clean variables - cardName = ""; - cardCost = ""; - cmc = ""; - cardType = ""; - cardPT = ""; - cardText = ""; - cardUuId = ""; - setNumber = ""; - rarity = ""; - cardLoyalty = ""; - colors.clear(); - - // loop cards and merge their contents - QString prefix = QString(" // "); - QString prefix2 = QString("\n\n---\n\n"); - for (const QVariantMap &inner_map : orderedMaps.values()) { - if (inner_map.contains("name")) { - if (!cardName.isEmpty()) - cardName += (orderedMaps.count() > 2) ? QString("/") : prefix; - cardName += inner_map.value("name").toString(); - } - if (inner_map.contains("manaCost")) { - if (!cardCost.isEmpty()) - cardCost += prefix; - cardCost += inner_map.value("manaCost").toString(); - } - if (inner_map.contains("convertedManaCost")) { - if (!cmc.isEmpty()) - cmc += prefix; - cmc += inner_map.value("convertedManaCost").toString(); - } - if (inner_map.contains("type")) { - if (!cardType.isEmpty()) - cardType += prefix; - cardType += inner_map.value("type").toString(); - } - if (inner_map.contains("power") || inner_map.contains("toughness")) { - if (!cardPT.isEmpty()) - cardPT += prefix; - cardPT += inner_map.value("power").toString() + QString('/') + inner_map.value("toughness").toString(); - } - if (inner_map.contains("text")) { - if (!cardText.isEmpty()) - cardText += prefix2; - cardText += inner_map.value("text").toString(); - } - if (inner_map.contains("uuid")) { - if (cardUuId.isEmpty()) - cardUuId = inner_map.value("uuid").toString(); - } - if (inner_map.contains("number")) { - if (setNumber.isEmpty()) - setNumber = inner_map.value("number").toString(); - } - if (inner_map.contains("rarity")) { - if (rarity.isEmpty()) - rarity = inner_map.value("rarity").toString(); - } - - colors << inner_map.value("colors").toStringList(); - } - - colors.removeDuplicates(); - if (colors.length() > 1) { - sortColors(colors); - } - - // Fortunately, there are no split cards that flip, transform or meld. - relatedCards = QList(); - reverseRelatedCards = QList(); - upsideDown = false; - - // add the card - CardInfoPtr card = - addCard(set->getShortName(), cardName, false, muid, cardUuId, setNumber, cardCost, cmc, cardType, cardPT, - cardLoyalty, cardText, colors, relatedCards, reverseRelatedCards, upsideDown, rarity); - - if (!set->contains(card)) { - card->addToSet(set); - cards++; - } + CardInfoPtr newCard = addCard(name, text, isToken, properties, relatedCards, setInfo); + numCards++; } - return cards; + return numCards; } -void OracleImporter::sortColors(QStringList &colors) +void OracleImporter::sortAndReduceColors(QString &colors) { - const QHash colorOrder{{"W", 0}, {"U", 1}, {"B", 2}, {"R", 3}, {"G", 4}}; - std::sort(colors.begin(), colors.end(), [&colorOrder](const QString a, const QString b) { + // sort + const QHash colorOrder{{'W', 0}, {'U', 1}, {'B', 2}, {'R', 3}, {'G', 4}}; + std::sort(colors.begin(), colors.end(), [&colorOrder](const QChar a, const QChar b) { return colorOrder.value(a, INT_MAX) < colorOrder.value(b, INT_MAX); }); + // reduce + QChar lastChar = '\0'; + for (int i = 0; i < colors.size(); ++i) { + if (colors.at(i) == lastChar) + colors.remove(i, 1); + else + lastChar = colors.at(i); + } } int OracleImporter::startImport() @@ -333,25 +341,21 @@ int OracleImporter::startImport() clear(); int setCards = 0, setIndex = 0; - QListIterator it(allSets); - const SetToDownload *curSet; - // add an empty set for tokens CardSetPtr tokenSet = CardSet::newInstance(TOKENS_SETNAME, tr("Dummy set containing tokens"), "Tokens"); sets.insert(TOKENS_SETNAME, tokenSet); - while (it.hasNext()) { - curSet = &it.next(); - CardSetPtr set = CardSet::newInstance(curSet->getShortName(), curSet->getLongName(), curSet->getSetType(), - curSet->getReleaseDate()); - if (!sets.contains(set->getShortName())) - sets.insert(set->getShortName(), set); + for (const SetToDownload &curSetToParse : allSets) { + CardSetPtr newSet = CardSet::newInstance(curSetToParse.getShortName(), curSetToParse.getLongName(), + curSetToParse.getSetType(), curSetToParse.getReleaseDate()); + if (!sets.contains(newSet->getShortName())) + sets.insert(newSet->getShortName(), newSet); - int setCardsHere = importTextSpoiler(set, curSet->getCards()); + int numCardsInSet = importCardsFromSet(newSet, curSetToParse.getCards()); ++setIndex; - emit setIndexChanged(setCardsHere, setIndex, curSet->getLongName()); + emit setIndexChanged(numCardsInSet, setIndex, curSetToParse.getLongName()); } emit setIndexChanged(setCards, setIndex, QString()); @@ -362,6 +366,6 @@ int OracleImporter::startImport() bool OracleImporter::saveToFile(const QString &fileName) { - CockatriceXml3Parser parser; + CockatriceXml4Parser parser; return parser.saveToFile(sets, cards, fileName); } \ No newline at end of file diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 68a7b62be..d4228e094 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -3,14 +3,14 @@ #include #include - #include +#include class SetToDownload { private: QString shortName, longName; - QVariant cards; + QList cards; QDate releaseDate; QString setType; @@ -23,7 +23,7 @@ public: { return longName; } - const QVariant &getCards() const + const QList &getCards() const { return cards; } @@ -35,12 +35,13 @@ public: { return releaseDate; } - SetToDownload(const QString &_shortName, - const QString &_longName, - const QVariant &_cards, - const QString &_setType = QString(), + SetToDownload(QString _shortName, + QString _longName, + QList _cards, + QString _setType = QString(), const QDate &_releaseDate = QDate()) - : shortName(_shortName), longName(_longName), cards(_cards), releaseDate(_releaseDate), setType(_setType) + : shortName(std::move(_shortName)), longName(std::move(_longName)), cards(std::move(_cards)), + releaseDate(_releaseDate), setType(std::move(_setType)) { } bool operator<(const SetToDownload &set) const @@ -49,6 +50,34 @@ public: } }; +class SplitCardPart +{ +public: + SplitCardPart(int _index, const QString &_text, const QVariantHash &_properties, CardInfoPerSet setInfo); + inline const int &getIndex() const + { + return index; + } + inline const QString &getText() const + { + return text; + } + inline const QVariantHash &getProperties() const + { + return properties; + } + inline const CardInfoPerSet &getSetInfo() const + { + return setInfo; + } + +private: + int index; + QString text; + QVariantHash properties; + CardInfoPerSet setInfo; +}; + class OracleImporter : public CardDatabase { Q_OBJECT @@ -57,33 +86,22 @@ private: QVariantMap setsMap; QString dataDir; - CardInfoPtr addCard(const QString &setName, - QString cardName, + CardInfoPtr addCard(QString name, + QString text, bool isToken, - int cardId, - QString &cardUuId, - QString &setNumber, - QString &cardCost, - QString &cmc, - const QString &cardType, - const QString &cardPT, - const QString &cardLoyalty, - const QString &cardText, - const QStringList &colors, - const QList &relatedCards, - const QList &reverseRelatedCards, - bool upsideDown, - QString &rarity); + QVariantHash properties, + QList &relatedCards, + CardInfoPerSet setInfo); signals: void setIndexChanged(int cardsImported, int setIndex, const QString &setName); void dataReadProgress(int bytesRead, int totalBytes); public: - OracleImporter(const QString &_dataDir, QObject *parent = 0); + explicit OracleImporter(const QString &_dataDir, QObject *parent = nullptr); bool readSetsFromByteArray(const QByteArray &data); int startImport(); bool saveToFile(const QString &fileName); - int importTextSpoiler(CardSetPtr set, const QVariant &data); + int importCardsFromSet(CardSetPtr currentSet, const QList &cards); QList &getSets() { return allSets; @@ -94,7 +112,8 @@ public: } protected: - void sortColors(QStringList &colors); + inline QString getStringPropertyFromMap(QVariantMap card, QString propertyName); + void sortAndReduceColors(QString &colors); }; #endif diff --git a/cockatrice/src/qt-json/AUTHORS b/oracle/src/qt-json/AUTHORS similarity index 100% rename from cockatrice/src/qt-json/AUTHORS rename to oracle/src/qt-json/AUTHORS diff --git a/cockatrice/src/qt-json/LICENSE b/oracle/src/qt-json/LICENSE similarity index 100% rename from cockatrice/src/qt-json/LICENSE rename to oracle/src/qt-json/LICENSE diff --git a/cockatrice/src/qt-json/README b/oracle/src/qt-json/README similarity index 100% rename from cockatrice/src/qt-json/README rename to oracle/src/qt-json/README diff --git a/cockatrice/src/qt-json/json.cpp b/oracle/src/qt-json/json.cpp similarity index 100% rename from cockatrice/src/qt-json/json.cpp rename to oracle/src/qt-json/json.cpp diff --git a/cockatrice/src/qt-json/json.h b/oracle/src/qt-json/json.h similarity index 100% rename from cockatrice/src/qt-json/json.h rename to oracle/src/qt-json/json.h diff --git a/oracle/translations/oracle_en.ts b/oracle/translations/oracle_en.ts index a65be73ad..3a793e74d 100644 --- a/oracle/translations/oracle_en.ts +++ b/oracle/translations/oracle_en.ts @@ -253,7 +253,7 @@ OracleImporter - + Dummy set containing tokens diff --git a/tests/carddatabase/CMakeLists.txt b/tests/carddatabase/CMakeLists.txt index 5ea6d8ff6..a592f0531 100644 --- a/tests/carddatabase/CMakeLists.txt +++ b/tests/carddatabase/CMakeLists.txt @@ -2,8 +2,9 @@ ADD_DEFINITIONS("-DCARDDB_DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/data/\"") add_executable(carddatabase_test carddatabase_test.cpp ../../cockatrice/src/carddatabase.cpp + ../../cockatrice/src/carddbparser/carddatabaseparser.cpp ../../cockatrice/src/carddbparser/cockatricexml3.cpp - + ../../cockatrice/src/carddbparser/cockatricexml4.cpp ) if(NOT GTEST_FOUND) add_dependencies(carddatabase_test gtest) diff --git a/tests/carddatabase/carddatabase_test.cpp b/tests/carddatabase/carddatabase_test.cpp index 795793dfe..0cff817b5 100644 --- a/tests/carddatabase/carddatabase_test.cpp +++ b/tests/carddatabase/carddatabase_test.cpp @@ -64,7 +64,6 @@ TEST(CardDatabaseTest, LoadXml) // ensure the card database is empty at start ASSERT_EQ(0, db->getCardList().size()) << "Cards not empty at start"; ASSERT_EQ(0, db->getSetList().size()) << "Sets not empty at start"; - ASSERT_EQ(0, db->getAllColors().size()) << "Colors not empty at start"; ASSERT_EQ(0, db->getAllMainCardTypes().size()) << "Types not empty at start"; ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status at start"; @@ -72,7 +71,6 @@ TEST(CardDatabaseTest, LoadXml) db->loadCardDatabases(); ASSERT_EQ(6, db->getCardList().size()) << "Wrong card count after load"; ASSERT_EQ(3, db->getSetList().size()) << "Wrong sets count after load"; - ASSERT_EQ(4, db->getAllColors().size()) << "Wrong colors count after load"; ASSERT_EQ(2, db->getAllMainCardTypes().size()) << "Wrong types count after load"; ASSERT_EQ(Ok, db->getLoadStatus()) << "Wrong status after load"; @@ -80,7 +78,6 @@ TEST(CardDatabaseTest, LoadXml) db->clear(); ASSERT_EQ(0, db->getCardList().size()) << "Cards not empty after clear"; ASSERT_EQ(0, db->getSetList().size()) << "Sets not empty after clear"; - ASSERT_EQ(0, db->getAllColors().size()) << "Colors not empty after clear"; ASSERT_EQ(0, db->getAllMainCardTypes().size()) << "Types not empty after clear"; ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear"; } From 495e232c227c90e8644b10a2612e25a141d50f23 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Thu, 24 Jan 2019 08:53:05 +0100 Subject: [PATCH 45/62] add the amount of cards logged to the reveal cards event (#3516) * add the amount of cards logged to the reveal cards event sets cardname to the amount for the call if not 1 for the fromstr only add braces for single line statements * zach cleanup --- cockatrice/src/messagelogwidget.cpp | 41 +++++++++++++++++------------ cockatrice/src/messagelogwidget.h | 13 ++++++--- cockatrice/src/player.cpp | 14 +++++----- cockatrice/src/player.h | 9 +++++-- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/cockatrice/src/messagelogwidget.cpp b/cockatrice/src/messagelogwidget.cpp index 2305970a6..dc0ac655c 100644 --- a/cockatrice/src/messagelogwidget.cpp +++ b/cockatrice/src/messagelogwidget.cpp @@ -457,9 +457,11 @@ void MessageLogWidget::logRevealCards(Player *player, int cardId, QString cardName, Player *otherPlayer, - bool faceDown) + bool faceDown, + int amount) { - QPair temp = getFromStr(zone, cardName, cardId, false); + // getFromStr uses cardname.empty() to check if it should contain the start zone, it's not actually used + QPair temp = getFromStr(zone, amount == 1 ? cardName : QString::number(amount), cardId, false); bool cardNameContainsStartZone = false; if (!temp.first.isEmpty()) { cardNameContainsStartZone = true; @@ -468,52 +470,57 @@ void MessageLogWidget::logRevealCards(Player *player, QString fromStr = temp.second; QString cardStr; - if (cardNameContainsStartZone) + if (cardNameContainsStartZone) { cardStr = cardName; - else if (cardName.isEmpty()) - cardStr = tr("a card"); - else + } else if (cardName.isEmpty()) { + cardStr = tr("%1 card(s)", "a card for singular, %1 cards for plural", amount) + .arg("" + QString::number(amount) + ""); + } else { cardStr = cardLink(cardName); - + } if (cardId == -1) { - if (otherPlayer) + if (otherPlayer) { appendHtmlServerMessage(tr("%1 reveals %2 to %3.") .arg(sanitizeHtml(player->getName())) .arg(zone->getTranslatedName(true, CaseRevealZone)) .arg(sanitizeHtml(otherPlayer->getName()))); - else + } else { appendHtmlServerMessage(tr("%1 reveals %2.") .arg(sanitizeHtml(player->getName())) .arg(zone->getTranslatedName(true, CaseRevealZone))); + } } else if (cardId == -2) { - if (otherPlayer) + if (otherPlayer) { appendHtmlServerMessage(tr("%1 randomly reveals %2%3 to %4.") .arg(sanitizeHtml(player->getName())) .arg(cardStr) .arg(fromStr) .arg(sanitizeHtml(otherPlayer->getName()))); - else + } else { appendHtmlServerMessage( tr("%1 randomly reveals %2%3.").arg(sanitizeHtml(player->getName())).arg(cardStr).arg(fromStr)); + } } else { if (faceDown && player == otherPlayer) { - if (cardName.isEmpty()) + if (cardName.isEmpty()) { appendHtmlServerMessage( tr("%1 peeks at face down card #%2.").arg(sanitizeHtml(player->getName())).arg(cardId)); - else + } else { appendHtmlServerMessage(tr("%1 peeks at face down card #%2: %3.") .arg(sanitizeHtml(player->getName())) .arg(cardId) .arg(cardStr)); - } else if (otherPlayer) + } + } else if (otherPlayer) { appendHtmlServerMessage(tr("%1 reveals %2%3 to %4.") .arg(sanitizeHtml(player->getName())) .arg(cardStr) .arg(fromStr) .arg(sanitizeHtml(otherPlayer->getName()))); - else + } else { appendHtmlServerMessage( tr("%1 reveals %2%3.").arg(sanitizeHtml(player->getName())).arg(cardStr).arg(fromStr)); + } } } @@ -787,8 +794,8 @@ void MessageLogWidget::connectToPlayer(Player *player) connect(player, SIGNAL(logStopDumpZone(Player *, CardZone *)), this, SLOT(logStopDumpZone(Player *, CardZone *))); connect(player, SIGNAL(logDrawCards(Player *, int)), this, SLOT(logDrawCards(Player *, int))); connect(player, SIGNAL(logUndoDraw(Player *, QString)), this, SLOT(logUndoDraw(Player *, QString))); - connect(player, SIGNAL(logRevealCards(Player *, CardZone *, int, QString, Player *, bool)), this, - SLOT(logRevealCards(Player *, CardZone *, int, QString, Player *, bool))); + connect(player, SIGNAL(logRevealCards(Player *, CardZone *, int, QString, Player *, bool, int)), this, + SLOT(logRevealCards(Player *, CardZone *, int, QString, Player *, bool, int))); connect(player, SIGNAL(logAlwaysRevealTopCard(Player *, CardZone *, bool)), this, SLOT(logAlwaysRevealTopCard(Player *, CardZone *, bool))); } diff --git a/cockatrice/src/messagelogwidget.h b/cockatrice/src/messagelogwidget.h index 2f6e5a02b..cf1c4205a 100644 --- a/cockatrice/src/messagelogwidget.h +++ b/cockatrice/src/messagelogwidget.h @@ -48,7 +48,7 @@ private: const QString stackConstant() const; QString sanitizeHtml(QString dirty) const; - QString cardLink(const QString cardName) const; + QString cardLink(QString cardName) const; QPair getFromStr(CardZone *zone, QString cardName, int position, bool ownerChange) const; public slots: @@ -83,8 +83,13 @@ public slots: void logMulligan(Player *player, int number); void logReplayStarted(int gameId); void logReadyStart(Player *player); - void - logRevealCards(Player *player, CardZone *zone, int cardId, QString cardName, Player *otherPlayer, bool faceDown); + void logRevealCards(Player *player, + CardZone *zone, + int cardId, + QString cardName, + Player *otherPlayer, + bool faceDown, + int amount); void logRollDie(Player *player, int sides, int roll); void logSay(Player *player, QString message); void logSetActivePhase(int phase); @@ -108,7 +113,7 @@ public: MessageLogWidget(const TabSupervisor *_tabSupervisor, const UserlistProxy *_userlistProxy, TabGame *_game, - QWidget *parent = 0); + QWidget *parent = nullptr); }; #endif diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index 65cdda5f2..0f815c379 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -1830,14 +1830,14 @@ void Player::eventRevealCards(const Event_RevealCards &event) } if (peeking) { - for (auto i : cardList) { - QString cardName = QString::fromStdString(i->name()); - CardItem *card = zone->getCard(i->id(), QString()); - if (!card) { + for (auto &card : cardList) { + QString cardName = QString::fromStdString(card->name()); + CardItem *cardItem = zone->getCard(card->id(), QString()); + if (!cardItem) { continue; } - card->setName(cardName); - emit logRevealCards(this, zone, i->id(), cardName, this, true); + cardItem->setName(cardName); + emit logRevealCards(this, zone, card->id(), cardName, this, true, 1); } } else { bool showZoneView = true; @@ -1854,7 +1854,7 @@ void Player::eventRevealCards(const Event_RevealCards &event) static_cast(scene())->addRevealedZoneView(this, zone, cardList, event.grant_write_access()); } - emit logRevealCards(this, zone, event.card_id(), cardName, otherPlayer, false); + emit logRevealCards(this, zone, event.card_id(), cardName, otherPlayer, false, cardList.size()); } } diff --git a/cockatrice/src/player.h b/cockatrice/src/player.h index 4f1dc96d4..229c96c3f 100644 --- a/cockatrice/src/player.h +++ b/cockatrice/src/player.h @@ -127,8 +127,13 @@ signals: void logSetAnnotation(Player *player, CardItem *card, QString newAnnotation); void logDumpZone(Player *player, CardZone *zone, int numberCards); void logStopDumpZone(Player *player, CardZone *zone); - void - logRevealCards(Player *player, CardZone *zone, int cardId, QString cardName, Player *otherPlayer, bool faceDown); + void logRevealCards(Player *player, + CardZone *zone, + int cardId, + QString cardName, + Player *otherPlayer, + bool faceDown, + int amount); void logAlwaysRevealTopCard(Player *player, CardZone *zone, bool reveal); void sizeChanged(); From 1ace060e34cf0ce67035ef91ca188f54e20a332a Mon Sep 17 00:00:00 2001 From: Zach H Date: Fri, 25 Jan 2019 13:36:43 -0500 Subject: [PATCH 46/62] zach's turn to run the show (#3521) --- CMakeLists.txt | 2 +- Dockerfile | 2 +- cockatrice/src/window_main.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 48f1d7fc9..830070fc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,7 +172,7 @@ IF(MSVC) ENDIF() # Package builder -set(CPACK_PACKAGE_CONTACT "Gavin Bisesi ") +set(CPACK_PACKAGE_CONTACT "Zach Halpern ") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_NAME}) set(CPACK_PACKAGE_VENDOR "Cockatrice Development Team") set(CPACK_PACKAGE_DESCRIPTION_FILE "${PROJECT_SOURCE_DIR}/README.md") diff --git a/Dockerfile b/Dockerfile index 6c1e50f50..1a61753a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:trusty -MAINTAINER Gavin Bisesi +MAINTAINER Zach Halpern RUN apt-get update && apt-get install -y software-properties-common RUN apt-add-repository ppa:ubuntu-sdk-team/ppa diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index 657ddc61b..2409d4a0d 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -299,9 +299,9 @@ void MainWindow::actAbout() QMessageBox::NoIcon, tr("About Cockatrice"), QString("Cockatrice (" + QString::fromStdString(BUILD_ARCHITECTURE) + ")
" + tr("Version") + QString(" %1").arg(VERSION_STRING) + "

" + - tr("Cockatrice Webpage") + "
" + "

" + tr("Project Manager:") + - "
Gavin Bisesi

" + "" + tr("Past Project Managers:") + - "
Max-Wilhelm Bruker
Marcus Schütz

" + "" + tr("Developers:") + "
" + + tr("Cockatrice Webpage") + "
" + "
" + tr("Project Manager:") + + "
Zach Halpern

" + "" + tr("Past Project Managers:") + + "
Gavin Bisesi
Max-Wilhelm Bruker
Marcus Schütz

" + "" + tr("Developers:") + "
" + "" + tr("Our Developers") + "
" + "" + tr("Help Develop!") + "

" + "" + tr("Translators:") + "
" + "" + tr("Our Translators") + From 37f9f62f03014df58120bc3fe541f0879747cd11 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Fri, 25 Jan 2019 21:41:39 +0100 Subject: [PATCH 47/62] correctly report exit code in clangify (#3520) --- .ci/travis-compile.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci/travis-compile.sh b/.ci/travis-compile.sh index a82f537aa..2d647962d 100644 --- a/.ci/travis-compile.sh +++ b/.ci/travis-compile.sh @@ -56,8 +56,8 @@ done if [[ $CHECK_FORMAT ]]; then echo "Checking your code using clang-format..." diff="$(./clangify.sh --diff --cf-version)" - ok=$? - case $ok in + err=$? + case $err in 1) cat < Date: Sun, 27 Jan 2019 11:42:22 +0100 Subject: [PATCH 48/62] fix #3523 (#3524) --- oracle/src/oracleimporter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 1b652b006..e080a9153 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -311,8 +311,7 @@ int OracleImporter::importCardsFromSet(CardSetPtr currentSet, const QList Date: Sun, 27 Jan 2019 12:11:22 +0100 Subject: [PATCH 49/62] Bump version to 2.6.3 (#3525) ## Short roundup of the initial problem A beta release for 2.6.3 has been relesed, but the codebase is still at 2.6.2 ## What will change with this Pull Request? Bumped version to 2.6.3 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 830070fc0..f3d8ee588 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,7 @@ endif() # A project name is needed for CPack # Version can be overriden by git tags, see cmake/getversion.cmake -PROJECT("Cockatrice" VERSION 2.6.2) +PROJECT("Cockatrice" VERSION 2.6.3) # Use c++11 for all targets set(CMAKE_CXX_STANDARD 11 CACHE STRING "C++ ISO Standard") From 7cd9b9c0c86d71b42aace48c5d5da609fe91965c Mon Sep 17 00:00:00 2001 From: Rob Blanckaert Date: Sun, 27 Jan 2019 09:50:41 -0800 Subject: [PATCH 50/62] Unconcede (#3515) --- cockatrice/src/gamescene.cpp | 2 +- cockatrice/src/messagelogwidget.cpp | 6 ++++++ cockatrice/src/messagelogwidget.h | 1 + cockatrice/src/player.cpp | 2 +- cockatrice/src/player.h | 2 +- cockatrice/src/tab_game.cpp | 28 +++++++++++++++++++++++++--- common/pb/command_concede.proto | 7 +++++++ common/pb/context_concede.proto | 6 ++++++ common/pb/game_commands.proto | 2 ++ common/pb/game_event_context.proto | 1 + common/server_game.h | 2 +- common/server_player.cpp | 28 ++++++++++++++++++++++++++++ common/server_player.h | 2 ++ 13 files changed, 82 insertions(+), 7 deletions(-) diff --git a/cockatrice/src/gamescene.cpp b/cockatrice/src/gamescene.cpp index 1f3c8d695..d45d6873e 100644 --- a/cockatrice/src/gamescene.cpp +++ b/cockatrice/src/gamescene.cpp @@ -40,7 +40,7 @@ void GameScene::addPlayer(Player *player) players << player; addItem(player); connect(player, SIGNAL(sizeChanged()), this, SLOT(rearrange())); - connect(player, SIGNAL(gameConceded()), this, SLOT(rearrange())); + connect(player, SIGNAL(playerCountChanged()), this, SLOT(rearrange())); } void GameScene::removePlayer(Player *player) diff --git a/cockatrice/src/messagelogwidget.cpp b/cockatrice/src/messagelogwidget.cpp index dc0ac655c..82e195686 100644 --- a/cockatrice/src/messagelogwidget.cpp +++ b/cockatrice/src/messagelogwidget.cpp @@ -169,6 +169,12 @@ void MessageLogWidget::logConcede(Player *player) appendHtmlServerMessage(tr("%1 has conceded the game.").arg(sanitizeHtml(player->getName())), true); } +void MessageLogWidget::logUnconcede(Player *player) +{ + soundEngine->playSound("player_concede"); + appendHtmlServerMessage(tr("%1 has unconceded the game.").arg(sanitizeHtml(player->getName())), true); +} + void MessageLogWidget::logConnectionStateChanged(Player *player, bool connectionState) { if (connectionState) { diff --git a/cockatrice/src/messagelogwidget.h b/cockatrice/src/messagelogwidget.h index cf1c4205a..449df4aa9 100644 --- a/cockatrice/src/messagelogwidget.h +++ b/cockatrice/src/messagelogwidget.h @@ -57,6 +57,7 @@ public slots: void logAlwaysRevealTopCard(Player *player, CardZone *zone, bool reveal); void logAttachCard(Player *player, QString cardName, Player *targetPlayer, QString targetCardName); void logConcede(Player *player); + void logUnconcede(Player *player); void logConnectionStateChanged(Player *player, bool connectionState); void logCreateArrow(Player *player, Player *startPlayer, diff --git a/cockatrice/src/player.cpp b/cockatrice/src/player.cpp index 0f815c379..12e5c7d5f 100644 --- a/cockatrice/src/player.cpp +++ b/cockatrice/src/player.cpp @@ -3037,8 +3037,8 @@ void Player::setConceded(bool _conceded) setVisible(!conceded); if (conceded) { clear(); - emit gameConceded(); } + emit playerCountChanged(); } void Player::setMirrored(bool _mirrored) diff --git a/cockatrice/src/player.h b/cockatrice/src/player.h index 229c96c3f..cb897dbca 100644 --- a/cockatrice/src/player.h +++ b/cockatrice/src/player.h @@ -137,7 +137,7 @@ signals: void logAlwaysRevealTopCard(Player *player, CardZone *zone, bool reveal); void sizeChanged(); - void gameConceded(); + void playerCountChanged(); public slots: void actUntapAll(); void actRollDie(); diff --git a/cockatrice/src/tab_game.cpp b/cockatrice/src/tab_game.cpp index 9fdc79b17..580af9838 100644 --- a/cockatrice/src/tab_game.cpp +++ b/cockatrice/src/tab_game.cpp @@ -620,11 +620,23 @@ void TabGame::actGameInfo() void TabGame::actConcede() { - if (QMessageBox::question(this, tr("Concede"), tr("Are you sure you want to concede this game?"), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) + Player *player = players.value(localPlayerId, nullptr); + if (player == nullptr) return; + if (!player->getConceded()) { + if (QMessageBox::question(this, tr("Concede"), tr("Are you sure you want to concede this game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) + return; - sendGameCommand(Command_Concede()); + sendGameCommand(Command_Concede()); + } else { + if (QMessageBox::question(this, tr("Unconcede"), + tr("You have already conceded. Do you want to return to this game?"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) + return; + + sendGameCommand(Command_Unconcede()); + } } void TabGame::actLeaveGame() @@ -1051,6 +1063,16 @@ void TabGame::eventPlayerPropertiesChanged(const Event_PlayerPropertiesChanged & break; } + case GameEventContext::UNCONCEDE: { + messageLog->logUnconcede(player); + player->setConceded(false); + + QMapIterator playerIterator(players); + while (playerIterator.hasNext()) + playerIterator.next().value()->updateZones(); + + break; + } case GameEventContext::DECK_SELECT: { Context_DeckSelect deckSelect = context.GetExtension(Context_DeckSelect::ext); messageLog->logDeckSelect(player, QString::fromStdString(deckSelect.deck_hash()), diff --git a/common/pb/command_concede.proto b/common/pb/command_concede.proto index 5ac742352..8377d958f 100644 --- a/common/pb/command_concede.proto +++ b/common/pb/command_concede.proto @@ -5,3 +5,10 @@ message Command_Concede { optional Command_Concede ext = 1017; } } + + +message Command_Unconcede { + extend GameCommand { + optional Command_Unconcede ext = 1032; + } +} \ No newline at end of file diff --git a/common/pb/context_concede.proto b/common/pb/context_concede.proto index 7b82aa316..a266ed2b7 100644 --- a/common/pb/context_concede.proto +++ b/common/pb/context_concede.proto @@ -6,3 +6,9 @@ message Context_Concede { optional Context_Concede ext = 1001; } } + +message Context_Unconcede { + extend GameEventContext { + optional Context_Unconcede ext = 1009; + } +} \ No newline at end of file diff --git a/common/pb/game_commands.proto b/common/pb/game_commands.proto index 47d15441d..266c218f8 100644 --- a/common/pb/game_commands.proto +++ b/common/pb/game_commands.proto @@ -33,6 +33,8 @@ message GameCommand { DECK_SELECT = 1029; SET_SIDEBOARD_LOCK = 1030; CHANGE_ZONE_PROPERTIES = 1031; + UNCONCEDE = 1032; + } extensions 100 to max; } diff --git a/common/pb/game_event_context.proto b/common/pb/game_event_context.proto index 98d5047b7..76dccad0d 100644 --- a/common/pb/game_event_context.proto +++ b/common/pb/game_event_context.proto @@ -10,6 +10,7 @@ message GameEventContext { PING_CHANGED = 1006; CONNECTION_STATE_CHANGED = 1007; SET_SIDEBOARD_LOCK = 1008; + UNCONCEDE = 1009; } extensions 100 to max; } diff --git a/common/server_game.h b/common/server_game.h index ca704a870..89ad6e31c 100644 --- a/common/server_game.h +++ b/common/server_game.h @@ -77,7 +77,6 @@ private: Server_Player *playerWhosAsking, bool omniscient, bool withUserInfo); - void sendGameStateToPlayers(); void storeGameInformation(); signals: void sigStartGameIfReady(); @@ -192,6 +191,7 @@ public: prepareGameEvent(const ::google::protobuf::Message &gameEvent, int playerId, GameEventContext *context = 0); GameEventContext prepareGameEventContext(const ::google::protobuf::Message &gameEventContext); + void sendGameStateToPlayers(); void sendGameEventContainer(GameEventContainer *cont, GameEventStorageItem::EventRecipients recipients = GameEventStorageItem::SendToPrivate | GameEventStorageItem::SendToOthers, diff --git a/common/server_player.cpp b/common/server_player.cpp index 22f7f7712..b9bde462f 100644 --- a/common/server_player.cpp +++ b/common/server_player.cpp @@ -776,6 +776,30 @@ Server_Player::cmdConcede(const Command_Concede & /*cmd*/, ResponseContainer & / return Response::RespOk; } +Response::ResponseCode +Server_Player::cmdUnconcede(const Command_Unconcede & /*cmd*/, ResponseContainer & /*rc*/, GameEventStorage &ges) +{ + if (spectator) + return Response::RespFunctionNotAllowed; + if (!game->getGameStarted()) + return Response::RespGameNotStarted; + if (!conceded) + return Response::RespContextError; + + setConceded(false); + + Event_PlayerPropertiesChanged event; + event.mutable_player_properties()->set_conceded(false); + ges.enqueueGameEvent(event, playerId); + ges.setGameEventContext(Context_Unconcede()); + + setupZones(); + + game->sendGameStateToPlayers(); + + return Response::RespOk; +} + Response::ResponseCode Server_Player::cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer & /*rc*/, GameEventStorage &ges) { @@ -1826,6 +1850,10 @@ Server_Player::processGameCommand(const GameCommand &command, ResponseContainer case GameCommand::CHANGE_ZONE_PROPERTIES: return cmdChangeZoneProperties(command.GetExtension(Command_ChangeZoneProperties::ext), rc, ges); break; + case GameCommand::UNCONCEDE: + return cmdUnconcede(command.GetExtension(Command_Unconcede::ext), rc, ges); + break; + default: return Response::RespInvalidCommand; } diff --git a/common/server_player.h b/common/server_player.h index 681934020..e8caa26f6 100644 --- a/common/server_player.h +++ b/common/server_player.h @@ -46,6 +46,7 @@ class Command_SetCardCounter; class Command_IncCardCounter; class Command_ReadyStart; class Command_Concede; +class Command_Unconcede; class Command_IncCounter; class Command_CreateCounter; class Command_SetCounter; @@ -190,6 +191,7 @@ public: Response::ResponseCode cmdKickFromGame(const Command_KickFromGame &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode cmdConcede(const Command_Concede &cmd, ResponseContainer &rc, GameEventStorage &ges); + Response::ResponseCode cmdUnconcede(const Command_Unconcede &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode cmdReadyStart(const Command_ReadyStart &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &rc, GameEventStorage &ges); Response::ResponseCode From c3d94a51eca2a81db00344ea0d11eee666b7323c Mon Sep 17 00:00:00 2001 From: ctrlaltca Date: Sun, 27 Jan 2019 18:51:03 +0100 Subject: [PATCH 51/62] fix #3498 (#3527) --- cockatrice/src/window_main.cpp | 51 +- cockatrice/translations/cockatrice_en.ts | 793 +++++++++++++---------- oracle/translations/oracle_en.ts | 2 +- 3 files changed, 491 insertions(+), 355 deletions(-) diff --git a/cockatrice/src/window_main.cpp b/cockatrice/src/window_main.cpp index 2409d4a0d..326e0dda1 100644 --- a/cockatrice/src/window_main.cpp +++ b/cockatrice/src/window_main.cpp @@ -301,14 +301,15 @@ void MainWindow::actAbout() tr("Version") + QString(" %1").arg(VERSION_STRING) + "

" + tr("Cockatrice Webpage") + "
" + "
" + tr("Project Manager:") + "
Zach Halpern

" + "" + tr("Past Project Managers:") + - "
Gavin Bisesi
Max-Wilhelm Bruker
Marcus Schütz

" + "" + tr("Developers:") + "
" + - "" + tr("Our Developers") + "
" + "" + tr("Help Develop!") + "

" + "" + tr("Translators:") + - "
" + "" + tr("Our Translators") + - "
" + "" + tr("Help Translate!") + "

" + - "" + tr("Support:") + "
" + "" + tr("Report an Issue") + - "
" + "" + tr("Troubleshooting") + "
" + - "" + tr("F.A.Q.") + "
"), + "
Gavin Bisesi
Max-Wilhelm Bruker
Marcus Schütz

" + "" + tr("Developers:") + + "
" + "" + tr("Our Developers") + "
" + + "" + tr("Help Develop!") + "

" + "" + + tr("Translators:") + "
" + "" + + tr("Our Translators") + "
" + "" + + tr("Help Translate!") + "

" + "" + tr("Support:") + "
" + "" + tr("Report an Issue") + "
" + "" + tr("Troubleshooting") + "
" + "" + tr("F.A.Q.") + "
"), QMessageBox::Ok, this); mb.setIconPixmap(QPixmap("theme:cockatrice").scaled(64, 64)); mb.setTextInteractionFlags(Qt::TextBrowserInteraction); @@ -705,15 +706,33 @@ void MainWindow::createActions() aExit->setMenuRole(QAction::QuitRole); aAbout->setMenuRole(QAction::AboutRole); - char const *foo; // avoid "warning: expression result unused" under clang - foo = QT_TRANSLATE_NOOP("QMenuBar", "Services"); - foo = QT_TRANSLATE_NOOP("QMenuBar", "Hide %1"); - foo = QT_TRANSLATE_NOOP("QMenuBar", "Hide Others"); - foo = QT_TRANSLATE_NOOP("QMenuBar", "Show All"); - foo = QT_TRANSLATE_NOOP("QMenuBar", "Preferences..."); - foo = QT_TRANSLATE_NOOP("QMenuBar", "Quit %1"); - foo = QT_TRANSLATE_NOOP("QMenuBar", "About %1"); + Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Services")); + Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide %1")); + Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide Others")); + Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Show All")); + Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Preferences...")); + Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Quit %1")); + Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "About %1")); #endif + // translate Qt's dialogs "default button text"; list taken from QPlatformTheme::defaultStandardButtonText() + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "OK")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save All")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Open")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&No")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Abort")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Retry")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Close")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Discard")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Help")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Apply")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Reset")); + Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults")); } void MainWindow::createMenus() diff --git a/cockatrice/translations/cockatrice_en.ts b/cockatrice/translations/cockatrice_en.ts index bc74d9a10..3c35f2d44 100644 --- a/cockatrice/translations/cockatrice_en.ts +++ b/cockatrice/translations/cockatrice_en.ts @@ -22,62 +22,62 @@ AppearanceSettingsPage - + Theme settings - + Current theme: - + Card rendering - + Display card names on cards having a picture - + Scale cards on mouse over - + 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: @@ -192,22 +192,22 @@ This is only saved for moderators and cannot be seen by the banned person. BetaReleaseChannel - + Beta Releases - + No reply received from the release update server. - + Invalid reply received from the release update server. - + No reply received from the file update server. @@ -215,32 +215,32 @@ This is only saved for moderators and cannot be seen by the banned person. CardDatabaseModel - + Name - + Sets - + Mana cost - + Card type - + P/T - + Color(s) @@ -330,17 +330,17 @@ This is only saved for moderators and cannot be seen by the banned person. CardFrame - + Image - + Description - + Both @@ -348,17 +348,17 @@ This is only saved for moderators and cannot be seen by the banned person. CardInfoText - + Related cards: - + Unknown card: - + Name: @@ -502,120 +502,120 @@ 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 - + Updating... - + Choose path - + URL Download Priority - + Spoilers - + Download Spoilers Automatically - + Spoiler Location: - + Download card pictures on the fly - + How to add a custom URL - + Delete Downloaded Images - + Reset Download URLs - + Last Updated - + Spoilers download automatically on launch - + Press the button to manually update without relaunching - + Do not close settings until manual update complete @@ -1540,12 +1540,12 @@ Make sure to enable the 'Token' set in the "Manage sets" dia DlgSettings - + Unknown Error loading card database - + Your card database is invalid. Cockatrice may not function correctly with an invalid database @@ -1556,7 +1556,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 @@ -1567,7 +1567,7 @@ Would you like to change your database location setting? - + Your card database did not finish loading Please file a ticket at http://github.com/Cockatrice/Cockatrice/issues with your cards.xml attached @@ -1576,21 +1576,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 http://github.com/Cockatrice/Cockatrice/issues @@ -1599,59 +1599,59 @@ 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 - + Card Sources - + Chat - + Sound - + Shortcuts @@ -2043,76 +2043,81 @@ You may have to build from source yourself. GeneralSettingsPage - - - - - + + + + + Choose path - + Personal settings - + Language: - + Paths (editing disabled in portable mode) - + Paths - + Decks directory: - + Replays directory: - + Pictures directory: - + Card database: - + Token database: - + Picture cache size: - + Update channel - + 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 @@ -2121,7 +2126,7 @@ You may have to build from source yourself. MainWindow - + The server has reached its maximum user capacity, please check back later. @@ -2152,7 +2157,7 @@ You may have to build from source yourself. - + Invalid username. @@ -2262,12 +2267,12 @@ Will now login. - + Translators: - + Help Translate! @@ -2277,199 +2282,199 @@ Will now login. - + Report an Issue - + Troubleshooting - + F.A.Q. - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + Error - + Server timeout - + Failed Login - + 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 ID's. 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 try closing and reopening your client and try again. If the error persists try updating your client to the most recent build and if need be contact your software provider. - + Account activation - + 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 - + 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. - + Your client seems to be missing features this server requires for connection. @@ -2479,365 +2484,376 @@ This usually means that your client version is out of date, and the server sent - + Our Translators - + To update your client, go to 'Help -> Check for Client Updates'. - + Your account has not been activated yet. You need to provide the activation token received in the activation email. - + The email address provider used during registration has been blacklisted for use on this server. - + 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. - + 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 forgot password to %1 as %2... - + &Connect... - + &Disconnect - + Start &local game... - + &Watch replay... - + &Deck editor - + &Full screen - + &Register to server... - + &Settings... - - + + &Exit - + A&ctions - + &Cockatrice - + C&ard Database - + Open custom image folder - + Open custom sets folder - + Add custom sets/cards - + Edit &tokens... - + &About Cockatrice - + &Tip of the Day - + Check for Client Updates - + View &debug log - + &Help - + Check for card updates... - + + New Version + + + + + Congratulations on updating to Cockatrice %1! +Oracle will now launch to update your 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 - + %1 new set(s) found in the card database Set code(s): %2 Do you want to enable it/them? - + View sets - + Welcome - - - + + + Information - + A card database update is already running. - + Unable to run the card database updater: - + failed to start. - + crashed. - + timed out. - + write error. - + read error. - + unknown error. - + The card database updater exited with an error: %1 - + Update completed successfully. Cockatrice will now reload the card database. - + You can only import XML databases at this time. - - - + + + Forgot Password - + Your password has been reset successfully, you now may 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. - - - - - + + + + + Load sets/cards - + &Manage sets... - + 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. - + 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. @@ -2845,18 +2861,18 @@ To update your client, go to Help -> Check for Updates. - + Selected file cannot be found. - + The new sets/cards have been added successfully. Cockatrice will now reload the card database. - + Sets/cards failed to import. @@ -2945,7 +2961,6 @@ Cockatrice will now reload the card database. - a card @@ -3013,68 +3028,77 @@ Cockatrice will now reload the card database. %1 is not watching the game any more (%2). + + + %1 card(s) + a card for singular, %1 cards for plural + + + + + - + Untap - + Upkeep - + Draw - + First Main - + Beginning of Combat - + Declare Attackers - + Declare Blockers - + Combat Damage - + End of Combat - + Second Main - + End/Cleanup - + Unknown Phase - + %1's turn. @@ -3109,12 +3133,12 @@ Cockatrice will now reload the card database. - + %1 has locked their sideboard. - + %1 has unlocked their sideboard. @@ -3134,17 +3158,17 @@ Cockatrice will now reload the card database. - + %1 places %2 %3 on %4 (now %5). - + %1 removes %2 %3 from %4 (now %5). - + red counter(s) @@ -3152,7 +3176,7 @@ Cockatrice will now reload the card database. - + yellow counter(s) @@ -3160,7 +3184,7 @@ Cockatrice will now reload the card database. - + green counter(s) @@ -3168,27 +3192,27 @@ Cockatrice will now reload the card database. - + %1 shuffles %2. - + Heads - + Tails - + %1 flipped a coin. It landed as %2. - + %1 rolls a %2 with a %3-sided die. @@ -3201,12 +3225,12 @@ Cockatrice will now reload the card database. - + %1 undoes their last draw. - + %1 undoes their last draw (%2). @@ -3286,7 +3310,7 @@ Cockatrice will now reload the card database. - + %1 unattaches %2. @@ -3336,47 +3360,47 @@ Cockatrice will now reload the card database. - + %1 taps their permanents. - + %1 untaps their permanents. - + %1 taps %2. - + %1 untaps %2. - + %1 sets counter %2 to %3 (%4%5). - + %1 sets %2 to not untap normally. - + %1 sets %2 to untap normally. - + %1 sets PT of %2 to %3. - + %1 sets annotation of %2 to %3. @@ -3386,47 +3410,47 @@ Cockatrice will now reload the card database. - + %1 stops looking at %2. - + %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. @@ -3434,105 +3458,105 @@ Cockatrice will now reload the card database. MessagesSettingsPage - + Word1 Word2 Word3 - + Add New URL - + Edit URL - + Remove URL - + Add message - - + + Message: - + Edit message - + Chat settings - + Custom alert words - + Enable chat mentions - + Enable mention completer - + In-game message macros - + Ignore chat room messages sent by unregistered users - + Ignore private messages sent by unregistered 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 @@ -3646,7 +3670,7 @@ Cockatrice will now reload the card database. Player - + Reveal top cards of library @@ -4002,7 +4026,7 @@ Cockatrice will now reload the card database. - + C&reate another %1 token @@ -4012,32 +4036,32 @@ Cockatrice will now reload the card database. - + Token: - + Place card X cards from top of library - + How many cards from the top of the deck should this card be placed: - + View related cards - + Attach to - + All tokens @@ -4088,7 +4112,7 @@ Cockatrice will now reload the card database. - + Number: @@ -4113,27 +4137,27 @@ Cockatrice will now reload the card database. - + Set power/toughness - + Please enter the new PT: - + Set annotation - + Please enter the new annotation: - + Set counters @@ -4215,6 +4239,99 @@ Cockatrice will now reload the card database. + + QPlatformTheme + + + Cancel + + + + + Discard + + + + + Help + + + + + Apply + + + + + &Yes + + + + + Save + + + + + Save All + + + + + Open + + + + + Yes to &All + + + + + &No + + + + + N&o to All + + + + + Abort + + + + + Retry + + + + + Ignore + + + + + Close + + + + + Reset + + + + + Restore Defaults + + + + + OK + + + RemoteDeckList_TreeModel @@ -4404,27 +4521,27 @@ Please check your shortcut settings! SoundSettingsPage - + Enable &sounds - + Current sounds theme: - + Test system sound engine - + Sound settings - + Master volume @@ -5840,42 +5957,42 @@ Please refrain from engaging in this activity or further actions may be taken ag UserInterfaceSettingsPage - + General interface settings - + Enable notifications in taskbar - + Notify in the taskbar for game events while you are spectating - + &Double-click cards to play them (instead of single-click) - + &Play all nonlands onto the stack (not the battlefield) by default - + Annotate card text on tokens - + Animation settings - + &Tap/untap animation diff --git a/oracle/translations/oracle_en.ts b/oracle/translations/oracle_en.ts index 3a793e74d..2152cffba 100644 --- a/oracle/translations/oracle_en.ts +++ b/oracle/translations/oracle_en.ts @@ -253,7 +253,7 @@ OracleImporter - + Dummy set containing tokens From c51377ca2a1c8ffd31d3d430ff28770390528863 Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 30 Jan 2019 14:57:48 +0100 Subject: [PATCH 52/62] fix #3500 --- cockatrice/src/messagelogwidget.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/messagelogwidget.cpp b/cockatrice/src/messagelogwidget.cpp index 82e195686..7a29e8797 100644 --- a/cockatrice/src/messagelogwidget.cpp +++ b/cockatrice/src/messagelogwidget.cpp @@ -479,8 +479,12 @@ void MessageLogWidget::logRevealCards(Player *player, if (cardNameContainsStartZone) { cardStr = cardName; } else if (cardName.isEmpty()) { - cardStr = tr("%1 card(s)", "a card for singular, %1 cards for plural", amount) - .arg("" + QString::number(amount) + ""); + if (amount == 0) { + cardStr = tr("cards", "an unknown amount of cards"); + } else { + cardStr = tr("%1 card(s)", "a card for singular, %1 cards for plural", amount) + .arg("" + QString::number(amount) + ""); + } } else { cardStr = cardLink(cardName); } From bb4ca4db4af5bd5941213a45825ef5256a0321de Mon Sep 17 00:00:00 2001 From: ebbit1q Date: Wed, 30 Jan 2019 15:29:24 +0100 Subject: [PATCH 53/62] set default minumum player count for multi column layout to 4 (#3535) fixes #3533 does not change existing settings --- cockatrice/src/settingscache.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/settingscache.cpp b/cockatrice/src/settingscache.cpp index 393b014de..8583885a4 100644 --- a/cockatrice/src/settingscache.cpp +++ b/cockatrice/src/settingscache.cpp @@ -233,7 +233,7 @@ SettingsCache::SettingsCache() displayCardNames = settings->value("cards/displaycardnames", true).toBool(); horizontalHand = settings->value("hand/horizontal", true).toBool(); invertVerticalCoordinate = settings->value("table/invert_vertical", false).toBool(); - minPlayersForMultiColumnLayout = settings->value("interface/min_players_multicolumn", 5).toInt(); + minPlayersForMultiColumnLayout = settings->value("interface/min_players_multicolumn", 4).toInt(); tapAnimation = settings->value("cards/tapanimation", true).toBool(); chatMention = settings->value("chat/mention", true).toBool(); chatMentionCompleter = settings->value("chat/mentioncompleter", true).toBool(); @@ -941,4 +941,4 @@ void SettingsCache::setMaxFontSize(int _max) { maxFontSize = _max; settings->setValue("game/maxfontsize", maxFontSize); -} \ No newline at end of file +} From 6e1b0a7590d27b9a74bfb8d30156eb9fee3893ab Mon Sep 17 00:00:00 2001 From: Filip Sufitchi Date: Thu, 31 Jan 2019 19:01:17 -0500 Subject: [PATCH 54/62] Fix up Dockerfile to use ubuntu:bionic and actually build (#3540) * Fix up Dockerfile to use ubuntu:bionic and actually build * Remove commented PPA * Fix up Dockerfile to use ubuntu:bionic and actually build * cleanup * fixup merge --- Dockerfile | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1a61753a9..e96f34055 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,23 @@ -FROM ubuntu:trusty +FROM ubuntu:bionic MAINTAINER Zach Halpern -RUN apt-get update && apt-get install -y software-properties-common -RUN apt-add-repository ppa:ubuntu-sdk-team/ppa -RUN add-apt-repository -y ppa:smspillaz/cmake-master RUN apt-get update && apt-get install -y\ - build-essential g++\ + build-essential\ cmake\ git\ libprotobuf-dev\ + libqt5sql5-mysql\ protobuf-compiler\ qt5-default\ qtbase5-dev\ qttools5-dev-tools\ qttools5-dev\ - libqt5sql5-mysql + software-properties-common + +COPY . /home/servatrice/code/ +WORKDIR /home/servatrice/code -ENV dir /home/servatrice/code -WORKDIR $dir RUN mkdir oracle -COPY LICENSE LICENSE -COPY CMakeLists.txt CMakeLists.txt -COPY cmake/ cmake/ -COPY common/ common/ -COPY servatrice/ servatrice/ -COPY README.md README.md WORKDIR build RUN cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 &&\ From c9c0fb28ee9331ad85d8b05926a879f277afc26f Mon Sep 17 00:00:00 2001 From: Rob Blanckaert Date: Sat, 2 Feb 2019 10:56:49 -0800 Subject: [PATCH 55/62] Counter expressions (#3534) * Add peglib * - Add expression engine - Take an expression when setting a counter * Shift + Click = Middleclick for counters * minor cleanup for clangify Signed-off-by: Zach Halpern * Added tip entry --- clangify.sh | 1 + cockatrice/cockatrice.qrc | 1 + .../tips/images/counter_expression.png | Bin 0 -> 44830 bytes cockatrice/resources/tips/tips_of_the_day.xml | 6 + cockatrice/src/abstractcounter.cpp | 22 +- cockatrice/src/abstractcounter.h | 26 +- cockatrice/src/dlg_tip_of_the_day.cpp | 10 +- common/CMakeLists.txt | 1 + common/expression.cpp | 108 + common/expression.h | 28 + common/lib/peglib.h | 3293 +++++++++++++++++ tests/CMakeLists.txt | 8 + tests/expression_test.cpp | 30 + 13 files changed, 3508 insertions(+), 26 deletions(-) create mode 100644 cockatrice/resources/tips/images/counter_expression.png create mode 100644 common/expression.cpp create mode 100644 common/expression.h create mode 100644 common/lib/peglib.h create mode 100644 tests/expression_test.cpp diff --git a/clangify.sh b/clangify.sh index 9113952e1..671c69b98 100755 --- a/clangify.sh +++ b/clangify.sh @@ -14,6 +14,7 @@ include=("common" \ "servatrice/src") exclude=("servatrice/src/smtp" \ "common/sfmt" \ +"common/lib" \ "oracle/src/zip" \ "oracle/src/lzma" \ "oracle/src/qt-json") diff --git a/cockatrice/cockatrice.qrc b/cockatrice/cockatrice.qrc index 3619cc0d5..aff2f002b 100644 --- a/cockatrice/cockatrice.qrc +++ b/cockatrice/cockatrice.qrc @@ -351,6 +351,7 @@ resources/tips/images/cockatrice_register.png resources/tips/images/cockatrice_wiki.png resources/tips/images/coin_flip.png + resources/tips/images/counter_expression.png resources/tips/images/face_down.png resources/tips/images/filter_games.png resources/tips/images/github_logo.png diff --git a/cockatrice/resources/tips/images/counter_expression.png b/cockatrice/resources/tips/images/counter_expression.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6bbcefe93cfb2747aff3b4e84492d33585f086 GIT binary patch literal 44830 zcmeFYbyQs4mM@CCyHiNe6z=Zst|3U_?oMzC9$bP;2<{FE5-hm8ySoIqm3*hq`Odrj z`o13H-TxX!)!2KjIe&AmX-oF5NEIb%R3suK2nYyNSs4j62nZ-F@O32u9QfO`YC zf{@8uL&sIk*aP6`>|kzX3j(-$If4KnPb+f>2+zfejDs3#ZuijF9;EaS&rrMXW8&f2 zjMhXoxfje_Xr%J|xAkkw;=2t;p$hq5uh!x{Hsj}H*n&>!n%k?I%dR-%IpH6_3nho9S319L3o9<>Ftlv5%)F^V% z8V!e%QBHJHgp*1&Z?xwyzAaAMitHaF&{jCplgINhV5ZkK+K{3WTq=^H!PAvDh!x($ zTJ?KsTSJQBKkMcZdE=7nquEM9msTj`i$$V2r68U@<*_O$_WilP zzWu(F`HsCJ;Fr1yHsM?5kr-GsVPWwRte4V?>&q-pql<+HrE_p3loz3 zuB~O&`^?mF>f3m*GWsfOe>D3~f#LEg^Gv^3$%pdxp|W8ueZLq|kHz}e%B5xdwlk;i zc}Yq_JJY5m+5YS)huX#;rb*XWg-jX>eytEoKmA8r7IoSR8?e|>fV+#6 zX@g+&)95eoLn#9z;RX6NC)F0eD@&7M#!zq_n)Zne6d zy=~KtG|dtkLJ6OF`@Oq*HTh(W;4UkpIPPtF{k??zT9VYtgj9pz9idxdLa(hPa_@XU z?8)0SMgG*GO8OLD2NGH@dC+l>oPJ#D+N?}Rm3@nz{#5k1I#jCV+|n<->EQv|8N(pr zZV5|MCFF3)R2Q1av~b%QDY?L11-DP@OG^}9rs!6&H849|N@V=Z!}bkanX28m;|>Lw zuE=K7YbFy+^+~431!OM^X&@C}Kh{qe=J_1p|v7yq%{3Ev!93lj~=I5Ox4xi$& z-Y|G4R{3y|V(W5Zn3pd;ed6fR@i=5UR!FfKY+_SdIQL=liC&SL|CuuntXyy)_s~() zP_M6xL3a6c=qK&Kzr4FBnEo*5=6)=clXgmSKo_PRnsk^O?Of@hS5cER@?9pMQ+Wzk z-!;tPE1&9$phSO;(!qIby)_?$owxn;WMe||f+dc(J*SxZk371m&g#L>N%{moff>xF zK0}LwV_N7yq+%wHzMAY&U}-+k3wmd@x8q>%TYXD?Y#_xnV9L?eCysY*+!_gI3K0ey zg7BfgZclL5xdq>tn~xp;JAF~fwihPM-R%??aJ^Tk6T*En1t%6zr103qqiX)p}eVjI)y zS%n4D1LSr*Zt8cd&gsUzAw}fl5|ze7$D?5-28187(tQ#?wIQLuNjo*NPZm~36J;0h z+tiT_y-eovBgMx(m=^wMCPYGrr~I^2%BEIGHG9yaQFN?x_MzLL4#IX?M+rwec2=j_ zZ8fraSKnw-Etha&v3_+va1?dKyJ+CKA6V)-l)(j4w};m8Q`E0KDd7MT!7e;e>zBCd zxN+v@RJ%<$M?uL5M28x`rpYp_0^!Wbq;$Lgr)nes7W#lOt~LILizG4}6+#tn#;R3i zv}o*u)e&lTzotD1N)jNBOC+eTV^XIf#d&c^;*BCyS0jNJ!<^tOvMkBZqHg7we3)Zn zop-tl`{9MwC;qG#_ z8jBLIX2kp27^Kc79UJDJJy7S#9~yqrZq$?=ZpO{MmWU1ikc?{VR^eFGA;@%1JIsd` z7tVZ_hI7kvqo!wLs6zE&gG!}YrJT<4OQpV3y#@66L0hMTp4kn8h1Gm@AggfZhgfGj z3uyQXY5-=us+CODFb-?nw)Z+J1q2R@S6&|fqbKvc$BM!1;9WgmUDaE5B?2?GB`YY; z<#u^T4O<9niLN(}rrLwL1L?~VG7M2>RQlecB~vAEg19RA$zPAnbxVWhxvNbnXy^yT zv&-)45LlOq_%NV*Ryh}M8?d#^A}JyHa^H9+XMLA5V0~-dVGXwjN` zAuoyCHv&wM>s+Cby&P67O@()kfKjf=qo8$e9D(t(-Ddg>Tdo~6Xr-}jKA$oQ$9IMn z9vc(C-cXAo)hUS>a%5;zw59ajCaLlB|(i0oBVwnR7Q zWFW1$b-!hEs$Eg(Jr6rof!`5P?QLLykcv!#83PIhk`?>2Bonf9M^jk=<2lDI8yKJC z#c7>$$wQN-QA$PpyBUD!;X6`lMTnf(MIPv_aQZG_ZIP1N8$gTQyV#X`>`beAb6P zRR(Bpn3!OfHfSf1Bc4+a=q|%pQ^sZw>Qp;;nr1u}4Gzv*tL29xlc)|uDdYiGZ|;bs zhS~{e3T+|}eaystap(Kp1zE0#Pjl({#?o{eGKsQ`prQMC z8)F#({p!w!0Rk|X?eI2IWt@gk&x)#k-yKV~uLOAt#|rH_>>DXs9&h}_&vD8o6T5XD z9405j;`#hn42JHHuo?9~k|)Rsc>5)Ue6~AAh(F_vd=TDGL{q)NxnSg%)e4>%sA%Q) zqECcGby25@M%lC-Pjj01a&9P&xG`th^`@&}MU;9WVn9G(_m3y%{0CyizDb7AEp5BJ3VLv{~DqwP*~$$C3&i45ke6Li;wo} za^R`7+uPR9+f7jYqU-xBVTrkd+#~|iLX8mHeoVd-<`T^bn7tIs3%6lH#XW0Qsu+2x zZ;@Kq3GtpoSi8jpWca@m5^Z3^+Cf(IvA^Nzo42wGn*zBp+oq}ySzMu7Sd~b{*90%n zjl!(CRMkk%%4nBMD@c6Q3Z+r{Hi9P~JJ(7e@vgajt+T4fwmOQGC()#6ODnWE??=WL z33}70hGmOJ%oDUKLqKf#^H#UngkO1aAV21J=nFLK81wRT0vbH0MO}`8Y4z%4+H;NZ zH^biq2sJQF!~{&;uu$&x@?VJ{WRR(dy=2=^B9^|k0-nFt@G&IAsxPM&cUUK~ec{!U zBc@4Rx}yE=E!^fz+?kx%cYME7qljp{SkwEfpqRqatk-?-yWsRTC5~rrVV>jo zZ&2-emPe`z?_WsJ?2HaYyvj?EYGx&ldR5jiF32>HE3p=(9#yLtGWj*^P&7qFEg>8v z3+;qY2i3^Ql^x1-C{E_OMJ$aod)0{|y;LOP=U zD*2FR=n;8ydTY0YIk$sV9W4CGdRbPOex-y0NTNP10OyJ>V^KJIT-({=WCQBA0U|0h z06dGlH~b8kdWz>nWM4%&Po$wg!x_9mxx9jU*X>(xA+gbeJ|GrtUlMK~rgjidlZA#} zr{4q3);t>pH<*a#ZZ$)~qidLj= z@~3t*fAsQF0pmCJ9Wk`>-xU5~Vcp-OwR%X)rF@KVesHTVg&f@`TqxuTDb-PcF4 zB(|27pYXU01@WpU{h5|Msw`aqG7aOWhun17R(LFx2gQ1QRbKPQK8E;`cT-x-ilXET zbxL>YO(*J~_tZbH^OC(X7ZeFsHU~6+8@p)mdpteAy9gj$d3L`K;C^&4KYRIf@Y-`< zt2$$>#$Xr`3txXu=$Vp2yxh>91}Fs)Z${_HP_aQ@5^vg;`NEUTn6%l)^C>370djG-}gmaSx!Ml5AJCUfbQP#lH@)dtV81NmeR2XG^jJni5vbTjN@XY zFksZC)pr-yfo%=WK@y2mi>KC+f7io_$V`cW1XzqWfo!S^Ye(m~cMh9{INv_RR)e?+ z)gdo`&Vu|!@r%|R?!Ao~D+hbNuV!_)%tRFjYzl-d`OZR1jMX@F;~+uq&-A+3&!b_8Mm3X zvZX6h9b{EW8SN*H$jRX`_=>?i2LGvG|a5Cs2OZ=^dskQRG|~Js%9Mc0(-< zA&mleL4okMi=vsr%?D2`?@!{!A6Yw$%){4@of;VC3(%nP723!aw~9=MrQ6>QoV|rBQAkuOudhQVQ0~IQK;`6@J0XvJD78i) z28duvJ{mva(j&D*!TH#_%FANzDvUasCwp?1qBo#E%#J>N>B@Rf#m9TQKm4W=(}?!% zd(3uJJ;9nW1e`*mGUQ~0a{HAn%2mW!6iCEa4-_&N9D&cMTQE{g2ikImv|O7<^Qy@o zRM+Tp=euDD;r3HI`{`0H(W3LLlcuKgrYEg1h*BfOXF3k)Y^k;}XK>WVmG-}ba?s+4 zg;3v^J(&t;k(}l|xSbBV|k6ui?w8 zc1-=~IY5^S_S2JbCq_0!imp%bBN=J_<^B`8!~m}7maNaNj9E~lTcb=>DgTGc3RFxx zOI`QRztCUN=JUC_3Vgkz$}Iq}b6@YM;#_5cl(2Z!kE8Sr4ZYlN;+vW|+@h)R(GXD{ z3HM~(LD<`dGRPBt?-ev?2HN)W^^Jj&|P9Jfy=~isOT*r0s9q3+(4`F53 zfvmB+*UZG_ND@)@{6<7benotgX>3aEQeIu0g0ri3Z;E<@nIlrs&UjFmRzFbL!XX$dOPCH-#XXiIZnh~!B7Qh<-aV2c02LLbq765DYQWp8V8PuL~j zkwssoiHXH5Cig@w5tgRzt@)DgRk$8}>EszsATggM2Mwj%5v>h!u=nhi41qv=YmD*h zwvQV|QnwGOSaPi;R=at=F=U?ynx(gr$Yp>D69!T!Z+Tt}qGM732W&*NOgo_UI#S8H z+XMZVVtbVmylf;FENkERGv0knWyP3HQ94OHTXJQI+NPhrySG_bd4)HYOF1bWu#vN6 zC6+JI)5IJ7nF-Mm%lcc&ZS(G4JR!c=(kj$hqpl%*aVH#hR2~jieH6j1S@E}Zl^(x1 zcC)yoRL)r!%z>{KccSUbj2ZO&(cj))@~L$rgxjD#xVmL1B~;fgr{4B|5O2OTK8Av< zPYsYAg;_{}b8DLIJ5doeGy`5~p7DJC{-iPWBR94d=Y2E{T*(ASF7{{HXD(`)j^TXr z#nw@;_v&Wn=u`T5Nmy`9_($7oXj{pJdYAK@aJCkt@eUZ*o~^?aYq}i@}KdSNv;`F=&nH(uCL7N$3uXQ!0A}IKpI~?7b;XR3)(;6jd5@fqHwO z>M+OokEyp#@(dJ^R?y}sF!Q;PlAp)X5h>w&5W@h#1>k@TS_+YXDm(`ZS*z@O@;c*6 zSDA@5ady-qQC-!Hh3n?f0$waXV_4+25c~)S^9ZErSvSIViR{!cmNjI$r zyK=yrz3r+An3mX}TGVnWxuVpj1+G4_b(a*M_yzd1fVIB-Gn6B{z*-Qci>U-c-9&{~I<@bW?$ugwBp3(22 z2AMS{Va$cMuCGp4{xs1wGsPg65x(0>f!dZHi{R8l?I}FY$&WFIigpscwL6)0fgRr# zV#apn)}DwXXFN)BD-PpYYi_ADjeP-^i{rvKX~h~upT+!{ZPe8lwLwCy0%&R3cy65H z&{e9cImR_p+fg^qbh8BorSbJdByRU81=cV!@eE1FPMhf&Vv{a{`=Qd)DOhIH-waHG zTncK9AGq^LfM?7(@Ql}V`&-23^(H=_Rs(XbYY^YDu|G{j7DuXzJ~0sXqCa%@Lmq>@Os9v?zxLDdLt(rzMgREu~4Eq)%U7m)$vCK#4?i8lD~^a*aDzKZ_v%sctES_n^ks`YNHq&U#5x2s_8PEbCsc3{%_ zlXjFGCB_aK_0hN3hlQGjMG7y7;kVSJuh(TySHr^ZF3xzv#vFs%jD+u+wAYYN$uw97F-@c1&`f_FWUiE-McAkPeQNDXxq~zy$9FD4Zb(%$A-as(b2g-h;oTh=M zQ4Ium9Bs!OzV;%zZc96-y%UorILmw|1TMnB```mY-l!(4)UWT>JGCr7lSvpJB$QU? znRvkhn4xgrr%o+!h*<=TRy$B$YGfU--XHICuTn3T|};d8)e+~;HAqvRTYAVe~8K)_JJFFv}T zV=JS-%*LN-zCFnrFblMFt!}-^QuaH;DD|30vl*Neh<%5DSfD9?CgU_x{Nt_n+z&_J z@G;p>Iu&vDH!OluI@*{p-j|!LD;*xM$S#`5vY`{pm=F-?Jyzf+CpwA>e5MX|OvYvo zCLktHJ4f)76$l6cVNXY6QyY*gzyxGrWiLp1*49M{urd>*)aF!VQFIgsSz5_>JA>4{ zl{8GfZA^L1D20WP1U&h`0Cpf(V}Pfft-TAMry%7YTt4vi?`CF7z#kG<8$n7PMHPU! zgEI)g!NkGD0+jT$a%ZCyLIMalo0;>eNl5(z0(>V(Y3b_f$j8j=;o-sL!OrC1Y{AUR z%gf8m!p6+T1_V<8UA*jFjXi<(E>yoE{=$#|xtKazIl5Xo*aLoJ8k;z{xe8KJg8Kpg z00;M)N_v7D{~_S_{U3N2S2Jc=@C^reJYWK5HWn5hAPXCijhFfF{NP?i#eZ_!yZpn7 zV0$uq8apzxGO;k*+5H=bi>svjzvTU=94;E*2UyH%AQuNWXH$@*JILOZ>hD4wZQWe{ zF4N5g^tc3N(nf{Z`(aqWR4~>~A zGsqTX2Uf%djLiCP^uHgp{gdP0#`C-9e^Uf3?mwsg4}1O5<&RzYBpgiLehZbA5TyLA zFQ1u%sg)VupG$5IV-SeNj0MQcX=V!K;AH~=c{oA5Ko)azR!&|MPEHVy$={%4?Oj}r z?M*?yp}^owR$v@<5SOtDw>dk|*p!P6$id3V2IS#kVF!X(&AGXZSy;KuL2Q46PaW??fi<1*m|vh(no138SjSb)ZyoE$(77Iu(1D;p~p3oq9nsNa6z z6IGEFq-0}a`B#gIt+A`QgR`9=rJ}JZKw14?JsMVaAaz&c-&SMgZ*lnes`25sksYW@Tq=0b+KvxA@cX+bw)5R-PbR9SJbF z{a>7%|G@iKukPP^*}4BxA3R<@ac7XRtAn$KgM+Og5gQ@ZF z#QQ@GGW(sA|Hw))V+-a#w*t)n74iRuq;BcpVgLUP=U=4%L=km%^>A>uQFc}~u?CsC z{?|DF8Sy`n)WG%7#nstM_Wv-b{|BAGUm-06=5=uP`kQ=pkkemxe-$lTt3Rd!0R9vT zK4a70RurUkF?I);{c#`Ij(>HTS{mD1fWQ^`AL;lX?N)EbzNjDJt^)-6#UTOBtW+ z?}$}*b9A(|0y+P?vHl}?{$J?+#{Z8%{Xen)o$N1eaR)~)aI#yvDtp-fm*W2u!oLs{ ztW3d)^Bjmqj z-~Tb!|CsB)Wr6<|@&82E|CsB)Wr6<|@&82E|8M3(`q$1JWDj2ad4Tub?9O%8;5|E> ziM+G~#Ov?>+>Vk@;1)zj8C@3$2)sAHKajF&)aT$vcvo3PN%$=&C?r^7cbRnp2nYa# ztc0kB=i+g;M;6Y`^N%%l{j*yJ4|r)|Z*3R}jlxnL$QpUun%;T&<2U_e^0tu~I#{O2 zHNTDxO(jJoisX{>Wm<-AI#$xX`~yZtPHo$kpeu97!si^@`0LomlJ+{*&i1@^2|+Kv z9)jExG4c&l><$D!7!z1#Nb!)+kf0S<5rr88LLzimA07)oDQylhR+){YvD!7RwQc$w z$f)96US94mHZTh1R4FzZ6TpE$prq8)f+M~zn4g~oGi@qc!UXS@>lSuqt8~8IxqO!Q zKVI7Or4b&q0+ui!F|4cM7?ed>8;l2EqJW? zEj1`L;nKFc9*EtYu3S7l+ts2P&Mh?B5id2`J@q-798P~(cAGPdCKv8p*wJEsH?sNT zW#V?P#*J3+|M#*l}f2@13mqIqZI68OYOrG0IPDO^L$# zf_^#*6@H6Ad8|N*gQTjXBEv#(%(Ev6hozQ%{)h+{96j$L{0W|X$@cbk{HNsL zr6bDq;r;?|gpU|Eif)z^*^8}2Wj0?2k+<3N&&bRy%+6{7l)5Z=M|>H@MX+yFwZe2* z&};VjlWuH&jy}|YE?MM zS4X3l?bha6LuL}CK#w!Jz-(RpUAc2n0de1Q`6JHF}MkA=aAu zTFw0wm!(k5?NSmPp)o2U9~h$3fOWArjPt7*&IkY@*qXs!CU52a1Y$@5O7b2OBvFV@ z@d@3a-Xx_Y2SE`M5^nGAN(FsvedIelJfve_po}M!vGMzOsC9l_{93DI_<@lhSy>ei zA~V?LzANMG-u}rWYxam*$^z9!a{J50)X>?zUb8E z0!2dvT3WbN6!W2wVE1TjV(qD~fT752S)R=WfT2A7V8nEk2hc;-VtA+(X*KJKNI2PH zeLB5s=R?gWRn0dULv*o>G4iB%Hs!4J}n)j`Sg_^oxo7oRDMx0$pfLdVuefCLM{% zRmx9Kzjsi9A_`*;eTVgGMn=k|^b)^|iFbwvO(RG0MJg(K9fc|4=PZLG9fpLh3Ob~X z3~M=}yY;9<kObvFk-q+6hX8AYWJMdgyEz6Wd&h zr-lkKoU{~NaWyq@pTLK?gc=Pn$E2=6^=wmcjJUZ=m6i=Pz$?=9?0j#i^P?8ii{Rev z{QiWzwmn_oKa9I)5I9^hS;)=f?;3jZM3aF?T`o>3qVxIiUhvlZ_`Ih5@cd} zuqzWwB8*%$yu^S{sp&XjQrEA2MSt&vDmP~$jbZF3O_xhR$NK<|q_C)}U8>l$)#Jmm zuU|!3&wj>+e=__m0uTWg(c;#ypr9c0xjW+>{Bm{t_2C2a(~lICvW58wAxL2J&2j?Q zdxj>m)O>O!CE~A zkzpx802NpbS6S^Y9np)@!xZupH7`wg91Z+-UxchQn_0Jt{qvsN$kPk^Xr>e48_8!6 zx(G)lAc$xv|4e~pUQ_}_7S7l>d~Q)G&T{u_}l_fq0VN*%xX>%J~PX zc0P`|DFK|~Os41K>XBza=mO-)w!A%Pw>NM4*F9`k+b>86CV?FZwC5GOEWd&H-eb!f z0sD>@p&|UmLr`^A4WeAqW@!hN%ULZ5gKhw^6Mq_+4XbpF6|E z8lu~n{G)>dxYGlcpIL5127A?;Uw$1_gZw9*ix$x(Lp+bFv}@QL$dleUu)Ru?-wEFk zbSy3m?psQWNqpT^viqXQV+?x*bjmlHgXG`V-SjT`xyv~m83fF>TT&znVV!#>ZUJ|~jc7glj%_9H zYr5iZr3IqXlYm1@47cfzL^fqbfqX7%hnBf1-m2oz4Y6Z~rT7gAXB;Zh9je5-Mt7T) z%dV$6h!)f5B_+%|1#iyIs<9K^mdbF^6VC>%k?%<$NQ)SQWc`JQ!Ww{;%=0+CM>jO6 z_E~2g@0h<8zyEl&G~G;+x@H5#2{mW=5Q3Z5^6MQJCzG;6P6s5M>G?RP#no}9QT4{2 z8c`3@vvh2ckrSR?X;xR38zS=3{LEHM7w|8}3(%bgkH3K%VMh$L zLotFfYLPnEG)PZah|wtKqiA5hkC&Dj3<-L?#4(|hI0%%d5<*Jpn-o5t^-ylE~OLBK;@3lwDz4@Qvn`?$^wq4)e&)c4)$K8PN(ln z()ny!`1q;H8CO>p>9)UkZ67^D^UmSjtmo} zf*ICADyb@>O6kVV$lcvXYxvV{?DJTcP-+gO>vKREB5p?kzcVpNixn_I)^<+Wv zFpr2rz=J`7H`$@B?99H5xMc9yWpRGT_xVbh2cjxph2sdz8|Cry2ugBMRmRmQpLM`; zl-P!9m&LG$(Z!_n@WNfR6Sz=BN{oLMlVEr)@-P}b7vdFR#n0x54;Om(EnQt6YpCJ* z$Mlm^Z-F#s`P9D8G-nozmqKQC+)AF6_;GK;EF~>Ypo-qQgl}7v$!)TV#wTnvesmVz z_+;Ys#DzJ>BlbN>9;d@3cKw+`3UEc#lbfq`t1N2cbo9Kw3BO<4)K+bsW4m)232M00~)-a9r=Iy&*=d7zwr@R^8 zW3s%)y%La87$RhLmFi&-3(g=)8N)@oYoWK`cWd|}VS@?$;Hk@*U#Oqjg}}=o|1RO; zPapxhjEszPFF#;PRY?Gkez5TRkZYzNWeAqKP+w5(DnWipfTvz|Kg4pnB#HAcUhrpu zV|KRMbv1MO>OQzMv@eTiyqg83^7Z+Eb#0LBu$J#j`W5Pm%=~BL>AJ&@NK&M{Buy9u zVh{{(dZbMVZrX1oVGH8O0uR%;ZNFQ7_VFcjD>=2QoQtTOdstOLAwq%f8K=DRBMhB( zs05TAA@0GW#SXj4lg!PALv93IUxDv-4w62th@LaSibo+^8(crh( z`_Rd@IwWamNBQo5C4G}{sRY#p-i0-c*Xifm5F*-_7sCuUaOmk6RnyD7^M5z#yii_k zI6O2ogg4yu#ff*-ej&w1R_q}}Ma6c``RKAf2vlOza?q5@>Qp7_3S`KA9jJl8Y;x#H zD`?rPg2!BLk05^Wg>HQnHIHFi`NmK75N6}hl?A!tLv8Ew6ldf(;f7wlR)g=nB`U7_ z{J{RZTHKTQM!hV0fC~vU$#v#OUh^Rgt$<6YyA@B!{*|~oBUu_w5U%aK#?J{;Z>T~? zAg6ujooFo)*Lex?*;W^o)MXnq*6_@-KZ#)RxGPZjRXDb7UG9`@oOV<1Wib)8oF=v& zIV@@oUoPumefVOydMCAJufzEIcViWg@~!C%2j$NyP5Y0<2k)BCE%s{fDC&4mpPO`ab>s5FG0iSg z0|e#PZ0p!FSvbU6gQO{lR2U+EXsbS7uW`r43F*pu_G2v1zG&O7MrUY?45rzBelfw_ z5Qo>lLecVXiLb;S>n|zWZy+k0!_}?Aw5~3b+Z3)t?7$2})=qTh>>~I6-s*36lEc3x zQ`m=CgvyggM^}ccK*z8Z7QMHnS(C*Bx=f#JNUAJy1ulKSw1UMVf9sptgx~&QvWSe6 zy#ItXhDIB@)cpK|^#^1W$+({kYId4dgxXM$WvDo?D7bw^LGqxgjqod{=S&2uk*ANl ze5Q`3;pTStsEJ`1B+=+N&a0~?{I}P?Q)|wnmZ;@C?yneiv*tvVhAl3>3-71 z6XxdS1wKALnmfEsb7qgUkKhe+zwsf*n|r%klCfh>Tza@4QH;^@T<75VR=xFfYV#4o zz&?4-=m}q*LCnB;i2Bl($zSgUmbo0A^{E#MY6Xe#{j}rU)|u0kuDZ&R|L(-cssig- zw`G@00vvHbG_ZWFI<$OsWnk#KcYD6)jz2Jrn{2lzPM*j27li-gt)RX4d6$9vSr-BC zji+^`Us9^j>5qgh`T6@3AAz>=gtZkm5=~FK1=_eKBj0Sdf-&kiIkM(Qa)P8>LanrQ z+t-+O^h_;JgBGY2xT58_wg7fEW5cd~5wT+tkpwi_5Nl4WV-#8v){dWu>`7eMXp)Mf zkz|J6tf1a+|L}!Jjm?4ypvHb!@3Zfb@jyjiGJ2%}-N8Z}T+u8Fz*Yu956&UWkg$Ov zyGc`z^c4qQXyiw1U}aF*cw4h@rNGrG+6uzOv2;L5p?-iI(o++GM4^rTWD&&4Iiaoh z9%}1L`=~AQ%@YuvO-9|XgHVD*2zsI@SMq7@#Q9TBrn07+CJf%~^SYv-G1W;kQ@QFYE$-Xs$ z|K(bn>G@asz@GHWt#$13SAwQ|#})A=$0yi;?e-RP1~s+|iAq6t< z7d5Hudue+C`?z^=zV~b%hK9qM9;-tiXDw)roEe|4N5yskr_Jr3ZC;(#%~l<%nlr7$ zf7YJ4Z5Lg8U3pguyvno3XEfq(`aw6D+&+(CLZuQl;D>JXWGAQB8hp9y0W@|0#G7bw zz_qKo9dmVfRY=7aLm0QhrB#3zqjvmG_+F=rku__7YGpHoh4*6Ql`OB&CO|IgQGQOX zHD$8FmRXvzFw_&g#2;s4RJl^%X}$umGX}#oz-g?61T#%8nXvI-kd6uy2@tRn%2FRW zepz?MM7Vd_3mS(+y;i}E>v#fi|Lla1JGA18sP+*r!ImY0hRYojO8&X#A7itiwoE*i zX)n3vzyG8lMPA39p9_&#&Zt|zf8$L*z?|Ee_jz$q?|WYL1N7-l{Kgo7n;*YvDce1X z|GRMf$EdO0t;Gai{TrZu5F_ONDfv*^F{=VFO@!M)^lSbunfTpa-rikNUM1R3l?#yB z=2hBkJOxa8XmHE(121?voV$6^O!-68X~%g{@pZReCM=TfiQP7+v|+rKi(Z zBpq?KyEPxa*K8M^5dk047bfW6%p9DdR9B{*w*Mnw6k3 zidx1ahUV+QHO!IK=&y{;$lWWj0*3ErKbI8TvjFAuap%iF$g!u#$fS6u^Oy5@n=#%3 zb8GI(GPbBo_eDcJw4cKraYGnI8R08%+TB8zoertCLEE%;o7Fjo+M1eu&8%Dg4}?Y2 z_FrQ={q2R#&CMcgVR^3BlkTCdoLbjLpXr4?%s6D)nk{3BvhF_*3=+tFl;;3 zVA~NbF~?@_-@Y_gMousO(G5721uqqC({i=g@_he>eY3H^B7R{KW5Hq6FDCaZ2`BP2 zM00I7E(9mGTpxq8gj0P5douLf{HC`>BWySrQJ8MZq`4Sj5rpAuWDHGO3)U(0FjCkA zF}dcVrFmH%U4EZF-3oIah_HV-9ZN|}l*@MFD5>Y2u-zLX-3~dCq27YQqhp~&2V~?My zCxXj^j#Uwd^&~3_8bON|zwhCljz1_Xv_^LipN#`9t%U73##rQi z6ci7sA+*#)OtnNzE4Z#fp)Byp8d0>|w1NtoB2Ovd>~{F2)c)aD5y=<EkbRs76VOwDw%5Yp65?dXjv zUV;XD&=SGNpVOm`lx~0vL^G~jp4{~z7RI7YsFe?jG4k}6?oIwHn&D3vex*w7#2YAN zl#VJPoJmE3p%q`;sgp*3r-{)&)Rl}#+1Y%2x12b0_Mi=p)Z4HU*85MZfmKAY}7yz1-aT7_*SC9Lh_ssT9!+agQ zG^hO_YEGo4b|5YWA(zNErKfbi8UD@P`MYY0b{60Bo|HG!Tr<7kDMdqOPE8ID3)qaQm7jni^VX;fre_A{i)+*z zYfFBzIs%>D2PS`ZB3VKOf=I_GMC$2MSxiKR<6Bi?tXTrC8Dn@BTkY>bA)7Vto?F{THq23d4~D|1Y{M+4gsDn z8s{$hM+;^G#P?GjP};Z9e7!YIMIH{zSj`3W*&axEc zE}{8(vHBtYL-iIvD$#T}_6CO60T=BIss8FuO<@{0%0eoNH#1R|l%bP5FlmTl3`!xc zrSFiJW+hXZ`DPVg_V;}+jmNQTme%gQ-|8ni2T3T=o$6R@?hVC&S73>YvTt~>ik6x( zau#vNcJ9CS#%3Iq+vKr?u!gs4N7_wB$l4`YBADtYMnMbA4%8y79Pb0GQ>ad#ZuMu7 z!pko=*-D}o?S96-p?LFV#sGza(k^!}Y(Oa$`{F`|O-Z9F#?WKKET)=l7;#Nx-~gRd ztGniA&Hpi*P~xU&J>(N^R4_IkxuZ0%aVyPlb*$|^CtOy>TrTENtU(14JmCJfH?gcSuU4`fC{Uk1HU}y_2CN*nTgU7tJNT@BFsMVT%nYNFEPf2`~49ZXU zvT_RAcH{nx{q3>&gdz7pmS+Mwf(KvJnp-qChe{=4O-mte0&I&)9VHnl8QBvNj)d~M zqT}BW+@U{l5z~BeKaK=Y&3fM zFDWJwr8enl!URz_W<1i8?(XrfS_VJ1PH^AcO0PWlB{jz`Qa+|%DWUpgvyMmRMEEdo zrZjn|X=AUyUrNQoQB&hTl`0%YoZOa}ji1+i_3aAA*3z|35w%o}5-T==k>wE`&WewY zPqZeb#eznhxk&0X!}OQOu1OEsB%uB7U)8+1`vVsI^Jg{LMpR5~nuL9Cx>@MHZA&*W za=H_BW;z^g5(h8lerj_R=UtGN&28TN3u<`_%JQ@NIo-moY!3FH;JE-$T9G3*&BYkU zJLWV!7o3tHi}CQWA@)2faKdNyEU>D(eW&-*!a6gf`vSmRn>Ox!!EJq5Jh|zGL1$Y6 zt7nLTsc-BNk6k~4I0GO2iu9E^wzBJ#O6O7an5)6foF%W~qYK|=aEckEqyTNS#Gjll zy9ty~lg?*pvnWiIq>;uUUn|E{Paw@EJ-MB{ch~5{U1A+x5S4dotfO&ZR09#?5rmM1 zO8QGxCHYZPQx_IaLQ9rq)J|oonD8bDlEA$J-E_T;P}8wVMPZ!2NeIwxTGC38{1(;= zqoSs!X7z?dmNc@`Yh^*J1tkxgpBL*>VFmK+3VuR#535{gVxgUyQ?zY*y;skLE(}bs z!*r`62MW(n6r5pg8`smdGT7U*7GxD0672;9J%}5QjOAm_Qr+|D@1L5+&#@ygJTEB% z1n#49ub<~{K-Y4v6cWEy%;Y#)A{@o!V3SGX zt;0lbB2a2b9|g;Z72c!ijK1Iu_K(qW4P#JpCDzm-Q=xp%h-PseJIbDnjE|2`lpBkY zxhaov1@DZV0%ZhfCBWxg_^u1^tqKu45~Wxfogab0wEVZR6$DDXZcZ#5@U->YS@FN8 z$52h+@n!=RhZ}*;6CEBw-@^~8mT|aE2HV_hzX7S!$lyv z+KV_16&@4n`*@Z9YawJvNJx^#kwJu61IU6`v^A?bW#v-p`1WbV990B=?hAkaOEnTR z3ZtV;FgAFppjM$BM8%qKhK{omZiz&hk)Ny-@)PMxD|Q{qCch?Mf4rk!_MokD$x|** ze~?PzN)d$#DPmrxBg=O{MmIt_NYhOix)m+9=KXJX>tV}ABo*it4@ZU=pGX>Hdt84R z4_?3g!DYNCK>rHW*oT^6$dSI?up7VKz#_pdP1)12e)quusGil~P#aBRr^&6$a2KgLXA0-83ep zp?~y<$;p*@Nq;|8XrRhcQFEm1kE|xB-ch`_7QTOY<1!Mlu4TClbwnOaUKzb|yB^-1 zoIH@NAsU+fJ|bSTljak)Ut6rg1A@~a0{xe3f`sV=2%h)T8d`ol{xOD<0sG>xyY3B@VNJsxwbnl{c|)GF)PBSF>Yl;f8TsPSHFCmxFCyw+u0}_k_!Cn z@XM?umg*dvmFAj5FE!h=m_&i@2Fl)a5;cUQlI$YSr-lG2vfKh1b6Por_j$eOA7fyF z!+|Wv&Rim4dIR&F*k&mdZyJ!Nxu&X<`WMrgLDz$O5qep2bvI(Xgpgb}+F{LX>x`yk>TgvOuh z$6CZeUUJ5d+(p-03f(bU?%OKJ3E^$~MC)XiHwtgr*Nj0=@-YcbdYJ%6EG{_Mu#@kc z@Iir;6-Z$jKNvyseSewZMnU}+T>XkdENi?>wVf$9xGwKYK5|_-GHp~Vg&1>M zO#i6KpUpxe;baWN{^I$2LKn-QFaSWZn0{xy9wLn_g)D%(CK@NLW+963%#n+NpnUPa z^mLpL3=(`p`fUBBG(yXSGUWDo$; z5=zu39~9p*Zbt4NyMCF|&vIlDtAbQT?$hnzvAj1%6b*KzDJ)#@}Gk5=R-Je?Bxzs8@m?oU7{@7g^({U8diWGb)+6FZyfu&Mye)%l_&b9$cl`+AX z4dwxboaTJ2TvwtRSMWM$Z~D-Sl$yJ^3W@s_&_=BNC1-qwMg%Dla zy*7=&kAtBK&gXZbZ0mkyKXI!YV$-H_CJO$1>_!QZhP!iBV3zOe4u4XtTK{sgd^yh% z>&u$$tlx(;%Ve(h2R47=J>dn#7ZY{sFtTD+)VczY!@tIUz6jT0E1e(nQ)oeeA*i%+|1j` z=*xGgW~lmA4&M*z1uZ?Xe#o_~JHL`FK@G|Qtz?ahnbRZ-qR;ny()p9evT@P=CIw9{ zcam@iqQgZfHl=P;x?00>l~#s5p6}ZcamcFgOXI{>D>$wK9)-gd_aMvyS1>%^Jn53T zS~6ymB+<}2bP3g7unQyR7rn8@1?G_k)!DaRs~=PQ-O58Isvdvo#}Uiav{UAD5(6nn zph^RbTF^ty??N27(GqM`C1?si-xKpEnGC&CLqe6Z#WXfDh|W1|7nRgKo*%tB8`+l8 zjixE;i5nS_hlGR>`L0>p%wqLst(y))`frEjc{&`=Pa1vnT5DQ$*z59zL7Ts4+fI^p z%3rW{TvBwdkO;(>ZM?@su;>1QqSbk2>&n%&v^~Mg$Ur)FOiaupj=D?)4zYSa(%wgsC^aUjiowWl~&N2xdZ|ojVS+&F03nZ@C z&iWs4a9B7_mj-E6Rds$}J{)Ww;uQVxzZNcb(iO{1d%Cteykqd!@u$0d5G)9eSRDxV z%Esw!FOuH0{2K%GBiUM`<}g(n3rD|i#_P*o3zX*FH8$9PrB9b{jK6VXZbm>uxx?(-6b-r}?wpicj@HKX8HT&zv~&Y-7bzND`UA^mxh#7xb$I@F zD|j8DY?gC<46m#(-I;NuM%ap%12XK!Z&xP=T5G=WpA<~;7xalvGy86CxFmy;TNGF2 z1`}SC&~2)@aN+Pi$f|U%5j3I$#&X|+2AYV2YK>6)P+J4GK3?NftTnE; zl7hJp{u;H{Qp1Cuj>h~Qrv-E#Z!B(}9uS~+ViQ}|R-$FoyRP9<{q8wVsxwEd6ssUP zofqQYJX!j3xWZ=2&7GDDqbh5e^O%SfTK&Da`&XFPhH19(QY-aFNu<)(3%L4 zv!7^kS}N$hb+7rO)b)Gv0;D{vg1$H3!|8i$Dk3*Onsn;l!uLV_Sx_Cxzqow8`U2te z_LmjxWdY0VcQ0U_<{NY(RM&hv@&F&w_Z?NT-B(D|2ZGS^@iHm>+tJ+`r;ar?O&Jl` z(uMz#9-BsF{xI29d4XKlN)Vqz7eCAL5SxrnPmb7;qHF0OIyoW~a5ey&WGvgAac*8@ z`?mDJ;lAG5(RNCb8M>06H)M=lcPpJ}6JA5>e#cFcec+xJxi=;Df_p)kqz$EkI%(-e zZ}4hIK^iek)y>Qsh5N^k+>v!$rcH_kqGG)dp>EQc_J)SnlhLV`%_rH6MP^+8Eh&YU zo3npHcibBbXxrh=n?=?Buri((OH8_6M;z-uCg1Y@Y(BMsy{~^ak1nK`6k3mX4u+W4uLMxf&fc zh5dPOiuMDfr%8u8Zie`epybSo7W*Upj9)z2^WW&%^Y?BA1FPSMX45 zQ8!~i=W2~9WYoeWwKJE^a`-{N7MwwyDEM!gygI17aNNO(!t@I^hM=ZDWd$vC11}&S zgHb>}5z?Ih50dm+P+3c0CT$-wTJ;$USY(F-4|EzEgtyvw0G4_e2ptzNPi!_~a+J6hGt{sxDjD^%RL zK~xvqKL`%BA9R~rn35`*nsU|KBCx~`0)H{NY7U~EA0NA9OE=yb{k8l!4)h-R8U-GC zuzMQXx!kEF&!-uUynRvmV~om~%#}Mt_JTfYjfV}Esx?1VCTmn8)ipP#>x){jDnfvC zR>L&MuGouFP9==0^|5ep*l!e+QnV9WZ1RWS`xMJHSRCap7Hebq1HKV*Y10^g?(_cD z`kEOadbkspQ(mF(&1$ZMYrQ_6?rBtspeZk(^hUWTcPdyLHERCJLQB4N?5An;gPW>0V<%5CLl=~?gibl1Ui;l(feG(RgibmAqXm04?E0|8H7 zE`{7-Pv-B;5C+tco2h7A%KG}}>FISXW8Evn zJuGK(n4i*7G|*Ak8>;<%OGVK4N{gv^51lQr_2Oe<$=&?2XEb-h*5++E;S=8v9K-Mh zM{*mZo(FXn3_B?Eo4NVO^lLlseN!i$#gFZt%Zb5C)?C}QTb|(O`{?$C+x_`VK^>y+ zv&T&}R@T9H)*oMWh|YtB`rvM<`pi;avKq`B&C5jTQ$U+E)adp8eLCD4M=NP9$SU7K z#4D8>y&EebTyaal}6a{=9sSB z&Btzm=-KMYok1j0Lvc^cO!cAm5qiW5@#I#cq+pTuEWDk;KyV!*4`s}xL@@Qb!kUTX&FyQ9W#*vbP452U zi_!hBROp@xVM&L^XMEh%8ysxm7{ezvy9=R6E9Q!Z(OXz9{%c6!&F5?T7VlBUT|Fl@ z(KpC@?)9maYTxeAXR@BNa*x|p9m9=VZCrtFcYEJa;7x7W13JAHIHzNQq-K*r7yX85jC#%6nLp!%&D#<1?O=yC zZdh;hGe_O3Th8999uvFlg;JnH-v_kKIhlQZSp~*rL2t3V80&P#B3;PeSq!i9yR(QI z4XXbMwR;l{h*#-6o?dUT{VU_w>WkYsXD@ahmU5n;S-axaP{#Fn?$RFJk9hd3heV+PbS|$M1+EO2Q$;y9t(j+s^{M2uxf@4BaXuHozlI|WB9ORkarVFS} z#xq((Z5Q^RdA2YPb7*Nm0uU(!2>H;N!I-UakOSU-ru38WHthY`R`DyVcCrV6X?6hg zWqyi(0?HOz&P|7o1&@F{r>>UE5tHor(}LOs!7SUt`kv(p|G66z#^%(Ob?;OpyB6}E zhSl=FaLb>~U4&R{7o2LWVzY9*iFK=4-9olc;zW7Xb7Lhfm*-q_7>Lwd=ay^*yo zlPAW)HL_DB+dx~YK^0^0bccI}b;hZ#ZxfR4*v2_EKovJr3#ADTmL!?%p+qYwF3r5~ z*B)J-D1ZE8EMWFcr}Vd89LYS3i;^y^U3FN{cZ5%#1GncYy#d=j$VY-;8+!%qp1xT1 z_eMww91CCVoL`^gwmWC8@yf3rSJrn936HI2^kAU=;BE$(kQBeB|8`7PX3M(1UShOq zWA zEz)VB{4H$SDP{cur`Q3$uEbT^__cGbWd?B>x3_eyVOb@cTjD|C|6UIknp+4S!>!Zv zzY;sAXFP~~_Hs#!3n)COPI&=L`@8i{xx2*z9Xh0$6+t+!t}U9H;t0e%pvu;#hxg-d@vHbWjoy18@4W!t$n zV))vXNrJKPnIy-ZYx7`}{U+o3#hd4-r#j1(QIXF9QIRF9g>vYo`{`w?{3TG*zfCAA_BoMe3U9U6+VgChU*Lw&kG+aU2j@M^ zp|7Dm1nJx}3b%?M^nFd{oPagtL;nMVeo`oC_McAkRsx#GE%@u>N$O?V z*QR*)ku2mpxZeWnQz-wrx;}qVM~}iiZNWv}h3n+^TSN~3D(w?(g)G+WM6QpK#~@oE zTFsbfPF=XRdbfZ2K_kWKR!W&5$J41xjSL!bg?43dwharA9q;dO43V+(o&lMQZHVdX#oRB5rc@tA;zEz;UFEgA>d8jeo z@aFb#uxQIKe=~D7&V_uTF%#t{)jnbQ|LXTP-U})jLjIkLSJ=NEpt~poL9+=g$dqhT zEYayxXP);4dJ8`~7hJQIXvM_EKOJPdlc=bi71e$-xPBi(Dz#BL13jkA)(d`(f3Y@0Q52XSd2s#|9w3 zakhbH8|2buSq2+y)F40Wjj7Y(?Pya^M!7SdUC;CBj>QUUoa}$Z)dqDeStu#|YdHOu zjnuW;8Hdek$j_IKpq`WlZCQ~d9MxQ$7l@0@%JzO)FR35= zbO&+|189CzbO7jrhC8Tb{DhH+uX0wp9`>4zeWP#jQH7%V&)jM7hV*8K(4s#YVaDo& z#6>}pV$FtPZBmWxcYNY9>-X20B!vGO18~kJtBP9NAdNICen5%J(4EQLAgvH*7)nXM z{e(i$>M8ogD^0vA-|}DYMd`hfXx2AdkO+CB-I@#w9u+-()OWIgg_D7uD=|82pw|lz zdFJom??C5}U~XJN0mbThHhn5gVDi6n@+{1lm>5$Fi=>Qk3*|Bf8H{%-2 zsMA>E^Tg)&?#ZsDrFDC@f~%(H&Mg(s5}$bNQ_G{Qrzby^Bd}y$-Ep|y!Bu4SWzel~HFf5}Q7q*Np8&jvF;?%nTKo z5=OHJgbo#A=^e=){v95M!YD6YI2d-i+CZpNza}2&c}oj~hGR1wj#qHKZ2Z^qWK(FM zFRnsNN1j(8yj2(DM}3Fl)@lfVR`7jf##+mKCajdb@ZZDxg^Q8<3^%(2Z_jjkxsY(L z-(N1+9X7wiq7Z-zXst;2@Rg*bT{X#+0Y*znt{-BzRY^9`R5t)`ly?Fpu1uMch%bdc z!uH(}LIeWJ2p-ueT`SDUP?K^T0n%hJw%B6oy#4yv+Ge#m0RfXbK07-bA~@FaT2p^N zNh4mhWTsYS1CWN>+>|vxO=MJ?I5%YEo~}eYw(}U&SZQ>s7(8VtCMV_iG{YzF7<_r) zd)uFv3IDs>)y_~-Qc|6$9FRr)W0to)(>_-s`(r*qus@kCtE@~;ljFhf+0t^@8e2g7 zI})l)Pe+wq30({dUfgk>id_?ZaVazqJrn`yr6GT|C=Y3f#2dM~87xJ?H3Fb28JOv9vT>tLktBX_tok4=i`(1kN20;j)(Qz^R+h7sU#rx;SFA1 zZg6R;VhIXC!mYJgVsmnK=J~nrQVRu%Y^3=oI7mqq-7VF~f_VBjt>A|8c5Uu(uymzO zKddBK{78NnNiU&&`@n*MKR^sb=?g=%=XxNY8~KxRVY?emDLF>3Ubc3(s}p4@=Oph& zRIlPJ(lb7a$z#-(I^E@lKFv>KOnQhtJihx^wB%}#(EjFb!Qh9U-o)irAAoy&F)L-% zm36nKn{W5L(QNpM4Vd0(*9HvG9ypFjBPJ)3FGl?(k%#94msY(C9v$&FZ63VR_yZ#A zNO*%^@an3~JqcInlY#Fm1 zsVc?L(je2&ggc4A2ZO<2JO#k7#krM&(Rh29i&>I7o#=tH~Z2s+NP7e%z9x_K( zA5h0qQpH3qBS4`lI11gd4t-M>SDT1aC1+9nBn}kP`(VVo{CuB`)?h*Cua|vj&ALpX zD1_A21Yb)P$Y^LH+gz=4Fx4;f%4A@CM3lEbBYe4b9cabYg(x#3Qyqw<|IDqdG85){ zBCk-P(*)RI%1jTy&Hcx$2dG;ywp0Zmdb6?Agp?Gsb@OkgWMu^u=n3^A%2-%xtSds5 zI<2Q3P&Mz=i%juvI9rcm1{X>)Oct;PF-&S2kZeOL7mF&kI38_Tkg*sKVE+hr zcPI(9Ku70vxkk158Wym}obXg-Wo7An?o2@c9t;#DR3HT4&&2Z$`^cEWYgVYRC1I#Q z-)Sk%t6~fe zLm}tD;VlN6*V_ZM;^;EajNc(dVTSGn(a~aI10*)9WxElCtRfYaHd``)MxX0*ji6u6 z#WwD$u8tEpjy$HoaWSKWjh$AOIbV45ZoiV^g33qla9dPg8`F&XBebEt!%odLpH2b> z9e8e2bMu1#nk+F;!?cF8l0(QiSo#f)w%J=DZok?ktzL$KYxw% z=OzeCNJxM%GmFK!|Ene`1oqdOjs32wqUPe_y51p%hp8Cvt!OSA3prL%#`luyHwa`Mh+U7#W)^aV|X-fO7tXH7^odu7w26E!MSOJ-x8dI=JkW}=PYkmsX7 z-34D1nsw@K?AOG}w+zM0ZK>+${eaJA7VYTWlG4Rz@ zb2BoeB_v=4bI}GO-}TDITqua-@j0Qzb{iSDHbYR*zfhlq2arQzsbA)&lc~5stIa8? zUOxFezWMcTvE(a-1MQNdDfGaJ>0XES_7gXYMRjnIFRkYCnD}W64daz0=eaXtHq>(u zn{LiXrlj~3p2mU0RpO!*-BB7YVxmBr2P4~ph8WnfvO_>grw*cOfF|FqhZn65FV`sW zq2=4_Rpn}k8bMKtlSd&vS=Dd8XYl@WP_T@WpxLfV2vcm5& zq%>J^ylD_X7ZrNF6r~X?BXIt3qdf0f%X2_Xa(;Ha95I;fcO&8TqZ7~Jdy;k%E~|t4 zoLT(GX>T;$QViDw3?KY_#^b*RDG5>$dmci6ML#^!mg!#fUu|B|5_#X0Cy!aHmDRniHjvX8^J*3EiV;zQ0QR~ED<~luQ@=V5g^=?X@-<(( zv&oK1+pSvW|KkF@-;CyfIr8bfiJbXzBVI~BD_#$kpBqXaec+8Ww0Aa3=m)3}GXeF~ zKXz-pE_!id*HGZ6p$TGCYxu(qfVk zp3QoD3p83KF5PBE929>|Y2@-K3O|oIDyH{)f;@yaV-}ZvaYb2l?J-@D*~xFZmyC(5 zrb0**9h|GyfKwyf+LlHDrMj5jOD*KN-G90+OF35kX{`_raD1fYh`>r}NtkW{m{SNq zCx5M(Y#32fu23hocuVA->;B7S>Ow{Z(fEo`=7_}ED4IvMGkyYEmS9`Zll2tY)N_@} zzc>Us7}3afh$7yY#K!tcy*5tj#JFmZT@#!K;OLf7hR-CvK)q4fe3@4lW zj-zKELpx}4o7rEuoD-kn1tj%$H2q+jpd8Q*=KO{q-%C_(EOwF@J@H;=E{+MK&+@g>4=g!-4`RipnV9wRZ10Nv8kJ;GT9q zF5>rdLET{%ihq^_h~#~-1zVFLMAJj<^wuL;P0quV1_^MgRp22(Xs6QUSIU$a6rY#t znAekl6)kpN9YD<45(u%tj00o9MM#GUk?l9OvX ze5cw^W!6KVkA~rHUb9<0r{x$O8_PP&`rFvJ3!CHrs->=81kWy^PrQ`-$(^+K%wS&g zLdetyhvW5;XR7m&IA+pvY6m*#`e({9$1w*u0Az-;7QO`%7;r8Cu3+wji6uqEMxW`XcF%&}LE+39v`=O~rDTDVk!ND2t@ zvBi}su$i^Y>fw^C5Rc5;O1eQXSH$!tAInuCCJbf6FX=-+(O)i^$h%o!GVNm9s>`Nh z)0ooe^P|hkRFsvcStU6rvuriML%*^N3}p*ZwZD)GUP%gKYB*hz{Am!vVkr4a^x&%a zOK z4%qZP@11kJuAmUle3xni+HUDHc>fY&dVufhJ;!?+#tt3dIAWh(WIQosT-U!F1JfxG zu$ek3c*8nTwc@*ds?=?~i|cwN@r;C;k!A;7qKlY`{7v)_{&E-z;QxK?tkVS+a=nm4 zM^Ddp7?0y{zWl&_zJqy`a3pbs#d5Zt)82GX{XD?9WId`T#~9Hu@Z-pjVWZ2D(4YS& z3yC;+-HARw%a&E@no~f#HeeXbnV2G{cr-2oshA2(i)OA)1wa^Kf|}Qu*%sHHD^SvX zbRgL0cCyou%P)HWg`E}d+Rkv*k>=(K4Mo|V%^JpHO(+s=T@Pmmm{j0e_8s_dx_vm_ zrDP=<4ro0tBF@4T8nGXti{3Nh+p-DVNG$BgdQGH{n7++%vne%oqz5mVnXxBx;H3j} z5*?!A+WO>~85x59BqBC4cB7U~IPh??U__xBYL=e>g|4WT)z_jFS5+Y|y0tyu9hGdU zHcT4uUbH(qUnnMl{RRg}Qqe2MUhqAkyo)=_`Mm+ka&vabU?j6iTumCBYGIWZs;iprln?3k)i+j#3y4hp^_2qf* z=K*)VA)dLI;k-^3ri=_*mVuJxs<6!F7^3QRoLM zPjlaVIFFDecyhn0H-*Z6Fg?Bzo2h;a9T`57@Jp5b&&}|~1L41^scFC9pQ)?ciAThh zRngkM#&MvSlyo1Ah6A3!kPyJ(vJj ztbRPg6jF>$p`Q;YI|k_HxKdaEpMyaKx!N;~e=lpZ>wi%0UQ34M*e9HH1@|2kBsCO6hP@MOuGsc ze_l>nIkQuHF?B`{|5#lzU-E%t#6)kcq6A6NTD!Tutnw}i$XauG$V$870K$DV4^Dml z26W!3Tu}?gJEW>DxB^W-%eUOp3o2!GK=wHXf-JqhxuJBB;w*a3x7TAnyQL`Duf^o^ z2#3pf>#T$%|8i$Nf8HDM2#~z&b)0+M7~4$4-A>@?zog*#WSuYm>g?f^_Q52Iuv=^W z8-LYk z#)}`)0ltZiPbJ$%COT!~-9RFzrKOFx(2yxZfsTmX7c0vHBFw9H2d4r6gSs}Akv=>A z-g|O4Q>nxCzX+Tb3jV;l?K5YIi~X7nqv|O&3Qac{Qk_x?|d0B*M^_&fTG5m zbftm;h|g6$+Xc$P2?zOifKnGqHVJBJ(*W1u*| zILv{kjAbrTO&y8@;`@bsf{Vp4Yd<0wl4& z%Vah>eMiQ|Ugq>{R+{WBW{c%HI5|~RRY40UYk$ZJR{Q)@KmA_NHTB4kBRyc>^lPcT zS62Scu9}+qqG_;abU(}+iqq)NGX0t6w8Rcz)S8a{T3OAA##B(J+WBvS%N}z;vZ~If z&m?hS`J!R9#si1|7ziXwxl@uXc+n8Oe!5KyC-M;A%NWS7cnB%X^?kxhO&z3QD-Buj zJoQvmg38LWe;(?*cXe>ZCGF5%+~qo#wUt;TNxXDwCVtV0vi0m6MAB#VFmpz3VMzPl z4NG$gi3z{@N`{`eSrgigmiXO?yr+i>odt&3&}0W?CBOzrXOJxwmqC_&Mv8q_M=jg= zoV3BkRbI_~io!~wY)7%J zGrqgubB;w6G7nD2dl|2p4~8QZJi_rsT{*-{d7^jD88?+k0X7Yk1jmlpvg^(s40>b; zmAV*@k=A_jw%ES5dDWh2Z19nGF*Q7w&0=?>?Fs>eGA-6`RXP-%E7*_6iGt`~+rR&) zTESs5NOOxRLI7|>PU6S!B9(}}vARlsNE~2}R^5~!pB?$1y#c5Jie_F4l2_EAusg&y zsWh>z4`%zG)a(#ZH>nA3@*=9Vv4(*NDx>L&8ny_^E5ky38ud!;Z+ig9yTInmNb_Zp z8wS9^<7siC0Q}6+W~nwrB>%6Wn#2}5cqSFZ8AX(+ty@xh3xNztR5nMKBJ-uaN~N?0 zNLvs?dXt?gyI0(J%F!PNf%x2WJA!8L?NLvZ1dj+i&Ia{jYT|JEVgaHp6m5;relP-< z{NhAmD{bjBNrMpj$Du%_RE|%TOl$&AoyH+Ut63|QRt0h&nd%4aD$AZAXSwm z0h%?TK3Af?+OLk}^Yj4{B0YTyBJcsH7P%r71ZFrxVBc={BESfg8J9nbJr4NwdnX+jahhe(Mm*=b=d{hk@Ma*qcji3awY7DLM>OS|<`NR@^10 zHZ}C1Pu_rAUJ!nUPL}@v&_PU?ALpS2! z)hj?K^jkS?TTxS94?Omb<-+r^EamvH%E)7hPWU9j^~s#m>EgKgQDOA zzxkO7V=_I?0MY_%^|*%;#HaWB=kU|E`2gKxH?_w(*JZweBIg(5i`lMIg}~wnX>fRK z0OoX6;**cxzw5tL3RKvpuma>zGzHp~q35FAVYD=KDTp*+2dIvYjvt(h!@6VR4dAM( zBAWw+|3E$fp+-hSW9R1|A0C$S0F-~r^U__(Rku{tfrw^RlfISRkfXpsj|)cR)-$LJ z8c)oCnVZ=6b~^0I>yl0Ab>^Fyd1`oL5u!$2uW?IR8hsK?kSLDuVqhgASb9w(UXJgM^zxASvU* zC!ME~!;PLjXT=_ox zxBdL-^J4`#%9w_DFgAoaCIMwz-WR*QXs}*T7IvOYn$@cAthea)8mC+US4&*xNgIH{ zh;-2be}TyT^Tt!*9RKUw827>P((3Kl0k_E#I#;_tbe8$bZ5G=`uC z#uLAR=MF-JuRB~@m)Af(_UCuB!$1<}R>KF(ppxw*2Q>ck?$80l4@6z)r;N)Y_8w0R z{f{JQXMW|IFV7FE7=RsxBL#9Oz{rY+pre#ddk|2rXR~R8h~1f>)rnmHDFcZ2c}-*e z|GY68P!L!NO-BcN9hd%Bd;u<&V>|)^0#}j_0fKC?3od?sB8B@$-$aE#4|tL$zzcKQ zE_39&4$hh_!p>LQsKZYWsBN$$zn`5|WX_FM%#~cv$K724Sp>8p>E%FIOYFqhLG5F@ z>{H#8y|Ltnpo9OIQ2U9Spy$Ox%WZ$rcxF?MyYHL$dD{uaZ?7i32O&q1Db)3QObb{!sCwD^ls=OmL^_D4Qn>)>hdBJ2jv^7`H=e$Lvq0$i{935fC?dE^R zRpYAoCR<|5{Bi3fuI$@PlCL}kO`p=@kpHjWC(&MD<5HIbdu&IHgL#r(&fBVu;-d1Z z$Teki0I(79yqOXsMs+(@?6##qwy7W_g(lnO^H?gQb89?Z=95}DmdYNtp;C@woxYB{9FDwsoLgGHF_{4Gz(T4JRGa&ou(ntSh z|7sy{8w-+B-b$i9$7P0n`Li2gvPCR83B?ZK@527NMz~dNfRwfYFPe1j>pesjBqa*4 zT21(kxBD>_O$Vd#SN%WUO2P20yRfdGl_e=Ur8VO3!&?c?9E^`0~G9U(>UMS{I4Ef{WpX%r(Oq$*6&A8Jl+&@_Ja?q zso-8uKj5p%8$FJVab-WbxE&A{WZ~uz8}7f_dBOPX_aI*Im2zt@68)4E`IaG^#6dzD zV6s4<(5dBMzx&+#c`-T6Kwzu%9So{O>znUzIAgpd#qANw{Z!q$^Zi|WVWWD7s5vct zShK_HoAds-^q!ci6xCfefb2%&{FHc@klN|jvdDJT#5IgtXlNhR$T{bqYIwbo%n5cb zs4|1R)E#T$YEBi^^)hCLxOje!0zVEhi|LNpJ53&EsIKy~-&_a{g>m^NND4WNzqma> zx-dpW+V%aaev58Rl@65p*RWbM(zWi+h=wPM2}SQ+_D`x`(!hWe_rKDy&^vdW;Ik!n zei8)6cZyalhLMDD34-Z!m(5BwgF18h$3;quUG9$#uDYwF>dp3t$^939SNy2R%a^du zzM_(@umJ-Yd-V-XF32_4{_Y>4=fegL_s%Yw!ytR^_AgI0zk4*<=F6#f z(82g!`;G}a&jF+V*x1D*`p~#j^MSSf9=M$A8vL}FA5xt4*;cML{qaO+bDIlE@B`=Z zAKGRpB=-g0;~{;I>gELrMjKbRTHvIT!;5X{f!Z{>+YZ>M%wEC7CtVa%Rw%+3E$@lg9nR8k~ z9*A5q#=4Zc-ZQ5*MmF?zMphMl4k(HIdz4>E^KK%$HthwS=!6grNxVCMe_YO>@sBk< z7z|p`2ZrFqQb9%jRhAhP}4rK`cckG>0q ztDQjbmijT?-?xag>J7(6InX7zy_9rz!7daNr~6Yis7VOLfJyQCf&5;-q&6Opo@QHo zsmGIyB2k4a8%R_-~LE1@5rg z+|Cfa&m4Ctz?FLs2Ld(v_Zo)8?K{x5^9CZ6WC8cW0o=~IT3Zam| z;R-?S&StFk-}vKhzBK+O=*d?ia65eJUVb~N-O`3f?t+ik?M~JEbD$%Gc(0j`eB6bD zq$C^=e~1i6oUJs~c|V-9+~K=YZFf)=K!%;PT2^Lo*^|&~RGE$>ej(S0mHYp>%?w3z zEu3=oO5?dQHSXBHD!sPDiwg?}2NvA};tE`}?&+nak!XDO!;=$}^VOE|Y(6d|s$FEb=f zDuWiAxdhSCHn||%LhXOAt6BkU4O^m?*zQM3cXuv6x6@$|OpVD9YHQoj$H_zOw@CjD zyXkMi)8eXO&It0RQJZlAY zh5n20*4sTLiD+M5UObOWOC0pl2BL7|UOcN7b+-ab+Lm}}509_G5tKV&gK)vHXCe(y zCpV!QwpFHGy@QuU!c(B0C-wE{ z^wjkJbm>2ZGonBG6Fa-`?{RDlYr`|ITd~Q#*7^|s37xWYFDS#|=@?NX=cn^?CUhDE zHW3e{Gh@l*e_tZED7vY=k4ghkPfvmRi3NlXoC|-~)+Vm^#t5%F!knwP9xH;2x`ZEk zBVy}fU3qTs;1*0ptV6FR_gdyV=)M+g z3r;^{fxs8!7_K`8O-)rsDf)WO)m2>YPujt5@po-SmNl1B<5WT_!chQ^G19cS1|*51rLd2Y~H<6B^@ybo@>fp|Nec@i?bEmEEbUvo_ z$7}7kG?BrmS+ua#1iPYOPq6zN2sg(su_a2Q_B@$AK4iO@GLrI-yd7sP>zrhF#QVmD zVaQa(UimyHN4cyqh&8W%JA=l5EZBW*5Q&dpp+evYxgvJk=;p~(LFZmep^5dYh_UGg z$@%kE%&DgzTw2$a?d#Uc2LAn+hTD?8Dii6UVgE425;hI@<3uIs4l zad~KCsPw>~7vVq;N?m>n1fB^?ASH$qRQ(Jd z@=&f5-ASBK1fSir;`<)v#m?>xlhKW5T0s3E16l2Vw=gBY>b+>ZGB9Y#0hc+g8TiLy zmoHGP(rM34Q;HL#7LzZ-ufk7N-73IVq7du|_u@sfwVxKqYI@#kzwbSF?M@r44iwpU zq-I2^NAn#IQ^W!~BbQ_@f)?j&{8UybssK-kPO%BP(x`emxcV15cp#4MU|E0Pw)t~n zsk%8m?(~C>Zq?ywbv9 z){AT$!)t?aLjYrd&T@f2KSZdrPuXU^Qi4DCk_fj_!nc7OMh(+?zM6m$qi~q_vq|4! zX1?B3`@aeMd4HT0x_!3A#lrs%tUh>0>~*^_Yj6^QZ!)5GDk5!o(2zK|5;gK}`l!!234;dzZ z3*LHT-)L$4UFVgho|Cl5Y%Hdfgm98#9D5*^UCJsoo|JlkKD8WZ7qkvf?$Q#H?uqA?> z?0eti{vQ_rI~DfGOL&AeOL1%D#a{gM^dOMWj(THyAR4{12kXBP7xENpw8WJRXXO zwUc(>eI!$nNezdlMBV$ZA79t*q3bbL-*zroo!6n%vz^psArl}e375DXfU{gv#V{iP zlY}@3C8Z!pW~IsTq{}yfM|=QWVD5&ITA>@F`sEt_dN5wp&1c#T!w`j%OfXqS4&+=hi|JseG88Uvg&Lm(=+!4 z707tNru^c?hyZJ{W@4%!fu;~(To>53#o$e;P>zd)1Qg&wS3fC1^Er7+IlpV7DK5Mq zBUA{6zA@;wjvv4@k8YNLjH)thqhftPPU5G+=%Mgn{tCjqs>%FUN-Og(uX zjPWUnuNXEsn>rZF!J)TlzA}~57#bq9F)WDrC@No_WD+#;>JfOh9>F!f3hnp4q@S$J zs?31RW+95m_5FaAY0EX1ZZcO7%ddtucNf-I8;0hA>6My_i0`B%hY8}PDrmsSr^GN8Fk#;mMSzI1+J6LSbwzIda1P%1|bYSPE@Ba0tvOjfl$Q(^VC(k~cr>U;MdGX0K`WIu3{wCsL7dY9Zot zKQ{$}`=usw!lm#SnnoLhG`lDqp|S}cWojA;XOU+xJU)5zlyjL& zMYb)39?=ks`=f&jnjqL;e<-3>Y-JCxFTsypZ^hqCAqY?BY4v71oTx3v{JkWH#4BVO z)e&M!hTVCl5TQ&FZbmh>jAugfx>izF`f4dtm8VK~bvb3X-yda=hNlISH+*oz|FoNw z5f3QS=Rr~T)xa-eI6y|bAb5Q34du$J#u z<&JoVw|rd=@-oB9P7opz3%{mGdh7yXe;r-;)%st~6l3!%uGu={&`@2bkKgAY?r?(8 z94n9OBD?kG?+=K38_DKxpdX^V-+t+c^Uht`Z>jb)_EbHU&@HNoF*fU7DXRTe%TpJJ z@n7NWtu$)+2GSAdMxAj)k3G|Kv)$c9z0h=8XLUWuqp?4nBy4AnvlBiK*DIDaP0vQRZJJ%s2?YVj>1j4<_in z%2k}}Zom-a1ML{Bw*{XQxoG`O39D+)FEKnE?MrR*KNgS_u@(E*FfIWI$g5v(OiXfH zL+Asc7eBv5@vHSOfLYAD+jbvG+O~9+>{A?+Zewj;4){kihIOKsxsPJ3OOg z1w&BH)Jw}eN1hU_zTR@=531r%);n~|PY$jE=krzL)5To2F{0G~BcCO|fg{eR*XQO2 z`p|^NH~&vb*BRAB*My~qYC`CQ7QoOuNKu-h2?j$4=|w`5CRIe~1nDA86bK!pNpI3Y z2rYC$K$_G@QIsZp@%{Fk{k7-p-Pt>LX6`f3o%zP?Hdp0@!)TcQN=e|AYYnu=GI6&5 z_2+Cp8{CLe9i9z^>mq3G#9DeijR%LZgQnF^ofl**Z!Ncrq3%az`~VIF$}*t}$;%it zT}1GhH|ouF#n;HVplup?JGBg_?J<|=VcyFp*a(QiuU4Qcl`I*0cf*a=!el)pqgxhb-l6Ib) znUSshlv%Y$XbUT`8DM`o+w`fwW$Qf_qwy+x*|VlI*Q)c%Z_Oh-@=2UdM(>P%!2!&^ z2|e%Ey{@s*cfU$X!7Sm0C zspU>zedIUWDN1`WnR$1d+e$+%I1%2j(m0lLG%mZy*_u>FuGUoWpEH7V0_N zU>fK31V_@&+o*Lx`~#t0?K{)=wUp zGy}pa2&S8un(8HBf$XH3hnKj$aBM>O5Z~2ORWj;?m;I|)QDJqp=YqV5g_}QqLBqyo zrCxsd{d>;GquDduM(g*CD`*}`oyEvkw%ISNP=-Sv=)wC*K!g+$m^S=gY-?UDWn%pw z!oLY5ifO8U{rNJfInC}lynx*f8V|f0>&9UPqHt75j_9yT>n1Bo4h4bJ!A;3~hV?#o znx4PSP#g;QzmX%F^GQpd|8!!yQB*u7_0;U)NRY}bhr(x_0}9LgoxGb>Hatu@FF!Mb zdgAa!IDuO|#JU`9Hzi{uRnkw0^(x0z3MV+2!m8X4FlPRP`TZ(DrKLi|14WqT&a-J%Kn4!wi(a~lEu5G&0gER3R z!nQiuU=oLu6uo7voa-6%;mne*BH_E{C_SKHq2so<@PaoVF*9_i3~tX)X#h+n0`OOj`J12FnyfyYtNmRZz;K16oVWHj@t~6Ez4DTR7--;PiqYV&# zzW2|Ly%j~)b9cct+;{!B^bkBZjHAN_Q=V7w%z?ZaA}J{Ejd%lBQ^!$Z{sqaX=I)9t zFSpVTrQ|29x&X^dE6pP3d?pcnqYcq}YJpoy0eAY>Xy3klG6J?bW@Kg$tBGZ@)zHxK zQrIPi!#aV`NUb{^^f8*hC<<8lCVylwZQi7zsqtFv5$q@wecAie!hdc|N{y$WB4gm{ zvWyrib*AIRNgoCxyH5?`!6ds#Ev!cBeTy7oWP3+p1CK?~r0_|kvVeY99a~X1kw42l zic^e&12!4Mb&^0FCfv7Z$OHwnd&f9B|IOU3(FU6DA)cP%bp)0CSmktiA?LZ=E_j!M zaP92wW}65umilJ>{?ht}sy997ch#l<&(T_)KC&kMPcKA?j98tt(jy9vVy`=JarCC! zXrug1$*lInP%ExHb&`$N_RybZX21*zZY8&DwDJi1+G zKWMe3xQFK|i5;(mbGHim6&g0L{kA5o86Y-jBR-Ucnza*u5K^et)966}UV6W__?GbT zMaRUja&o@+cl)!x@~Uv1*&*apV_phr6e&n8E*7QB*S7^9{yT`J#7R<6DZ`%1EV1f1 zqi_Nc%_0@3;KRtdlSwf(r|NM^G(!+6T{@|0VEkvD&lhwWW0M=AL+_d2=NipPWkw*k ze9BwXY0Jt}QUpgS7aL0w927XWd|KPm3nKfz{b1gv%L8V;G`SId$o(Psqyd-dqoTPV z6LRx&$hKvzk2XtFmj&hemgyW8YK!dEmp6n#fB_CtC0=a*>cIZwokh+#OEF!%z|jmXwi^ zDN1w^QMs&4R6<(T6YccH@}93R_q6h@4%+l}r7}eE-F-0ysHH_De?}9Pp639AYceXMrsd89ijY{e$Tc;bZ@Dhi~Hop(lcp9QvoF9rn<6@OB7Uxit{-nAeX$)u}t z$^rlV`*$Q)<;DHcq3&+A-v=uyqU=!(K4MY8b$_vvY&V7b+G*qg(gk(OM};;ue7#}; zkqITFChOh86kj`*r-s8#U$PRqLVTjK|Lg+EJI;fXkUa$VIRl&9h9#0P-%E)BdOIr=MdM<{S^jte(7Wrta45#}D)6 zOCk}Ogo1-B`9MD{sJmqeZ(`(45(kL|vd!SI=uXcVI0j^wN5VwR%(FMh`o3@wJv{NP zur>W?+_ju=3J6&-GvDG1qh=Dxl*%FS6=KL>3*H3tlT-7~t}cPyl{OZ1)fdoky6RdmS={l3JuHP83SadoXMkQpPry zCu!voK`{2vW_@mfwQ+W41|8&}8Mma#BSXP{g5e^7$jdxfX~q9gVlRmELKE~!)`B*W z$ODZ|8hBjs#6#B+&!UNnMd#t-TWG^g-+|cd!q77Z*7wfCyUOn%)xW>3Dii zbnWbocCF#=qHJh7zXgkvOb?)XC!VN&lc+n17jEkr4pWq{{h%!a)#T7UEf7mpG27Ii zc>G#LQ8MDiVW}!AeIYguEq?=p)U?!tlff;G>OYFZs3>1vU4;e*2WLtVflhV{{@@NMmXl%sE*Qndb)jc+oRC&6nx$RKD6kyuCc)Izm-Rhh7(=py$nxTW0uRP)_5*2bV`1ii;gKWkv*+!sOhX zmZ#?d5t&Ark-e7}aItZbNok4}sB<8AdX+#%r!#jO3@qHZ!w!e*Zt(@}nAX(~Z1_SA zP&$0Hz}3htgY0vK;OVW$v2E{8{y0J_?y-@8zWna=Q>t>hb0Z5;#jv9mol3&e!S8GZxZPQayw=a^4Ua>F%o3LW9WS*&MKP@|MmYeZj)}V2}>e zcQXSvohC#i82;k0;78OA2S)*VXl+>2D~V!cZ00}&SUDmdXU*G|$1M!?Ayy9LkkT+S zRxz(~YQOjW=~K~l2LjuThL#q$xTv&~Bk<;aj3{hi-4bE?FUr||FMHECh!gKu@WKvw8Nx3x!R{ya8j&cuBFPI1r(UI6Z>@ zf}!f)vZk4w>V8hZ`5{O{BtQsMnB2t7?Pms7*uqfhDO`h|t`b4cs2l>J3;Z44MvQ9p zh2sCp`ihmj{%3DSvO6;;r^Z)NUNX&BIy>iAtVmUsg6GU?w-j#@qtU}8pd?vhYJ*^5 zdCl%Vk_4W>yU2skEnW>bujk1v7He$1mo3z6%9;LM6C>Gr=P^m@&`)W?g2S;~`fc|4 zOFDA6%TkZU`Nq+2-)!e2VY%Zm4}vhDtXZ}qhDMKddAh2-#BA=Ey;voBw7oB}TQB<) z{j#1XbD!J=p%%Xqv|p(XU@2FtCt?+E?r^CVz(ActnR(3yG59(X#PpWgQp?l7dG}bo z;=3fn&wQB?t!`gNT5WqIqa1 zEFevk;}l4vpk5aaF6h=yz_IaKCbv8$XHqLlOC~SSP>%!!Q5;zScra=GuDljne`vp>LkQrwQp4fGO$kIne{#O5!D*qcXCT~!d9kc8%#4y zGt5)Dt1Q%94hc4N11LO>0R@*cV3WQFu}lvHRUYu{c>zxt9zQ43>iVD^Q66nYM3$!x z%J<`-NhE%x<4#*eFJkTY6ob*v{&uvxTl!Mlf`n7wgHJXa-Ad<@2+y8JIrls`v68wO zpjuB)Z!r9nCk0A=3;Rir9)y(OYq^OAx~yr}V=1S|Vd}S~HX>)b&?ry?NYUP4`kHn( zu}G6b4B+S7H?w>c^Z_VOXrm-AnN{Uv;kxeSC0Zw`j3_LY%bxlAfcM`jG^^WzLqtmj zbY_f=tV)GSTfTkgVcuZjN8!1nB6u<&9`J%!rAi<`LgAV)4_jP8JPuXa{@QzDfHo>} za@kvf%2ipQ{J!lFM0c+8k8(}gr9Qn#s{63NsYgB7ukY<}->Z^=HF$hegq7T_+mqUV zYjd^ll{4A8%yx(gF0j0c)h2*Wgc<6igpy?}%-uvHVv+WGryODlhw15Vl@bHKy!AMi zPO%>3L%+KqJK<6nkdOV)!pWyS1%)anBS*L!C!Oh>6g$OuOU`iBW~up-bX`(Z#chMo zD?1@EBTG+u@-4ERUbV1+yvSETu^#Qh&r!5?Y_qY0hRi9ltF-11?Pm7)NJF*PJ!okX zolVcxHC?zYsmj^798=4~8QpGk$61x_^}J=o@@p`6ZzO54dM`>*0`Scs?>w0Z9l2t1 zZVP<6e2P8R$Q0V&T1d&TzzA{~(^^0x(!lw9Kp%BkD(X%_tIw1KM2|i)zJ8HeP2WTO zFKa9PLgWWP4q=eE@a$A0tM225y_9!WwB5aMf=%gA_?O@1%U%+rBbk$ubP2l5V)h!m z&6U4THC&ayul^;>hf`71vznT*vCD5F%vdmT<+MLegHC~_%G5}ZR&#G4z|nC!_ON=6 zg<-fEwT|(lE%>|nI5f0wH+NXfDD7~JV`yM%{5~Vldyc2l6rRoa<-@#`#q)PYL3h)0 zoP~jo$(e-R`@$c`J5})PpA$6PWiwweXH_o>i)1W5czgC!;%dT7d139>&}7;v2E|Ef zby!z}cZYNG@I#BJoQVb6Jzh)vQT{IZ*5rDm@VwVT)`N(+qt^6M1NJPpAV*)!66Wj9 zcZ*w8Xz=)_my4dKu;_O@_Y<+PBzF}AzHoB;D0A1}5S57&vgIkH4()s%&nTQuwaW|l zxz8JnL)9Y%@OcdhNV)rM%g0ii-T9b1s5cOCDj}uRRN`ZuT8bik4xl>w;_ zvK-CjgfVeiYWs_*@#8&Z2S{%?1y!t%9cIn#ZD9JFm>zC=pPVi!evCc!3DV$G3(q*N>Bu9W$ zO@kOlN?r(fWJ=AaN&0gjvL`J1xlH(#ff`$H#k*s9yJuU&d6(194`x`A{&^czn(p1P zBqbedo1YWMkk732rW@k@r#-%F|0iK|S%UYh!2|v|DZjh33e~}PA3fNM?Urxu`Pq(9it4w8f&WmR6B7fucs$WXoh;TM!!()s`A6-}O zy3J@o4rJVWYe;$%9p_WbqBs%3XHKG~ZiN~QIvjfz=IJ``wo~KECZXsdA6_XhJy9)a z8QY`yb-zi@TnjV)N!}!XcqGaYUf+vu|7D5hLui|m@=NqY<{X+l(ar@IcX!Bq0v@#* z21C^a2X&tdu@||g$u$ohEc8PmvLe(W4sedbZt-diu zX5!}RYjqbm*}QH0cv7adpp;ajUt>#4(br33k>e0_nIIqi!Ib+MY~7M~S^W;5`CAN! z?(3(2FmG7p9iqPWXJ`Nd0;^Luvgv3>?W)$MGPnpXwIQ4)M#6?S-|5C;`GFj zKNrkPN25R(I2w`!W^Vs+pQ=OppZ|Mzt@Gp`U4G07%%S2<1Gp0@*%bZngLdA;~Br43J+6?&mX$-KSoFLKOUqu0fn1E*8RhkQiW+g?1ba$@q5xoC>>wI}S79z?-E zO?1CGxB}W-Y&Yyznhfbi@VCz_;VQ$Q(J=UM@`>!=4~3&5Ky(jP0a#CU@E0~ndt!iQ z0geTqN350~LbOVPVe0CR=CM(wnYAU<@^jO5S8UnmIMDchjAmbg!$K#21nE2|S$m>s zWGS=g1sf{X14BktlQ)%1I(d0ufGGdIdlnnG;V6P9M3FVgAY!>;gg|UR!CRe*PtqnYK4P=<;9U02cgPE9e= z5dSBIKwpUmulxOrKC|3T>_qOKvshn!cWXJZQQ3O*=7||MqkHta>o(3h?9K1FB;C~+ zBw%0r?SmZ(R;Ikma6#}7?$YoFerbQ2*o`ce!Y;nO`#o!$EBfWpoBe9@_Vc8b{{2lY ze#E5zEeC^sKL#4_RS+XR{w2>S22#PB(rp{8M2{yarR1Zx3^!4_QLJu94~H-29I}7b zP*|m;OrGS9;uxA6eyIBL*f6j76BbpN5kGk{!^(?Ts)f+VBVX@V>inJ&u|8?6$ZBwR zvM*F2gfDFWC{OEsjtcaKqYWbsz7r*#YsLgfD(GMO*1GxHeI~G$8aqm+$2Tg`+YsUc5lT1=laj$7pfTd#vi?o+LFpoV z8s7rLHpCxX5i-%NFpIc^-Y$Yj7vy3=du?UL6_?+~xNPH9S)=wmXTiS~s=wze9jIY? zkK?57&M=x=t2;Uz#^PB4fWZR;^WzK;S~4<2yEMDn5+T-7%CY8eREW0Y2Pyc7MPHuO zud$CGoeWsTY#)z5e$oe*TXLDvyY=c$v43HTnT4 z!g76nXwp;5aWs+mGLII-GjRHa&l^)*KZlkJ1Ct?{8BEkCKtE`Rw~TMxAoJ0Is~P>; z^K3PyUa-0tH>5OqEPq10-<|DPEvm5fu|gj2=jl2R|D{Ucx#s-0YDS-MQJN+33a%v^Om2ygpyw!s>GIeq!D06C-T9ZqgPi!6f8t1pPf6qn^q66CB zI*fgqc&n09-xKy!v>>NOMA%nU8WEoWo+4!961GT!hcq(k+)os*e(L6~MKL!T`twV3 z^lL9qyu1B$GY@-=iRYdKU=nI>V5f;zUDfyM*!&B?X^`$@-Y*B#=rio>VS0eoph#!U z$1jeqWbxa6qvxxGO7knmJ|3>_TVOU&D&brk5*W2iCPb(4S2{{)9Xf5z{TN6 ziaZ2DDaoU$M@Hb3F5h%H%Jyr&IIs4a`{TLsaz{XnotCQBP3HD1+8m#uitgcrYylUcP!W4f7CVT;=Cp_TBdbqj((>{$ zr2_pr*}b|g);*?Pmn23(uPg!BkISosuhQfQc-yX)0@AG;O>J{cjtJgljQwnINZ>IN zhaA1K8M$?T06ny%8A8>TzD6h{m0RP^v8>c(wbGd)+H?&tWG`0o+^%mo><$l?(RTTK z=7x>PvNh&HS{iAncFYtkv-kcXno(-Z@1QexSDAv8HOGuUq z{grUI=d2T~80tX78}ZCQA?1&PorXM!6fyv?%mou#&+}Gs+w5x zvxu`VbciXB7v!M&LdK%7}`6goskP6N-627~saUvOopqJndo+{PYx%)b5=f}stQ zWL1ZL(sPOjP3GEQ0(``F%r vW)D{)1e^SBM~D_36y@n1*18#dMX$L=Zd1)~QimkoAY3{c2zZscZTSBHzBmcp literal 0 HcmV?d00001 diff --git a/cockatrice/resources/tips/tips_of_the_day.xml b/cockatrice/resources/tips/tips_of_the_day.xml index 5625fbe07..aa2cb463f 100644 --- a/cockatrice/resources/tips/tips_of_the_day.xml +++ b/cockatrice/resources/tips/tips_of_the_day.xml @@ -83,4 +83,10 @@ face_down.png 2018-03-01 + + Counter expressions + When setting a counter value, you can type a math expression in the box and the counter will be set to the result.<br>The "x" variable contains the current counter value. + counter_expression.png + 2019-02-02 + \ No newline at end of file diff --git a/cockatrice/src/abstractcounter.cpp b/cockatrice/src/abstractcounter.cpp index 42cf6ad83..d90951382 100644 --- a/cockatrice/src/abstractcounter.cpp +++ b/cockatrice/src/abstractcounter.cpp @@ -1,9 +1,11 @@ #include "abstractcounter.h" +#include "expression.h" #include "pb/command_inc_counter.pb.h" #include "pb/command_set_counter.pb.h" #include "player.h" #include "settingscache.h" #include +#include #include #include #include @@ -17,7 +19,7 @@ AbstractCounter::AbstractCounter(Player *_player, bool _useNameForShortcut, QGraphicsItem *parent) : QGraphicsItem(parent), player(_player), id(_id), name(_name), value(_value), - useNameForShortcut(_useNameForShortcut), hovered(false), aDec(0), aInc(0), dialogSemaphore(false), + useNameForShortcut(_useNameForShortcut), hovered(false), aDec(nullptr), aInc(nullptr), dialogSemaphore(false), deleteAfterDialog(false), shownInCounterArea(_shownInCounterArea) { setAcceptHoverEvents(true); @@ -114,7 +116,11 @@ void AbstractCounter::setValue(int _value) void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (isUnderMouse() && player->getLocal()) { - if (event->button() == Qt::LeftButton) { + if (event->button() == Qt::MidButton || (QApplication::keyboardModifiers() & Qt::ShiftModifier)) { + if (menu) + menu->exec(event->screenPos()); + event->accept(); + } else if (event->button() == Qt::LeftButton) { Command_IncCounter cmd; cmd.set_counter_id(id); cmd.set_delta(1); @@ -126,10 +132,6 @@ void AbstractCounter::mousePressEvent(QGraphicsSceneMouseEvent *event) cmd.set_delta(-1); player->sendGameCommand(cmd); event->accept(); - } else if (event->button() == Qt::MidButton) { - if (menu) - menu->exec(event->screenPos()); - event->accept(); } } else event->ignore(); @@ -160,8 +162,12 @@ void AbstractCounter::setCounter() { bool ok; dialogSemaphore = true; - int newValue = QInputDialog::getInt(0, tr("Set counter"), tr("New value for counter '%1':").arg(name), value, - -2000000000, 2000000000, 1, &ok); + QString expression = QInputDialog::getText(nullptr, tr("Set counter"), tr("New value for counter '%1':").arg(name), + QLineEdit::Normal, QString::number(value), &ok); + + Expression exp(value); + int newValue = static_cast(exp.parse(expression)); + if (deleteAfterDialog) { deleteLater(); return; diff --git a/cockatrice/src/abstractcounter.h b/cockatrice/src/abstractcounter.h index 99bcd7ca2..2e592348c 100644 --- a/cockatrice/src/abstractcounter.h +++ b/cockatrice/src/abstractcounter.h @@ -11,6 +11,7 @@ class AbstractCounter : public QObject, public QGraphicsItem { Q_OBJECT Q_INTERFACES(QGraphicsItem) + protected: Player *player; int id; @@ -18,15 +19,17 @@ protected: int value; bool useNameForShortcut, hovered; - void mousePressEvent(QGraphicsSceneMouseEvent *event); - void hoverEnterEvent(QGraphicsSceneHoverEvent *event); - void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; private: QAction *aSet, *aDec, *aInc; QMenu *menu; bool dialogSemaphore, deleteAfterDialog; bool shownInCounterArea; + bool shortcutActive; + private slots: void refreshShortcuts(); void incrementCounter(); @@ -39,14 +42,19 @@ public: bool _shownInCounterArea, int _value, bool _useNameForShortcut = false, - QGraphicsItem *parent = 0); - ~AbstractCounter(); + QGraphicsItem *parent = nullptr); + ~AbstractCounter() override; + + void retranslateUi(); + void setValue(int _value); + void setShortcutsActive(); + void setShortcutsInactive(); + void delCounter(); QMenu *getMenu() const { return menu; } - void retranslateUi(); int getId() const { @@ -64,12 +72,6 @@ public: { return value; } - void setValue(int _value); - void delCounter(); - - void setShortcutsActive(); - void setShortcutsInactive(); - bool shortcutActive; }; #endif diff --git a/cockatrice/src/dlg_tip_of_the_day.cpp b/cockatrice/src/dlg_tip_of_the_day.cpp index 67ecc400e..37d6465f1 100644 --- a/cockatrice/src/dlg_tip_of_the_day.cpp +++ b/cockatrice/src/dlg_tip_of_the_day.cpp @@ -13,7 +13,7 @@ #define MIN_TIP_IMAGE_HEIGHT 200 #define MIN_TIP_IMAGE_WIDTH 200 #define MAX_TIP_IMAGE_HEIGHT 300 -#define MAX_TIP_IMAGE_WIDTH 300 +#define MAX_TIP_IMAGE_WIDTH 500 DlgTipOfTheDay::DlgTipOfTheDay(QWidget *parent) : QDialog(parent) { @@ -149,9 +149,9 @@ void DlgTipOfTheDay::updateTip(int tipId) qDebug() << "Image failed to load from" << imagePath; imageLabel->clear(); } else { - int h = std::min(std::max(image->height(), MIN_TIP_IMAGE_HEIGHT), MAX_TIP_IMAGE_HEIGHT); + int h = std::min(std::max(imageLabel->height(), MIN_TIP_IMAGE_HEIGHT), MAX_TIP_IMAGE_HEIGHT); int w = std::min(std::max(imageLabel->width(), MIN_TIP_IMAGE_WIDTH), MAX_TIP_IMAGE_WIDTH); - imageLabel->setPixmap(image->scaled(h, w, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + imageLabel->setPixmap(image->scaled(w, h, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } date->setText("Tip added on: " + tip.getDate().toString("yyyy.MM.dd") + ""); @@ -163,9 +163,7 @@ void DlgTipOfTheDay::updateTip(int tipId) void DlgTipOfTheDay::resizeEvent(QResizeEvent *event) { - int h = imageLabel->height(); - int w = imageLabel->width(); - imageLabel->setPixmap(image->scaled(w, h, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + imageLabel->setPixmap(image->scaled(imageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); QWidget::resizeEvent(event); } diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index f32d59a26..cf04f1956 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -26,6 +26,7 @@ SET(common_SOURCES server_room.cpp serverinfo_user_container.cpp sfmt/SFMT.c + expression.cpp ) set(ORACLE_LIBS) diff --git a/common/expression.cpp b/common/expression.cpp new file mode 100644 index 000000000..1671d04b1 --- /dev/null +++ b/common/expression.cpp @@ -0,0 +1,108 @@ +#include "expression.h" +#include "./lib/peglib.h" + +#include +#include +#include +#include + +peg::parser math(R"( + EXPRESSION <- P0 + P0 <- P1 (P1_OPERATOR P1)* + P1 <- P2 (P2_OPERATOR P2)* + P2 <- P3 (P3_OPERATOR P3)* + P3 <- NUMBER / FUNCTION / VARIABLE / '(' P0 ')' + + P1_OPERATOR <- < [-+] > + P2_OPERATOR <- < [/*] > + P3_OPERATOR <- < '^' > + + NUMBER <- < '-'? [0-9]+ > + NAME <- < [a-z][a-z0-9]* > + VARIABLE <- < [x] > + FUNCTION <- NAME '(' EXPRESSION ( [,\n] EXPRESSION )* ')' + + %whitespace <- [ \t\r]* + )"); + +QMap> *default_functions = nullptr; + +Expression::Expression(double initial) : value(initial) +{ + if (default_functions == nullptr) { + default_functions = new QMap>(); + default_functions->insert("sin", [](double a) { return sin(a); }); + default_functions->insert("cos", [](double a) { return cos(a); }); + default_functions->insert("tan", [](double a) { return tan(a); }); + default_functions->insert("sqrt", [](double a) { return sqrt(a); }); + default_functions->insert("log", [](double a) { return log(a); }); + default_functions->insert("log10", [](double a) { return log(a); }); + default_functions->insert("trunc", [](double a) { return trunc(a); }); + default_functions->insert("abs", [](double a) { return abs(a); }); + + default_functions->insert("floor", [](double a) { return floor(a); }); + default_functions->insert("ceil", [](double a) { return ceil(a); }); + default_functions->insert("round", [](double a) { return round(a); }); + default_functions->insert("trunc", [](double a) { return trunc(a); }); + } + fns = QMap>(*default_functions); +} + +double Expression::eval(const peg::Ast &ast) +{ + const auto &nodes = ast.nodes; + if (ast.name == "NUMBER") { + return stod(ast.token); + } else if (ast.name == "FUNCTION") { + QString name = QString::fromStdString(nodes[0]->token); + if (!fns.contains(name)) + return 0; + return fns[name](eval(*nodes[1])); + } else if (ast.name == "VARIABLE") { + return value; + } else if (ast.name[0] == 'P') { + double result = eval(*nodes[0]); + for (int i = 1; i < nodes.size(); i += 2) { + double arg = eval(*nodes[i + 1]); + char operation = nodes[i]->token[0]; + switch (operation) { + case '+': + result += arg; + break; + case '-': + result -= arg; + break; + case '*': + result *= arg; + break; + case '/': + result /= arg; + break; + case '^': + result = pow(result, arg); + break; + default: + result = 0; + break; + } + } + return result; + } else { + return -1; + } +} + +double Expression::parse(const QString &expr) +{ + QByteArray ba = expr.toLocal8Bit(); + + math.enable_ast(); + + std::shared_ptr ast; + if (math.parse(ba.data(), ast)) { + ast = peg::AstOptimizer(true).optimize(ast); + return eval(*ast); + } + + return 0; +} diff --git a/common/expression.h b/common/expression.h new file mode 100644 index 000000000..8c6a94932 --- /dev/null +++ b/common/expression.h @@ -0,0 +1,28 @@ +#ifndef EXPRESSION_H +#define EXPRESSION_H + +#include +#include +#include + +namespace peg +{ +template struct AstBase; +struct EmptyType; +typedef AstBase Ast; +} // namespace peg + +class Expression +{ +public: + double value; + + explicit Expression(double initial = 0); + double parse(const QString &expr); + +private: + double eval(const peg::Ast &ast); + QMap> fns; +}; + +#endif diff --git a/common/lib/peglib.h b/common/lib/peglib.h new file mode 100644 index 000000000..5c5e4c011 --- /dev/null +++ b/common/lib/peglib.h @@ -0,0 +1,3293 @@ +// +// peglib.h +// +// Copyright (c) 2015-18 Yuji Hirose. All rights reserved. +// MIT License +// + +#ifndef CPPPEGLIB_PEGLIB_H +#define CPPPEGLIB_PEGLIB_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// guard for older versions of VC++ +#ifdef _MSC_VER +// VS2013 has no constexpr +#if (_MSC_VER == 1800) +#define PEGLIB_NO_CONSTEXPR_SUPPORT +#elif (_MSC_VER >= 1800) +// good to go +#else (_MSC_VER < 1800) +#error "Requires C+11 support" +#endif +#endif + +// define if the compiler doesn't support unicode characters reliably in the +// source code +//#define PEGLIB_NO_UNICODE_CHARS + +namespace peg { + +/*----------------------------------------------------------------------------- + * any + *---------------------------------------------------------------------------*/ + +class any +{ +public: + any() : content_(nullptr) {} + + any(const any& rhs) : content_(rhs.clone()) {} + + any(any&& rhs) : content_(rhs.content_) { + rhs.content_ = nullptr; + } + + template + any(const T& value) : content_(new holder(value)) {} + + any& operator=(const any& rhs) { + if (this != &rhs) { + if (content_) { + delete content_; + } + content_ = rhs.clone(); + } + return *this; + } + + any& operator=(any&& rhs) { + if (this != &rhs) { + if (content_) { + delete content_; + } + content_ = rhs.content_; + rhs.content_ = nullptr; + } + return *this; + } + + ~any() { + delete content_; + } + + bool is_undefined() const { + return content_ == nullptr; + } + + template < + typename T, + typename std::enable_if::value, std::nullptr_t>::type = nullptr + > + T& get() { + if (!content_) { + throw std::bad_cast(); + } + auto p = dynamic_cast*>(content_); + assert(p); + if (!p) { + throw std::bad_cast(); + } + return p->value_; + } + + template < + typename T, + typename std::enable_if::value, std::nullptr_t>::type = nullptr + > + T& get() { + return *this; + } + + template < + typename T, + typename std::enable_if::value, std::nullptr_t>::type = nullptr + > + const T& get() const { + assert(content_); + auto p = dynamic_cast*>(content_); + assert(p); + if (!p) { + throw std::bad_cast(); + } + return p->value_; + } + + template < + typename T, + typename std::enable_if::value, std::nullptr_t>::type = nullptr + > + const any& get() const { + return *this; + } + +private: + struct placeholder { + virtual ~placeholder() {} + virtual placeholder* clone() const = 0; + }; + + template + struct holder : placeholder { + holder(const T& value) : value_(value) {} + placeholder* clone() const override { + return new holder(value_); + } + T value_; + }; + + placeholder* clone() const { + return content_ ? content_->clone() : nullptr; + } + + placeholder* content_; +}; + +/*----------------------------------------------------------------------------- + * scope_exit + *---------------------------------------------------------------------------*/ + +// This is based on "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". + +template +struct scope_exit +{ + explicit scope_exit(EF&& f) + : exit_function(std::move(f)) + , execute_on_destruction{true} {} + + scope_exit(scope_exit&& rhs) + : exit_function(std::move(rhs.exit_function)) + , execute_on_destruction{rhs.execute_on_destruction} { + rhs.release(); + } + + ~scope_exit() { + if (execute_on_destruction) { + this->exit_function(); + } + } + + void release() { + this->execute_on_destruction = false; + } + +private: + scope_exit(const scope_exit&) = delete; + void operator=(const scope_exit&) = delete; + scope_exit& operator=(scope_exit&&) = delete; + + EF exit_function; + bool execute_on_destruction; +}; + +template +auto make_scope_exit(EF&& exit_function) -> scope_exit { + return scope_exit::type>(std::forward(exit_function)); +} + +/*----------------------------------------------------------------------------- + * UTF8 functions + *---------------------------------------------------------------------------*/ + +inline size_t codepoint_length(const char *s8, size_t l) { + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + return 1; + } else if ((b & 0xE0) == 0xC0) { + return 2; + } else if ((b & 0xF0) == 0xE0) { + return 3; + } else if ((b & 0xF8) == 0xF0) { + return 4; + } + } + return 0; +} + +inline size_t encode_codepoint(char32_t cp, char *buff) { + if (cp < 0x0080) { + buff[0] = static_cast(cp & 0x7F); + return 1; + } else if (cp < 0x0800) { + buff[0] = static_cast(0xC0 | ((cp >> 6) & 0x1F)); + buff[1] = static_cast(0x80 | (cp & 0x3F)); + return 2; + } else if (cp < 0xD800) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0xE000) { + // D800 - DFFF is invalid... + return 0; + } else if (cp < 0x10000) { + buff[0] = static_cast(0xE0 | ((cp >> 12) & 0xF)); + buff[1] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[2] = static_cast(0x80 | (cp & 0x3F)); + return 3; + } else if (cp < 0x110000) { + buff[0] = static_cast(0xF0 | ((cp >> 18) & 0x7)); + buff[1] = static_cast(0x80 | ((cp >> 12) & 0x3F)); + buff[2] = static_cast(0x80 | ((cp >> 6) & 0x3F)); + buff[3] = static_cast(0x80 | (cp & 0x3F)); + return 4; + } + return 0; +} + +inline std::string encode_codepoint(char32_t cp) { + char buff[4]; + auto l = encode_codepoint(cp, buff); + return std::string(buff, l); +} + +inline bool decode_codepoint(const char *s8, size_t l, size_t &bytes, + char32_t &cp) { + if (l) { + auto b = static_cast(s8[0]); + if ((b & 0x80) == 0) { + bytes = 1; + cp = b; + return true; + } else if ((b & 0xE0) == 0xC0) { + if (l >= 2) { + bytes = 2; + cp = ((static_cast(s8[0] & 0x1F)) << 6) | + (static_cast(s8[1] & 0x3F)); + return true; + } + } else if ((b & 0xF0) == 0xE0) { + if (l >= 3) { + bytes = 3; + cp = ((static_cast(s8[0] & 0x0F)) << 12) | + ((static_cast(s8[1] & 0x3F)) << 6) | + (static_cast(s8[2] & 0x3F)); + return true; + } + } else if ((b & 0xF8) == 0xF0) { + if (l >= 4) { + bytes = 4; + cp = ((static_cast(s8[0] & 0x07)) << 18) | + ((static_cast(s8[1] & 0x3F)) << 12) | + ((static_cast(s8[2] & 0x3F)) << 6) | + (static_cast(s8[3] & 0x3F)); + return true; + } + } + } + return false; +} + +inline size_t decode_codepoint(const char *s8, size_t l, char32_t &out) { + size_t bytes; + if (decode_codepoint(s8, l, bytes, out)) { + return bytes; + } + return 0; +} + +inline char32_t decode_codepoint(const char *s8, size_t l) { + char32_t out = 0; + decode_codepoint(s8, l, out); + return out; +} + +inline std::u32string decode(const char *s8, size_t l) { + std::u32string out; + size_t i = 0; + while (i < l) { + auto beg = i++; + while (i < l && (s8[i] & 0xc0) == 0x80) { + i++; + } + out += decode_codepoint(&s8[beg], (i - beg)); + } + return out; +} + +/*----------------------------------------------------------------------------- + * resolve_escape_sequence + *---------------------------------------------------------------------------*/ + +inline bool is_hex(char c, int& v) { + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } else if ('a' <= c && c <= 'f') { + v = c - 'a' + 10; + return true; + } else if ('A' <= c && c <= 'F') { + v = c - 'A' + 10; + return true; + } + return false; +} + +inline bool is_digit(char c, int& v) { + if ('0' <= c && c <= '9') { + v = c - '0'; + return true; + } + return false; +} + +inline std::pair parse_hex_number(const char* s, size_t n, size_t i) { + int ret = 0; + int val; + while (i < n && is_hex(s[i], val)) { + ret = static_cast(ret * 16 + val); + i++; + } + return std::make_pair(ret, i); +} + +inline std::pair parse_octal_number(const char* s, size_t n, size_t i) { + int ret = 0; + int val; + while (i < n && is_digit(s[i], val)) { + ret = static_cast(ret * 8 + val); + i++; + } + return std::make_pair(ret, i); +} + +inline std::string resolve_escape_sequence(const char* s, size_t n) { + std::string r; + r.reserve(n); + + size_t i = 0; + while (i < n) { + auto ch = s[i]; + if (ch == '\\') { + i++; + switch (s[i]) { + case 'n': r += '\n'; i++; break; + case 'r': r += '\r'; i++; break; + case 't': r += '\t'; i++; break; + case '\'': r += '\''; i++; break; + case '"': r += '"'; i++; break; + case '[': r += '['; i++; break; + case ']': r += ']'; i++; break; + case '\\': r += '\\'; i++; break; + case 'x': + case 'u': { + char32_t cp; + std::tie(cp, i) = parse_hex_number(s, n, i + 1); + r += encode_codepoint(cp); + break; + } + default: { + char32_t cp; + std::tie(cp, i) = parse_octal_number(s, n, i); + r += encode_codepoint(cp); + break; + } + } + } else { + r += ch; + i++; + } + } + return r; +} + +/*----------------------------------------------------------------------------- + * PEG + *---------------------------------------------------------------------------*/ + +/* +* Line information utility function +*/ +inline std::pair line_info(const char* start, const char* cur) { + auto p = start; + auto col_ptr = p; + auto no = 1; + + while (p < cur) { + if (*p == '\n') { + no++; + col_ptr = p + 1; + } + p++; + } + + auto col = p - col_ptr + 1; + + return std::make_pair(no, col); +} + +/* +* Semantic values +*/ +struct SemanticValues : protected std::vector +{ + // Input text + const char* path; + const char* ss; + + // Matched string + const char* c_str() const { return s_; } + size_t length() const { return n_; } + + std::string str() const { + return std::string(s_, n_); + } + + // Line number and column at which the matched string is + std::pair line_info() const { + return peg::line_info(ss, s_); + } + + // Choice count + size_t choice_count() const { return choice_count_; } + + // Choice number (0 based index) + size_t choice() const { return choice_; } + + // Tokens + std::vector> tokens; + + std::string token(size_t id = 0) const { + if (!tokens.empty()) { + assert(id < tokens.size()); + const auto& tok = tokens[id]; + return std::string(tok.first, tok.second); + } + return std::string(s_, n_); + } + + // Transform the semantic value vector to another vector + template + auto transform(size_t beg = 0, size_t end = static_cast(-1)) const -> vector { + return this->transform(beg, end, [](const any& v) { return v.get(); }); + } + + SemanticValues() : s_(nullptr), n_(0), choice_count_(0), choice_(0) {} + + using std::vector::iterator; + using std::vector::const_iterator; + using std::vector::size; + using std::vector::empty; + using std::vector::assign; + using std::vector::begin; + using std::vector::end; + using std::vector::rbegin; + using std::vector::rend; + using std::vector::operator[]; + using std::vector::at; + using std::vector::resize; + using std::vector::front; + using std::vector::back; + using std::vector::push_back; + using std::vector::pop_back; + using std::vector::insert; + using std::vector::erase; + using std::vector::clear; + using std::vector::swap; + using std::vector::emplace; + using std::vector::emplace_back; + +private: + friend class Context; + friend class Sequence; + friend class PrioritizedChoice; + friend class Holder; + + const char* s_; + size_t n_; + size_t choice_count_; + size_t choice_; + + template + auto transform(F f) const -> vector::type> { + vector::type> r; + for (const auto& v: *this) { + r.emplace_back(f(v)); + } + return r; + } + + template + auto transform(size_t beg, size_t end, F f) const -> vector::type> { + vector::type> r; + end = (std::min)(end, size()); + for (size_t i = beg; i < end; i++) { + r.emplace_back(f((*this)[i])); + } + return r; + } + + void reset() { + path = nullptr; + ss = nullptr; + tokens.clear(); + + s_ = nullptr; + n_ = 0; + choice_count_ = 0; + choice_ = 0; + } +}; + +/* + * Semantic action + */ +template < + typename R, typename F, + typename std::enable_if::value, std::nullptr_t>::type = nullptr, + typename... Args> +any call(F fn, Args&&... args) { + fn(std::forward(args)...); + return any(); +} + +template < + typename R, typename F, + typename std::enable_if::type, any>::value, std::nullptr_t>::type = nullptr, + typename... Args> +any call(F fn, Args&&... args) { + return fn(std::forward(args)...); +} + +template < + typename R, typename F, + typename std::enable_if< + !std::is_void::value && + !std::is_same::type, any>::value, std::nullptr_t>::type = nullptr, + typename... Args> +any call(F fn, Args&&... args) { + return any(fn(std::forward(args)...)); +} + +class Action +{ +public: + Action() = default; + + Action(const Action& rhs) : fn_(rhs.fn_) {} + + template ::value && !std::is_same::value, std::nullptr_t>::type = nullptr> + Action(F fn) : fn_(make_adaptor(fn, &F::operator())) {} + + template ::value, std::nullptr_t>::type = nullptr> + Action(F fn) : fn_(make_adaptor(fn, fn)) {} + + template ::value, std::nullptr_t>::type = nullptr> + Action(F /*fn*/) {} + + template ::value && !std::is_same::value, std::nullptr_t>::type = nullptr> + void operator=(F fn) { + fn_ = make_adaptor(fn, &F::operator()); + } + + template ::value, std::nullptr_t>::type = nullptr> + void operator=(F fn) { + fn_ = make_adaptor(fn, fn); + } + + template ::value, std::nullptr_t>::type = nullptr> + void operator=(F /*fn*/) {} + + Action& operator=(const Action& rhs) = default; + + operator bool() const { + return bool(fn_); + } + + any operator()(SemanticValues& sv, any& dt) const { + return fn_(sv, dt); + } + +private: + template + struct TypeAdaptor_sv { + TypeAdaptor_sv(std::function fn) + : fn_(fn) {} + any operator()(SemanticValues& sv, any& /*dt*/) { + return call(fn_, sv); + } + std::function fn_; + }; + + template + struct TypeAdaptor_csv { + TypeAdaptor_csv(std::function fn) + : fn_(fn) {} + any operator()(SemanticValues& sv, any& /*dt*/) { + return call(fn_, sv); + } + std::function fn_; + }; + + template + struct TypeAdaptor_sv_dt { + TypeAdaptor_sv_dt(std::function fn) + : fn_(fn) {} + any operator()(SemanticValues& sv, any& dt) { + return call(fn_, sv, dt); + } + std::function fn_; + }; + + template + struct TypeAdaptor_csv_dt { + TypeAdaptor_csv_dt(std::function fn) + : fn_(fn) {} + any operator()(SemanticValues& sv, any& dt) { + return call(fn_, sv, dt); + } + std::function fn_; + }; + + typedef std::function Fty; + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(SemanticValues& sv) const) { + return TypeAdaptor_sv(fn); + } + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(const SemanticValues& sv) const) { + return TypeAdaptor_csv(fn); + } + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(SemanticValues& sv)) { + return TypeAdaptor_sv(fn); + } + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(const SemanticValues& sv)) { + return TypeAdaptor_csv(fn); + } + + template + Fty make_adaptor(F fn, R (* /*mf*/)(SemanticValues& sv)) { + return TypeAdaptor_sv(fn); + } + + template + Fty make_adaptor(F fn, R (* /*mf*/)(const SemanticValues& sv)) { + return TypeAdaptor_csv(fn); + } + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(SemanticValues& sv, any& dt) const) { + return TypeAdaptor_sv_dt(fn); + } + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(const SemanticValues& sv, any& dt) const) { + return TypeAdaptor_csv_dt(fn); + } + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(SemanticValues& sv, any& dt)) { + return TypeAdaptor_sv_dt(fn); + } + + template + Fty make_adaptor(F fn, R (F::* /*mf*/)(const SemanticValues& sv, any& dt)) { + return TypeAdaptor_csv_dt(fn); + } + + template + Fty make_adaptor(F fn, R(* /*mf*/)(SemanticValues& sv, any& dt)) { + return TypeAdaptor_sv_dt(fn); + } + + template + Fty make_adaptor(F fn, R(* /*mf*/)(const SemanticValues& sv, any& dt)) { + return TypeAdaptor_csv_dt(fn); + } + + Fty fn_; +}; + +/* + * Semantic predicate + */ +// Note: 'parse_error' exception class should be be used in sematic action handlers to reject the rule. +struct parse_error { + parse_error() = default; + parse_error(const char* s) : s_(s) {} + const char* what() const { return s_.empty() ? nullptr : s_.c_str(); } +private: + std::string s_; +}; + +/* + * Result + */ +inline bool success(size_t len) { + return len != static_cast(-1); +} + +inline bool fail(size_t len) { + return len == static_cast(-1); +} + +/* + * Context + */ +class Context; +class Ope; +class Definition; + +typedef std::function Tracer; + +class Context +{ +public: + const char* path; + const char* s; + const size_t l; + + const char* error_pos; + const char* message_pos; + std::string message; // TODO: should be `int`. + + std::vector> value_stack; + size_t value_stack_size; + std::vector>> args_stack; + + size_t nest_level; + + bool in_token; + + std::shared_ptr whitespaceOpe; + bool in_whitespace; + + std::shared_ptr wordOpe; + + std::vector> capture_scope_stack; + + const size_t def_count; + const bool enablePackratParsing; + std::vector cache_registered; + std::vector cache_success; + + std::map, std::tuple> cache_values; + + std::function tracer; + + Context( + const char* a_path, + const char* a_s, + size_t a_l, + size_t a_def_count, + std::shared_ptr a_whitespaceOpe, + std::shared_ptr a_wordOpe, + bool a_enablePackratParsing, + Tracer a_tracer) + : path(a_path) + , s(a_s) + , l(a_l) + , error_pos(nullptr) + , message_pos(nullptr) + , value_stack_size(0) + , nest_level(0) + , in_token(false) + , whitespaceOpe(a_whitespaceOpe) + , in_whitespace(false) + , wordOpe(a_wordOpe) + , def_count(a_def_count) + , enablePackratParsing(a_enablePackratParsing) + , cache_registered(enablePackratParsing ? def_count * (l + 1) : 0) + , cache_success(enablePackratParsing ? def_count * (l + 1) : 0) + , tracer(a_tracer) + { + args_stack.resize(1); + capture_scope_stack.resize(1); + } + + template + void packrat(const char* a_s, size_t def_id, size_t& len, any& val, T fn) { + if (!enablePackratParsing) { + fn(val); + return; + } + + auto col = a_s - s; + auto idx = def_count * static_cast(col) + def_id; + + if (cache_registered[idx]) { + if (cache_success[idx]) { + auto key = std::make_pair(col, def_id); + std::tie(len, val) = cache_values[key]; + return; + } else { + len = static_cast(-1); + return; + } + } else { + fn(val); + cache_registered[idx] = true; + cache_success[idx] = success(len); + if (success(len)) { + auto key = std::make_pair(col, def_id); + cache_values[key] = std::make_pair(len, val); + } + return; + } + } + + SemanticValues& push() { + assert(value_stack_size <= value_stack.size()); + if (value_stack_size == value_stack.size()) { + value_stack.emplace_back(std::make_shared()); + } + auto& sv = *value_stack[value_stack_size++]; + if (!sv.empty()) { + sv.clear(); + } + sv.reset(); + sv.path = path; + sv.ss = s; + return sv; + } + + void pop() { + value_stack_size--; + } + + void push_args(const std::vector>& args) { + args_stack.push_back(args); + } + + void pop_args() { + args_stack.pop_back(); + } + + const std::vector>& top_args() const { + return args_stack[args_stack.size() - 1]; + } + + void push_capture_scope() { + capture_scope_stack.resize(capture_scope_stack.size() + 1); + } + + void pop_capture_scope() { + capture_scope_stack.pop_back(); + } + + void shift_capture_values() { + assert(capture_scope_stack.size() >= 2); + auto it = capture_scope_stack.rbegin(); + auto it_prev = it + 1; + for (const auto& kv: *it) { + (*it_prev)[kv.first] = kv.second; + } + } + + void set_error_pos(const char* a_s) { + if (error_pos < a_s) error_pos = a_s; + } + + void trace(const char* name, const char* a_s, size_t n, SemanticValues& sv, any& dt) const { + if (tracer) tracer(name, a_s, n, sv, *this, dt); + } +}; + +/* + * Parser operators + */ +class Ope +{ +public: + struct Visitor; + + virtual ~Ope() {} + virtual size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const = 0; + virtual void accept(Visitor& v) = 0; +}; + +class Sequence : public Ope +{ +public: + Sequence(const Sequence& rhs) : opes_(rhs.opes_) {} + +#if defined(_MSC_VER) && _MSC_VER < 1900 // Less than Visual Studio 2015 + // NOTE: Compiler Error C2797 on Visual Studio 2013 + // "The C++ compiler in Visual Studio does not implement list + // initialization inside either a member initializer list or a non-static + // data member initializer. Before Visual Studio 2013 Update 3, this was + // silently converted to a function call, which could lead to bad code + // generation. Visual Studio 2013 Update 3 reports this as an error." + template + Sequence(const Args& ...args) { + opes_ = std::vector>{ static_cast>(args)... }; + } +#else + template + Sequence(const Args& ...args) : opes_{ static_cast>(args)... } {} +#endif + + Sequence(const std::vector>& opes) : opes_(opes) {} + Sequence(std::vector>&& opes) : opes_(opes) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("Sequence", s, n, sv, dt); + auto& chldsv = c.push(); + size_t i = 0; + for (const auto& ope : opes_) { + c.nest_level++; + auto se = make_scope_exit([&]() { c.nest_level--; }); + const auto& rule = *ope; + auto len = rule.parse(s + i, n - i, chldsv, c, dt); + if (fail(len)) { + return static_cast(-1); + } + i += len; + } + sv.insert(sv.end(), chldsv.begin(), chldsv.end()); + sv.s_ = chldsv.c_str(); + sv.n_ = chldsv.length(); + sv.tokens.insert(sv.tokens.end(), chldsv.tokens.begin(), chldsv.tokens.end()); + return i; + } + + void accept(Visitor& v) override; + + std::vector> opes_; +}; + +class PrioritizedChoice : public Ope +{ +public: +#if defined(_MSC_VER) && _MSC_VER < 1900 // Less than Visual Studio 2015 + // NOTE: Compiler Error C2797 on Visual Studio 2013 + // "The C++ compiler in Visual Studio does not implement list + // initialization inside either a member initializer list or a non-static + // data member initializer. Before Visual Studio 2013 Update 3, this was + // silently converted to a function call, which could lead to bad code + // generation. Visual Studio 2013 Update 3 reports this as an error." + template + PrioritizedChoice(const Args& ...args) { + opes_ = std::vector>{ static_cast>(args)... }; + } +#else + template + PrioritizedChoice(const Args& ...args) : opes_{ static_cast>(args)... } {} +#endif + + PrioritizedChoice(const std::vector>& opes) : opes_(opes) {} + PrioritizedChoice(std::vector>&& opes) : opes_(opes) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("PrioritizedChoice", s, n, sv, dt); + size_t id = 0; + for (const auto& ope : opes_) { + c.nest_level++; + auto& chldsv = c.push(); + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.nest_level--; + c.pop(); + c.pop_capture_scope(); + }); + const auto& rule = *ope; + auto len = rule.parse(s, n, chldsv, c, dt); + if (success(len)) { + sv.insert(sv.end(), chldsv.begin(), chldsv.end()); + sv.s_ = chldsv.c_str(); + sv.n_ = chldsv.length(); + sv.choice_count_ = opes_.size(); + sv.choice_ = id; + sv.tokens.insert(sv.tokens.end(), chldsv.tokens.begin(), chldsv.tokens.end()); + + c.shift_capture_values(); + return len; + } + id++; + } + return static_cast(-1); + } + + void accept(Visitor& v) override; + + size_t size() const { return opes_.size(); } + + std::vector> opes_; +}; + +class ZeroOrMore : public Ope +{ +public: + ZeroOrMore(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("ZeroOrMore", s, n, sv, dt); + auto save_error_pos = c.error_pos; + size_t i = 0; + while (n - i > 0) { + c.nest_level++; + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.nest_level--; + c.pop_capture_scope(); + }); + auto save_sv_size = sv.size(); + auto save_tok_size = sv.tokens.size(); + const auto& rule = *ope_; + auto len = rule.parse(s + i, n - i, sv, c, dt); + if (success(len)) { + c.shift_capture_values(); + } else { + if (sv.size() != save_sv_size) { + sv.erase(sv.begin() + static_cast(save_sv_size)); + } + if (sv.tokens.size() != save_tok_size) { + sv.tokens.erase(sv.tokens.begin() + static_cast(save_tok_size)); + } + c.error_pos = save_error_pos; + break; + } + i += len; + } + return i; + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class OneOrMore : public Ope +{ +public: + OneOrMore(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("OneOrMore", s, n, sv, dt); + size_t len = 0; + { + c.nest_level++; + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.nest_level--; + c.pop_capture_scope(); + }); + const auto& rule = *ope_; + len = rule.parse(s, n, sv, c, dt); + if (success(len)) { + c.shift_capture_values(); + } else { + return static_cast(-1); + } + } + auto save_error_pos = c.error_pos; + auto i = len; + while (n - i > 0) { + c.nest_level++; + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.nest_level--; + c.pop_capture_scope(); + }); + auto save_sv_size = sv.size(); + auto save_tok_size = sv.tokens.size(); + const auto& rule = *ope_; + len = rule.parse(s + i, n - i, sv, c, dt); + if (success(len)) { + c.shift_capture_values(); + } else { + if (sv.size() != save_sv_size) { + sv.erase(sv.begin() + static_cast(save_sv_size)); + } + if (sv.tokens.size() != save_tok_size) { + sv.tokens.erase(sv.tokens.begin() + static_cast(save_tok_size)); + } + c.error_pos = save_error_pos; + break; + } + i += len; + } + return i; + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class Option : public Ope +{ +public: + Option(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("Option", s, n, sv, dt); + auto save_error_pos = c.error_pos; + c.nest_level++; + auto save_sv_size = sv.size(); + auto save_tok_size = sv.tokens.size(); + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.nest_level--; + c.pop_capture_scope(); + }); + const auto& rule = *ope_; + auto len = rule.parse(s, n, sv, c, dt); + if (success(len)) { + c.shift_capture_values(); + return len; + } else { + if (sv.size() != save_sv_size) { + sv.erase(sv.begin() + static_cast(save_sv_size)); + } + if (sv.tokens.size() != save_tok_size) { + sv.tokens.erase(sv.tokens.begin() + static_cast(save_tok_size)); + } + c.error_pos = save_error_pos; + return 0; + } + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class AndPredicate : public Ope +{ +public: + AndPredicate(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("AndPredicate", s, n, sv, dt); + c.nest_level++; + auto& chldsv = c.push(); + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.nest_level--; + c.pop(); + c.pop_capture_scope(); + }); + const auto& rule = *ope_; + auto len = rule.parse(s, n, chldsv, c, dt); + if (success(len)) { + return 0; + } else { + return static_cast(-1); + } + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class NotPredicate : public Ope +{ +public: + NotPredicate(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("NotPredicate", s, n, sv, dt); + auto save_error_pos = c.error_pos; + c.nest_level++; + auto& chldsv = c.push(); + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.nest_level--; + c.pop(); + c.pop_capture_scope(); + }); + const auto& rule = *ope_; + auto len = rule.parse(s, n, chldsv, c, dt); + if (success(len)) { + c.set_error_pos(s); + return static_cast(-1); + } else { + c.error_pos = save_error_pos; + return 0; + } + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class LiteralString : public Ope + , public std::enable_shared_from_this +{ +public: + LiteralString(const std::string& s) + : lit_(s) + , init_is_word_(false) + , is_word_(false) + {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override; + + void accept(Visitor& v) override; + + std::string lit_; + mutable bool init_is_word_; + mutable bool is_word_; +}; + +class CharacterClass : public Ope + , public std::enable_shared_from_this +{ +public: + CharacterClass(const std::string& s) { + auto chars = decode(s.c_str(), s.length()); + auto i = 0u; + while (i < chars.size()) { + if (i + 2 < chars.size() && chars[i + 1] == '-') { + auto cp1 = chars[i]; + auto cp2 = chars[i + 2]; + ranges_.emplace_back(std::make_pair(cp1, cp2)); + i += 3; + } else { + auto cp = chars[i]; + ranges_.emplace_back(std::make_pair(cp, cp)); + i += 1; + } + } + } + + CharacterClass(const std::vector>& ranges) : ranges_(ranges) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("CharacterClass", s, n, sv, dt); + + if (n < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + + char32_t cp; + auto len = decode_codepoint(s, n, cp); + + if (!ranges_.empty()) { + for (const auto& range: ranges_) { + if (range.first <= cp && cp <= range.second) { + return len; + } + } + } + + c.set_error_pos(s); + return static_cast(-1); + } + + void accept(Visitor& v) override; + + std::vector> ranges_; +}; + +class Character : public Ope + , public std::enable_shared_from_this +{ +public: + Character(char ch) : ch_(ch) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("Character", s, n, sv, dt); + if (n < 1 || s[0] != ch_) { + c.set_error_pos(s); + return static_cast(-1); + } + return 1; + } + + void accept(Visitor& v) override; + + char ch_; +}; + +class AnyCharacter : public Ope + , public std::enable_shared_from_this +{ +public: + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("AnyCharacter", s, n, sv, dt); + auto len = codepoint_length(s, n); + if (len < 1) { + c.set_error_pos(s); + return static_cast(-1); + } + return len; + } + + void accept(Visitor& v) override; +}; + +class CaptureScope : public Ope +{ +public: + CaptureScope(const std::shared_ptr& ope) + : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.push_capture_scope(); + auto se = make_scope_exit([&]() { + c.pop_capture_scope(); + }); + const auto& rule = *ope_; + auto len = rule.parse(s, n, sv, c, dt); + return len; + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class Capture : public Ope +{ +public: + typedef std::function MatchAction; + + Capture(const std::shared_ptr& ope, MatchAction ma) + : ope_(ope), match_action_(ma) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + const auto& rule = *ope_; + auto len = rule.parse(s, n, sv, c, dt); + if (success(len) && match_action_) { + match_action_(s, len, c); + } + return len; + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; + MatchAction match_action_; +}; + +class TokenBoundary : public Ope +{ +public: + TokenBoundary(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override; + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class Ignore : public Ope +{ +public: + Ignore(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& /*sv*/, Context& c, any& dt) const override { + const auto& rule = *ope_; + auto& chldsv = c.push(); + auto se = make_scope_exit([&]() { + c.pop(); + }); + return rule.parse(s, n, chldsv, c, dt); + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +typedef std::function Parser; + +class User : public Ope +{ +public: + User(Parser fn) : fn_(fn) {} + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + c.trace("User", s, n, sv, dt); + assert(fn_); + return fn_(s, n, sv, dt); + } + void accept(Visitor& v) override; + std::function fn_; +}; + +class WeakHolder : public Ope +{ +public: + WeakHolder(const std::shared_ptr& ope) : weak_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + auto ope = weak_.lock(); + assert(ope); + const auto& rule = *ope; + return rule.parse(s, n, sv, c, dt); + } + + void accept(Visitor& v) override; + + std::weak_ptr weak_; +}; + +class Holder : public Ope +{ +public: + Holder(Definition* outer) + : outer_(outer) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override; + + void accept(Visitor& v) override; + + any reduce(SemanticValues& sv, any& dt) const; + + std::shared_ptr ope_; + Definition* outer_; + + friend class Definition; +}; + +typedef std::unordered_map Grammar; + +class Reference : public Ope + , public std::enable_shared_from_this +{ +public: + Reference( + const Grammar& grammar, + const std::string& name, + const char* s, + bool is_macro, + const std::vector>& args) + : grammar_(grammar) + , name_(name) + , s_(s) + , is_macro_(is_macro) + , args_(args) + , rule_(nullptr) + , iarg_(0) + {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override; + + void accept(Visitor& v) override; + + std::shared_ptr get_core_operator() const; + + const Grammar& grammar_; + const std::string name_; + const char* s_; + + const bool is_macro_; + const std::vector> args_; + + Definition* rule_; + size_t iarg_; +}; + +class Whitespace : public Ope +{ +public: + Whitespace(const std::shared_ptr& ope) : ope_(ope) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override { + if (c.in_whitespace) { + return 0; + } + c.in_whitespace = true; + auto se = make_scope_exit([&]() { c.in_whitespace = false; }); + const auto& rule = *ope_; + return rule.parse(s, n, sv, c, dt); + } + + void accept(Visitor& v) override; + + std::shared_ptr ope_; +}; + +class BackReference : public Ope +{ +public: + BackReference(const std::string& name) : name_(name) {} + + size_t parse(const char* s, size_t n, SemanticValues& sv, Context& c, any& dt) const override; + + void accept(Visitor& v) override; + + std::string name_; +}; + +/* + * Factories + */ +template +std::shared_ptr seq(Args&& ...args) { + return std::make_shared(static_cast>(args)...); +} + +template +std::shared_ptr cho(Args&& ...args) { + return std::make_shared(static_cast>(args)...); +} + +inline std::shared_ptr zom(const std::shared_ptr& ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr oom(const std::shared_ptr& ope) { + return std::make_shared(ope); +} + +inline std::shared_ptr opt(const std::shared_ptr& ope) { + return std::make_shared