Merge branch 'master' into External-master
All checks were successful
/ build_release (push) Successful in 16m24s

This commit is contained in:
Failure 2026-01-17 17:16:17 -08:00
commit cc0bcaa46f
92 changed files with 1485 additions and 189 deletions

View file

@ -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

View file

@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
- package-ecosystem: 'maven'
directory: '/'
schedule:
interval: 'weekly'

23
.github/labeler.yml vendored
View file

@ -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/**' ]

View file

@ -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'

View file

@ -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 }}

View file

@ -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

1
.gitignore vendored
View file

@ -62,3 +62,4 @@ Utils/*implemented.txt
# build tools
mage-bundle.zip
.env
.classpath

36
Dockerfile Normal file
View file

@ -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 <<EOF
wget $(curl -Ls -o /dev/null -w %{url_effective} "https://git.cef.icu/Failure/foul-magics/actions/runs/latest" | sed 's/$/\/artifacts\/server.zip/')
unzip server.zip
unzip mage-server.zip
rm server.zip mage-server.zip
EOF
CMD ["./dockerRun.sh"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -7,6 +7,7 @@ import mage.cards.decks.Deck;
import mage.cards.repository.CardRepository;
import mage.cards.repository.CardScanner;
import mage.cards.repository.RepositoryUtil;
import mage.cards.repository.TagRepository;
import mage.client.cards.BigCard;
import mage.client.chat.ChatPanelBasic;
import mage.client.components.*;
@ -58,11 +59,14 @@ import org.apache.log4j.Logger;
import org.junit.Assert;
import org.mage.card.arcane.ManaSymbols;
import org.mage.card.arcane.SvgUtils;
import org.mage.plugins.card.dl.sources.TagSource;
import org.mage.plugins.card.images.DownloadPicturesService;
import org.mage.plugins.card.info.CardInfoPaneImpl;
import org.mage.plugins.card.utils.CardImageUtils;
import org.mage.plugins.card.utils.impl.ImageManagerImpl;
import com.google.gson.Gson;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
@ -90,7 +94,7 @@ import java.util.stream.Collectors;
*/
public class MageFrame extends javax.swing.JFrame implements MageClient {
private static final String TITLE_NAME = "XMage";
private static final String TITLE_NAME = "XMage (Foul Magics)";
private static final Logger LOGGER = Logger.getLogger(MageFrame.class);
private static final String LITE_MODE_ARG = "-lite";
@ -297,14 +301,6 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
errorDialog.setLocation(100, 100);
desktopPane.add(errorDialog, errorDialog.isModal() ? JLayeredPane.MODAL_LAYER : JLayeredPane.PALETTE_LAYER);
try {
this.whatsNewDialog = new WhatsNewDialog();
} catch (Throwable e) {
// example: JavaFX is not supported on old MacOS with OpenJDK
// https://bugs.openjdk.java.net/browse/JDK-8202132
LOGGER.error("JavaFX is not supported by your system. What's new page will be disabled.", e);
this.whatsNewDialog = null;
}
PING_SENDER_EXECUTOR.scheduleAtFixedRate(SessionHandler::ping, TablesPanel.PING_SERVER_SECS, TablesPanel.PING_SERVER_SECS, TimeUnit.SECONDS);
@ -385,11 +381,6 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
setWindowTitle(); // make sure title is actual on startup
});
// run what's new checks (loading in background)
SwingUtilities.invokeLater(() -> {
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();
}// </editor-fold>//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);

View file

@ -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()) {

View file

@ -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<Pair<String, String[]>> foulMagicsSets = ImmutableList.of(
new Pair<String, String[]>("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<String, String[]>("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<String, String[]>("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<String, String[]>("Set 1 - In Da Beegeening", new String[] {
"Foundations",
"Foundations Jumpstart",
"* Guilds of Ravnica Block",
"* Return to Ravnica Block",
}),
new Pair<String, String[]>("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 {
}
}
}

View file

@ -222,6 +222,46 @@
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnExpansionSearchActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JComboBox" name="cbFoulMagicPresets">
<Properties>
<Property name="model" type="javax.swing.ComboBoxModel" editor="org.netbeans.modules.form.editors2.ComboBoxModelEditor">
<StringArray count="0"/>
</Property>
<Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[120, 20]"/>
</Property>
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[120, 20]"/>
</Property>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[120, 20]"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="cbFoulMagicPresetSelected"/>
</Events>
<AuxValues>
<AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="FoulMagicPreset"/>
</AuxValues>
</Component>
<Component class="javax.swing.JButton" name="btnFoulMagicPreset">
<Properties>
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
<Image iconType="3" name="/buttons/brick.png"/>
</Property>
<Property name="toolTipText" type="java.lang.String" value="Set to Foul Magic preset"/>
<Property name="alignmentX" type="float" value="1.0"/>
<Property name="focusable" type="boolean" value="false"/>
<Property name="horizontalTextPosition" type="int" value="0"/>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[23, 23]"/>
</Property>
<Property name="verticalTextPosition" type="int" value="3"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnFoulMagicPresetSet"/>
</Events>
</Component>
<Component class="javax.swing.JToolBar$Separator" name="jSeparator2">
</Component>
<Component class="javax.swing.JCheckBox" name="chkPennyDreadful">

