491 lines
18 KiB
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);
|
|
}
|
|
}
|
|
}
|