Merge pull request #5501 from hitch17/add-cockatrice-deck-format-5493

UI: add cockatrice deck format support for import (*.cod)
This commit is contained in:
Oleg Agafonov 2019-01-11 07:17:48 +04:00 committed by GitHub
commit 11f93cf762
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 584 additions and 162 deletions

View file

@ -0,0 +1,16 @@
package mage.cards.decks.importer;
import java.util.Optional;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
public class CardLookup {
public static final CardLookup instance = new CardLookup();
public Optional<CardInfo> lookupCardInfo(String name) {
return Optional.ofNullable(CardRepository.instance.findPreferedCoreExpansionCard(name, true));
}
}

View file

@ -0,0 +1,117 @@
package mage.cards.decks.importer;
import static javax.xml.xpath.XPathConstants.NODESET;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import mage.cards.decks.DeckCardInfo;
import mage.cards.decks.DeckCardLists;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
public class CodDeckImporter extends DeckImporter {
private XPathFactory xpathFactory = XPathFactory.newInstance();
private DocumentBuilder builder = getDocumentBuilder();
@Override
public DeckCardLists importDeck(String filename, StringBuilder errorMessages) {
try {
Document doc = getXmlDocument(filename);
DeckCardLists decklist = new DeckCardLists();
List<Node> mainCards = getNodes(doc, "/cockatrice_deck/zone[@name='main']/card");
decklist.setCards(mainCards.stream()
.flatMap(toDeckCardInfo(getCardLookup(), errorMessages))
.collect(Collectors.toList()));
List<Node> sideboardCards = getNodes(doc, "/cockatrice_deck/zone[@name='side']/card");
decklist.setSideboard(sideboardCards.stream()
.flatMap(toDeckCardInfo(getCardLookup(), errorMessages))
.collect(Collectors.toList()));
getNodes(doc, "/cockatrice_deck/deckname")
.forEach(n -> decklist.setName(n.getTextContent().trim()));
return decklist;
} catch (Exception e) {
logger.error("Error loading deck", e);
errorMessages.append("There was an error loading the deck.");
return new DeckCardLists();
}
}
private static int getQuantityFromNode(Node node) {
Node numberNode = node.getAttributes().getNamedItem("number");
if (numberNode == null) {
return 1;
}
try {
return Math.min(100, Math.max(1, Integer.parseInt(numberNode.getNodeValue())));
} catch (NumberFormatException e) {
return 1;
}
}
private static Function<Node, Stream<DeckCardInfo>> toDeckCardInfo(CardLookup lookup, StringBuilder errors) {
return node -> {
String name = node.getAttributes().getNamedItem("name").getNodeValue().trim();
Optional<CardInfo> cardInfo = lookup.lookupCardInfo(name);
if (cardInfo.isPresent()) {
CardInfo info = cardInfo.get();
return Collections.nCopies(
getQuantityFromNode(node),
new DeckCardInfo(info.getName(), info.getCardNumber(), info.getSetCode())).stream();
} else {
errors.append("Could not find card: '").append(name).append("'\n");
return Stream.empty();
}
};
}
private List<Node> getNodes(Document doc, String xpathExpression) throws XPathExpressionException {
NodeList nodes = (NodeList) xpathFactory.newXPath().evaluate(xpathExpression, doc, NODESET);
ArrayList<Node> list = new ArrayList<>();
for (int i = 0; i < nodes.getLength(); i++) {
list.add(nodes.item(i));
}
return list;
}
private DocumentBuilder getDocumentBuilder() {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
return factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException();
}
}
protected Document getXmlDocument(String filename) throws IOException {
try {
return builder.parse(new File(filename));
} catch (SAXException e) {
throw new IOException(e);
}
}
}

View file

