diff --git a/.gitattributes b/.gitattributes index b7bd20a80f0..7866683ccfd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9611,11 +9611,13 @@ src/main/java/forge/card/CardDb.java -text src/main/java/forge/card/CardInSet.java -text src/main/java/forge/card/CardManaCost.java -text src/main/java/forge/card/CardManaCostShard.java -text +src/main/java/forge/card/CardParsingException.java -text src/main/java/forge/card/CardPool.java -text src/main/java/forge/card/CardPoolView.java -text src/main/java/forge/card/CardPrinted.java -text src/main/java/forge/card/CardRarity.java -text src/main/java/forge/card/CardRules.java -text +src/main/java/forge/card/CardRulesReader.java svneol=native#text/plain src/main/java/forge/card/CardSet.java -text src/main/java/forge/card/CardSuperType.java -text src/main/java/forge/card/CardType.java -text diff --git a/src/main/java/forge/card/CardParsingException.java b/src/main/java/forge/card/CardParsingException.java new file mode 100755 index 00000000000..1104b7c8da2 --- /dev/null +++ b/src/main/java/forge/card/CardParsingException.java @@ -0,0 +1,33 @@ +package forge.card; + +/** + * Indicates an error parsing a card txt file. + */ +public class CardParsingException extends Exception { + + private static final long serialVersionUID = -6504223115741449784L; + + /** + * Constructor with message. + * + * @param txtFile name of txt file with the problem + * @param lineNum indicates the line number containing the problem + * @param message describes the nature of the problem in the file + */ + public CardParsingException(final String txtFile, final int lineNum, final String message) { + super("in '" + txtFile + "' line " + lineNum + ": " + message); + } + + /** + * Constructor with message and cause. + * + * @param txtFile name of txt file with the problem + * @param lineNum indicates the line number containing the problem + * @param message describes the nature of the problem in the file + * @param cause the original cause for the exception + */ + public CardParsingException(final String txtFile, final int lineNum, final String message, final Throwable cause) + { + super("in '" + txtFile + "' line " + lineNum + ": " + message, cause); + } +} diff --git a/src/main/java/forge/card/CardRules.java b/src/main/java/forge/card/CardRules.java index 5a88bc75566..009d2e2bf37 100644 --- a/src/main/java/forge/card/CardRules.java +++ b/src/main/java/forge/card/CardRules.java @@ -36,6 +36,9 @@ public final class CardRules { private Map setsPrinted = null; + private boolean removedFromAIDecks = false; + private boolean removedFromRandomDecks = false; + // Ctor and builders are needed here public String getName() { return name; } public CardType getType() { return type; } @@ -50,6 +53,22 @@ public final class CardRules { public int getIntToughness() { return iToughness; } public String getLoyalty() { return loyalty; } + /** + * Getter for removedFromAIDecks. + * @return the removedFromAIDecks value + */ + public final boolean isRemovedFromAIDecks() { + return removedFromAIDecks; + } + + /** + * Getter for removedFromRandomDecks. + * @return the removedFromRandomDecks value + */ + public final boolean isRemovedFromRandomDecks() { + return removedFromRandomDecks; + } + public String getPTorLoyalty() { if (getType().isCreature()) { return power + "/" + toughness; } if (getType().isPlaneswalker()) { return loyalty; } @@ -57,13 +76,17 @@ public final class CardRules { } public CardRules(final String cardName, final CardType cardType, final String manacost, - final String ptLine, final String[] cardRules, final Map setsData) + final String ptLine, final String[] cardRules, final Map setsData, + final boolean removedFromRandomDecks0, final boolean removedFromAIDecks0) { this.name = cardName; this.type = cardType; this.cost = manacost == null ? CardManaCost.empty : new CardManaCost(manacost); this.rules = cardRules; this.color = new CardColor(cost); + this.removedFromAIDecks = removedFromAIDecks0; + this.removedFromRandomDecks = removedFromRandomDecks0; + if (cardType.isCreature()) { int slashPos = ptLine.indexOf('/'); if (slashPos == -1) { diff --git a/src/main/java/forge/card/CardRulesReader.java b/src/main/java/forge/card/CardRulesReader.java new file mode 100644 index 00000000000..c613fb320b6 --- /dev/null +++ b/src/main/java/forge/card/CardRulesReader.java @@ -0,0 +1,651 @@ +package forge.card; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.TreeMap; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import net.slightlymagic.braids.util.UtilFunctions; +import net.slightlymagic.braids.util.generator.FindNonDirectoriesSkipDotDirectoriesGenerator; +import net.slightlymagic.braids.util.generator.GeneratorFunctions; +import net.slightlymagic.braids.util.progress_monitor.BaseProgressMonitor; +import net.slightlymagic.braids.util.progress_monitor.StderrProgressMonitor; + +import com.google.code.jyield.Generator; +import com.google.code.jyield.YieldUtils; + +import forge.card.trigger.TriggerHandler; +import forge.error.ErrorViewer; +import forge.properties.NewConstants; +import forge.view.FView; + + +/** + *

