forked from External/mage
refactor: improved deck import, added docs and miss tests for dek-files;
This commit is contained in:
parent
124d60e2b7
commit
889c1125e8
32 changed files with 238 additions and 124 deletions
|
|
@ -38,13 +38,13 @@ public class MtgArenaDeckExporter extends DeckExporter {
|
|||
for (DeckCardInfo card : sourceCards) {
|
||||
String setCode = card.getSetCode().toUpperCase(Locale.ENGLISH);
|
||||
setCode = SET_CODE_REPLACEMENTS.getOrDefault(setCode, setCode);
|
||||
String name = card.getCardName() + " (" + setCode + ") " + card.getCardNum();
|
||||
String name = card.getCardName() + " (" + setCode + ") " + card.getCardNumber();
|
||||
String code = prefix + name;
|
||||
int curAmount = amount.getOrDefault(code, 0);
|
||||
if (curAmount == 0) {
|
||||
res.add(name);
|
||||
}
|
||||
amount.put(code, curAmount + card.getQuantity());
|
||||
amount.put(code, curAmount + card.getAmount());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public class MtgOnlineDeckExporter extends DeckExporter {
|
|||
if (curAmount == 0) {
|
||||
res.add(card.getCardName());
|
||||
}
|
||||
amount.put(code, curAmount + card.getQuantity());
|
||||
amount.put(code, curAmount + card.getAmount());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ public class XmageDeckExporter extends DeckExporter {
|
|||
if (curAmount == 0) {
|
||||
deckMain.add(card);
|
||||
}
|
||||
amount.put(code, curAmount + card.getQuantity());
|
||||
amount.put(code, curAmount + card.getAmount());
|
||||
}
|
||||
// sideboard
|
||||
for (DeckCardInfo card : deck.getSideboard()) {
|
||||
|
|
@ -49,15 +49,15 @@ public class XmageDeckExporter extends DeckExporter {
|
|||
if (curAmount == 0) {
|
||||
deckSideboard.add(card);
|
||||
}
|
||||
amount.put(code, curAmount + card.getQuantity());
|
||||
amount.put(code, curAmount + card.getAmount());
|
||||
}
|
||||
|
||||
// cards print
|
||||
for (DeckCardInfo card : deckMain) {
|
||||
out.printf("%d [%s:%s] %s%n", amount.get("M@" + card.getCardKey()), card.getSetCode(), card.getCardNum(), card.getCardName());
|
||||
out.printf("%d [%s:%s] %s%n", amount.get("M@" + card.getCardKey()), card.getSetCode(), card.getCardNumber(), card.getCardName());
|
||||
}
|
||||
for (DeckCardInfo card : deckSideboard) {
|
||||
out.printf("SB: %d [%s:%s] %s%n", amount.get("S@" + card.getCardKey()), card.getSetCode(), card.getCardNum(), card.getCardName());
|
||||
out.printf("SB: %d [%s:%s] %s%n", amount.get("S@" + card.getCardKey()), card.getSetCode(), card.getCardNumber(), card.getCardName());
|
||||
}
|
||||
|
||||
// layout print
|
||||
|
|
@ -86,7 +86,7 @@ public class XmageDeckExporter extends DeckExporter {
|
|||
out.print("(");
|
||||
for (int i = 0; i < stack.size(); ++i) {
|
||||
DeckCardInfo info = stack.get(i);
|
||||
out.printf("[%s:%s]", info.getSetCode(), info.getCardNum());
|
||||
out.printf("[%s:%s]", info.getSetCode(), info.getCardNumber());
|
||||
if (i != stack.size() - 1) {
|
||||
out.print(",");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ public class XmageInfoDeckExporter extends DeckExporter {
|
|||
if (curAmount == 0) {
|
||||
deckMain.add(card);
|
||||
}
|
||||
amount.put(code, curAmount + card.getQuantity());
|
||||
amount.put(code, curAmount + card.getAmount());
|
||||
}
|
||||
|
||||
// Sideboard
|
||||
|
|
@ -53,16 +53,16 @@ public class XmageInfoDeckExporter extends DeckExporter {
|
|||
if (curAmount == 0) {
|
||||
deckSideboard.add(card);
|
||||
}
|
||||
amount.put(code, curAmount + card.getQuantity());
|
||||
amount.put(code, curAmount + card.getAmount());
|
||||
}
|
||||
|
||||
// Cards print
|
||||
for (DeckCardInfo card : deckMain) {
|
||||
CardInfo cardInfo = CardRepository.instance.findCard(card.getCardName());
|
||||
if (cardInfo == null) {
|
||||
out.printf("%d [%s:%s] %s%n\n", amount.get("M@" + card.getCardKey()), card.getSetCode(), card.getCardNum(), card.getCardName());
|
||||
out.printf("%d [%s:%s] %s%n\n", amount.get("M@" + card.getCardKey()), card.getSetCode(), card.getCardNumber(), card.getCardName());
|
||||
} else {
|
||||
out.printf("%d [%s:%s] %s ;; %s ;; %s ;; %d %n", amount.get("M@" + card.getCardKey()), card.getSetCode(), card.getCardNum(), card.getCardName(),
|
||||
out.printf("%d [%s:%s] %s ;; %s ;; %s ;; %d %n", amount.get("M@" + card.getCardKey()), card.getSetCode(), card.getCardNumber(), card.getCardName(),
|
||||
cardInfo.getColor().getDescription(), cardInfo.getTypes().toString(), cardInfo.getManaValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -70,9 +70,9 @@ public class XmageInfoDeckExporter extends DeckExporter {
|
|||
for (DeckCardInfo card : deckSideboard) {
|
||||
CardInfo cardInfo = CardRepository.instance.findCard(card.getCardName());
|
||||
if (cardInfo == null) {
|
||||
out.printf("SB: %d [%s:%s] %s%n\n", amount.get("S@" + card.getCardKey()), card.getSetCode(), card.getCardNum(), card.getCardName());
|
||||
out.printf("SB: %d [%s:%s] %s%n\n", amount.get("S@" + card.getCardKey()), card.getSetCode(), card.getCardNumber(), card.getCardName());
|
||||
} else {
|
||||
out.printf("SB: %d [%s:%s] %s ;; %s ;; %s ;; %d %n", amount.get("S@" + card.getCardKey()), card.getSetCode(), card.getCardNum(), card.getCardName(),
|
||||
out.printf("SB: %d [%s:%s] %s ;; %s ;; %s ;; %d %n", amount.get("S@" + card.getCardKey()), card.getSetCode(), card.getCardNumber(), card.getCardName(),
|
||||
cardInfo.getColor().getDescription(), cardInfo.getTypes().toString(), cardInfo.getManaValue());
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ public class XmageInfoDeckExporter extends DeckExporter {
|
|||
out.print("(");
|
||||
for (int i = 0; i < stack.size(); ++i) {
|
||||
DeckCardInfo info = stack.get(i);
|
||||
out.printf("[%s:%s]", info.getSetCode(), info.getCardNum());
|
||||
out.printf("[%s:%s]", info.getSetCode(), info.getCardNumber());
|
||||
if (i != stack.size() - 1) {
|
||||
out.print(",");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import mage.cards.repository.CardRepository;
|
|||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Deck import: helper class to mock cards repository
|
||||
*/
|
||||
public class CardLookup {
|
||||
|
||||
public static final CardLookup instance = new CardLookup();
|
||||
|
|
|
|||
|
|
@ -13,12 +13,15 @@ import java.util.function.Function;
|
|||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Deck import: Cockatrice app
|
||||
*/
|
||||
public class CodDeckImporter extends XmlDeckImporter {
|
||||
|
||||
/**
|
||||
* @param filename
|
||||
* @param fileName
|
||||
* @param errorMessages
|
||||
* @param saveAutoFixedFile do not supported for current format
|
||||
* @param saveAutoFixedFile do not support for current format
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
|
|
@ -43,7 +46,7 @@ public class CodDeckImporter extends XmlDeckImporter {
|
|||
return decklist;
|
||||
} catch (Exception e) {
|
||||
logger.error("Error loading deck", e);
|
||||
errorMessages.append("There was an error loading the deck.");
|
||||
errorMessages.append("There was an error loading the deck: " + e.getMessage());
|
||||
return new DeckCardLists();
|
||||
}
|
||||
}
|
||||
|
|
@ -66,9 +69,12 @@ public class CodDeckImporter extends XmlDeckImporter {
|
|||
Optional<CardInfo> cardInfo = lookup.lookupCardInfo(name);
|
||||
if (cardInfo.isPresent()) {
|
||||
CardInfo info = cardInfo.get();
|
||||
int amount = getQuantityFromNode(node);
|
||||
DeckCardInfo.makeSureCardAmountFine(amount, info.getName());
|
||||
return Collections.nCopies(
|
||||
getQuantityFromNode(node),
|
||||
new DeckCardInfo(info.getName(), info.getCardNumber(), info.getSetCode())).stream();
|
||||
amount,
|
||||
new DeckCardInfo(info.getName(), info.getCardNumber(), info.getSetCode())
|
||||
).stream();
|
||||
} else {
|
||||
errors.append("Could not find card: '").append(name).append("'\n");
|
||||
return Stream.empty();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Original xmage's deck format (uses by deck editor)
|
||||
* Deck import: native xmage format (uses by deck editor)
|
||||
*
|
||||
* @author North
|
||||
*/
|
||||
|
|
@ -39,7 +39,7 @@ public class DckDeckImporter extends PlainTextDeckImporter {
|
|||
if (line.isEmpty() || line.startsWith("#")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
line = CardNameUtil.normalizeCardName(line);
|
||||
|
||||
// AUTO-FIX apply (if card number was fixed before then it can be replaced in layout or other lines too)
|
||||
|
|
@ -61,6 +61,8 @@ public class DckDeckImporter extends PlainTextDeckImporter {
|
|||
String cardNum = m.group(4);
|
||||
String cardName = m.group(5);
|
||||
|
||||
DeckCardInfo.makeSureCardAmountFine(count, cardName);
|
||||
|
||||
cardNum = cardNum == null ? "" : cardNum.trim();
|
||||
setCode = setCode == null ? "" : setCode.trim();
|
||||
cardName = cardName == null ? "" : cardName.trim();
|
||||
|
|
@ -128,9 +130,9 @@ public class DckDeckImporter extends PlainTextDeckImporter {
|
|||
if (deckCardInfo != null) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (!sideboard) {
|
||||
deckList.getCards().add(deckCardInfo);
|
||||
deckList.getCards().add(deckCardInfo.copy());
|
||||
} else {
|
||||
deckList.getSideboard().add(deckCardInfo);
|
||||
deckList.getSideboard().add(deckCardInfo.copy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import mage.cards.repository.CardInfo;
|
|||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Deck import: Decked Builder, Apprentice and old Magic Online
|
||||
* <p>
|
||||
* Outdated, see actual format in TxtDeckImporter
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class DecDeckImporter extends PlainTextDeckImporter {
|
||||
|
|
@ -34,11 +38,13 @@ public class DecDeckImporter extends PlainTextDeckImporter {
|
|||
sbMessage.append("Could not find card: '").append(lineName).append("' at line ").append(lineCount).append('\n');
|
||||
} else {
|
||||
CardInfo cardInfo = cardLookup.get();
|
||||
DeckCardInfo.makeSureCardAmountFine(num, cardInfo.getName());
|
||||
DeckCardInfo deckCardInfo = new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode());
|
||||
for (int i = 0; i < num; i++) {
|
||||
if (!sideboard) {
|
||||
deckList.getCards().add(new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode()));
|
||||
if (sideboard) {
|
||||
deckList.getSideboard().add(deckCardInfo.copy());
|
||||
} else {
|
||||
deckList.getSideboard().add(new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode()));
|
||||
deckList.getCards().add(deckCardInfo.copy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import java.io.File;
|
|||
import java.util.Locale;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Deck import: base class for all importers
|
||||
*/
|
||||
public abstract class DeckImporter {
|
||||
|
||||
public static class FixedInfo {
|
||||
|
|
|
|||
|
|
@ -3,34 +3,45 @@ package mage.cards.decks.importer;
|
|||
import mage.cards.decks.DeckCardInfo;
|
||||
import mage.cards.decks.DeckCardLists;
|
||||
import mage.cards.repository.CardInfo;
|
||||
import mage.cards.repository.CardRepository;
|
||||
|
||||
/**
|
||||
* Created by royk on 11-Sep-16.
|
||||
* Deck import: MTGO xml format
|
||||
* <p>
|
||||
* Outdated, see actual format in TxtDeckImporter
|
||||
*
|
||||
* @author royk
|
||||
*/
|
||||
public class DekDeckImporter extends PlainTextDeckImporter {
|
||||
|
||||
@Override
|
||||
protected void readLine(String line, DeckCardLists deckList, FixedInfo fixedInfo) {
|
||||
|
||||
if (line.isEmpty() || line.startsWith("#") || !line.contains("<Cards CatID")) {
|
||||
if (line.isEmpty() || line.startsWith("#") || !line.contains("<Cards ")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// e.g. <Cards CatID="61202" Quantity="1" Sideboard="false" Name="Vildin-Pack Outcast" />
|
||||
Integer cardCount = Integer.parseInt(extractAttribute(line, "Quantity"));
|
||||
String cardName = extractAttribute(line, "Name");
|
||||
DeckCardInfo.makeSureCardAmountFine(cardCount, cardName);
|
||||
|
||||
// fix double faces name to be compatible with xmage
|
||||
// Refuse/Cooperate -> Refuse // Cooperate
|
||||
if (!cardName.contains("//") && cardName.contains("/")) {
|
||||
cardName = cardName.replace("/", " // ");
|
||||
}
|
||||
|
||||
boolean isSideboard = "true".equals(extractAttribute(line, "Sideboard"));
|
||||
CardInfo cardInfo = CardRepository.instance.findPreferredCoreExpansionCard(cardName);
|
||||
CardInfo cardInfo = getCardLookup().lookupCardInfo(cardName).orElse(null);
|
||||
if (cardInfo == null) {
|
||||
sbMessage.append("Could not find card: '").append(cardName).append("' at line ").append(lineCount).append('\n');
|
||||
} else {
|
||||
DeckCardInfo deckCardInfo = new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode());
|
||||
for (int i = 0; i < cardCount; i++) {
|
||||
DeckCardInfo deckCardInfo = new DeckCardInfo(cardInfo.getName(), cardInfo.getCardNumber(), cardInfo.getSetCode());
|
||||
if (isSideboard) {
|
||||
deckList.getSideboard().add(deckCardInfo);
|
||||
deckList.getSideboard().add(deckCardInfo.copy());
|
||||
} else {
|
||||
deckList.getCards().add(deckCardInfo);
|
||||
deckList.getCards().add(deckCardInfo.copy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import mage.cards.repository.CardInfo;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Deck import: xmage draft logs
|
||||
*/
|
||||
public class DraftLogImporter extends PlainTextDeckImporter {
|
||||
|
||||
private static final Pattern SET_PATTERN = Pattern.compile("------ (\\p{Alnum}+) ------$");
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
package mage.cards.decks.importer;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
import mage.cards.decks.DeckCardLists;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
|
||||
/**
|
||||
* @author github: timhae
|
||||
* Deck import: helper class for all json base formats
|
||||
* TODO: improve files structure
|
||||
*
|
||||
* @author timhae
|
||||
*/
|
||||
public abstract class JsonDeckImporter extends DeckImporter {
|
||||
|
||||
protected StringBuilder sbMessage = new StringBuilder();
|
||||
|
||||
/**
|
||||
* @param fileName file to import
|
||||
* @param fileName file to import
|
||||
* @param errorMessages you can setup output messages to showup to user
|
||||
* @param saveAutoFixedFile do not supported for that format
|
||||
* @return decks list
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import mage.cards.decks.DeckCardLists;
|
|||
import mage.cards.repository.CardInfo;
|
||||
|
||||
/**
|
||||
* Deck import: Magic Workstation app
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public class MWSDeckImporter extends PlainTextDeckImporter {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import mage.cards.repository.CardInfo;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static mage.cards.decks.CardNameUtil.CARD_NAME_PATTERN;
|
||||
|
||||
/**
|
||||
* Deck import: MTGA official app
|
||||
*/
|
||||
public class MtgaImporter extends PlainTextDeckImporter {
|
||||
|
||||
private static final Map<String, String> SET_REMAPPING = ImmutableMap.of("DAR", "DOM");
|
||||
|
|
@ -32,9 +34,9 @@ public class MtgaImporter extends PlainTextDeckImporter {
|
|||
|
||||
@Override
|
||||
protected void readLine(String line, DeckCardLists deckList, FixedInfo fixedInfo) {
|
||||
|
||||
|
||||
line = line.trim();
|
||||
|
||||
|
||||
if (line.equals("Deck")) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -45,30 +47,33 @@ public class MtgaImporter extends PlainTextDeckImporter {
|
|||
}
|
||||
|
||||
Matcher pattern = MTGA_PATTERN.matcher(CardNameUtil.normalizeCardName(line));
|
||||
|
||||
|
||||
if (!pattern.matches()) {
|
||||
sbMessage.append("Error reading '").append(line).append("'\n");
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<CardInfo> found;
|
||||
|
||||
CardInfo found;
|
||||
int count = Integer.parseInt(pattern.group(1));
|
||||
String name = pattern.group(2);
|
||||
String name = pattern.group(2);
|
||||
if (pattern.group(3) != null && pattern.group(4) != null) {
|
||||
String set = SET_REMAPPING.getOrDefault(pattern.group(3), pattern.group(3));
|
||||
String cardNumber = pattern.group(4);
|
||||
found = lookup.lookupCardInfo(name, set, cardNumber);
|
||||
found = lookup.lookupCardInfo(name, set, cardNumber).orElse(null);
|
||||
} else {
|
||||
found = lookup.lookupCardInfo(name);
|
||||
found = lookup.lookupCardInfo(name).orElse(null);
|
||||
}
|
||||
|
||||
if (!found.isPresent()) {
|
||||
sbMessage.append("Cound not find card for '").append(line).append("'\n");
|
||||
} else {
|
||||
final List<DeckCardInfo> zone = sideboard ? deckList.getSideboard() : deckList.getCards();
|
||||
found.ifPresent(card -> zone.addAll(Collections.nCopies(count,
|
||||
new DeckCardInfo(card.getName(), card.getCardNumber(), card.getSetCode()))));
|
||||
|
||||
if (found == null) {
|
||||
sbMessage.append("Could not find card for '").append(line).append("'\n");
|
||||
return;
|
||||
}
|
||||
|
||||
DeckCardInfo.makeSureCardAmountFine(count, found.getName());
|
||||
|
||||
List<DeckCardInfo> zone = sideboard ? deckList.getSideboard() : deckList.getCards();
|
||||
DeckCardInfo deckCardInfo = new DeckCardInfo(found.getName(), found.getCardNumber(), found.getSetCode());
|
||||
zone.addAll(Collections.nCopies(count, deckCardInfo.copy()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import java.util.Optional;
|
|||
|
||||
|
||||
/**
|
||||
* @author github: timhae
|
||||
* Deck import: mtgjson service
|
||||
*
|
||||
* @author timhae
|
||||
*/
|
||||
public class MtgjsonDeckImporter extends JsonDeckImporter {
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import java.util.function.Function;
|
|||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Deck import: OCTGN app
|
||||
*/
|
||||
public class O8dDeckImporter extends XmlDeckImporter {
|
||||
|
||||
/**
|
||||
|
|
@ -72,5 +75,4 @@ public class O8dDeckImporter extends XmlDeckImporter {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import java.util.List;
|
|||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* Deck import: helper class for all text base formats
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
public abstract class PlainTextDeckImporter extends DeckImporter {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import java.util.Locale;
|
|||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
* Deck import: text deck, compatible with MTGO and many other apps/services
|
||||
*
|
||||
* @author BetaSteward_at_googlemail.com, JayDi85
|
||||
*/
|
||||
public class TxtDeckImporter extends PlainTextDeckImporter {
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import java.util.List;
|
|||
|
||||
import static javax.xml.xpath.XPathConstants.NODESET;
|
||||
|
||||
/**
|
||||
* Deck import: helper class for all xml base formats
|
||||
*/
|
||||
public abstract class XmlDeckImporter extends DeckImporter {
|
||||
|
||||
private final XPathFactory xpathFactory = XPathFactory.newInstance();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue