diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 00000000000..f7ae44eb544 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,44 @@ +on: + push: + branches: + - 'master' + +concurrency: + group: "release" + cancel-in-progress: true + +jobs: + build_release: + runs-on: node-debian + container: + image: maven:3-eclipse-temurin-11 + steps: + - name: Install prerequisites + run: | + apt-get update + apt-get -y install git nodejs + + - uses: actions/checkout@v3 + + - name: Build Mage + run: | + mvn -T 12 clean install -DskipTests + + - name: Build Client + run: | + cd Mage.Client && mvn package assembly:single + + - name: Build Server + run: | + cd Mage.Server && mvn package assembly:single + + - uses: forgejo/upload-artifact@v4 + with: + name: client.zip + path: ./Mage.Client/target/mage-client.zip + + - uses: forgejo/upload-artifact@v4 + with: + name: server.zip + path: ./Mage.Server/target/mage-server.zip + diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 189b112f4b0..00000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -updates: - - package-ecosystem: 'github-actions' - directory: '/' - schedule: - interval: 'weekly' - - package-ecosystem: 'maven' - directory: '/' - schedule: - interval: 'weekly' diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 9c214fa0542..00000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,23 +0,0 @@ -dev: - - changed-files: - - any-glob-to-any-file: [ '*', 'Utils/**', '/.github/**' ] - -engine: - - changed-files: - - any-glob-to-any-file: [ 'Mage/**' ] - -client: - - changed-files: - - any-glob-to-any-file: [ 'Mage.Client/**', 'Mage.Common/**', 'Mage.Plugins/**' ] - -server: - - changed-files: - - any-glob-to-any-file: [ 'Mage.Server*/**' ] - -tests: - - changed-files: - - any-glob-to-any-file: [ 'Mage.Verify/**', 'Mage.Tests/**', 'Mage.Reports/**' ] - -cards: - - changed-files: - - any-glob-to-any-file: [ 'Mage.Sets/**' ] \ No newline at end of file diff --git a/.github/workflows/labeler-auto.yml b/.github/workflows/labeler-auto.yml deleted file mode 100644 index 2f88b117f98..00000000000 --- a/.github/workflows/labeler-auto.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: "Pull Request Labeler (auto)" -on: - - pull_request_target - -jobs: - labeler: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - id: label-the-PR - uses: actions/labeler@v5 - with: - configuration-path: '.github/labeler.yml' \ No newline at end of file diff --git a/.github/workflows/labeler-manual.yml b/.github/workflows/labeler-manual.yml deleted file mode 100644 index 846436ad507..00000000000 --- a/.github/workflows/labeler-manual.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: "Pull Request Labeler (manual)" -on: - workflow_dispatch: - inputs: - oldPRs: - # no multi lines support, so call by single PR only - description: 'PR number to process' - required: true - type: string - default: '123' - -jobs: - labeler: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - id: label-the-PR - uses: actions/labeler@v5 - with: - configuration-path: '.github/labeler.yml' - pr-number: | - ${{ github.event.inputs.oldPRs }} \ No newline at end of file diff --git a/.github/workflows/mtg-fetch-cards.yml b/.github/workflows/mtg-fetch-cards.yml deleted file mode 100644 index 71fa97a1976..00000000000 --- a/.github/workflows/mtg-fetch-cards.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Mtg Card Fetch Bot - -on: - issue_comment: - types: [created] - issues: - types: [opened] - pull_request_review: - types: [submitted] - pull_request_review_comment: - types: [created] - -jobs: - fetch-card-references: - name: Fetch MTG Card - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: ldeluigi/mtg-fetch-action@v1 diff --git a/.gitignore b/.gitignore index f1052671c3f..70260d0197e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ Utils/*implemented.txt # build tools mage-bundle.zip .env +.classpath diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..013572a57bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM eclipse-temurin:11 + +# Set XMage config defaults +ENV LANG=C.UTF-8 \ + XMAGE_DOCKER_SERVER_ADDRESS="0.0.0.0" \ + XMAGE_DOCKER_PORT="17171" \ + XMAGE_DOCKER_SEONDARY_BIND_PORT="17179" \ + XMAGE_DOCKER_MAX_SECONDS_IDLE="600" \ + XMAGE_DOCKER_AUTHENTICATION_ACTIVATED="false" \ + XMAGE_DOCKER_SERVER_NAME="mage-server" \ + XMAGE_DOCKER_MAILGUN_API_KEY="" \ + XMAGE_DOCKER_MAILGUN_DOMAIN="" \ + XMAGE_DOCKER_MAIL_SMTP_HOST="" \ + XMAGE_DOCKER_MAIL_SMTP_PORT="" \ + XMAGE_DOCKER_MAIL_USER="" \ + XMAGE_DOCKER_MAIL_PASSWORD="" \ + XMAGE_DOCKER_MAIL_FROM_ADDRESS="" \ + XMAGE_DOCKER_MAX_GAME_THREADS="10" \ + XMAGE_DOCKER_MAX_AI_OPPONENTS="15" \ + XMAGE_DOCKER_JAVA_OPTS="-Xmx1024m -XX:MaxPermSize=384m -Dlog4j.configuration=file:./config/log4j.properties" +# Install dependencies +RUN set -ex && \ + apt update && \ + apt install -y curl ca-certificates bash jq unzip + +# Download latest xmage +WORKDIR /xmage + +RUN < { - showWhatsNewDialog(false); - }); } /** @@ -998,6 +989,8 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { popupDownload = new javax.swing.JPopupMenu(); menuDownloadSymbols = new javax.swing.JMenuItem(); menuDownloadImages = new javax.swing.JMenuItem(); + menuDownloadTags = new javax.swing.JMenuItem(); + desktopPane = new MageJDesktop(); mageToolbar = new javax.swing.JToolBar(); btnPreferences = new javax.swing.JButton(); @@ -1058,6 +1051,14 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { } }); popupDownload.add(menuDownloadImages); + + menuDownloadTags.setText("Download Scryfall Tagger tags (this will freeze for a bit)"); + menuDownloadTags.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + TagSource.instance.syncTagRepositiory(); + } + }); + popupDownload.add(menuDownloadTags); setDefaultCloseOperation(javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE); setMinimumSize(new java.awt.Dimension(1000, 500)); @@ -1199,7 +1200,9 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { pack(); }// //GEN-END:initComponents - private void btnDeckEditorActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnDeckEditorActionPerformed + + + private void btnDeckEditorActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnDeckEditorActionPerformed showDeckEditor(DeckEditorMode.FREE_BUILDING, null, null, null, 0); }//GEN-LAST:event_btnDeckEditorActionPerformed @@ -1648,6 +1651,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { private javax.swing.JMenuItem menuDebugTestModalDialog; private javax.swing.JMenuItem menuDownloadImages; private javax.swing.JMenuItem menuDownloadSymbols; + private javax.swing.JMenuItem menuDownloadTags; private javax.swing.JPopupMenu popupDebug; private javax.swing.JPopupMenu popupDownload; private javax.swing.JToolBar.Separator separatorDebug; @@ -1960,6 +1964,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { menuDownloadSymbols.setFont(font); menuDownloadImages.setFont(font); + menuDownloadTags.setFont(font); menuDebugTestModalDialog.setFont(font); menuDebugTestCardRenderModesDialog.setFont(font); menuDebugTestCustomCode.setFont(font); diff --git a/Mage.Client/src/main/java/mage/client/components/tray/MageTray.java b/Mage.Client/src/main/java/mage/client/components/tray/MageTray.java index 59ad366a3f5..7772b693f5c 100644 --- a/Mage.Client/src/main/java/mage/client/components/tray/MageTray.java +++ b/Mage.Client/src/main/java/mage/client/components/tray/MageTray.java @@ -19,7 +19,7 @@ public enum MageTray { private Image flashedImage; private TrayIcon trayIcon; - private int state = 0; + private int state = 3; public void install() { if (!SystemTray.isSupported()) { diff --git a/Mage.Client/src/main/java/mage/client/constants/Constants.java b/Mage.Client/src/main/java/mage/client/constants/Constants.java index e40bdc36a8b..b392f675214 100644 --- a/Mage.Client/src/main/java/mage/client/constants/Constants.java +++ b/Mage.Client/src/main/java/mage/client/constants/Constants.java @@ -2,9 +2,15 @@ package mage.client.constants; import javax.swing.*; import javax.swing.border.Border; + +import com.google.common.collect.ImmutableList; + +import javafx.util.Pair; + import java.awt.*; import java.io.File; + /** * @author BetaSteward_at_googlemail.com */ @@ -14,6 +20,72 @@ public final class Constants { throw new AssertionError(); } + public static final ImmutableList> foulMagicsSets = ImmutableList.of( + new Pair("Set 3 - Old Fat Men", new String[] { + "Ninth Edition", + "Tenth Edition", + "* Time Spiral Block", + "* Lorwyn Block", + "* Shards of Alara Block", + "Scars of Mirrodin", + "Zendikar", + "Rise of the Eldrazi", + "Worldwake", + "* Innistrad Block", + "Foul Magic Block 3 Extras" + }), + new Pair("Set 2.5 - Ravnica Cultural Exchange", new String[] { + "* March of the Machine Block", + "* Phyrexia: All Will Be One Block", + "* The Brothers' War Block", + "* Dominaria United Block", + "* Kamigawa: Neon Dynasty Block", + "* Theros Beyond Death Block", + "* Strixhaven: School of Mages Block", + "* The Lost Caverns of Ixalan Block", + "* Ikoria: Lair of Behemoths Block", + "* Adventures in the Forgotten Realms Block", + "Modern Horizons 3", + "The Lord of the Rings: Tales of Middle-earth", + "Double Masters 2022", + "Rise of the Eldrazi", + "Modern Horizons 2", + "Foundations", + "Foundations Jumpstart", + "* Guilds of Ravnica Block", + "* Return to Ravnica Block", + }), + new Pair("Set 2 - Phyrexians, Eldrazi, Asians - Oh my!", new String[] { + "* March of the Machine Block", + "* Phyrexia: All Will Be One Block", + "* The Brothers' War Block", + "* Dominaria United Block", + "* Kamigawa: Neon Dynasty Block", + "* Theros Beyond Death Block", + "* Strixhaven: School of Mages Block", + "* The Lost Caverns of Ixalan Block", + "* Ikoria: Lair of Behemoths Block", + "* Adventures in the Forgotten Realms Block", + "Modern Horizons 3", + "The Lord of the Rings: Tales of Middle-earth", + "Double Masters 2022", + "Rise of the Eldrazi", + "Modern Horizons 2" + }), + new Pair("Set 1 - In Da Beegeening", new String[] { + "Foundations", + "Foundations Jumpstart", + "* Guilds of Ravnica Block", + "* Return to Ravnica Block", + }), + new Pair("Set 0.5 - Foundationally Gaming", new String[] { + "Foundations", + "Foundations Jumpstart", + }) + ); + + + public static final int FRAME_MAX_HEIGHT = 367; public static final int FRAME_MAX_WIDTH = 256; public static final int ART_MAX_HEIGHT = 168; @@ -147,5 +219,6 @@ public final class Constants { } } + } diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.form b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.form index 94ff570fdef..ec58db0c03b 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.form +++ b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.form @@ -222,6 +222,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java index 88e006f6f84..c0f1dff1ce8 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/CardSelector.java @@ -9,9 +9,11 @@ import mage.cards.decks.PennyDreadfulLegalityUtil; import mage.cards.repository.*; import mage.client.MageFrame; import mage.client.cards.*; +import mage.client.constants.Constants; import mage.client.constants.Constants.SortBy; import mage.client.dialog.PreferencesDialog; import mage.client.deckeditor.table.TableModel; +import mage.client.deckeditor.table.TaggerModel; import mage.client.dialog.CheckBoxList; import mage.client.util.GUISizeHelper; import mage.client.util.gui.FastSearchUtil; @@ -31,13 +33,20 @@ import mage.view.CardsView; import org.apache.log4j.Logger; import org.mage.card.arcane.ManaSymbolsCellRenderer; +import javafx.util.Pair; + import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableColumnModel; + import java.awt.*; import java.awt.event.*; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; import java.util.*; import static mage.client.dialog.PreferencesDialog.*; @@ -93,6 +102,10 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene cardSelectorScrollPane.setOpaque(false); cardSelectorScrollPane.getViewport().setOpaque(false); + + taggerScrollPane.setOpaque(false); + taggerScrollPane.getViewport().setOpaque(false); + cbSortBy.setModel(new DefaultComboBoxModel<>(SortBy.values())); cbSortBy.setSelectedItem(sortSetting.getSortBy()); jTextFieldSearch.addActionListener(searchAction); @@ -102,19 +115,36 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene tbColor.setOpaque(true); // false = transparent tbTypes.setBackground(PreferencesDialog.getCurrentTheme().getDeckEditorToolbarBackgroundColor()); tbTypes.setOpaque(true); // false = transparent + taggerScrollPane.setBackground(PreferencesDialog.getCurrentTheme().getDeckEditorToolbarBackgroundColor()); + taggerScrollPane.setOpaque(true); cardSelectorBottomPanel.setBackground(PreferencesDialog.getCurrentTheme().getDeckEditorToolbarBackgroundColor()); cardSelectorBottomPanel.setOpaque(true); // false = transparent } private void initListViewComponents() { + taggerTable = new JTable(); mainTable = new JTable(); - + mainModel = new TableModel(); + tagsModel = new TaggerModel(() -> filterCards()); + + tagsModel.addListeners(taggerTable); + taggerTable.setModel(tagsModel); + TableColumnModel taggerTableModel = taggerTable.getColumnModel(); + taggerTableModel.getColumn(0).setMaxWidth(30); + taggerTableModel.getColumn(0).setPreferredWidth(30); + taggerTableModel.getColumn(1).setMaxWidth(30); + taggerTableModel.getColumn(1).setPreferredWidth(30); + taggerTableModel.getColumn(2).setMaxWidth(160); + taggerTableModel.getColumn(2).setPreferredWidth(160); + taggerTableModel.getColumn(4).setMaxWidth(60); + DefaultTableCellRenderer myRenderer = (DefaultTableCellRenderer) mainTable.getDefaultRenderer(String.class); + + mainModel.addListeners(mainTable); mainTable.setModel(mainModel); mainTable.setForeground(Color.white); - DefaultTableCellRenderer myRenderer = (DefaultTableCellRenderer) mainTable.getDefaultRenderer(String.class); myRenderer.setBackground(new Color(0, 0, 0, 100)); mainTable.getColumnModel().getColumn(0).setMaxWidth(0); mainTable.getColumnModel().getColumn(0).setPreferredWidth(10); @@ -132,6 +162,11 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene // mainTable.setToolTipText(cardSelectorScrollPane.getToolTipText()); cardSelectorScrollPane.setViewportView(mainTable); + + taggerScrollPane.setViewportView(taggerTable); + + taggerTable.setOpaque(false); + mainTable.setOpaque(false); cbSortBy.setEnabled(false); chkPiles.setEnabled(false); @@ -184,6 +219,9 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene mainTable.setFont(GUISizeHelper.tableFont); mainTable.setRowHeight(GUISizeHelper.tableRowHeight); + taggerTable.getTableHeader().setFont(GUISizeHelper.tableFont); + taggerTable.setFont(GUISizeHelper.tableFont); + taggerTable.setRowHeight(GUISizeHelper.tableRowHeight); } public void switchToGrid() { @@ -237,26 +275,44 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene if (limited) { List> predicates = new ArrayList<>(); + List> exclusion = new ArrayList<>(); if (this.tbGreen.isSelected()) { predicates.add(new ColorPredicate(ObjectColor.GREEN)); + } else { + exclusion.add(new ColorPredicate(ObjectColor.GREEN)); } if (this.tbRed.isSelected()) { predicates.add(new ColorPredicate(ObjectColor.RED)); + } else { + exclusion.add(new ColorPredicate(ObjectColor.RED)); } if (this.tbBlack.isSelected()) { predicates.add(new ColorPredicate(ObjectColor.BLACK)); + } else { + exclusion.add(new ColorPredicate(ObjectColor.BLACK)); } if (this.tbBlue.isSelected()) { predicates.add(new ColorPredicate(ObjectColor.BLUE)); + } else { + exclusion.add(new ColorPredicate(ObjectColor.BLUE)); } if (this.tbWhite.isSelected()) { predicates.add(new ColorPredicate(ObjectColor.WHITE)); + } else { + exclusion.add(new ColorPredicate(ObjectColor.WHITE)); } + if (this.tbColorless.isSelected()) { predicates.add(ColorlessPredicate.instance); + } else { + exclusion.add(ColorlessPredicate.instance); + } + if (this.tbLimitColors.isSelected()) { + filter.add(Predicates.and(Predicates.not(Predicates.or(exclusion)), Predicates.or(predicates))); + } else { + filter.add(Predicates.or(predicates)); } - filter.add(Predicates.or(predicates)); predicates.clear(); if (this.tbLand.isSelected()) { @@ -346,6 +402,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene criteria.red(this.tbRed.isSelected()); criteria.white(this.tbWhite.isSelected()); criteria.colorless(this.tbColorless.isSelected()); + criteria.limitColors(this.tbLimitColors.isSelected()); // if you add new type filter then sync it with CardType if (this.tbLand.isSelected()) { @@ -463,7 +520,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene if (limited) { for (Card card : cards) { - if (filter.match(card, null)) { + if (filter.match(card, null) && tagsModel.testCard(card)) { filteredCards.add(card); } } @@ -482,6 +539,10 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene if (!filter.match(card, null)) { continue; } + + if (!tagsModel.testCard(card)) { + continue; + } // found filteredCards.add(card); } @@ -559,9 +620,12 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene tbBlack = new javax.swing.JToggleButton(); tbWhite = new javax.swing.JToggleButton(); tbColorless = new javax.swing.JToggleButton(); + tbLimitColors = new javax.swing.JToggleButton(); jSeparator1 = new javax.swing.JToolBar.Separator(); cbExpansionSet = new javax.swing.JComboBox<>(); btnExpansionSearch = new javax.swing.JButton(); + cbFoulMagicPresets = new javax.swing.JComboBox<>(); + btnFoulMagicPreset = new javax.swing.JButton(); jSeparator2 = new javax.swing.JToolBar.Separator(); chkPennyDreadful = new javax.swing.JCheckBox(); btnBooster = new javax.swing.JButton(); @@ -594,6 +658,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene jButtonRemoveFromMain = new javax.swing.JButton(); jButtonAddToSideboard = new javax.swing.JButton(); jButtonRemoveFromSideboard = new javax.swing.JButton(); + jButtonRandomCard = new javax.swing.JButton(); jTextFieldSearch = new javax.swing.JTextField(); chkNames = new javax.swing.JCheckBox(); chkTypes = new javax.swing.JCheckBox(); @@ -603,7 +668,72 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene jButtonClean = new javax.swing.JButton(); cardCountLabel = new javax.swing.JLabel(); cardCount = new javax.swing.JLabel(); + + // Brings me back to the Tk days + + taggerScrollPane = new javax.swing.JScrollPane(); + taggerContainer = new javax.swing.JPanel(); + taggerContainer.setPreferredSize(new Dimension(200, 400)); + taggerContainer.setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.weighty = 1; + gbc.weightx = 1; + gbc.gridx = 0; + gbc.gridy = 0; + + taggerControlBar = new javax.swing.JPanel(); + taggerControlBar.setLayout(new GridBagLayout()); + + taggerReset = new javax.swing.JButton("Reset"); + taggerReset.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + tagsModel.clear(); + } + }); + taggerContainer.add(taggerScrollPane, gbc); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weighty = 0; + gbc.weightx = 1; + gbc.gridx = 0; + gbc.gridy = 1; + taggerContainer.add(taggerControlBar, gbc); + + gbc.fill = GridBagConstraints.NONE; + gbc.weighty = 0; + gbc.weightx = 0; + gbc.gridx = 0; + gbc.gridy = 0; + + taggerControlBar.add(taggerReset, gbc); + + taggerSearch = new javax.swing.JTextField(); + taggerSearch.getDocument().addDocumentListener(new DocumentListener() { + public void removeUpdate(DocumentEvent e) { + changedUpdate(e); + } + public void insertUpdate(DocumentEvent e) { + changedUpdate(e); + } + public void changedUpdate(DocumentEvent e) { + tagsModel.search(taggerSearch.getText()); + } + }); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weighty = 1; + gbc.weightx = 1; + gbc.gridx = 1; + gbc.gridy = 0; + taggerControlBar.add(taggerSearch, gbc); + + // End hell + + tablePanel = new javax.swing.JSplitPane(JSplitPane.HORIZONTAL_SPLIT, cardSelectorScrollPane, taggerContainer); + tablePanel.setOneTouchExpandable(true); + tablePanel.setResizeWeight(1.0); + tablePanel.setDividerLocation(1.0); + tbColor.setFloatable(false); tbColor.setRollover(true); tbColor.setToolTipText("Hold the ALT-key while clicking to deselect all other colors or hold the CTRL-key to select only all other colors."); @@ -701,6 +831,23 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene } }); tbColor.add(tbColorless); + + tbLimitColors.setIcon(new javax.swing.ImageIcon(getClass().getResource("/buttons/lock.png"))); // NOI18N + tbLimitColors.setSelected(false); + tbLimitColors.setToolTipText("Limit results to ONLY these colors"); + tbLimitColors.setActionCommand("LimitColors"); + tbLimitColors.setFocusable(false); + tbLimitColors.setPreferredSize(new java.awt.Dimension(28, 28)); + tbLimitColors.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + tbLimitColors.setSelectedIcon(new javax.swing.ImageIcon(getClass().getResource("/buttons/lock.png"))); // NOI18N + tbLimitColors.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + tbLimitColors.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + tbLimitColorsActionPerformed(evt); + } + }); + tbColor.add(tbLimitColors); + tbColor.add(jSeparator1); reloadSetsCombobox(); @@ -741,6 +888,31 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene } }); tbColor.add(btnExpansionSearch); + + List setNames = new LinkedList(); + for (Pair pair : Constants.foulMagicsSets) { + setNames.add(pair.getKey()); + } + + DefaultComboBoxModel presetModel = new DefaultComboBoxModel<>(setNames.toArray()); + cbFoulMagicPresets.setModel(presetModel); + + tbColor.add(cbFoulMagicPresets); + + btnFoulMagicPreset.setIcon(new javax.swing.ImageIcon(getClass().getResource("/buttons/brick.png"))); // NOI18N + btnFoulMagicPreset.setToolTipText("Set to Foul Magic preset"); + btnFoulMagicPreset.setAlignmentX(1.0F); + btnFoulMagicPreset.setFocusable(false); + btnFoulMagicPreset.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + btnFoulMagicPreset.setPreferredSize(new java.awt.Dimension(24, 24)); + btnFoulMagicPreset.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + btnFoulMagicPreset.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + btnFoulMagicPresetSet(evt); + } + }); + tbColor.add(btnFoulMagicPreset); + tbColor.add(jSeparator2); chkPennyDreadful.setText("Penny Dreadful Only"); @@ -1036,6 +1208,8 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene cardSelectorBottomPanel.setOpaque(false); cardSelectorBottomPanel.setPreferredSize(new java.awt.Dimension(897, 40)); + tablePanel.setOpaque(false); + tablePanel.setPreferredSize(new java.awt.Dimension(600, 40)); jButtonAddToMain.setIcon(new javax.swing.ImageIcon(getClass().getResource("/buttons/deck_in.png"))); // NOI18N jButtonAddToMain.setToolTipText("Add selected cards to deck.
\nAlternative: Double click the card in card selector to move a card to the deck."); @@ -1084,6 +1258,18 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene jButtonRemoveFromSideboardActionPerformed(evt); } }); + + jButtonRandomCard.setIcon(new javax.swing.ImageIcon(getClass().getResource("/buttons/dice.png"))); // NOI18N + jButtonRandomCard.setToolTipText("Add a random card from the current search to your deck."); + jButtonRandomCard.setMargin(null); + jButtonRandomCard.setMaximumSize(new java.awt.Dimension(35, 23)); + jButtonRandomCard.setMinimumSize(new java.awt.Dimension(35, 23)); + jButtonRandomCard.setPreferredSize(new java.awt.Dimension(30, 28)); + jButtonRandomCard.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jButtonRandomCardActionPerformed(evt); + } + }); jTextFieldSearch.setToolTipText("Search cards by any data like name or mana symbols like {W}, {U}, {C}, etc (use quotes for exact search)"); @@ -1167,8 +1353,9 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene cardCountLabel.setToolTipText("Number of cards currently shown."); cardCount.setText("0"); - + javax.swing.GroupLayout cardSelectorBottomPanelLayout = new javax.swing.GroupLayout(cardSelectorBottomPanel); + cardSelectorBottomPanel.setLayout(cardSelectorBottomPanelLayout); cardSelectorBottomPanelLayout.setHorizontalGroup( cardSelectorBottomPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -1181,6 +1368,8 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene .addComponent(jButtonAddToSideboard, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(2, 2, 2) .addComponent(jButtonRemoveFromSideboard, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(2,2,2) + .addComponent(jButtonRandomCard, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jTextFieldSearch, javax.swing.GroupLayout.PREFERRED_SIZE, 219, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) @@ -1215,6 +1404,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene .addComponent(jButtonRemoveFromMain, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jButtonAddToSideboard, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jButtonRemoveFromSideboard, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jButtonRandomCard, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(cardSelectorBottomPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jTextFieldSearch, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jButtonSearch) @@ -1235,9 +1425,10 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(tbColor, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(tbTypes, javax.swing.GroupLayout.DEFAULT_SIZE, 1057, Short.MAX_VALUE) - .addComponent(cardSelectorScrollPane) + .addComponent(tablePanel) .addComponent(cardSelectorBottomPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 1057, Short.MAX_VALUE) ); + layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() @@ -1245,10 +1436,13 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene .addGap(0, 0, 0) .addComponent(tbTypes, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(0, 0, 0) - .addComponent(cardSelectorScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 237, Short.MAX_VALUE) + .addComponent(tablePanel, javax.swing.GroupLayout.DEFAULT_SIZE, 237, Short.MAX_VALUE) .addGap(0, 0, 0) .addComponent(cardSelectorBottomPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 31, javax.swing.GroupLayout.PREFERRED_SIZE)) ); + + + }// //GEN-END:initComponents private void cbExpansionSetActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbExpansionSetActionPerformed @@ -1364,6 +1558,14 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene } } }//GEN-LAST:event_jButtonAddToMainActionPerformed + + private void jButtonRandomCardActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonAddToMainActionPerformed + int n = mainTable.getRowCount(); + mainModel.doubleClick(ThreadLocalRandom.current().nextInt(n), null, false); + if (limited) { + mainModel.fireTableDataChanged(); + } + } private void jButtonAddToSideboardActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonAddToSideboardActionPerformed if (mainTable.getSelectedRowCount() > 0) { @@ -1421,7 +1623,11 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene private void tbColorlessActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tbColorlessActionPerformed filterCardsColor(evt.getModifiers(), evt.getActionCommand()); }//GEN-LAST:event_tbColorlessActionPerformed - + + private void tbLimitColorsActionPerformed (java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tbColorlessActionPerformed + filterCards(); + } + private void tbCreaturesActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tbCreaturesActionPerformed filterCardsType(evt.getModifiers(), evt.getActionCommand()); }//GEN-LAST:event_tbCreaturesActionPerformed @@ -1465,6 +1671,33 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene private void chkUniqueActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkRulesActionPerformed // TODO add your handling code here: }//GEN-LAST:event_chkRulesActionPerformed + + private void btnFoulMagicPresetSet(java.awt.event.ActionEvent evt) { + reloadSetsCombobox(); + if (cbExpansionSet.getItemAt(0).startsWith(MULTI_SETS_SELECTION_TEXT)) { + cbExpansionSet.removeItemAt(0); + } + + listCodeSelected.uncheckAll(); + String[] selectedFormats = Constants.foulMagicsSets.get(this.cbFoulMagicPresets.getSelectedIndex()).getValue(); + if (selectedFormats.length == 1) { + this.cbExpansionSet.setSelectedItem(selectedFormats[0]); + filterCards(); + return; + } + + List formats = ConstructedFormats.getTypes(false); + for (int i = 0; i < formats.size(); i++) { + if (Arrays.stream(selectedFormats).anyMatch(formats.get(i)::equals)) { + listCodeSelected.setChecked(i - 1, true); + } + } + + String message = String.format("%s: %s", MULTI_SETS_SELECTION_TEXT, "[Foul Magics]"); + cbExpansionSet.insertItemAt(message, 0); + cbExpansionSet.setSelectedIndex(0); + filterCards(); + } private void btnExpansionSearchActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnExpansionSearchActionPerformed // search and check multiple items @@ -1505,6 +1738,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene isSetsFilterLoading = false; } + // update data filterCards(); }); @@ -1565,7 +1799,9 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene } private TableModel mainModel; + private TaggerModel tagsModel; private JTable mainTable; + private JTable taggerTable; private ICardGrid currentView; private final CheckBoxList listCodeSelected; @@ -1575,12 +1811,14 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene private javax.swing.JButton btnBooster; private javax.swing.JButton btnClear; private javax.swing.JButton btnExpansionSearch; + private javax.swing.JButton btnFoulMagicPreset; private javax.swing.JLabel cardCount; private javax.swing.JLabel cardCountLabel; private javax.swing.JPanel cardSelectorBottomPanel; private javax.swing.JScrollPane cardSelectorScrollPane; private javax.swing.JComboBox cbExpansionSet; private javax.swing.JComboBox cbSortBy; + private javax.swing.JComboBox cbFoulMagicPresets; private javax.swing.JCheckBox chkNames; private javax.swing.JCheckBox chkPennyDreadful; private javax.swing.JCheckBox chkPiles; @@ -1592,6 +1830,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene private javax.swing.JButton jButtonClean; private javax.swing.JButton jButtonRemoveFromMain; private javax.swing.JButton jButtonRemoveFromSideboard; + private javax.swing.JButton jButtonRandomCard; private javax.swing.JButton jButtonSearch; private javax.swing.JToolBar.Separator jSeparator1; private javax.swing.JToolBar.Separator jSeparator2; @@ -1607,6 +1846,7 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene private javax.swing.JToggleButton tbBlue; private javax.swing.JToolBar tbColor; private javax.swing.JToggleButton tbColorless; + private javax.swing.JToggleButton tbLimitColors; private javax.swing.JToggleButton tbCommon; private javax.swing.JToggleButton tbCreatures; private javax.swing.JToggleButton tbEnchantments; @@ -1623,6 +1863,17 @@ public class CardSelector extends javax.swing.JPanel implements ComponentListene private javax.swing.JToolBar tbTypes; private javax.swing.JToggleButton tbUncommon; private javax.swing.JToggleButton tbWhite; + + private javax.swing.JScrollPane taggerScrollPane; + private javax.swing.JPanel taggerControlBar; + private javax.swing.JTextField taggerSearch; + private javax.swing.JButton taggerReset; + + + + private javax.swing.JPanel taggerContainer; + private javax.swing.JSplitPane tablePanel; + // End of variables declaration//GEN-END:variables private final mage.client.cards.CardGrid cardGrid; // grid for piles view mode (example: selected cards in drafting) diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/table/TaggerModel.java b/Mage.Client/src/main/java/mage/client/deckeditor/table/TaggerModel.java new file mode 100644 index 00000000000..943b5f68e17 --- /dev/null +++ b/Mage.Client/src/main/java/mage/client/deckeditor/table/TaggerModel.java @@ -0,0 +1,253 @@ +package mage.client.deckeditor.table; + +import mage.client.cards.BigCard; +import mage.cards.Card; +import mage.cards.repository.CardInfo; +import mage.cards.repository.Tag; +import mage.cards.repository.TagRepository; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.log4j.Logger; +import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumnModel; +import java.awt.event.*; +import java.util.List; +import java.util.*; + + +public class TaggerModel extends AbstractTableModel { + + private static final long serialVersionUID = -528008802935423048L; + + private static final Logger log = Logger.getLogger(TableModel.class); + + protected BigCard bigCard; + protected UUID gameId; + private List view = new ArrayList(); + public Map filtered = new HashMap(); + public Set whitelist = new HashSet(); + public Set blacklist = new HashSet(); + + private final String[] column = {"Inc", "Exc", "Name", "Description", "# Cards"}; + + private boolean descending = false; + private int columnSortedBy = 0; + private int recentSortedColumn; + private boolean recentAscending; + private Runnable doSort; + private int whitelistCount = 0; + + + public TaggerModel(Runnable sortFn) { + doSort = sortFn; + view = TagRepository.instance.getAllTags(); + if (view.size() == 0) { + Tag placeholder = new Tag(); + placeholder.id = ""; + placeholder.description = "Please download Scryfall tags from the 'download' tab & reopen"; + placeholder.label = "Data missing!"; + + Tag placeholder2 = new Tag(); + placeholder2.id = ""; + placeholder2.description = "You may need do a Scryfall download to associate IDs"; + placeholder2.label = "Also!"; + view.add(placeholder2); + } + } + + public void clear() { + view = TagRepository.instance.getAllTags(); + filtered.clear(); + this.whitelist.clear(); + this.blacklist.clear(); + fireTableDataChanged(); + doSort.run(); + } + + public void search(String query) { + view = TagRepository.instance.searchTags(query); + fireTableDataChanged(); + } + + + @Override + public int getRowCount() { + return view.size(); + } + + @Override + public int getColumnCount() { + return column.length; + } + + @Override + public String getColumnName(int n) { + return column[n]; + } + + @Override + public Object getValueAt(int row, int column) { + Tag tag = view.get(row); + switch (column) { + case 0: + return filtered.getOrDefault(tag.id, false) == true ? "X" : ""; + case 1: + return filtered.getOrDefault(tag.id, true) == false ? "X" : ""; + case 2: + return view.get(row).label; + case 3: + return view.get(row).description; + case 4: + return Long.toString(TagRepository.instance.getTagCardCount(view.get(row))); + } + return ""; + } + + + public void doubleClick(int index) { + Tag tag = view.get(index); + if (!filtered.containsKey(tag.id)) { + filtered.put(tag.id, true); + } else if (filtered.get(tag.id)) { + filtered.put(tag.id, false); + } else { + filtered.remove(tag.id); + } + fireTableCellUpdated(index, 0); + fireTableCellUpdated(index, 1); + buildBlacklist(); + buildWhitelist(); + doSort.run(); + } + + public void addListeners(final JTable table) { + // sorts + MouseListener mouse = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) { + return; + } + + TableColumnModel columnModel = table.getColumnModel(); + + int viewColumn = columnModel.getColumnIndexAtX(e.getX()); + int column = table.convertColumnIndexToModel(viewColumn); + + if (column != -1) { + descending = !descending; + columnSortedBy = column; + sort(); + fireTableDataChanged(); + } + + } + }; + table.getTableHeader().addMouseListener(mouse); + + // updates card detail, listens to any mouse clicks + table.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e)) { + return; + } + if (e.getClickCount() % 2 == 0) { + doubleClick(table.getSelectedRow()); + } + } + }); + } + + public boolean sort() { + switch (columnSortedBy) { + // I do not care. + case 0: + view.sort((t1, t2) -> (filtered.getOrDefault(t1.id, false) == true ? "X" : "").compareTo((filtered.getOrDefault(t2.id, false) == true ? "X" : ""))); + break; + case 1: + view.sort((t1, t2) -> (filtered.getOrDefault(t1.id, true) == false ? "X" : "").compareTo((filtered.getOrDefault(t2.id, true) == false ? "X" : ""))); + break; + case 2: + view.sort((t1, t2) -> ObjectUtils.compare(t1.label, t2.label)); + break; + case 3: + view.sort((t1, t2) -> ObjectUtils.compare(t1.description, t2.description)); + break; + case 4: + view.sort((t1, t2) -> TagRepository.instance.getTagCardCount(t2) - TagRepository.instance.getTagCardCount(t1)); + break; + } + + if (descending) { + Collections.reverse(view); + } + + fireTableDataChanged(); + + return true; + } + + public Set buildWhitelist() { + whitelist.clear(); + whitelistCount = 0; + // whitelists are a bit weird, each one needs to reduce the next + Set temp = new HashSet(); + + boolean fresh = true; + + for (String key : filtered.keySet()) { + temp.clear(); + if (filtered.get(key)) { + whitelistCount++; + for (CardInfo card : TagRepository.instance.getCardsByTagId(key)) { + temp.add(card.getName()); + } + if (fresh) { + whitelist.addAll(temp); + } else { + whitelist.retainAll(temp); + } + fresh = false; + } + } + + return whitelist; + } + + + public Set buildBlacklist() { + blacklist.clear(); + for (String key : filtered.keySet()) { + if (!filtered.get(key)) { + for (CardInfo card : TagRepository.instance.getCardsByTagId(key)) { + blacklist.remove(card.getName()); + } + } + } + return blacklist; + } + + public boolean testCard(Card card) { + if (!hasAnySelected()) { + return true; + } + return !blacklist.contains(card.getName()) && (whitelist.size() == 0 || whitelist.contains(card.getName())); + } + + + public boolean hasAnySelected() { + return filtered.size() > 0; + } + + public int getRecentSortedColumn() { + return recentSortedColumn; + } + + public boolean isRecentAscending() { + return recentAscending; + } + + +} diff --git a/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java b/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java index 3d10a77241e..6ea0279ddf2 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/ConnectDialog.java @@ -329,6 +329,7 @@ public class ConnectDialog extends MageDialog { }); btnFindBeta.setText("BETA"); + btnFindBeta.setEnabled(false); btnFindBeta.setToolTipText("Connect to BETA server, AI disabled (use any username without registration)"); btnFindBeta.setAlignmentY(0.0F); btnFindBeta.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); diff --git a/Mage.Client/src/main/java/mage/client/dialog/DownloadImagesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/DownloadImagesDialog.java index d0ca1c614bc..c2b8c07fb38 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/DownloadImagesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/DownloadImagesDialog.java @@ -222,6 +222,7 @@ public class DownloadImagesDialog extends MageDialog { comboSets = new javax.swing.JComboBox<>(); fillerMode1 = new javax.swing.Box.Filler(new java.awt.Dimension(5, 0), new java.awt.Dimension(5, 0), new java.awt.Dimension(5, 32767)); buttonSearchSet = new javax.swing.JButton(); + panelRedownload = new javax.swing.JPanel(); checkboxRedownload = new javax.swing.JCheckBox(); filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 5), new java.awt.Dimension(0, 3), new java.awt.Dimension(32767, 5)); @@ -354,8 +355,9 @@ public class DownloadImagesDialog extends MageDialog { buttonSearchSetActionPerformed(evt); } }); + panelModeSelect.add(buttonSearchSet); - + panelModeInner.add(panelModeSelect); panelMode.add(panelModeInner); @@ -426,7 +428,7 @@ public class DownloadImagesDialog extends MageDialog { private void buttonSearchSetActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_buttonSearchSetActionPerformed FastSearchUtil.showFastSearchForStringComboBox(comboSets, FastSearchUtil.DEFAULT_EXPANSION_SEARCH_MESSAGE, 400, 500); }//GEN-LAST:event_buttonSearchSetActionPerformed - + private void buttonStopActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_buttonStopActionPerformed // TODO implement stop feature for cancel button }//GEN-LAST:event_buttonStopActionPerformed diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form index 771001da5c3..f2e2c1410a1 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.form @@ -1332,6 +1332,11 @@
+ + + + + diff --git a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java index 13004ab13bf..b31d6c4c567 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/PreferencesDialog.java @@ -66,6 +66,9 @@ public class PreferencesDialog extends javax.swing.JDialog { public static final String KEY_GAME_USE_PROFANITY_FILTER = "gameUseProfanityFilter"; // size settings + public static final String KEY_GUI_VERTICAL_LAYOUT = "guiVerticalLayoyut"; + + public static final String KEY_GUI_CARD_BATTLEFIELD_SIZE = "guiCardBattlefieldSize"; public static final String KEY_GUI_CARD_HAND_SIZE = "guiCardHandSize"; public static final String KEY_GUI_CARD_EDITOR_SIZE = "guiCardEditorSize"; @@ -865,6 +868,7 @@ public class PreferencesDialog extends javax.swing.JDialog { buttonSizeDefault4 = new javax.swing.JButton(); buttonSizeDefault5 = new javax.swing.JButton(); buttonSizeDefault6 = new javax.swing.JButton(); + cbVerticalLayout = new javax.swing.JCheckBox(); panelSizeDetailedSettings = new javax.swing.JPanel(); labelSizeGroup1 = new javax.swing.JLabel(); panelSize1 = new javax.swing.JPanel(); @@ -1735,6 +1739,9 @@ public class PreferencesDialog extends javax.swing.JDialog { buttonSizeDefault6.setText("set to default"); panelSizeDefaultSettings.add(buttonSizeDefault6); + + cbVerticalLayout.setText("Vertical Layout (Experimental)"); + panelSizeDetailedSettings.add(cbVerticalLayout); panelSizeDetailedSettings.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), "Detailed settings")); panelSizeDetailedSettings.setLayout(new java.awt.GridLayout(16, 1)); @@ -3023,6 +3030,8 @@ public class PreferencesDialog extends javax.swing.JDialog { prefs.putInt(paramName, paramValue); updateCache(paramName, Integer.toString(paramValue)); } + // Hopefully this works + save(prefs, dialog.cbVerticalLayout, KEY_GUI_VERTICAL_LAYOUT, "true", "false"); saveGUISize(false, false); @@ -3448,6 +3457,8 @@ public class PreferencesDialog extends javax.swing.JDialog { load(prefs, dialog.cbDraftLogAutoSave, KEY_DRAFT_LOG_AUTO_SAVE, "true"); load(prefs, dialog.cbLimitedDeckAutoSave, KEY_LIMITED_DECK_AUTO_SAVE, "true"); load(prefs, dialog.cbGameJsonLogAutoSave, KEY_JSON_GAME_LOG_AUTO_SAVE, "true", "false"); + + load(prefs, dialog.cbVerticalLayout, KEY_GUI_VERTICAL_LAYOUT, "true", "false"); String autoTargetParam; try { @@ -4101,6 +4112,7 @@ public class PreferencesDialog extends javax.swing.JDialog { private javax.swing.JCheckBox cbUseDefaultImageFolder; private javax.swing.JCheckBox cbUseRandomBattleImage; private javax.swing.JCheckBox cbUseSameSettingsForReplacementEffect; + private javax.swing.JCheckBox cbVerticalLayout; private javax.swing.JCheckBox checkBoxBeforeCOthers; private javax.swing.JCheckBox checkBoxBeforeCYou; private javax.swing.JCheckBox checkBoxDrawOthers; diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 5c778dac428..662038293cb 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -131,6 +131,8 @@ public final class GamePanel extends javax.swing.JPanel { private final Map splitters = new LinkedHashMap<>(); // settings key, splitter // do not save splitters in intermediate state, e.g. connection to new server with active game private boolean isSplittersFullyRestored = false; + + private boolean vertical = false; public static class MageSplitter { JSplitPane splitPane; @@ -236,6 +238,7 @@ public final class GamePanel extends javax.swing.JPanel { } public GamePanel() { + this.vertical = PreferencesDialog.getCachedValue(KEY_GUI_VERTICAL_LAYOUT, "false").equals("true"); initComponents = true; initComponents(); @@ -267,9 +270,16 @@ public final class GamePanel extends javax.swing.JPanel { pnlCommandsSkipAndStack.add(pnlShortCuts, BorderLayout.NORTH); pnlCommandsSkipAndStack.add(stackObjects, BorderLayout.CENTER); // ... split: feedback + hand <|> skip + stack - splitHandAndStack.setLeftComponent(pnlCommandsFeedbackAndHand); - splitHandAndStack.setRightComponent(pnlCommandsSkipAndStack); - splitHandAndStack.setResizeWeight(DIVIDER_KEEP_RIGHT_COMPONENT); + if (vertical) { + splitHandAndStack.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); + splitHandAndStack.setBottomComponent(pnlCommandsFeedbackAndHand); + splitHandAndStack.setTopComponent(pnlCommandsSkipAndStack); + splitHandAndStack.setResizeWeight(DIVIDER_KEEP_RIGHT_COMPONENT); + } else { + splitHandAndStack.setLeftComponent(pnlCommandsFeedbackAndHand); + splitHandAndStack.setRightComponent(pnlCommandsSkipAndStack); + splitHandAndStack.setResizeWeight(DIVIDER_KEEP_RIGHT_COMPONENT); + } pnlCommandsFeedbackAndHand.setMinimumSize(new Dimension(0, 0)); // allow any sizes for hand pnlCommandsSkipAndStack.setMinimumSize(new Dimension(0, 0)); // allow any sizes for stack // ... all @@ -310,7 +320,9 @@ public final class GamePanel extends javax.swing.JPanel { final JLayeredPane jLayeredBackgroundPane = new JLayeredPane(); jLayeredBackgroundPane.setSize(1024, 768); this.add(jLayeredBackgroundPane); - jLayeredBackgroundPane.add(splitGameAndBigCard, JLayeredPane.DEFAULT_LAYER); + var basePane = this.vertical ? splitBattlefieldAndChats : splitGameAndBigCard; + jLayeredBackgroundPane.add(basePane, JLayeredPane.DEFAULT_LAYER); + Map myUi = getUIComponents(jLayeredBackgroundPane); Plugins.instance.updateGamePanel(myUi); @@ -322,7 +334,7 @@ public final class GamePanel extends javax.swing.JPanel { int width = ((JComponent) e.getSource()).getWidth(); int height = ((JComponent) e.getSource()).getHeight(); jLayeredBackgroundPane.setSize(width, height); - splitGameAndBigCard.setSize(width, height); + basePane.setSize(width, height); if (height < storedHeight) { // TODO: wtf, is it needs? Research and delete that code with storedHeight @@ -560,7 +572,11 @@ public final class GamePanel extends javax.swing.JPanel { float guiScale = GUISizeHelper.dialogGuiScale; int hGap = GUISizeHelper.guiSizeScale(SKIP_BUTTONS_SPACE_H, guiScale); int vGap = GUISizeHelper.guiSizeScale(SKIP_BUTTONS_SPACE_V, guiScale); - pnlShortCuts.setLayout(new FlowLayout(FlowLayout.RIGHT, hGap, vGap)); + if (vertical) { + pnlShortCuts.setLayout(new FlowLayout(FlowLayout.CENTER, 1, vGap)); + } else { + pnlShortCuts.setLayout(new FlowLayout(FlowLayout.RIGHT, hGap, vGap)); + } // skip buttons - sizes Dimension strictSize = new Dimension(2 * GUISizeHelper.gameCommandButtonHeight, GUISizeHelper.gameCommandButtonHeight); setSkipButtonSize(btnCancelSkip, guiScale, strictSize); @@ -2277,7 +2293,7 @@ public final class GamePanel extends javax.swing.JPanel { } @SuppressWarnings("unchecked") - private void initComponents() { + private boolean initComponents() { abilityPicker = new mage.client.components.ability.AbilityPicker(GUISizeHelper.dialogGuiScale); pnlHelperHandButtonsStackArea = new javax.swing.JPanel(); pnlShortCuts = new javax.swing.JPanel(); @@ -2361,7 +2377,11 @@ public final class GamePanel extends javax.swing.JPanel { // split: chat <|> game logs splitChatAndLogs = new javax.swing.JSplitPane(); - splitChatAndLogs.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); + if (vertical) { + splitChatAndLogs.setOrientation(javax.swing.JSplitPane.HORIZONTAL_SPLIT); + } else { + splitChatAndLogs.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); + } splitChatAndLogs.setResizeWeight(DIVIDER_KEEP_LEFT_COMPONENT); splitChatAndLogs.setTopComponent(userChatPanel); splitChatAndLogs.setBottomComponent(gameChatPanel); @@ -2371,8 +2391,15 @@ public final class GamePanel extends javax.swing.JPanel { splitBattlefieldAndChats.setBorder(null); splitBattlefieldAndChats.setResizeWeight(DIVIDER_KEEP_RIGHT_COMPONENT); splitBattlefieldAndChats.setOneTouchExpandable(true); - splitBattlefieldAndChats.setLeftComponent(pnlHelperHandButtonsStackArea); - splitBattlefieldAndChats.setRightComponent(splitChatAndLogs); + + if (vertical) { + splitBattlefieldAndChats.setOrientation(javax.swing.JSplitPane.VERTICAL_SPLIT); + splitBattlefieldAndChats.setBottomComponent(pnlHelperHandButtonsStackArea); + splitBattlefieldAndChats.setTopComponent(splitChatAndLogs); + } else { + splitBattlefieldAndChats.setLeftComponent(pnlHelperHandButtonsStackArea); + splitBattlefieldAndChats.setRightComponent(splitChatAndLogs); + } // warning, it's important to store/restore splitters in same order as real life GUI // from outer to inner (otherwise panels will be hidden or weird) @@ -2755,7 +2782,10 @@ public final class GamePanel extends javax.swing.JPanel { // split: game <|> chat/log splitGameAndBigCard.setLeftComponent(splitBattlefieldAndChats); - splitGameAndBigCard.setRightComponent(bigCardPanel); + if (!vertical) { + splitGameAndBigCard.setRightComponent(bigCardPanel); + } + return vertical; } private void removeListener() { @@ -2823,7 +2853,7 @@ public final class GamePanel extends javax.swing.JPanel { this.btnSkipForward.removeActionListener(al); } - final BasicSplitPaneUI myUi = (BasicSplitPaneUI) splitGameAndBigCard.getUI(); + final BasicSplitPaneUI myUi = vertical ? (BasicSplitPaneUI) splitBattlefieldAndChats.getUI() : (BasicSplitPaneUI) splitGameAndBigCard.getUI(); final BasicSplitPaneDivider divider = myUi.getDivider(); final JButton upArrowButton = (JButton) divider.getComponent(0); for (ActionListener al : upArrowButton.getActionListeners()) { diff --git a/Mage.Client/src/main/java/mage/client/table/TablesPanel.java b/Mage.Client/src/main/java/mage/client/table/TablesPanel.java index 9030054dc6a..eb1c9822903 100644 --- a/Mage.Client/src/main/java/mage/client/table/TablesPanel.java +++ b/Mage.Client/src/main/java/mage/client/table/TablesPanel.java @@ -893,7 +893,7 @@ public class TablesPanel extends javax.swing.JPanel { formatFilterList.add(RowFilter.regexFilter("^Oathbreaker", TablesTableModel.COLUMN_DECK_TYPE)); } if (btnFormatLimited.isSelected()) { - formatFilterList.add(RowFilter.regexFilter("^Limited", TablesTableModel.COLUMN_DECK_TYPE)); + formatFilterList.add(RowFilter.regexFilter("^(?:(?:Unl)|L)imited", TablesTableModel.COLUMN_DECK_TYPE)); } if (btnFormatOther.isSelected()) { formatFilterList.add(RowFilter.regexFilter("^Momir Basic|^Constructed - Pauper|^Constructed - Frontier|^Constructed - Extended|^Constructed - Eternal|^Constructed - Historical|^Constructed - Super|^Constructed - Freeform|^Constructed - Freeform Unlimited|^Australian Highlander|^European Highlander|^Canadian Highlander|^Constructed - Old|^Constructed - Historic", TablesTableModel.COLUMN_DECK_TYPE)); diff --git a/Mage.Client/src/main/java/mage/client/util/ClientEventType.java b/Mage.Client/src/main/java/mage/client/util/ClientEventType.java index c695d94a9b0..65f4303f7ee 100644 --- a/Mage.Client/src/main/java/mage/client/util/ClientEventType.java +++ b/Mage.Client/src/main/java/mage/client/util/ClientEventType.java @@ -18,5 +18,7 @@ public enum ClientEventType { DRAFT_PICK_CARD, DRAFT_MARK_CARD, // - PLAYER_TYPE_CHANGED + PLAYER_TYPE_CHANGED, + // + TAG_DOUBLE_CLICK } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java index daef9997e42..4540ca065f4 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GathererSets.java @@ -109,7 +109,7 @@ public class GathererSets implements Iterable { "NEC", "YNEO", "NEO", "SNC", "NCC", "CLB", "2X2", "DMU", "DMC", "40K", "GN3", "UNF", "BRO", "BRC", "BOT", "J22", "DMR", "ONE", "ONC", "SCH", "MOM", "MOC", "MUL", "MAT", "LTR", "CMM", "WOE", "WHO", "RVR", "WOT", - "WOC", "SPG", "LCI", "LCC", "REX", "PIP", "MKM", "MKC", "CLU", "OTJ", + "WOC", "SPG", "LCI", "LCC", "REX", "PIP", "YMKM", "MKM", "MKC", "CLU", "OTJ", "OTC", "OTP", "BIG", "MH3", "M3C", "ACR", "BLB", "BLC", "DSK", "DSC", "MB2", "FDN", "INR", "J25", "DRC", "DFT", "TDC", "TDM", "FCA", "FIC", "FIN", "SIS", "SIR", "SLD", "AKR", "MD1", "ANB", "LTC", "BRR", "HA1", @@ -182,6 +182,7 @@ public class GathererSets implements Iterable { codeReplacements.put("WTH", "WL"); codeReplacements.put("YMID", "Y22"); codeReplacements.put("YNEO", "Y22NEO"); + } public GathererSets() { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java index b07e82734eb..e33faeceaf2 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallApiCard.java @@ -32,7 +32,7 @@ public class ScryfallApiCard { transient public String imageLarge = ""; // potentially interesting fields, can be used in other places - //public UUID oracle_id; // TODO: implement card hint with oracle/cr ruling texts (see Rulings bulk data) + public String oracle_id; // TODO: implement card hint with oracle/cr ruling texts (see Rulings bulk data) //public Integer edhrec_rank; // TODO: use it to rating cards for AI and draft bots //public Object legalities; // TODO: add verify check for bans list //public Boolean full_art; // TODO: add verify check for full art usage in sets diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java index 8afcbdf4205..00ff1b9fd30 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java @@ -6,6 +6,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import mage.MageException; +import mage.cards.repository.CardRepository; import mage.client.remote.XmageURLConnection; import mage.client.util.CardLanguage; import mage.util.JsonUtil; @@ -477,7 +478,17 @@ public class ScryfallImageSource implements CardImageSource { // prepare data // memory optimization: fewer data, from 1145 MB to 470 MB - card.prepareCompatibleData(); + try { + card.prepareCompatibleData(); + } catch (Exception e) { + logger.warn("Failed to process card: ".concat(card.name)); + logger.warn("Skipping..."); + continue; + } + + // I LOVE POTENTIAL SQL INJECTION!!!!! + CardRepository.instance.execSQL("UPDATE card SET oracleId = '"+ card.oracle_id +"' WHERE setCode = '"+ card.set +"' AND cardNumber = '"+ card.collector_number +"'"); + // keep only usefully languages // memory optimization: fewer items, from 470 MB to 96 MB diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java index 01f26a871c2..483e4fe873a 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java @@ -2,10 +2,16 @@ package org.mage.plugins.card.dl.sources; import org.tritonus.share.ArraySet; +import mage.cards.CardSetInfo; +import mage.cards.CardWithHalves; +import mage.cards.ExpansionSet; +import mage.cards.Sets; + import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -570,6 +576,7 @@ public class ScryfallImageSupportCards { add("RVR"); // Ravnica Remastered add("PL24"); // Year of the Dragon 2024 add("PIP"); // Fallout + add("YMKM"); // Alchemy: Murders at Karlov Manor add("MKM"); // Murders at Karlov Manor add("MKC"); // Murders at Karlov Manor Commander add("PSS4"); // MKM Standard Showdown @@ -624,6 +631,9 @@ public class ScryfallImageSupportCards { // Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks add("CALC"); // Custom Alchemized versions of existing cards + + // Foul Magics sets + add("FMB3E"); } }; @@ -771,6 +781,32 @@ public class ScryfallImageSupportCards { put("ECL/Steam Vents/348b", "https://api.scryfall.com/cards/ecl/348/en?format=image&face=back"); put("ECL/Temple Garden/351b", "https://api.scryfall.com/cards/ecl/351/en?format=image&face=back"); + for (ExpansionSet set : Sets.getInstance().values()) { + for (Integer key : set.cardAliases.keySet()) { + var value = set.cardAliases.get(key); + + put( + String.format("%s/%s/%s", set.getCode(), value.cardInfo.getName(), value.cardInfo.getCardNumber()), + String.format("https://api.scryfall.com/cards/%s/%s/en?face=front&format=image", value.targetSet.toLowerCase(), value.targetSetNumber) + ); + if (CardWithHalves.class.isAssignableFrom(value.cardInfo.getCardClass())) { + try { + UUID uid = UUID.randomUUID(); + CardSetInfo info = new CardSetInfo(set.getName(), set.getCode(), value.targetSet, value.cardInfo.getRarity()); + CardWithHalves backCard = (CardWithHalves) value.cardInfo.getCardClass().getDeclaredConstructor(UUID.class, CardSetInfo.class).newInstance( + uid, info); + + put( + String.format("%s/%s/%s", set.getCode(), backCard.getRightHalfCard().getName(), value.cardInfo.getCardNumber()), + String.format("https://api.scryfall.com/cards/%s/%s/en?face=back", value.targetSet.toLowerCase(), value.targetSetNumber) + ); + } catch (Exception e) { + System.out.println(e); + } + } + } + } + } }; @@ -794,6 +830,7 @@ public class ScryfallImageSupportCards { if (directDownloadLinks.containsKey(linkCode2)) { return linkCode2; } + // default return null; diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/TagSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/TagSource.java new file mode 100644 index 00000000000..792a0dcb1ff --- /dev/null +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/TagSource.java @@ -0,0 +1,49 @@ +package org.mage.plugins.card.dl.sources; + + +import java.util.List; + +import org.apache.log4j.Logger; + +import com.google.gson.Gson; + +import mage.cards.repository.Tag; +import mage.cards.repository.TagRepository; +import mage.client.remote.XmageURLConnection; + +class RawTag { + public String object; + public String id; + public String label; + public String type; + public String description; + public List oracle_ids; +} + +class OracleResponse { + public String object; + public boolean has_more; + public List data; +} + +public enum TagSource { + instance; + private static final Logger LOGGER = Logger.getLogger(TagSource.class); + + public void syncTagRepositiory() { + String oracle = XmageURLConnection.downloadText("https://api.scryfall.com/private/tags/oracle"); + OracleResponse response = new Gson().fromJson(oracle, OracleResponse.class); + String tagCount = Integer.toString(response.data.size()); + int i = 0; + for (RawTag rawTag : response.data) { + LOGGER.debug("Syncing tag: " + rawTag.label + " (" + Integer.toString(i) + "/" + tagCount + ")"); + Tag tag = new Tag(); + tag.id = rawTag.id; + tag.description = rawTag.description; + tag.label = rawTag.label; + TagRepository.instance.syncTag(tag, rawTag.oracle_ids); + } + + } + +} diff --git a/Mage.Client/src/main/resources/buttons/brick.png b/Mage.Client/src/main/resources/buttons/brick.png new file mode 100644 index 00000000000..0b250c0b7b3 Binary files /dev/null and b/Mage.Client/src/main/resources/buttons/brick.png differ diff --git a/Mage.Client/src/main/resources/buttons/dice.png b/Mage.Client/src/main/resources/buttons/dice.png new file mode 100644 index 00000000000..a2ab43d45a8 Binary files /dev/null and b/Mage.Client/src/main/resources/buttons/dice.png differ diff --git a/Mage.Client/src/main/resources/buttons/lock.png b/Mage.Client/src/main/resources/buttons/lock.png new file mode 100644 index 00000000000..ab5fdd91bab Binary files /dev/null and b/Mage.Client/src/main/resources/buttons/lock.png differ diff --git a/Mage.Client/src/main/resources/buttons/search_128.png b/Mage.Client/src/main/resources/buttons/search_128.png index 574748a87a0..04779d90a17 100644 Binary files a/Mage.Client/src/main/resources/buttons/search_128.png and b/Mage.Client/src/main/resources/buttons/search_128.png differ diff --git a/Mage.Client/src/main/resources/buttons/search_24.png b/Mage.Client/src/main/resources/buttons/search_24.png index a3cd50658eb..cd59d5f3c46 100644 Binary files a/Mage.Client/src/main/resources/buttons/search_24.png and b/Mage.Client/src/main/resources/buttons/search_24.png differ diff --git a/Mage.Client/src/main/resources/buttons/search_32.png b/Mage.Client/src/main/resources/buttons/search_32.png index d78217ac92f..502f3692488 100644 Binary files a/Mage.Client/src/main/resources/buttons/search_32.png and b/Mage.Client/src/main/resources/buttons/search_32.png differ diff --git a/Mage.Client/src/main/resources/buttons/search_64.png b/Mage.Client/src/main/resources/buttons/search_64.png index d2b766cfe12..0fe192f7a63 100644 Binary files a/Mage.Client/src/main/resources/buttons/search_64.png and b/Mage.Client/src/main/resources/buttons/search_64.png differ diff --git a/Mage.Server.Plugins/Mage.Deck.Limited/src/mage/deck/Unlimited.java b/Mage.Server.Plugins/Mage.Deck.Limited/src/mage/deck/Unlimited.java new file mode 100644 index 00000000000..0724d45feb3 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Deck.Limited/src/mage/deck/Unlimited.java @@ -0,0 +1,30 @@ +package mage.deck; + +import mage.cards.decks.Deck; +import mage.cards.decks.DeckValidator; + +/** + * @author BetaSteward_at_googlemail.com + */ +public class Unlimited extends DeckValidator { + + public Unlimited() { + super("Unlimited", null); + } + + @Override + public int getDeckMinSize() { + return 0; + } + + @Override + public int getSideboardMinSize() { + return 0; + } + + @Override + public boolean validate(Deck deck) { + boolean valid = true; + return valid; + } +} diff --git a/Mage.Server/config/config.xml b/Mage.Server/config/config.xml index 48b09948dc3..1c5c476e419 100644 --- a/Mage.Server/config/config.xml +++ b/Mage.Server/config/config.xml @@ -47,7 +47,7 @@ socketWriteTimeout="10000" maxGameThreads="10" maxSecondsIdle="300" - minUserNameLength="3" + minUserNameLength="1" maxUserNameLength="14" invalidUserNamePattern="[^a-z0-9_]" minPasswordLength="8" @@ -63,6 +63,8 @@ mailUser="" mailPassword="" mailFromAddress="" + httpAuth="false" + authUrl="" /> @@ -225,5 +227,6 @@ + diff --git a/Mage.Server/release/config/config.xml b/Mage.Server/release/config-example/config.xml similarity index 99% rename from Mage.Server/release/config/config.xml rename to Mage.Server/release/config-example/config.xml index f30470ab1d1..b30707c2210 100644 --- a/Mage.Server/release/config/config.xml +++ b/Mage.Server/release/config-example/config.xml @@ -59,6 +59,8 @@ mailUser="" mailPassword="" mailFromAddress="" + httpAuth="false" + authUrl="" /> @@ -219,5 +221,6 @@ + diff --git a/Mage.Server/release/config/init.example.txt b/Mage.Server/release/config-example/init.example.txt similarity index 100% rename from Mage.Server/release/config/init.example.txt rename to Mage.Server/release/config-example/init.example.txt diff --git a/Mage.Server/release/config/log4j.properties b/Mage.Server/release/config-example/log4j.properties similarity index 97% rename from Mage.Server/release/config/log4j.properties rename to Mage.Server/release/config-example/log4j.properties index 5ea19fd97fb..4b600b70d28 100644 --- a/Mage.Server/release/config/log4j.properties +++ b/Mage.Server/release/config-example/log4j.properties @@ -1,36 +1,36 @@ -#SAMPLE SERVER CONFIG (you must enable it by command line) - -#default log level and active appenders (dest for logs) -log4j.rootLogger=info, console, logfile - -#custom log level for java classes -log4j.logger.com.j256.ormlite=warn -#log4j.logger.mage.player.ai=warn - -#console log -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.layout=org.apache.log4j.PatternLayout -log4j.appender.console.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n -log4j.appender.console.Threshold=info - -#file log - without rolling -log4j.appender.logfile=org.apache.log4j.FileAppender -log4j.appender.logfile.layout=org.apache.log4j.PatternLayout -log4j.appender.logfile.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n -log4j.appender.logfile.File=mageserver.log - -#file log - rolling by index -log4j.appender.logfileByIndex=org.apache.log4j.RollingFileAppender -log4j.appender.logfileByIndex.layout=org.apache.log4j.PatternLayout -log4j.appender.logfileByIndex.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n -log4j.appender.logfileByIndex.File=mageserver.log -log4j.appender.logfileByIndex.MaxFileSize=10MB -log4j.appender.logfileByIndex.MaxBackupIndex=5 -log4j.appender.logfileByIndex.append=true - -#file log - rolling by dayly -log4j.appender.logfileByDayly=org.apache.log4j.DailyRollingFileAppender -log4j.appender.logfileByDayly.layout=org.apache.log4j.PatternLayout -log4j.appender.logfileByDayly.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n -log4j.appender.logfileByDayly.File=mageserver.log +#SAMPLE SERVER CONFIG (you must enable it by command line) + +#default log level and active appenders (dest for logs) +log4j.rootLogger=info, console, logfile + +#custom log level for java classes +log4j.logger.com.j256.ormlite=warn +#log4j.logger.mage.player.ai=warn + +#console log +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.layout=org.apache.log4j.PatternLayout +log4j.appender.console.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n +log4j.appender.console.Threshold=info + +#file log - without rolling +log4j.appender.logfile=org.apache.log4j.FileAppender +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +log4j.appender.logfile.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n +log4j.appender.logfile.File=mageserver.log + +#file log - rolling by index +log4j.appender.logfileByIndex=org.apache.log4j.RollingFileAppender +log4j.appender.logfileByIndex.layout=org.apache.log4j.PatternLayout +log4j.appender.logfileByIndex.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n +log4j.appender.logfileByIndex.File=mageserver.log +log4j.appender.logfileByIndex.MaxFileSize=10MB +log4j.appender.logfileByIndex.MaxBackupIndex=5 +log4j.appender.logfileByIndex.append=true + +#file log - rolling by dayly +log4j.appender.logfileByDayly=org.apache.log4j.DailyRollingFileAppender +log4j.appender.logfileByDayly.layout=org.apache.log4j.PatternLayout +log4j.appender.logfileByDayly.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss,SSS} %-90m =>[%t] %C{1}.%M %n +log4j.appender.logfileByDayly.File=mageserver.log log4j.appender.logfileByDayly.DatePattern='.'yyyy-MM-dd \ No newline at end of file diff --git a/Mage.Server/release/config/readme.txt b/Mage.Server/release/config-example/readme.txt similarity index 100% rename from Mage.Server/release/config/readme.txt rename to Mage.Server/release/config-example/readme.txt diff --git a/Mage.Server/release/config/security.policy b/Mage.Server/release/config-example/security.policy similarity index 96% rename from Mage.Server/release/config/security.policy rename to Mage.Server/release/config-example/security.policy index dda47ba9183..5d74bde76d8 100644 --- a/Mage.Server/release/config/security.policy +++ b/Mage.Server/release/config-example/security.policy @@ -1,3 +1,3 @@ -grant { - permission java.security.AllPermission; +grant { + permission java.security.AllPermission; }; \ No newline at end of file diff --git a/Mage.Server/src/main/java/mage/server/AuthorizedUser.java b/Mage.Server/src/main/java/mage/server/AuthorizedUser.java index 43e0573b5bb..6f1aa97458d 100644 --- a/Mage.Server/src/main/java/mage/server/AuthorizedUser.java +++ b/Mage.Server/src/main/java/mage/server/AuthorizedUser.java @@ -64,6 +64,7 @@ public class AuthorizedUser { public boolean doCredentialsMatch(String name, String password) { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(this.hashAlgorithm); matcher.setHashIterations(this.hashIterations); + AuthenticationToken token = new UsernamePasswordToken(name, password); AuthenticationInfo info = new SimpleAuthenticationInfo(this.name, ByteSource.Util.bytes(Base64.decode(this.password)), diff --git a/Mage.Server/src/main/java/mage/server/Main.java b/Mage.Server/src/main/java/mage/server/Main.java index 51f43b87591..ea5fec70f27 100644 --- a/Mage.Server/src/main/java/mage/server/Main.java +++ b/Mage.Server/src/main/java/mage/server/Main.java @@ -371,6 +371,7 @@ public final class Main { if (throwable instanceof ClientDisconnectedException) { // client called a disconnect command (full disconnect without tables keep) // no need to keep session + // the above i think is a lie logger.info("CLIENT DISCONNECTED - " + sessionInfo); logger.debug("- cause: client called disconnect command"); managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true); diff --git a/Mage.Server/src/main/java/mage/server/Session.java b/Mage.Server/src/main/java/mage/server/Session.java index 9aba9c4ea82..23423bb81ed 100644 --- a/Mage.Server/src/main/java/mage/server/Session.java +++ b/Mage.Server/src/main/java/mage/server/Session.java @@ -18,6 +18,17 @@ import org.jboss.remoting.callback.Callback; import org.jboss.remoting.callback.HandleCallbackException; import org.jboss.remoting.callback.InvokerCallbackHandler; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -153,8 +164,8 @@ public class Session { if (userName.length() > config.getMaxUserNameLength()) { return "User name may not be longer than " + config.getMaxUserNameLength() + " characters"; } - if (userName.length() <= 2) { - return "User name is too short (2 characters or fewer)"; + if (userName.length() <= 1) { + return "User name is too short (1 characters or fewer)"; } if (userName.length() >= 250) { return "User name is too long (250 characters or more)"; @@ -243,6 +254,7 @@ public class Session { // find auth user AuthorizedUser authorizedUser = null; + if (managerFactory.configSettings().isAuthenticationActivated()) { authorizedUser = AuthorizedUserRepository.getInstance().getByName(userName); String errorMsg = "Wrong username or password. You must register your account first."; @@ -268,6 +280,51 @@ public class Session { } } } + + if (managerFactory.configSettings().isHttpAuth()) { + try { + JsonObject body = new JsonObject(); + body.addProperty("token", password); + body.addProperty("username", userName); + + String json = body.toString(); + + URL url = new URL(managerFactory.configSettings().getAuthUrl()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Content-Length", Integer.toString(json.length())); + conn.setRequestProperty("User-Agent", "Tainted-Mage/1.0"); + + conn.setDoOutput(true); + + + OutputStream os = conn.getOutputStream(); + os.write(json.getBytes()); + os.flush(); + os.close(); + + int responseCode = conn.getResponseCode(); + + if (responseCode == HttpURLConnection.HTTP_OK) { + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + + String resp = in.readLine(); + in.close(); + JsonElement response = JsonParser.parseString(resp); + if (response.isJsonObject() && response.getAsJsonObject().has("success") && response.getAsJsonObject().get("success").getAsBoolean()) { + // s'all good, man + } else { + return "Failed to authenticate"; + } + } else { + return "Failed to authenticate: " + Integer.toString(responseCode); + } + } catch (Exception e) { + return "Error with external authentication. Please try again later."; + } + } // create new user instance (auth or anon) boolean isReconnection = false; diff --git a/Mage.Server/src/main/java/mage/server/managers/ConfigSettings.java b/Mage.Server/src/main/java/mage/server/managers/ConfigSettings.java index be6d6c6c23d..17e8af29697 100644 --- a/Mage.Server/src/main/java/mage/server/managers/ConfigSettings.java +++ b/Mage.Server/src/main/java/mage/server/managers/ConfigSettings.java @@ -69,4 +69,8 @@ public interface ConfigSettings { List getDraftCubes(); List getDeckTypes(); + + boolean isHttpAuth(); + + String getAuthUrl(); } diff --git a/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java b/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java index 93137c076b4..8ba9dc9963f 100644 --- a/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java +++ b/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java @@ -44,6 +44,7 @@ public enum UserStatsRepository { TableUtils.createTableIfNotExists(connectionSource, UserStats.class); statsDao = DaoManager.createDao(connectionSource, UserStats.class); + statsDao.executeRaw("PRAGMA journal_mode=WAL;"); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error creating user_stats repository - ", ex); } diff --git a/Mage.Server/src/main/java/mage/server/util/Config.xsd b/Mage.Server/src/main/java/mage/server/util/Config.xsd index bc0ab04a678..695b01eb9c0 100644 --- a/Mage.Server/src/main/java/mage/server/util/Config.xsd +++ b/Mage.Server/src/main/java/mage/server/util/Config.xsd @@ -19,18 +19,21 @@ - - - - - + + + + + - - - - - + + + + + + + + diff --git a/Mage.Server/src/main/java/mage/server/util/ConfigWrapper.java b/Mage.Server/src/main/java/mage/server/util/ConfigWrapper.java index 20be1a64256..ecd9055277a 100644 --- a/Mage.Server/src/main/java/mage/server/util/ConfigWrapper.java +++ b/Mage.Server/src/main/java/mage/server/util/ConfigWrapper.java @@ -142,5 +142,13 @@ public class ConfigWrapper implements ConfigSettings { public List getDeckTypes() { return config.getDeckTypes().getDeckType(); } + + public boolean isHttpAuth() { + return config.getServer().isHttpAuth(); + } + + public String getAuthUrl() { + return config.getServer().getAuthUrl(); + } } diff --git a/Mage.Server/src/main/xml-resources/jaxb/Config/Config.xsd b/Mage.Server/src/main/xml-resources/jaxb/Config/Config.xsd index ac33721e7cb..07b96737586 100644 --- a/Mage.Server/src/main/xml-resources/jaxb/Config/Config.xsd +++ b/Mage.Server/src/main/xml-resources/jaxb/Config/Config.xsd @@ -44,6 +44,8 @@ + + diff --git a/Mage.Sets/src/mage/cards/e/EmporiumThopterist.java b/Mage.Sets/src/mage/cards/e/EmporiumThopterist.java new file mode 100644 index 00000000000..01e95ea29bc --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EmporiumThopterist.java @@ -0,0 +1,50 @@ +package mage.cards.e; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.ConjureCardEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.constants.SubType; +import mage.filter.common.FilterCreaturePermanent; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; + +/** + * + * @author Failure + */ +public final class EmporiumThopterist extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("Thopter creatures"); + + static { + filter.add(SubType.THOPTER.getPredicate()); + } + + public EmporiumThopterist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.VEDALKEN); + this.subtype.add(SubType.ARTIFICER); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Thopters you control get +2/+0. + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect(2, 0, Duration.WhileOnBattlefield, filter, false))); + // At the beginning of your upkeep, conjure a card named Ornithopter into your hand. + this.addAbility(new BeginningOfUpkeepTriggeredAbility(new ConjureCardEffect("Ornithopter"))); + } + + private EmporiumThopterist(final EmporiumThopterist card) { + super(card); + } + + @Override + public EmporiumThopterist copy() { + return new EmporiumThopterist(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AlchemyMurdersAtKarlovManor.java b/Mage.Sets/src/mage/sets/AlchemyMurdersAtKarlovManor.java new file mode 100644 index 00000000000..32934d78ecd --- /dev/null +++ b/Mage.Sets/src/mage/sets/AlchemyMurdersAtKarlovManor.java @@ -0,0 +1,28 @@ + +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.cards.ExpansionSet.SetCardInfo; +import mage.constants.Rarity; +import mage.constants.SetType; + +/** + * @author JayDi85 + */ +public final class AlchemyMurdersAtKarlovManor extends ExpansionSet { + + private static final AlchemyMurdersAtKarlovManor instance = new AlchemyMurdersAtKarlovManor(); + + public static AlchemyMurdersAtKarlovManor getInstance() { + return instance; + } + + private AlchemyMurdersAtKarlovManor() { + super("Alchemy: Murders at Karlov Manor", "YMKM", ExpansionSet.buildDate(2024, 3, 5), SetType.SUPPLEMENTAL); + this.hasBasicLands = false; + + cards.add(new SetCardInfo("Emporium Thopterist", 5, Rarity.UNCOMMON, mage.cards.e.EmporiumThopterist.class)); + + + } +} diff --git a/Mage.Sets/src/mage/sets/FoulMagicBlock3Extras.java b/Mage.Sets/src/mage/sets/FoulMagicBlock3Extras.java new file mode 100644 index 00000000000..dd671b4b843 --- /dev/null +++ b/Mage.Sets/src/mage/sets/FoulMagicBlock3Extras.java @@ -0,0 +1,49 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.cards.ExpansionSet.SetCardInfo; +import mage.constants.Rarity; +import mage.constants.SetType; + +public class FoulMagicBlock3Extras extends ExpansionSet { + + + private static final FoulMagicBlock3Extras instance = new FoulMagicBlock3Extras(); + + public static FoulMagicBlock3Extras getInstance() { + return instance; + } + + private FoulMagicBlock3Extras() { + super("Foul Magic Block 3 Extras", "FMB3E", ExpansionSet.buildDate(2025, 10, 25), SetType.CUSTOM_SET); + this.hasBasicLands = false; + addDualAlias("MH3", "252", new SetCardInfo("Bloodsoaked Insight", 1, Rarity.UNCOMMON, mage.cards.b.BloodsoakedInsight.class)); + addDualAlias("MH3", "243", new SetCardInfo("Boggart Trawler", 2, Rarity.UNCOMMON, mage.cards.b.BoggartTrawler.class)); + addDualAlias("MH3", "249", new SetCardInfo("Bridgeworks Battle", 3, Rarity.UNCOMMON, mage.cards.b.BridgeworksBattle.class)); + addDualAlias("MH3", "250", new SetCardInfo("Disciple of Freyalise", 4, Rarity.UNCOMMON, mage.cards.d.DiscipleOfFreyalise.class)); + addDualAlias("MH3", "253", new SetCardInfo("Drowner of Truth", 5, Rarity.UNCOMMON, mage.cards.d.DrownerOfTruth.class)); + addDualAlias("MH3", "244", new SetCardInfo("Fell the Profane", 6, Rarity.UNCOMMON, mage.cards.f.FellTheProfane.class)); + addDualAlias("MH3", "254", new SetCardInfo("Glasswing Grace", 7, Rarity.UNCOMMON, mage.cards.g.GlasswingGrace.class)); + addDualAlias("MH3", "240", new SetCardInfo("Hydroelectric Specimen", 8, Rarity.UNCOMMON, mage.cards.h.HydroelectricSpecimen.class)); + addDualAlias("MH3", "255", new SetCardInfo("Legion Leadership", 9, Rarity.UNCOMMON, mage.cards.l.LegionLeadership.class)); + addDualAlias("MH3", "246", new SetCardInfo("Pinnacle Monk", 10, Rarity.UNCOMMON, mage.cards.p.PinnacleMonk.class)); + addDualAlias("MH3", "238", new SetCardInfo("Razorgrass Ambush", 11, Rarity.UNCOMMON, mage.cards.r.RazorgrassAmbush.class)); + addDualAlias("MH3", "256", new SetCardInfo("Revitalizing Repast", 12, Rarity.UNCOMMON, mage.cards.r.RevitalizingRepast.class)); + addDualAlias("MH3", "257", new SetCardInfo("Rush of Inspiration", 13, Rarity.UNCOMMON, mage.cards.r.RushOfInspiration.class)); + addDualAlias("MH3", "241", new SetCardInfo("Sink into Stupor", 14, Rarity.UNCOMMON, mage.cards.s.SinkIntoStupor.class)); + addDualAlias("MH3", "258", new SetCardInfo("Strength of the Harvest", 15, Rarity.UNCOMMON, mage.cards.s.StrengthOfTheHarvest.class)); + addDualAlias("MH3", "259", new SetCardInfo("Stump Stomp", 16, Rarity.UNCOMMON, mage.cards.s.StumpStomp.class)); + addDualAlias("MH3", "248", new SetCardInfo("Sundering Eruption", 17, Rarity.UNCOMMON, mage.cards.s.SunderingEruption.class)); + addDualAlias("MH3", "260", new SetCardInfo("Suppression Ray", 18, Rarity.UNCOMMON, mage.cards.s.SuppressionRay.class)); + addDualAlias("MH3", "261", new SetCardInfo("Waterlogged Teachings", 19, Rarity.UNCOMMON, mage.cards.w.WaterloggedTeachings.class)); + addDualAlias("MH3", "239", new SetCardInfo("Witch Enchanter", 20, Rarity.UNCOMMON, mage.cards.w.WitchEnchanter.class)); + + addAlias("KTK", "230", new SetCardInfo("Bloodstained Mire", 21, Rarity.RARE, mage.cards.b.BloodstainedMire.class)); + addAlias("KTK", "233", new SetCardInfo("Flooded Strand", 22, Rarity.RARE, mage.cards.f.FloodedStrand.class)); + addAlias("KTK", "239", new SetCardInfo("Polluted Delta", 23, Rarity.RARE, mage.cards.p.PollutedDelta.class)); + addAlias("KTK", "248", new SetCardInfo("Windswept Heath", 24, Rarity.RARE, mage.cards.w.WindsweptHeath.class)); + addAlias("KTK", "249", new SetCardInfo("Wooded Foothills", 25, Rarity.RARE, mage.cards.w.WoodedFoothills.class)); + + } + +} diff --git a/Mage.Sets/src/mage/sets/ModernHorizons3.java b/Mage.Sets/src/mage/sets/ModernHorizons3.java index 57fe3560988..997d3b1210a 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons3.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons3.java @@ -21,7 +21,7 @@ public final class ModernHorizons3 extends ExpansionSet { public static ModernHorizons3 getInstance() { return instance; } - + private ModernHorizons3() { super("Modern Horizons 3", "MH3", ExpansionSet.buildDate(2024, 6, 7), SetType.SUPPLEMENTAL_MODERN_LEGAL); this.blockName = "Modern Horizons 3"; diff --git a/Mage/src/main/java/mage/cards/ExpansionSet.java b/Mage/src/main/java/mage/cards/ExpansionSet.java index 88d5a506841..2bb5d252cf4 100644 --- a/Mage/src/main/java/mage/cards/ExpansionSet.java +++ b/Mage/src/main/java/mage/cards/ExpansionSet.java @@ -18,12 +18,41 @@ import java.io.Serializable; import java.util.*; import java.util.stream.Collectors; + + /** * @author BetaSteward_at_googlemail.com */ public abstract class ExpansionSet implements Serializable { + + public class CardAlias { + public String targetSet; + public String targetSetNumber; + public boolean hasBack; + public SetCardInfo cardInfo; + public CardAlias(String targetSet, String targetSetNumber, boolean hasBack, SetCardInfo cardInfo) { + this.targetSet = targetSet; + this.targetSetNumber = targetSetNumber; + this.hasBack = hasBack; + this.cardInfo = cardInfo; + } + } private static final Logger logger = Logger.getLogger(ExpansionSet.class); + + // Foul magic alias tweaks + public final HashMap cardAliases = new HashMap(); + + public void addAlias(String setCode, String setNumber, SetCardInfo cardInfo) { + cards.add(cardInfo); + cardAliases.put(cardInfo.getCardNumberAsInt(), new CardAlias(setCode, setNumber, false, cardInfo)); + } + + public void addDualAlias(String setCode, String setNumber, SetCardInfo cardInfo) { + cards.add(cardInfo); + cardAliases.put(cardInfo.getCardNumberAsInt(), new CardAlias(setCode, setNumber, true, cardInfo)); + } + // TODO: remove all usage to default (see below), keep bfz/zen/ust art styles for specific sets only // the main different in art styles - full art lands can have big mana icon at the bottom diff --git a/Mage/src/main/java/mage/cards/repository/CardCriteria.java b/Mage/src/main/java/mage/cards/repository/CardCriteria.java index 286f619846a..4f299d3a7b8 100644 --- a/Mage/src/main/java/mage/cards/repository/CardCriteria.java +++ b/Mage/src/main/java/mage/cards/repository/CardCriteria.java @@ -3,10 +3,13 @@ package mage.cards.repository; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.SelectArg; import com.j256.ormlite.stmt.Where; + +import mage.MageObject; import mage.constants.CardType; import mage.constants.Rarity; import mage.constants.SubType; import mage.constants.SuperType; +import mage.filter.predicate.Predicate; import java.sql.SQLException; import java.util.ArrayList; @@ -39,6 +42,7 @@ public class CardCriteria { private boolean red; private boolean white; private boolean colorless; + private boolean limitColors; private Integer manaValue; private String sortBy; private Long start; @@ -68,6 +72,12 @@ public class CardCriteria { this.minCardNumber = Integer.MIN_VALUE; this.maxCardNumber = Integer.MAX_VALUE; } + + public CardCriteria limitColors(boolean limitColors) { + this.limitColors = limitColors; + return this; + } + public CardCriteria black(boolean black) { this.black = black; @@ -311,35 +321,56 @@ public class CardCriteria { clausesCount++; } + List exclusion = new ArrayList<>(); + int colorClauses = 0; if (black) { where.eq("black", true); colorClauses++; + } else { + exclusion.add("black"); } if (blue) { where.eq("blue", true); colorClauses++; + } else { + exclusion.add("blue"); } if (green) { where.eq("green", true); colorClauses++; + } else { + exclusion.add("green"); } if (red) { where.eq("red", true); colorClauses++; + } else { + exclusion.add("red"); } if (white) { where.eq("white", true); colorClauses++; + } else { + exclusion.add("white"); } if (colorless) { where.eq("black", false).eq("blue", false).eq("green", false).eq("red", false).eq("white", false); where.and(5); colorClauses++; } + if (colorClauses > 0) { where.or(colorClauses); clausesCount++; + + if (this.limitColors) { + for (String color : exclusion) { + where.not(); + where.eq(color, true); + clausesCount++; + } + } } if (minCardNumber != Integer.MIN_VALUE) { diff --git a/Mage/src/main/java/mage/cards/repository/CardInfo.java b/Mage/src/main/java/mage/cards/repository/CardInfo.java index ecf3225f6fc..3db32d2fdad 100644 --- a/Mage/src/main/java/mage/cards/repository/CardInfo.java +++ b/Mage/src/main/java/mage/cards/repository/CardInfo.java @@ -40,6 +40,8 @@ public class CardInfo { protected String setCode; @DatabaseField(indexName = "setCode_cardNumber_index") protected String cardNumber; + @DatabaseField(indexName = "oracleId_index", canBeNull = true) + protected String oracleId; /** * Fast access to numerical card number (number without prefix/postfix: 123b -> 123) */ diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index d3ebbcfbc35..37115721b84 100644 --- a/Mage/src/main/java/mage/cards/repository/CardRepository.java +++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java @@ -38,8 +38,10 @@ public enum CardRepository { // TODO: delete db version from cards and expansions due un-used (cause dbs re-created on each update now) private static final String VERSION_ENTITY_NAME = "card"; - private static final long CARD_DB_VERSION = 54; // raise this if db structure was changed + private static final long CARD_DB_VERSION = 55; // raise this if db structure was changed private static final long CARD_CONTENT_VERSION = 241; // raise this if new cards were added to the server + private static final long CARD_CONTENT_VERSION_FOUL = 2; // raise this if specifically foul magic patches changed things + private Dao cardsDao; @@ -578,6 +580,16 @@ public enum CardRepository { return Collections.emptyList(); } + + public List findCardsWithTagRelations(List tagRelations) { + try { + List result = cardsDao.queryBuilder().where().in("oracleId", tagRelations.stream().map(id -> id.oracle_id).toArray()).query(); + return result; + } catch (SQLException e) { + e.printStackTrace(); + return new ArrayList(); + } + } public List findCards(String name, long limitByMaxAmount) { return findCards(name, limitByMaxAmount, false, true); @@ -670,7 +682,7 @@ public enum CardRepository { } public long getContentVersionConstant() { - return CARD_CONTENT_VERSION; + return CARD_CONTENT_VERSION + CARD_CONTENT_VERSION_FOUL; } public void closeDB(boolean writeCompact) { diff --git a/Mage/src/main/java/mage/cards/repository/Tag.java b/Mage/src/main/java/mage/cards/repository/Tag.java new file mode 100644 index 00000000000..79bcbeb59eb --- /dev/null +++ b/Mage/src/main/java/mage/cards/repository/Tag.java @@ -0,0 +1,20 @@ +package mage.cards.repository; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + +/** + * @author JayDi85 + */ +@DatabaseTable(tableName = "tag") +public class Tag { + + @DatabaseField(id = true) + public String id; + + @DatabaseField() + public String label; + + @DatabaseField(canBeNull = true) + public String description; +} diff --git a/Mage/src/main/java/mage/cards/repository/TagRelation.java b/Mage/src/main/java/mage/cards/repository/TagRelation.java new file mode 100644 index 00000000000..24201fbaa25 --- /dev/null +++ b/Mage/src/main/java/mage/cards/repository/TagRelation.java @@ -0,0 +1,18 @@ +package mage.cards.repository; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + +/** + * @author JayDi85 + */ +@DatabaseTable(tableName = "tag_relation") +public class TagRelation { + + @DatabaseField(indexName = "tags_oracle_id_index", uniqueCombo = true) + protected String oracle_id; + + @DatabaseField(indexName = "tags_tag_id_index", foreign = true, columnName = "tag_id", uniqueCombo = true) + protected Tag tag; + +} diff --git a/Mage/src/main/java/mage/cards/repository/TagRepository.java b/Mage/src/main/java/mage/cards/repository/TagRepository.java new file mode 100644 index 00000000000..15990c6fa51 --- /dev/null +++ b/Mage/src/main/java/mage/cards/repository/TagRepository.java @@ -0,0 +1,180 @@ +package mage.cards.repository; + +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.jdbc.JdbcConnectionSource; +import com.j256.ormlite.stmt.DeleteBuilder; +import com.j256.ormlite.stmt.QueryBuilder; +import com.j256.ormlite.support.ConnectionSource; +import com.j256.ormlite.support.DatabaseConnection; +import com.j256.ormlite.table.TableUtils; + +import mage.constants.SetType; +import org.apache.log4j.Logger; + +import java.io.File; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author North, JayDi85 + */ +public enum TagRepository { + + instance; + + private static final Logger logger = Logger.getLogger(TagRepository.class); + + // fixes limit for out of memory problems + private static final AtomicInteger databaseFixes = new AtomicInteger(); + + private static final int MAX_DATABASE_FIXES = 10; + + private static final String VERSION_ENTITY_NAME = "tags"; + private static final long TAG_VERSION = 3; // raise this if db structure was changed + + private Dao tagsDao; + private Dao tagRelationDao; + + + TagRepository() { + File file = new File("db"); + if (!file.exists()) { + file.mkdirs(); + } + try { + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true)); + + boolean isObsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, TAG_VERSION); + boolean isNewBuild = RepositoryUtil.isNewBuildRun(connectionSource, VERSION_ENTITY_NAME, TagRepository.class); // recreate db on new build + if (isObsolete || isNewBuild) { + //System.out.println("Local cards db is outdated, cleaning..."); + TableUtils.dropTable(connectionSource, TagRelation.class, true); + TableUtils.dropTable(connectionSource, Tag.class, true); + } + + TableUtils.createTableIfNotExists(connectionSource, Tag.class); + TableUtils.createTableIfNotExists(connectionSource, TagRelation.class); + + tagsDao = DaoManager.createDao(connectionSource, Tag.class); + tagRelationDao = DaoManager.createDao(connectionSource, TagRelation.class); + } catch (SQLException e) { + Logger.getLogger(TagRepository.class).error("Error creating tags repository - " + e, e); + } + } + + public List getTagsFromCard(CardInfo card) { + try { + List relations = tagRelationDao.queryForEq("oracle_id", card.oracleId); + return tagsDao.queryBuilder().where().in("id", relations.stream().map((TagRelation rel) -> rel.tag)).query(); + } catch (SQLException e) { + return new ArrayList(); + } + } + + public List getCardsByTag(Tag tag) { + try { + List relations = tagRelationDao.queryForEq("tag_id", tag.id); + return CardRepository.instance.findCardsWithTagRelations(relations); + } catch (SQLException e) { + e.printStackTrace(); + return new ArrayList(); + } + } + + public List getCardsByTagId(String id) { + try { + return getCardsByTag(tagsDao.queryForId(id)); + } catch (SQLException e) { + e.printStackTrace(); + return new ArrayList(); + } + } + + public void syncTag(final Tag tag, List oracleIds ) { + if (tag == null) { + return; + } + try { + tagsDao.createOrUpdate(tag); + // clear out old ones + DeleteBuilder cleanser = tagRelationDao.deleteBuilder(); + cleanser.where().eq("tag_id", tag); + cleanser.delete(); + + tagRelationDao.callBatchTasks(() -> { + // only add new cards (no updates) + logger.info("DB: refreshing tag " + tag.label); + + for (String oracleId : oracleIds) { + TagRelation relation = new TagRelation(); + relation.oracle_id = oracleId; + relation.tag = tag; + try { + tagRelationDao.create(relation); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + return null; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + + public void closeDB(boolean writeCompact) { + try { + if (tagsDao != null && tagsDao.getConnectionSource() != null) { + DatabaseConnection conn = tagsDao.getConnectionSource().getReadWriteConnection(tagsDao.getTableName()); + if (writeCompact) { + conn.executeStatement("SHUTDOWN COMPACT", DatabaseConnection.DEFAULT_RESULT_FLAGS); // compact data and rewrite whole db + } else { + conn.executeStatement("SHUTDOWN IMMEDIATELY", DatabaseConnection.DEFAULT_RESULT_FLAGS); // close without any writes + } + tagsDao.getConnectionSource().releaseConnection(conn); + } + } catch (SQLException ignore) { + } + } + + public void openDB() { + try { + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true)); + tagsDao = DaoManager.createDao(connectionSource, Tag.class); + tagRelationDao = DaoManager.createDao(connectionSource, TagRelation.class); + + } catch (SQLException e) { + Logger.getLogger(TagRepository.class).error("Error opening tag repository - " + e, e); + } + } + + public List getAllTags() { + try { + return tagsDao.queryForAll(); + } catch (SQLException e) { + return new ArrayList(); + } + } + + public List searchTags(String query) { + try { + return tagsDao.queryBuilder().where().like("label", "%"+query.replace(' ', '-')+"%").query(); + } catch (SQLException e) { + return new ArrayList(); + } + } + + public int getTagCardCount(Tag tag) { + try { + return (int) tagRelationDao.queryBuilder().where().eq("tag_id", tag.id).countOf(); + } catch (SQLException e) { + return 0; + } + } + +} diff --git a/pom.xml b/pom.xml index 2be9fcc6f7a..bd7866f868a 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,10 @@ org.apache.maven.plugins maven-compiler-plugin + + 11 + 11 +