Images: added direct image links support in scryfall, added SLD's alternative images from second sides (example: Zndrsplt, Eye of Wisdom);

This commit is contained in:
Oleg Agafonov 2021-11-28 01:17:54 +04:00
parent 903a9215cc
commit a6b2bea8af
8 changed files with 105 additions and 45 deletions

View file

@ -74,17 +74,23 @@ public enum ScryfallImageSource implements CardImageSource {
// CARDS TRY
// direct links to images via hardcoded API path. Used for cards with non-ASCII collector numbers
// direct links to images via hardcoded API path
// used for cards with non-ASCII collector numbers or another use cases
if (baseUrl == null) {
String apiUrl = ScryfallImageSupportCards.findDirectDownloadLink(card.getSet(), card.getName(), card.getCollectorId());
if (apiUrl != null) {
baseUrl = apiUrl + localizedCode + "?format=image";
alternativeUrl = apiUrl + defaultCode + "?format=image";
String link = ScryfallImageSupportCards.findDirectDownloadLink(card.getSet(), card.getName(), card.getCollectorId());
if (link != null) {
if (ScryfallImageSupportCards.isApiLink(link)) {
// api
baseUrl = link + localizedCode + "?format=image";
alternativeUrl = link + defaultCode + "?format=image";
// workaround to use cards without english images (some promos or special cards)
if (Objects.equals(baseUrl, alternativeUrl) && baseUrl.endsWith("/en?format=image")) {
alternativeUrl = alternativeUrl.replace("/en?format=image", "/?format=image");
}
} else {
// image
baseUrl = link;
}
}
}
@ -103,18 +109,22 @@ public enum ScryfallImageSource implements CardImageSource {
// basic cards by api call (redirect to img link)
// example: https://api.scryfall.com/cards/xln/121/en?format=image
if (baseUrl == null) {
baseUrl = "https://api.scryfall.com/cards/" + formatSetName(card.getSet(), isToken) + "/"
+ card.getCollectorId() + "/" + localizedCode + "?format=image";
alternativeUrl = "https://api.scryfall.com/cards/" + formatSetName(card.getSet(), isToken) + "/"
+ card.getCollectorId() + "/" + defaultCode + "?format=image";
baseUrl = String.format("https://api.scryfall.com/cards/%s/%s/%s?format=image",
formatSetName(card.getSet(), isToken),
card.getCollectorId(),
localizedCode);
alternativeUrl = String.format("https://api.scryfall.com/cards/%s/%s/%s?format=image",
formatSetName(card.getSet(), isToken),
card.getCollectorId(),
defaultCode);
// workaround to use cards without english images (some promos or special cards)
// bug: https://github.com/magefree/mage/issues/6829
// example: Mysterious Egg from IKO https://api.scryfall.com/cards/iko/385/?format=image
if (Objects.equals(baseUrl, alternativeUrl)) {
// without loc code scryfall must return first available image
alternativeUrl = "https://api.scryfall.com/cards/" + formatSetName(card.getSet(), isToken) + "/"
+ card.getCollectorId() + "/?format=image";
alternativeUrl = String.format("https://api.scryfall.com/cards/%s/%s/?format=image",
formatSetName(card.getSet(), isToken),
card.getCollectorId());
}
}
@ -126,6 +136,7 @@ public enum ScryfallImageSource implements CardImageSource {
final String localizedCode = languageAliases.getOrDefault(this.getCurrentLanguage(), defaultCode);
List<String> needUrls = new ArrayList<>();
int needFaceIndex = card.isSecondSide() ? 1 : 0;
String apiUrl = ScryfallImageSupportCards.findDirectDownloadLink(card.getSet(), card.getName(), card.getCollectorId());
if (apiUrl != null) {
@ -143,11 +154,15 @@ public enum ScryfallImageSource implements CardImageSource {
} else {
// BY CARD NUMBER
// localized and default
needUrls.add("https://api.scryfall.com/cards/"
+ formatSetName(card.getSet(), isToken) + "/" + card.getCollectorId() + "/" + localizedCode);
needUrls.add(String.format("https://api.scryfall.com/cards/%s/%s/%s",
formatSetName(card.getSet(), isToken),
card.getCollectorId(),
localizedCode));
if (!localizedCode.equals(defaultCode)) {
needUrls.add("https://api.scryfall.com/cards/"
+ formatSetName(card.getSet(), isToken) + "/" + card.getCollectorId() + "/" + defaultCode);
needUrls.add(String.format("https://api.scryfall.com/cards/%s/%s/%s",
formatSetName(card.getSet(), isToken),
card.getCollectorId(),
defaultCode));
}
}
@ -178,8 +193,11 @@ public enum ScryfallImageSource implements CardImageSource {
if (jsonFaces == null) {
throw new MageException("Couldn't find card_faces in card's JSON data: " + jsonUrl);
}
if (jsonFaces.size() < needFaceIndex + 1) {
throw new MageException("card_faces doesn't contains face index in card's JSON data: " + jsonUrl);
}
JsonObject jsonFace = jsonFaces.get(card.isSecondSide() ? 1 : 0).getAsJsonObject();
JsonObject jsonFace = jsonFaces.get(needFaceIndex).getAsJsonObject();
JsonObject jsonImages = JsonUtil.getAsObject(jsonFace, "image_uris");
if (jsonImages == null) {
throw new MageException("Couldn't find image_uris in card's JSON data: " + jsonUrl);

View file

@ -508,21 +508,23 @@ public class ScryfallImageSupportCards {
private static final Map<String, String> directDownloadLinks = new HashMap<String, String>() {
{
// xmage card -> api link:
// examples:
// api example: https://api.scryfall.com/cards/trix/6/
// api format is primary
// xmage card -> api or image link
// WARNING, try use api links as much as possible (it supports build-in translation)
//
// code form for one card:
// example:
// api link: https://api.scryfall.com/cards/trix/6/
// image link: https://c1.scryfall.com/file/scryfall-cards/large/back/d/5/d5dfd236-b1da-4552-b94f-ebf6bb9dafdf.jpg
//
// key for one card:
// set/card_name
//
// code form for same name cards (alternative images):
// set/card_name/card_number
// set/card_name/card_number
// key for same name cards (alternative images):
// set/card_name/card_number_1
// set/card_name/card_number_2
//
// Cards with non-ASCII collector numbers must use direct download (cause xmage uses different card number)
// Verify checks must check and show missing data from that list
// WARNING, must use as API link, not image
// 10E
put("10E/Air Elemental/64*", "https://api.scryfall.com/cards/10e/64★/");
put("10E/Anaba Bodyguard/187*", "https://api.scryfall.com/cards/10e/187★/");
@ -786,10 +788,6 @@ public class ScryfallImageSupportCards {
put("PM14/Hive Stirrings/21*", "https://api.scryfall.com/cards/pm14/21★/");
put("PM14/Megantic Sliver/185*", "https://api.scryfall.com/cards/pm14/185★/");
put("PM14/Ratchet Bomb/215*", "https://api.scryfall.com/cards/pm14/215★/");
// PROE
put("PROE/Emrakul, the Aeons Torn/4*", "https://api.scryfall.com/cards/proe/4★/");
put("PROE/Lord of Shatterskull Pass/156*", "https://api.scryfall.com/cards/proe/156★/");
//
// PMBS
put("PMBS/Glissa, the Traitor/96*", "https://api.scryfall.com/cards/pmbs/96★/");
put("PMBS/Hero of Bladehold/8*", "https://api.scryfall.com/cards/pmbs/8★/");
@ -948,6 +946,18 @@ public class ScryfallImageSupportCards {
put("WAR/Ugin, the Ineffable/2*", "https://api.scryfall.com/cards/war/2★/");
put("WAR/Vivien, Champion of the Wilds/180*", "https://api.scryfall.com/cards/war/180★/");
put("WAR/Vraska, Swarm's Eminence/236*", "https://api.scryfall.com/cards/war/236★/");
// SLD
// TODO: update direct image links in 2022 for HQ images
put("SLD/Zndrsplt, Eye of Wisdom/379", "https://api.scryfall.com/cards/sld/379/");
put("SLD/Zndrsplt, Eye of Wisdom/379b", "https://c1.scryfall.com/file/scryfall-cards/large/back/d/5/d5dfd236-b1da-4552-b94f-ebf6bb9dafdf.jpg");
put("SLD/Krark's Thumb/383", "https://api.scryfall.com/cards/sld/383/");
put("SLD/Krark's Thumb/383b", "https://c1.scryfall.com/file/scryfall-cards/large/back/9/f/9f63277b-e139-46c8-b9e3-0cfb647f44cc.jpg");
put("SLD/Okaun, Eye of Chaos/380", "https://api.scryfall.com/cards/sld/380/");
put("SLD/Okaun, Eye of Chaos/380b", "https://c1.scryfall.com/file/scryfall-cards/large/back/9/4/94eea6e3-20bc-4dab-90ba-3113c120fb90.jpg");
put("SLD/Propaganda/381", "https://api.scryfall.com/cards/sld/381/");
put("SLD/Propaganda/381b", "https://c1.scryfall.com/file/scryfall-cards/large/back/3/e/3e3f0bcd-0796-494d-bf51-94b33c1671e9.jpg");
put("SLD/Stitch in Time/382", "https://api.scryfall.com/cards/sld/382/");
put("SLD/Stitch in Time/382b", "https://c1.scryfall.com/file/scryfall-cards/large/back/0/8/087c3a0d-c710-4451-989e-596b55352184.jpg");
}
};
@ -1001,6 +1011,10 @@ public class ScryfallImageSupportCards {
return "";
}
public static boolean isApiLink(String link) {
return !link.endsWith(".jpg") && !link.endsWith(".png");
}
public static Map<String, String> getDirectDownloadLinks() {
return directDownloadLinks;
}

View file

@ -436,6 +436,8 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
String cardName = card.getName();
boolean isType2 = type2SetsFilter.contains(card.getSetCode());
CardDownloadData url = new CardDownloadData(cardName, card.getSetCode(), card.getCardNumber(), card.usesVariousArt(), 0, "", "", false, card.isDoubleFaced(), card.isNightCard());
// variations must have diff file names with additional postfix
if (url.getUsesVariousArt()) {
url.setDownloadName(createDownloadName(card));
}
@ -444,7 +446,10 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements
url.setSplitCard(card.isSplitCard());
url.setType2(isType2);
// main side
allCardsUrls.add(url);
// second side (xmage's set doesn't have info about it, so generate it here)
if (card.isDoubleFaced()) {
if (card.getSecondSideName() == null || card.getSecondSideName().trim().isEmpty()) {
throw new IllegalStateException("Second side card can't have empty name.");

View file

@ -71,7 +71,7 @@ class EmpoweredAutogeneratorManaEffect extends ManaEffect {
public List<Mana> getNetMana(Game game, Ability source) {
List<Mana> netMana = new ArrayList<>();
if (game != null) {
Permanent sourcePermanent = game.getState().getPermanent(source.getSourceId());
Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (sourcePermanent != null) {
int counters = sourcePermanent.getCounters(game).getCount(CounterType.CHARGE) + 1; // one counter will be added on real mana call
if (counters > 0) {
@ -89,7 +89,7 @@ class EmpoweredAutogeneratorManaEffect extends ManaEffect {
return mana;
}
game.getState().processAction(game);
Permanent sourcePermanent = game.getState().getPermanent(source.getSourceId());
Permanent sourcePermanent = game.getPermanent(source.getSourceId());
if (sourcePermanent == null) {
return mana;
}

View file

@ -357,11 +357,16 @@ public class SecretLairDrop extends ExpansionSet {
cards.add(new SetCardInfo("Craterhoof Behemoth", 376, Rarity.MYTHIC, mage.cards.c.CraterhoofBehemoth.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Metalwork Colossus", 377, Rarity.RARE, mage.cards.m.MetalworkColossus.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Metalwork Colossus", 378, Rarity.RARE, mage.cards.m.MetalworkColossus.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Zndrsplt, Eye of Wisdom", 379, Rarity.RARE, mage.cards.z.ZndrspltEyeOfWisdom.class));
cards.add(new SetCardInfo("Okaun, Eye of Chaos", 380, Rarity.RARE, mage.cards.o.OkaunEyeOfChaos.class));
cards.add(new SetCardInfo("Propaganda", 381, Rarity.RARE, mage.cards.p.Propaganda.class));
cards.add(new SetCardInfo("Stitch in Time", 382, Rarity.RARE, mage.cards.s.StitchInTime.class));
cards.add(new SetCardInfo("Krark's Thumb", 383, Rarity.RARE, mage.cards.k.KrarksThumb.class));
cards.add(new SetCardInfo("Zndrsplt, Eye of Wisdom", 379, Rarity.RARE, mage.cards.z.ZndrspltEyeOfWisdom.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Zndrsplt, Eye of Wisdom", "379b", Rarity.RARE, mage.cards.z.ZndrspltEyeOfWisdom.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Okaun, Eye of Chaos", 380, Rarity.RARE, mage.cards.o.OkaunEyeOfChaos.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Okaun, Eye of Chaos", "380b", Rarity.RARE, mage.cards.o.OkaunEyeOfChaos.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Propaganda", 381, Rarity.RARE, mage.cards.p.Propaganda.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Propaganda", "381b", Rarity.RARE, mage.cards.p.Propaganda.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Stitch in Time", 382, Rarity.RARE, mage.cards.s.StitchInTime.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Stitch in Time", "382b", Rarity.RARE, mage.cards.s.StitchInTime.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Krark's Thumb", 383, Rarity.RARE, mage.cards.k.KrarksThumb.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Krark's Thumb", "383b", Rarity.RARE, mage.cards.k.KrarksThumb.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Path to Exile", 477, Rarity.RARE, mage.cards.p.PathToExile.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Rhystic Study", 478, Rarity.RARE, mage.cards.r.RhysticStudy.class));
cards.add(new SetCardInfo("Counterflux", 482, Rarity.RARE, mage.cards.c.Counterflux.class));

View file

@ -40,6 +40,7 @@ public final class MtgJsonCard {
|| "flip".equals(layout)
|| "adventure".equals(layout)
|| "modal_dfc".equals(layout)
|| "reversible_card".equals(layout) // example: Zndrsplt, Eye of Wisdom
|| "split".equals(layout)
|| "aftermath".equals(layout)
|| "meld".equals(layout)) { // mtgjson uses composite names for meld cards, but scryfall uses simple face names

View file

@ -196,6 +196,7 @@ public class VerifyCardDataTest {
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "UND"); // un-sets don't have full implementation of card variations
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "UST"); // un-sets don't have full implementation of card variations
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "SOI", "Tamiyo's Journal"); // not all variations implemented
skipListAddName(SKIP_LIST_WRONG_CARD_NUMBERS, "SLD", "Zndrsplt, Eye of Wisdom"); // xmage adds additional card for alternative image (second side)
// scryfall download sets (missing from scryfall website)
@ -700,10 +701,16 @@ public class VerifyCardDataTest {
}
for (ExpansionSet.SetCardInfo card : set.getSetCardInfo()) {
if (skipListHaveName(SKIP_LIST_WRONG_CARD_NUMBERS, set.getCode(), card.getName())) {
continue;
}
MtgJsonCard jsonCard = MtgJsonService.cardFromSet(set.getCode(), card.getName(), card.getCardNumber());
if (jsonCard == null) {
// see convertMtgJsonToXmageCardNumber for card number convert notation
if (!skipListHaveName(SKIP_LIST_WRONG_CARD_NUMBERS, set.getCode(), card.getName())) {
errorsList.add("Error: scryfall download can't find card from mtgjson " + set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + card.getCardNumber());
}
continue;
}
@ -718,6 +725,16 @@ public class VerifyCardDataTest {
errorsList.add("Error: scryfall download can't find non-ascii card link in direct download list " + set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + jsonCard.number);
}
}
// CHECK: reversible_card must be in direct download list (xmage must have 2 cards with diff image face)
if (jsonCard.layout.equals("reversible_card")) {
String key = ScryfallImageSupportCards.findDirectDownloadKey(set.getCode(), card.getName(), card.getCardNumber());
if (key != null) {
foundedDirectDownloadKeys.add(key);
} else {
errorsList.add("Error: scryfall download can't find face image of reversible_card in direct download list " + set.getCode() + " - " + set.getName() + " - " + card.getName() + " - " + jsonCard.number);
}
}
}
}
@ -731,7 +748,7 @@ public class VerifyCardDataTest {
continue;
}
// skip non implemented cards list
// skip non-implemented cards list
if (CardRepository.instance.findCard(cardName) == null) {
continue;
}

View file

@ -504,7 +504,7 @@ public enum CardRepository {
}
/**
* Warning, don't use db functions in card's code - it generate heavy db loading in AI simulations. If you
* Warning, don't use db functions in card's code - it generates heavy db loading in AI simulations. If you
* need that feature then check for simulation mode. See https://github.com/magefree/mage/issues/7014
*
* @param criteria