CardReader class.

+ * + * Forked from forge.CardReader at rev 10010. + * + * @version $Id$ + */ +public class CardRulesReader + //implements Runnable, // NOPMD by Braids on 8/18/11 10:55 PM + implements NewConstants +{ + private static final String CARD_FILE_DOT_EXTENSION = ".txt"; // NOPMD by Braids on 8/18/11 11:04 PM + + /** Default charset when loading from files. */ + public static final String DEFAULT_CHARSET_NAME = "US-ASCII"; // NOPMD by Braids on 8/18/11 10:54 PM + + /** Regex that matches a single hyphen (-) or space. */ + public static final Pattern HYPHEN_OR_SPACE = Pattern.compile("[ -]"); + + /** Regex for punctuation that we omit from card file names. */ + public static final Pattern PUNCTUATION_TO_ZAP = Pattern.compile("[,'\"]"); // NOPMD by Braids on 8/18/11 10:54 PM + + /** Regex that matches two or more underscores (_). */ + public static final Pattern MULTIPLE_UNDERSCORES = Pattern.compile("__+"); // NOPMD by Braids on 8/18/11 10:54 PM + + /** Special value for estimatedFilesRemaining. */ + protected static final int UNKNOWN_NUMBER_OF_FILES_REMAINING = -1; // NOPMD by Braids on 8/18/11 10:54 PM + + private transient Map mapToFill; + private transient File cardsfolder; + + private transient ZipFile zip; + private transient Charset charset; + + private transient Enumeration zipEnum; + + private transient long estimatedFilesRemaining = // NOPMD by Braids on 8/18/11 10:56 PM + UNKNOWN_NUMBER_OF_FILES_REMAINING; + + private transient Iterable findNonDirsIterable; // NOPMD by Braids on 8/18/11 10:56 PM + + + + /** + * This is a convenience for CardReader(cardsfolder, mapToFill, true); . + * + * @param theCardsFolder indicates location of the cardsFolder + * + * @param theMapToFill maps card names to Card instances; this is where we + * place the cards once read + * + public CardReader(final File theCardsFolder, final Map theMapToFill) { + this(theCardsFolder, theMapToFill, true); + } + */ + + /** + *

Constructor for CardReader.