@ -15,7 +15,7 @@ import mage.cards.repository.CardRepository;
*
* @author North
*/
public class DckDeckImporter extends DeckImporter {
public class DckDeckImporter extends PlainTextDeckImporter {
private static final Pattern pattern = Pattern.compile("(SB:)?\\s*(\\d*)\\s*\\[([^]:]+):([^]:]+)\\]\\s*(.*)\\s*$");

View file

@ -1,6 +1,8 @@
package mage.cards.decks.importer;
import java.util.Optional;
import mage.cards.decks.DeckCardInfo;
import mage.cards.decks.DeckCardLists;
import mage.cards.repository.CardInfo;
@ -10,7 +12,7 @@ import mage.cards.repository.CardRepository;
*
* @author BetaSteward_at_googlemail.com
*/
public class DecDeckImporter extends DeckImporter {
public class DecDeckImporter extends PlainTextDeckImporter {
@Override
protected void readLine(String line, DeckCardLists deckList) {
@ -29,10 +31,11 @@ public class DecDeckImporter extends DeckImporter {
String lineName = line.substring(delim).trim();
try {
int num = Integer.parseInt(lineNum);
CardInfo cardInfo = CardRepository.instance.findPreferedCoreExpansionCard(lineName, true);
if (cardInfo == null) {
Optional<CardInfo> cardLookup = getCardLookup().lookupCardInfo(lineName);
if (!cardLookup.isPresent()) {
sbMessage.append("Could not find card: '").append(lineName).append("' at line ").append(lineCount).append('\n');
} else {
CardInfo cardInfo = cardLookup.get();
for (int i = 0; i < num; i++) {
if (!sideboard) {
deckList.getCards().add(new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode()));

View file

@ -1,74 +1,87 @@
package mage.cards.decks.importer;
import mage.cards.decks.DeckCardLists;
import org.apache.log4j.Logger;
import java.io.File;
import java.util.Locale;
import java.util.Optional;
import java.util.Scanner;
/**
*
* @author BetaSteward_at_googlemail.com
*/
import mage.cards.decks.DeckCardLists;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
public abstract class DeckImporter {
private static final Logger logger = Logger.getLogger(DeckImporter.class);
protected static final Logger logger = Logger.getLogger(DeckImporter.class);
protected StringBuilder sbMessage = new StringBuilder(); //TODO we should stop using this not garbage collectable StringBuilder. It just bloats
protected int lineCount;
private static final String[] SIDEBOARD_MARKS = new String[]{"//sideboard", "sb: "};
public static DeckImporter getDeckImporter(String file) {
if (file == null) {
return null;
} if (file.toLowerCase(Locale.ENGLISH).endsWith("dec")) {
return new DecDeckImporter();
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("mwdeck")) {
return new MWSDeckImporter();
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("txt")) {
return new TxtDeckImporter(haveSideboardSection(file));
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("dck")) {
return new DckDeckImporter();
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("dek")) {
return new DekDeckImporter();
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("cod")) {
return new CodDeckImporter();
} else {
return null;
}
}
/**
*
* @param file file to import
* @param errorMessages you can setup output messages to showup to user (set null for fatal exception on messages.count > 0)
* @return decks list
*/
public DeckCardLists importDeck(String file, StringBuilder errorMessages) {
File f = new File(file);
DeckCardLists deckList = new DeckCardLists();
if (!f.exists()) {
logger.warn("Deckfile " + file + " not found.");
return deckList;
public static DeckCardLists importDeckFromFile(String file) {
return importDeckFromFile(file, new StringBuilder());
}
public static DeckCardLists importDeckFromFile(String file, StringBuilder errorMessages) {
DeckImporter deckImporter = getDeckImporter(file);
if (deckImporter != null) {
return deckImporter.importDeck(file, errorMessages);
} else {
return new DeckCardLists();
}
}
public abstract DeckCardLists importDeck(String file, StringBuilder errorMessages);
public DeckCardLists importDeck(String file) {
return importDeck(file, new StringBuilder());
}
public CardLookup getCardLookup() {
return CardLookup.instance;
}
private static boolean haveSideboardSection(String file) {
// search for sideboard section:
// or //sideboard
// or SB: 1 card name -- special deckstats.net
File f = new File(file);
try (Scanner scanner = new Scanner(f)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim().toLowerCase(Locale.ENGLISH);
for (String mark : SIDEBOARD_MARKS) {
if (line.startsWith(mark)) {
return true;
}
}
lineCount = 0;
sbMessage.setLength(0);
try {
try (Scanner scanner = new Scanner(f)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
lineCount++;
readLine(line, deckList);
}
if (sbMessage.length() > 0) {
if(errorMessages != null) {
// normal output for user
errorMessages.append(sbMessage);
}else{
// fatal error
logger.fatal(sbMessage);
}
}
} catch (Exception ex) {
logger.fatal(null, ex);
}
} catch (Exception ex) {
logger.fatal(null, ex);
}
return deckList;
}
} catch (Exception e) {
// ignore error, deckimporter will process it
}
public DeckCardLists importDeck(String file) {
return importDeck(file, null);
}
// not found
return false;
}
public String getErrors(){
return sbMessage.toString();
}
protected abstract void readLine(String line, DeckCardLists deckList);
}

View file

@ -1,71 +0,0 @@
package mage.cards.decks.importer;
import java.io.File;
import java.util.Locale;
import java.util.Scanner;
import mage.cards.decks.DeckCardLists;
/**
*
* @author North
*/
public final class DeckImporterUtil {
private static final String[] SIDEBOARD_MARKS = new String[]{"//sideboard", "sb: "};
public static boolean haveSideboardSection(String file) {
// search for sideboard section:
// or //sideboard
// or SB: 1 card name -- special deckstats.net
File f = new File(file);
try (Scanner scanner = new Scanner(f)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim().toLowerCase(Locale.ENGLISH);
for (String mark : SIDEBOARD_MARKS) {
if (line.startsWith(mark)) {
return true;
}
}
}
} catch (Exception e) {
// ignore error, deckimporter will process it
}
// not found
return false;
}
public static DeckImporter getDeckImporter(String file) {
if (file == null) {
return null;
} if (file.toLowerCase(Locale.ENGLISH).endsWith("dec")) {
return new DecDeckImporter();
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("mwdeck")) {
return new MWSDeckImporter();
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("txt")) {
return new TxtDeckImporter(haveSideboardSection(file));
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("dck")) {
return new DckDeckImporter();
} else if (file.toLowerCase(Locale.ENGLISH).endsWith("dek")) {
return new DekDeckImporter();
} else {
return null;
}
}
public static DeckCardLists importDeck(String file, StringBuilder errorMessages) {
DeckImporter deckImporter = getDeckImporter(file);
if (deckImporter != null) {
return deckImporter.importDeck(file, errorMessages);
} else {
return new DeckCardLists();
}
}
public static DeckCardLists importDeck(String file) {
return importDeck(file, null);
}
}

View file

@ -8,7 +8,7 @@ import mage.cards.repository.CardRepository;
/**
* Created by royk on 11-Sep-16.
*/
public class DekDeckImporter extends DeckImporter {
public class DekDeckImporter extends PlainTextDeckImporter {
@Override
protected void readLine(String line, DeckCardLists deckList) {

View file

@ -14,7 +14,7 @@ import mage.util.RandomUtil;
*
* @author BetaSteward_at_googlemail.com
*/
public class MWSDeckImporter extends DeckImporter {
public class MWSDeckImporter extends PlainTextDeckImporter {
@Override
protected void readLine(String line, DeckCardLists deckList) {

View file

@ -0,0 +1,67 @@
package mage.cards.decks.importer;
import java.io.File;
import java.util.Scanner;
import mage.cards.decks.DeckCardLists;
/**
*
* @author BetaSteward_at_googlemail.com
*/
public abstract class PlainTextDeckImporter extends DeckImporter {
protected StringBuilder sbMessage = new StringBuilder(); //TODO we should stop using this not garbage collectable StringBuilder. It just bloats
protected int lineCount;
/**
*
* @param file file to import
* @param errorMessages you can setup output messages to showup to user (set null for fatal exception on messages.count > 0)
* @return decks list
*/
public DeckCardLists importDeck(String file, StringBuilder errorMessages) {
File f = new File(file);
DeckCardLists deckList = new DeckCardLists();
if (!f.exists()) {
logger.warn("Deckfile " + file + " not found.");
return deckList;
}
lineCount = 0;
sbMessage.setLength(0);
try {
try (Scanner scanner = new Scanner(f)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
lineCount++;
readLine(line, deckList);
}
if (sbMessage.length() > 0) {
if(errorMessages != null) {
// normal output for user
errorMessages.append(sbMessage);
}else{
// fatal error
logger.fatal(sbMessage);
}
}
} catch (Exception ex) {
logger.fatal(null, ex);
}
} catch (Exception ex) {
logger.fatal(null, ex);
}
return deckList;
}
public DeckCardLists importDeck(String file) {
return importDeck(file, null);
}
protected abstract void readLine(String line, DeckCardLists deckList);
}

View file

@ -14,7 +14,7 @@ import mage.cards.repository.CardRepository;
*
* @author BetaSteward_at_googlemail.com
*/
public class TxtDeckImporter extends DeckImporter {
public class TxtDeckImporter extends PlainTextDeckImporter {
private static final String[] SET_VALUES = new String[]{"lands", "creatures", "planeswalkers", "other spells", "sideboard cards",
"Instant", "Land", "Enchantment", "Artifact", "Sorcery", "Planeswalker", "Creature"};