View file

@ -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<Predicate<MageObject>> predicates = new ArrayList<>();
List<Predicate<MageObject>> 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<String> setNames = new LinkedList<String>();
for (Pair<String, String[]> 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("<html>Add selected cards to deck.<br/>\nAlternative: <strong>Double click</strong> 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))
);
}// </editor-fold>//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<String> 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<String> cbExpansionSet;
private javax.swing.JComboBox<SortBy> cbSortBy;
private javax.swing.JComboBox<SortBy> 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)

View file

@ -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<Tag> view = new ArrayList<Tag>();
public Map<String, Boolean> filtered = new HashMap<String, Boolean>();
public Set<String> whitelist = new HashSet<String>();
public Set<String> blacklist = new HashSet<String>();
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<String> buildWhitelist() {
whitelist.clear();
whitelistCount = 0;
// whitelists are a bit weird, each one needs to reduce the next
Set<String> temp = new HashSet<String>();
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<String> 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;
}
}

View file

@ -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);

View file

@ -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

View file

@ -1332,6 +1332,11 @@
<Property name="text" type="java.lang.String" value="set to default"/>
</Properties>
</Component>
<Component class="javax.swing.JCheckBox" name="cbVerticalLayout">
<Properties>
<Property name="text" type="java.lang.String" value="Vertical Layout (Experimental)"/>
</Properties>
</Component>
</SubComponents>
</Container>
<Container class="javax.swing.JPanel" name="panelSizeDetailedSettings">

View file

@ -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;

View file

@ -131,6 +131,8 @@ public final class GamePanel extends javax.swing.JPanel {
private final Map<String, MageSplitter> 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<String, JComponent> 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()) {

View file

@ -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));

View file

@ -18,5 +18,7 @@ public enum ClientEventType {
DRAFT_PICK_CARD,
DRAFT_MARK_CARD,
//
PLAYER_TYPE_CHANGED
PLAYER_TYPE_CHANGED,
//
TAG_DOUBLE_CLICK
}

View file

@ -109,7 +109,7 @@ public class GathererSets implements Iterable<DownloadJob> {
"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<DownloadJob> {
codeReplacements.put("WTH", "WL");
codeReplacements.put("YMID", "Y22");
codeReplacements.put("YNEO", "Y22NEO");
}
public GathererSets() {

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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<String> oracle_ids;
}
class OracleResponse {
public String object;
public boolean has_more;
public List<RawTag> 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);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 B

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 B

After

Width:  |  Height:  |  Size: 5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before After
Before After

View file

@ -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;
}
}

View file

@ -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=""
/>
<playerTypes>
<playerType name="Human" jar="mage-player-human.jar" className="mage.player.human.HumanPlayer"/>
@ -225,5 +227,6 @@
<deckType name="Block Constructed - Zendikar" jar="mage-deck-constructed.jar" className="mage.deck.ZendikarBlock"/>
<deckType name="Block Constructed Custom - Star Wars" jar="mage-deck-constructed.jar" className="mage.deck.StarWarsBlock"/>
<deckType name="Limited" jar="mage-deck-limited.jar" className="mage.deck.Limited"/>
<deckType name="Unlimited" jar="mage-deck-limited.jar" className="mage.deck.Unlimited"/>
</deckTypes>
</config>

View file

@ -59,6 +59,8 @@
mailUser=""
mailPassword=""
mailFromAddress=""
httpAuth="false"
authUrl=""
/>
<playerTypes>
<playerType name="Human" jar="mage-player-human-${project.version}.jar" className="mage.player.human.HumanPlayer"/>
@ -219,5 +221,6 @@
<deckType name="Block Constructed - Zendikar" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.ZendikarBlock"/>
<deckType name="Block Constructed Custom - Star Wars" jar="mage-deck-constructed-${project.version}.jar" className="mage.deck.StarWarsBlock"/>
<deckType name="Limited" jar="mage-deck-limited-${project.version}.jar" className="mage.deck.Limited"/>
<deckType name="Unlimited" jar="mage-deck-limited-${project.version}.jar" className="mage.deck.Unlimited"/>
</deckTypes>
</config>

View file

@ -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

View file

@ -1,3 +1,3 @@
grant {
permission java.security.AllPermission;
grant {
permission java.security.AllPermission;
};

View file

@ -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)),

View file

@ -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);

View file

@ -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;

View file

@ -69,4 +69,8 @@ public interface ConfigSettings {
List<Plugin> getDraftCubes();
List<Plugin> getDeckTypes();
boolean isHttpAuth();
String getAuthUrl();
}

View file

@ -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);
}

View file

@ -19,18 +19,21 @@
<xs:attribute name="serverAddress" type="xs:string" use="required"/>
<xs:attribute name="serverName" type="xs:string" use="required"/>
<xs:attribute name="port" type="xs:positiveInteger" use="required"/>
<xs:attribute name="secondaryBindPort" type="xs:integer" use="required"/>
<xs:attribute name="backlogSize" type="xs:positiveInteger" use="required"/>
<xs:attribute name="numAcceptThreads" type="xs:positiveInteger" use="required"/>
<xs:attribute name="maxPoolSize" type="xs:positiveInteger" use="required"/>
<xs:attribute name="leasePeriod" type="xs:positiveInteger" use="required"/>
<xs:attribute name="secondaryBindPort" type="xs:integer" use="required"/>
<xs:attribute name="backlogSize" type="xs:positiveInteger" use="required"/>
<xs:attribute name="numAcceptThreads" type="xs:positiveInteger" use="required"/>
<xs:attribute name="maxPoolSize" type="xs:positiveInteger" use="required"/>
<xs:attribute name="leasePeriod" type="xs:positiveInteger" use="required"/>
<xs:attribute name="maxGameThreads" type="xs:positiveInteger" use="required"/>
<xs:attribute name="maxSecondsIdle" type="xs:positiveInteger" use="required"/>
<xs:attribute name="minUserNameLength" type="xs:positiveInteger" use="required"/>
<xs:attribute name="maxUserNameLength" type="xs:positiveInteger" use="required"/>
<xs:attribute name="userNamePattern" type="xs:string" use="required"/>
<xs:attribute name="maxAiOpponents" type="xs:string" use="optional"/>
<xs:attribute name="saveGameActivated" type="xs:boolean" use="optional"/>
<xs:attribute name="minUserNameLength" type="xs:positiveInteger" use="required"/>
<xs:attribute name="maxUserNameLength" type="xs:positiveInteger" use="required"/>
<xs:attribute name="userNamePattern" type="xs:string" use="required"/>
<xs:attribute name="maxAiOpponents" type="xs:string" use="optional"/>
<xs:attribute name="saveGameActivated" type="xs:boolean" use="optional"/>
<xs:attribute name="httpAuth" type="xs:boolean" use="optional" />
<xs:attribute name="authUrl" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>

View file

@ -142,5 +142,13 @@ public class ConfigWrapper implements ConfigSettings {
public List<Plugin> getDeckTypes() {
return config.getDeckTypes().getDeckType();
}
public boolean isHttpAuth() {
return config.getServer().isHttpAuth();
}
public String getAuthUrl() {
return config.getServer().getAuthUrl();
}
}

View file

@ -44,6 +44,8 @@
<xs:attribute name="mailUser" type="xs:string" use="optional"/>
<xs:attribute name="mailPassword" type="xs:string" use="optional"/>
<xs:attribute name="mailFromAddress" type="xs:string" use="optional"/>
<xs:attribute name="httpAuth" type="xs:boolean" use="optional" />
<xs:attribute name="authUrl" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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";

View file

@ -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<Integer, CardAlias> cardAliases = new HashMap<Integer, CardAlias>();
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

View file

@ -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<String> 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) {

View file

@ -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)
*/

View file

@ -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<CardInfo, Object> cardsDao;
@ -578,6 +580,16 @@ public enum CardRepository {
return Collections.emptyList();
}
public List<CardInfo> findCardsWithTagRelations(List<TagRelation> tagRelations) {
try {
List<CardInfo> 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<CardInfo>();
}
}
public List<CardInfo> 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) {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<Tag, Object> tagsDao;
private Dao<TagRelation, Object> 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<Tag> getTagsFromCard(CardInfo card) {
try {
List<TagRelation> 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<Tag>();
}
}
public List<CardInfo> getCardsByTag(Tag tag) {
try {
List<TagRelation> relations = tagRelationDao.queryForEq("tag_id", tag.id);
return CardRepository.instance.findCardsWithTagRelations(relations);
} catch (SQLException e) {
e.printStackTrace();
return new ArrayList<CardInfo>();
}
}
public List<CardInfo> getCardsByTagId(String id) {
try {
return getCardsByTag(tagsDao.queryForId(id));
} catch (SQLException e) {
e.printStackTrace();
return new ArrayList<CardInfo>();
}
}
public void syncTag(final Tag tag, List<String> oracleIds ) {
if (tag == null) {
return;
}
try {
tagsDao.createOrUpdate(tag);
// clear out old ones
DeleteBuilder<TagRelation, Object> 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<Tag> getAllTags() {
try {
return tagsDao.queryForAll();
} catch (SQLException e) {
return new ArrayList<Tag>();
}
}
public List<Tag> searchTags(String query) {
try {
return tagsDao.queryBuilder().where().like("label", "%"+query.replace(' ', '-')+"%").query();
} catch (SQLException e) {
return new ArrayList<Tag>();
}
}
public int getTagCardCount(Tag tag) {
try {
return (int) tagRelationDao.queryBuilder().where().eq("tag_id", tag.id).countOf();
} catch (SQLException e) {
return 0;
}
}
}

View file

@ -56,6 +56,10 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>