magarena/src/magic/data/CardDefinitions.java

491 lines
18 KiB
Java

package magic.data;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Path;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import groovy.lang.GroovyShell;
import groovy.transform.CompileStatic;
import magic.model.MagicCardDefinition;
import magic.model.MagicChangeCardDefinition;
import magic.model.MagicColor;
import magic.ui.MagicCardImages;
import magic.ui.screen.images.download.CardImageDisplayMode;
import magic.utility.FileIO;
import magic.utility.MagicFileSystem;
import magic.utility.MagicFileSystem.DataPath;
import magic.utility.MagicSystem;
import magic.utility.ProgressReporter;
public class CardDefinitions {
private static final File CARDS_SNAPSHOT_FILE =
MagicFileSystem.getDataPath().resolve("snapshot.dat").toFile();
private static final File SCRIPTS_DIRECTORY =
MagicFileSystem.getDataPath(DataPath.SCRIPTS).toFile();
private static final GeneralConfig CONFIG = GeneralConfig.getInstance();
// A MagicCardDefinition is a bit of a misnomer in that it represents a single
// playable aspect of a card. For example, double faced or flip cards will be
// represented by two MagicCardDefinitions, one for each of the faces or aspects
// of that card that can be played.
// Contains reference to all playable MagicCardDefinitions indexed by card name.
private static final Map<String, MagicCardDefinition> playableCards = new ConcurrentHashMap<>();
private static final Map<String, MagicCardDefinition> missingCards = new ConcurrentHashMap<>();
private static final AtomicInteger cdefIndex = new AtomicInteger(1);
static {
CompilerConfiguration.DEFAULT.getOptimizationOptions().put(CompilerConfiguration.INVOKEDYNAMIC, Boolean.TRUE);
}
// groovy shell for evaluating groovy card scripts with automatic imports
private static final GroovyShell shell = new GroovyShell(
new CompilerConfiguration().addCompilationCustomizers(
new ImportCustomizer()
.addStarImports(
"java.util",
"magic.data",
"magic.model",
"magic.model.action",
"magic.model.choice",
"magic.model.condition",
"magic.model.event",
"magic.model.mstatic",
"magic.model.stack",
"magic.model.target",
"magic.model.trigger",
"magic.model.phase",
"magic.card"
).addStaticStars(
"magic.model.target.MagicTargetFilterFactory",
"magic.model.choice.MagicTargetChoice"
),
new ASTTransformationCustomizer(CompileStatic.class)
)
);
private static void setProperty(final MagicCardDefinition card,final String property,final String value) {
try {
CardProperty.valueOf(property.toUpperCase(Locale.ENGLISH)).setProperty(card, value);
} catch (IllegalArgumentException e) {
throw new RuntimeException("unknown card property value \"" + property + "\" = \"" + value + "\"", e);
}
}
private static void addDefinition(final MagicCardDefinition cardDef) {
assert cardDef != null : "CardDefinitions.addDefinition passed null";
assert cardDef.getIndex() == -1 : "cardDefinition has been assigned index";
cardDef.setIndex(cdefIndex.getAndIncrement());
playableCards.put(cardDef.getAsciiName(), cardDef);
}
private static MagicCardDefinition prop2carddef(final File scriptFile, final boolean isMissing) {
final Properties content = FileIO.toProp(scriptFile);
final MagicCardDefinition cardDefinition = new MagicCardDefinition();
if (isMissing) {
cardDefinition.setInvalid();
}
for (final String key : content.stringPropertyNames()) {
try {
setProperty(cardDefinition, key, content.getProperty(key));
} catch (Exception e) {
if (isMissing) {
cardDefinition.setInvalid();
} else {
throw e;
}
}
}
try {
cardDefinition.validate();
} catch (Exception e) {
if (isMissing) {
cardDefinition.setInvalid();
} else {
throw e;
}
}
return cardDefinition;
}
//link to groovy script that returns array of MagicChangeCardDefinition objects
static void addCardSpecificGroovyCode(final MagicCardDefinition cardDefinition, final String cardName) {
try {
final File groovyFile = new File(SCRIPTS_DIRECTORY, getCanonicalName(cardName) + ".groovy");
if (!groovyFile.isFile()) {
throw new RuntimeException("groovy file not found: " + groovyFile);
}
@SuppressWarnings("unchecked")
final List<MagicChangeCardDefinition> defs = (List<MagicChangeCardDefinition>)shell.evaluate(groovyFile);
for (MagicChangeCardDefinition ccd : defs) {
ccd.change(cardDefinition);
}
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
}
public static String getCanonicalName(String fullName) {
return fullName.replace("\u00C6", "Ae").replaceAll("[^A-Za-z0-9]", "_");
}
public static String getASCII(String fullName) {
return Normalizer.normalize(fullName, Form.NFD)
.replaceAll("\\p{InCombiningDiacriticalMarks}+", "")
.replace("\u00C6", "Ae");
}
private static void loadCardDefinition(final File file) {
try {
final MagicCardDefinition cdef = prop2carddef(file, false);
addDefinition(cdef);
} catch (final Throwable cause) {
if (MagicSystem.isParseMissing()) {
System.out.println("ERROR file: " + file + " cause: " + cause.getMessage());
} else {
throw new RuntimeException("Error loading " + file, cause);
}
}
}
public static void loadCardDefinition(final String cardName) {
final File cardFile = new File(SCRIPTS_DIRECTORY, getCanonicalName(cardName) + ".txt");
if (!cardFile.isFile()) {
throw new RuntimeException("card script file not found: " + cardFile);
}
loadCardDefinition(cardFile);
}
/**
* loads playable cards.
*/
public static void loadCardDefinitions(final ProgressReporter reporter) {
reporter.setMessage("Sorting card script files...");
final File[] scriptFiles = MagicFileSystem.getSortedScriptFiles(SCRIPTS_DIRECTORY);
reporter.setMessage("Loading cards...0%");
final double totalFiles = scriptFiles.length;
int fileCount = 0;
for (final File file : scriptFiles) {
loadCardDefinition(file);
//
// display percentage complete message every 10%.
final double percentageComplete = (fileCount++ / totalFiles) * 100;
final double m = percentageComplete % 10d;
if (isZero(m, 0.01d)) {
// This should only be called ten times.
// It can have a serious effect on load time if called too many times.
reporter.setMessage("Loading cards..." + ((int)percentageComplete + 10) + "%");
}
}
reporter.setMessage("Loading cards...100%");
}
public static void postCardDefinitions() {
printStatistics();
updateNewCardsLog(loadCardsSnapshotFile());
}
private static boolean isZero(double value, double delta){
return value >= -delta && value <= delta;
}
public static void loadCardAbilities() {
final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
Stream.concat(getDefaultPlayableCardDefStream(), getTokensCardDefStream()).forEach(cdef -> {
executor.execute(() -> {
try {
cdef.loadAbilities();
if (MagicSystem.isParseMissing() && !cdef.isToken()) {
System.out.println("OK card: " + cdef);
}
} catch (Throwable cause) {
if (MagicSystem.isParseMissing()) {
System.out.println("ERROR card: " + cdef + " cause: " + cause.getMessage());
} else {
throw new RuntimeException("Unable to load " + cdef, cause);
}
}
});
});
executor.shutdown();
try {
executor.awaitTermination(100, TimeUnit.SECONDS);
} catch (final InterruptedException ex) {
throw new RuntimeException(ex);
}
}
public static MagicCardDefinition getToken(final int p, final int t, final String name) {
return MagicCardDefinition.token(CardDefinitions.getToken(name), cdef -> cdef.setPowerToughness(p, t));
}
public static MagicCardDefinition getToken(final String original) {
final MagicCardDefinition token = getCard(original);
if (token.isToken()) {
return token;
} else {
throw new RuntimeException("unknown token: \"" + original + "\"");
}
}
public static MagicCardDefinition getMissingOrCard(final String original) {
final String key = getASCII(original);
return missingCards.containsKey(key) ? missingCards.get(key) : getCard(original);
}
public static MagicCardDefinition getCard(final String original) {
final String key = getASCII(original);
// lazy loading of card scripts
if (!playableCards.containsKey(key)) {
loadCardDefinition(original);
}
if (playableCards.containsKey(key)) {
return playableCards.get(key);
} else {
throw new RuntimeException("unknown card: \"" + original + "\"");
}
}
public static MagicCardDefinition getBasicLand(final MagicColor color) {
switch (color) {
case Black:
return getCard("Swamp");
case Blue:
return getCard("Island");
case Green:
return getCard("Forest");
case Red:
return getCard("Mountain");
case White:
return getCard("Plains");
}
throw new RuntimeException("No matching basic land for MagicColor " + color);
}
private static Stream<MagicCardDefinition> getDefaultPlayableCardDefStream() {
return getAllPlayableCardDefs().stream()
.filter(MagicCardDefinition::isPlayable);
}
private static Stream<MagicCardDefinition> getTokensCardDefStream() {
return getAllPlayableCardDefs().stream()
.filter(MagicCardDefinition::isToken);
}
/**
* Returns a list of all playable MagicCardDefinitions except those classed as hidden.
* <p>
* Only contains reference to the main MagicCardDefinition aspect of a card. This is
* required for functions like the Deck Editor where you should not be able to select
* the reverse side of a double-side card, for example.
*/
public static List<MagicCardDefinition> getDefaultPlayableCardDefs() {
return getDefaultPlayableCardDefStream()
.collect(Collectors.toList());
}
/**
* Returns a list all playable MagicCardDefinitions INCLUDING those classed as hidden.
*/
public static Collection<MagicCardDefinition> getAllPlayableCardDefs() {
MagicSystem.waitForPlayableCards();
return playableCards.values();
}
public static synchronized List<MagicCardDefinition> getAllCards() {
final List<MagicCardDefinition> combined = new ArrayList<>();
combined.addAll(getAllPlayableCardDefs());
combined.addAll(getMissingCards());
return combined;
}
public static Stream<MagicCardDefinition> getNonBasicLandCards() {
return getDefaultPlayableCardDefStream()
.filter(card -> card.isLand() && !card.isBasic());
}
public static List<MagicCardDefinition> getSpellCards() {
return getDefaultPlayableCardDefStream()
.filter(card -> !card.isLand())
.collect(Collectors.toList());
}
private static void printStatistics() {
if (MagicSystem.showStartupStats()) {
final CardStatistics statistics=new CardStatistics(getDefaultPlayableCardDefs());
statistics.printStatistics(System.err);
}
}
public static void loadMissingCards() {
final File[] scriptFiles = getSortedMissingScriptFiles();
if (scriptFiles != null) {
for (final File file : scriptFiles) {
MagicCardDefinition cdef = prop2carddef(file, true);
missingCards.put(cdef.getAsciiName(), cdef);
}
}
}
/**
* Gets a sorted list of all the script files in the "missing" folder.
*/
private static File[] getSortedMissingScriptFiles() {
final Path cardsPath = MagicFileSystem.getDataPath(DataPath.SCRIPTS_MISSING);
final File[] files = cardsPath.toFile().listFiles((dir, name) -> {
return name.toLowerCase(Locale.ENGLISH).endsWith(".txt");
});
if (files != null) {
Arrays.sort(files);
}
return files;
}
public static boolean requiresNewImageDownload(MagicCardDefinition card, Date lastDownloadDate) {
if (!card.hasImageUrl()) {
return false;
}
if (MagicCardImages.isCustomCardImageFound(card)) {
return false;
}
if (card.isImageUpdatedAfter(lastDownloadDate)) {
return true;
}
if (CONFIG.getCardImageDisplayMode() == CardImageDisplayMode.PRINTED) {
return MagicCardImages.isPrintedCardImageMissing(card);
} else { // PROXY
return MagicCardImages.isCroppedCardImageMissing(card)
&& MagicCardImages.isPrintedCardImageMissing(card);
}
}
public static boolean isMissingPlayableImages() {
Date aDate = CONFIG.getPlayableImagesDownloadDate();
return getAllPlayableCardDefs().stream()
.anyMatch(card -> requiresNewImageDownload(card, aDate));
}
public static void checkForMissingFiles() {
new Thread(() -> {
GeneralConfig.setIsMissingFiles(isMissingPlayableImages());
}).start();
}
public static String getScriptFilename(final MagicCardDefinition card) {
return card.getFilename() + ".txt";
}
public static String getGroovyFilename(final MagicCardDefinition card) {
return card.getFilename() + ".groovy";
}
public static boolean isCardPlayable(MagicCardDefinition card) {
return playableCards.containsKey(card.getAsciiName());
}
public static boolean isCardMissing(MagicCardDefinition card) {
return missingCards.containsKey(card.getAsciiName());
}
public static boolean isPotential(MagicCardDefinition card) {
return card.hasStatus() ? isCardMissing(card) && !card.getStatus().contains("not supported") : isCardMissing(card);
}
public static Collection<MagicCardDefinition> getMissingCards() {
MagicSystem.waitForMissingCards();
return missingCards.values();
}
public static List<String> getMissingCardNames() {
List<String> names = new ArrayList<>(getMissingCards().size());
for (final MagicCardDefinition cdef : getMissingCards()) {
names.add(cdef.getName());
}
return names;
}
private static void saveCardsSnapshotFile() {
MagicFileSystem.serializeStringList(getPlayableNonTokenCardNames(), CARDS_SNAPSHOT_FILE);
}
private static List<String> loadCardsSnapshotFile() {
if (CARDS_SNAPSHOT_FILE.exists()) {
try {
return MagicFileSystem.deserializeStringList(CARDS_SNAPSHOT_FILE);
} catch (Exception ex) {
Logger.getLogger(CardDefinitions.class.getName()).log(Level.WARNING, null, ex);
return new ArrayList<>();
}
} else {
saveCardsSnapshotFile();
return new ArrayList<>();
}
}
private static List<String> getPlayableNonTokenCardNames() {
return getAllPlayableCardDefs().stream().filter(card -> !card.isToken()).map(MagicCardDefinition::getName).collect(Collectors.toList());
}
public static void updateNewCardsLog(final List<String> snapshot) {
final List<String> cardNames = getPlayableNonTokenCardNames();
cardNames.removeAll(snapshot);
if (!cardNames.isEmpty()) {
saveNewCardsLog(cardNames);
saveCardsSnapshotFile();
}
}
private static void saveNewCardsLog(final Collection<String> cardNames) {
final Path LOGS_PATH = MagicFileSystem.getDataPath(DataPath.LOGS);
final File LOG_FILE = LOGS_PATH.resolve("newcards.log").toFile();
try (final PrintWriter writer = new PrintWriter(LOG_FILE, UTF_8.name())) {
cardNames.forEach(writer::println);
} catch (FileNotFoundException|UnsupportedEncodingException ex) {
System.err.println("Failed to save " + LOG_FILE + " - " + ex);
}
}
}