+ * + * @param theCardsFolder indicates location of the cardsFolder + * + * @param theMapToFill maps card names to Card instances; this is where we + * place the cards once read + * + * @param useZip if true, attempts to load cards from a zip file, if one exists. + public CardReader(final File theCardsFolder, final Map theMapToFill, final boolean useZip) { + if (theMapToFill == null) { + throw new NullPointerException("theMapToFill must not be null."); // NOPMD by Braids on 8/18/11 10:53 PM + } + this.mapToFill = theMapToFill; + + if (!theCardsFolder.exists()) { + throw new RuntimeException(// NOPMD by Braids on 8/18/11 10:53 PM + "CardReader : constructor error -- file not found -- filename is " + + theCardsFolder.getAbsolutePath()); + } + + if (!theCardsFolder.isDirectory()) { + throw new RuntimeException(// NOPMD by Braids on 8/18/11 10:53 PM + "CardReader : constructor error -- not a directory -- " + + theCardsFolder.getAbsolutePath()); + } + + this.cardsfolder = theCardsFolder; + + + final File zipFile = new File(theCardsFolder, "cardsfolder.zip"); + + // Prepare resources to read cards lazily. + if (useZip && zipFile.exists()) { + try { + this.zip = new ZipFile(zipFile); + } catch (Exception exn) { + System.err.println("Error reading zip file \"" // NOPMD by Braids on 8/18/11 10:53 PM + + zipFile.getAbsolutePath() + "\": " + exn + ". " + + "Defaulting to txt files in \"" + + theCardsFolder.getAbsolutePath() + + "\"." + ); + } + + } + + if (useZip && zip != null) { + zipEnum = zip.entries(); + estimatedFilesRemaining = zip.size(); + } + + setEncoding(DEFAULT_CHARSET_NAME); + + } //CardReader() + */ + + + /** + * This finalizer helps assure there is no memory or thread leak with + * findNonDirsIterable, which was created with YieldUtils.toIterable. + * + * @throws Throwable indirectly + */ + protected final void finalize() throws Throwable { + try { + if (findNonDirsIterable != null) { + for (@SuppressWarnings("unused") File ignored + : findNonDirsIterable) + { + // Do nothing; just exercising the Iterable. + } + } + } finally { + super.finalize(); + } + } + + + /** + * Reads the rest of ALL the cards into memory. This is not lazy. + public final void run() { + loadCardsUntilYouFind(null); + } + */ + + /** + * Starts reading cards into memory until the given card is found. + * + * After that, we save our place in the list of cards (on disk) in case we + * need to load more. + * + * @param cardName the name to find; if null, load all cards. + * + * @return the Card or null if it was not found. + protected final Card loadCardsUntilYouFind(final String cardName) { + Card result = null; + + // Try to retrieve card loading progress monitor model. + // If no progress monitor present, output results to console. + BaseProgressMonitor monitor = null; + final FView view = Singletons.getView(); + if (view != null) { + monitor = view.getCardLoadingProgressMonitor(); + } + + if (monitor == null) { + monitor = new StderrProgressMonitor(1, 0L); + } + + // Iterate through txt files or zip archive. + // Report relevant numbers to progress monitor model. + if (zip == null) { + if (estimatedFilesRemaining == UNKNOWN_NUMBER_OF_FILES_REMAINING) { + final Generator findNonDirsGen = new FindNonDirectoriesSkipDotDirectoriesGenerator(cardsfolder); + estimatedFilesRemaining = GeneratorFunctions.estimateSize(findNonDirsGen); + findNonDirsIterable = YieldUtils.toIterable(findNonDirsGen); + } + + monitor.setTotalUnitsThisPhase(estimatedFilesRemaining); + + for (File cardTxtFile : findNonDirsIterable) { + if (!cardTxtFile.getName().endsWith(CARD_FILE_DOT_EXTENSION)) { + monitor.incrementUnitsCompletedThisPhase(1L); + continue; + } + + result = loadCard(cardTxtFile); + monitor.incrementUnitsCompletedThisPhase(1L); + + if (cardName != null && cardName.equals(result.getName())) { + break; // no thread leak here if entire card DB is loaded, or if this object is finalized. + } + + } //endfor + + } else { + monitor.setTotalUnitsThisPhase(estimatedFilesRemaining); + ZipEntry entry; + + // zipEnum was initialized in the constructor. + while (zipEnum.hasMoreElements()) { + entry = (ZipEntry) zipEnum.nextElement(); + + if (entry.isDirectory() || !entry.getName().endsWith(CARD_FILE_DOT_EXTENSION)) { + monitor.incrementUnitsCompletedThisPhase(1L); + continue; + } + + result = loadCard(entry); + monitor.incrementUnitsCompletedThisPhase(1L); + + if (cardName != null && cardName.equals(result.getName())) { + break; + } + } + + } //endif + + return result; + } //loadCardsUntilYouFind(String) + */ + + + /** + *

Reads a line from the given reader and handles exceptions.

+ * + * @return a {@link java.lang.String} object. + * @param reader a {@link java.io.BufferedReader} object. + */ + public static String readLine(final BufferedReader reader) { + //makes the checked exception, into an unchecked runtime exception + try { + String line = reader.readLine(); + if (line != null) { + line = line.trim(); + } + return line; + } catch (Exception ex) { + ErrorViewer.showError(ex); + throw new RuntimeException("CardReader : readLine(Card) error", ex); // NOPMD by Braids on 8/18/11 10:53 PM + } + } //readLine(BufferedReader) + + /** + *

Load a card from an InputStream.

+ * + * @param txtFileLocator describes the location of the txt file we are + * parsing + * + * @param inputStream the stream from which to load the card's information + * + * @return the card loaded from the stream + * + * @throws CardParsingException if there is something wrong with the + * stream's contents + */ + protected final CardRules loadCard(final String txtFileLocator, final InputStream inputStream) + throws CardParsingException + { + int lineNum = 0; + + String cardName = null; + CardType cardType = null; + String manacost = null; + String ptLine = null; + String[] cardRules = null; + Map setsData = new TreeMap(); + boolean removedFromAIDecks = false; + boolean removedFromRandomDecks = false; + + InputStreamReader inputStreamReader = null; + BufferedReader reader = null; + try { + inputStreamReader = new InputStreamReader(inputStream, charset); + reader = new BufferedReader(inputStreamReader); + + while (true) { + String line = readLine(reader); + lineNum++; + + if (line.charAt(0) == '#') { + //no need to do anything, this indicates a comment line + continue; + + } else if (line.startsWith("Name:")) { + cardName = getValueAfterKey(line, "Name:"); + + if (cardName == null || cardName.isEmpty()) { + throw new CardParsingException(txtFileLocator, lineNum, "Card name is empty"); + } + + } else if (line.startsWith("ManaCost:")) { + final String value = getValueAfterKey(line, "ManaCost:"); + + if (!"no cost".equals(value)) { + manacost = value; + } + else { + assert manacost == null; + } + + } else if (line.startsWith("Types:")) { + final String value = getValueAfterKey(line, "Types:"); + + try { + cardType = CardType.parse(value); + } + catch (Throwable exn) { + throw new CardParsingException(txtFileLocator, lineNum, + "In Types: " + exn.getMessage(), exn); + } + + } else if (line.startsWith("Oracle:")) { + final String value = getValueAfterKey(line, "Oracle:"); + cardRules = value.split("\\n"); + + } else if (line.startsWith("PT:")) { + throwCPEIfPTIsNotNull(ptLine, txtFileLocator, lineNum); + ptLine = getValueAfterKey(line, "PT:"); + + } else if (line.startsWith("Loyalty:")) { + throwCPEIfPTIsNotNull(ptLine, txtFileLocator, lineNum); + ptLine = getValueAfterKey(line, "Loyalty:"); + + } else if (line.startsWith("SVar:RemAIDeck:")) { + final String value = getValueAfterKey(line, "SVar:RemAIDeck:"); + removedFromAIDecks = ("True".equalsIgnoreCase(value)); + + } else if (line.startsWith("SVar:RemRandomDeck:")) { + final String value = getValueAfterKey(line, "SVar:RemRandomDeck:"); + removedFromRandomDecks = ("True".equalsIgnoreCase(value)); + + } else if (line.startsWith("SetInfo:")) { + parseSetInfoLine(txtFileLocator, lineNum, line, setsData); + + } else if ("End".equals(line)) { + break; + } + + } // while true + + } finally { + try { + reader.close(); + } catch (IOException ignored) { // NOPMD by Braids on 8/18/11 11:08 PM + } + try { + inputStreamReader.close(); + } catch (IOException ignored) { // NOPMD by Braids on 8/18/11 11:08 PM + } + } + + try { + return new CardRules(cardName, cardType, manacost, ptLine, cardRules, setsData, removedFromRandomDecks, + removedFromAIDecks); + } + catch (Throwable exn) { + throw new CardParsingException(txtFileLocator, lineNum, + "Error constructing CardRules instance: " + exn.toString(), + exn); + } + } + + /** + * Parse a SetInfo line from a card txt file. + * + * @param txtFileLocator used in error messages + * @param lineNum used in error messages + * @param line must begin with "SetInfo:" + * @param setsData the current mapping of set names to CardInSet instances + * + * @throws CardParsingException if there is a problem parsing the line + */ + public static void parseSetInfoLine(final String txtFileLocator, final int lineNum, final String line, + final Map setsData) + throws CardParsingException + { + final int setCodeIx = 0; + final int rarityIx = 1; + final int numPicIx = 3; + + // Sample SetInfo line: + //SetInfo:POR|Land|http://magiccards.info/scans/en/po/203.jpg|4 + + final String value = line.substring("SetInfo:".length()); + final String[] pieces = value.split("\\|"); + + if (pieces.length <= rarityIx) { + throw new CardParsingException(txtFileLocator, lineNum, + "SetInfo line <<" + value + ">> has insufficient pieces"); + } + + final String setCode = pieces[setCodeIx]; + final String txtRarity = pieces[rarityIx]; + // pieces[2] is the magiccards.info URL for illustration #1, which we do not need. + int numIllustrations = 1; + + if (setsData.containsKey(setCode)) { + throw new CardParsingException(txtFileLocator, lineNum, + "Found multiple SetInfo lines for set code <<" + setCode + ">>"); + } + + if (pieces.length > numPicIx) { + try { + numIllustrations = Integer.parseInt(pieces[numPicIx]); + } + catch (NumberFormatException nfe) { + throw new CardParsingException(txtFileLocator, lineNum, + "Fourth item of SetInfo is not an integer in <<" + + value + ">>"); + } + + if (numIllustrations < 1) { + throw new CardParsingException(txtFileLocator, lineNum, + "Fourth item of SetInfo is not a positive integer, but" + + numIllustrations); + } + } + + CardRarity rarity = null; + if ("Land".equals(txtRarity)) { + rarity = CardRarity.BasicLand; + } + else if ("Common".equals(txtRarity)) { + rarity = CardRarity.Common; + } + else if ("Uncommon".equals(txtRarity)) { + rarity = CardRarity.Uncommon; + } + else if ("Rare".equals(txtRarity)) { + rarity = CardRarity.Rare; + } + else if ("Mythic".equals(txtRarity)) { + rarity = CardRarity.MythicRare; + } + else if ("Special".equals(txtRarity)) { + rarity = CardRarity.Special; + } + else { + throw new CardParsingException(txtFileLocator, lineNum, + "Unrecognized rarity string <<" + txtRarity + ">>"); + } + + CardInSet cardInSet = new CardInSet(rarity, numIllustrations); + + setsData.put(setCode, cardInSet); + } + + /** + * Test if ptLine is null; if it is not, throw a CardParsingException. + * + * @param ptLine the previously seen power/toughness or loyalty value, if any + * @param txtFileLocator describes location of the card's txt file + * @param lineNum the line number just read + * + * @throws CardParsingException iff ptLine is not null + */ + public static void throwCPEIfPTIsNotNull(final String ptLine, final String txtFileLocator, final int lineNum) + throws CardParsingException + { + if (ptLine != null) { + throw new CardParsingException(txtFileLocator, lineNum, + "more than one PT or Loyalty is present"); + } + } + + /** + * Parse the value from a card.txt line. + * + * Throws {@link IndexOutOfBoundsException} if fieldNameWithColon is not in line. + * + * @param line the raw line; may have newline at end + * + * @param fieldNameWithColon the field name with its colon, used to + * identify the key + * + * @return the value after the colon, with its leading and trailing + * whitespace removed + */ + public static String getValueAfterKey(final String line, final String fieldNameWithColon) { + final int startIx = fieldNameWithColon.length(); + final String lineAfterColon = line.substring(startIx); + return lineAfterColon.trim(); + } + + + /** + * Set the character encoding to use when loading cards. + * + * @param charsetName the name of the charset, for example, "UTF-8" + */ + public final void setEncoding(final String charsetName) { + this.charset = Charset.forName(charsetName); + } + + + /** + * Load a card from a txt file. + * + * @param pathToTxtFile the full or relative path to the file to load + * + * @return a new Card instance + protected final Card loadCard(final File pathToTxtFile) { + FileInputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(pathToTxtFile); + return loadCard(fileInputStream); + } catch (FileNotFoundException ex) { + ErrorViewer.showError(ex, "File \"%s\" exception", pathToTxtFile.getAbsolutePath()); + throw new RuntimeException(// NOPMD by Braids on 8/18/11 10:53 PM + "CardReader : run error -- file exception -- filename is " + + pathToTxtFile.getPath(), ex); + } finally { + try { + fileInputStream.close(); + } catch (IOException ignored) { // NOPMD by Braids on 8/18/11 11:08 PM + } + } + } + */ + + /** + * Load a card from an entry in a zip file. + * + * @param entry to load from + * + * @return a new Card instance + protected final Card loadCard(final ZipEntry entry) { + InputStream zipInputStream = null; + try { + zipInputStream = zip.getInputStream(entry); + return loadCard(zipInputStream); + + } catch (IOException exn) { + throw new RuntimeException(exn); // NOPMD by Braids on 8/18/11 10:53 PM + } finally { + try { + if (zipInputStream != null) { + zipInputStream.close(); + } + } catch (IOException ignored) { // NOPMD by Braids on 8/18/11 11:08 PM + } + } + } + */ + + + /** + * Attempt to guess what the path to a given card's txt file would be. + * + * @param asciiCardName the card name in canonicalized ASCII form + * + * @return the likeliest path of the card's txt file, excluding + * cardsFolder but including the subdirectory of that and the ".txt" + * suffix. For example, "e/elvish_warrior.txt" + * + * @see CardUtil#canonicalizeCardName + */ + public final String toMostLikelyPath(final String asciiCardName) { + String baseFileName = asciiCardName; + + /* + * friarsol wrote: "hyphens and spaces are converted to underscores, + * commas and apostrophes are removed (I'm not sure if there are any + * other punctuation used)." + * + * @see http://www.slightlymagic.net/forum/viewtopic.php?f=52&t=4887#p63189 + */ + + baseFileName = HYPHEN_OR_SPACE.matcher(baseFileName).replaceAll("_"); + baseFileName = MULTIPLE_UNDERSCORES.matcher(baseFileName).replaceAll("_"); + baseFileName = PUNCTUATION_TO_ZAP.matcher(baseFileName).replaceAll(""); + + // Place the file within a single-letter subdirectory. + final StringBuffer buf = new StringBuffer(1 + 1 + baseFileName.length() + CARD_FILE_DOT_EXTENSION.length()); + buf.append(Character.toLowerCase(baseFileName.charAt(0))); + + // Zip file is always created with unix-style path names. + buf.append('/'); + + buf.append(baseFileName.toLowerCase(Locale.ENGLISH)); + buf.append(CARD_FILE_DOT_EXTENSION); + + return buf.toString(); + } + + /** + * Attempt to load a card by its canonical ASCII name. + * + * @param canonicalASCIIName the canonical ASCII name of the card + * + * @return a new Card instance having that name, or null if not found + public final Card findCard(final String canonicalASCIIName) { // NOPMD by Braids on 8/18/11 11:08 PM + UtilFunctions.checkNotNull("canonicalASCIIName", canonicalASCIIName); + + final String cardFilePath = toMostLikelyPath(canonicalASCIIName); + + Card result = null; + + if (zip != null) { + final ZipEntry entry = zip.getEntry(cardFilePath); + + if (entry != null) { + result = loadCard(entry); + } + } + + if (result == null) { + result = loadCard(new File(cardsfolder, cardFilePath)); + } + + if (result == null || !(result.getName().equals(canonicalASCIIName))) { + //System.err.println(":Could not find \"" + cardFilePath + "\"."); + result = loadCardsUntilYouFind(canonicalASCIIName); + } + + return result; + } + */ +} diff --git a/src/main/java/forge/card/MtgDataParser.java b/src/main/java/forge/card/MtgDataParser.java index bff90388b96..880ec6fb791 100644 --- a/src/main/java/forge/card/MtgDataParser.java +++ b/src/main/java/forge/card/MtgDataParser.java @@ -118,7 +118,9 @@ public final class MtgDataParser implements Iterator { if (sets.isEmpty()) { return null; } // that was a bad card - it won't be added by invoker - return new CardRules(name, type, manaCost, ptOrLoyalty, strs.toArray(emptyArray), sets); + return new CardRules(name, type, manaCost, ptOrLoyalty, strs.toArray(emptyArray), sets, + // TODO: fix last two parameters + false, false); } private Map getValidEditions(final String sets, final boolean isBasicLand) {