From b57a5e9ad16faec839d59846d8caee92de59a121 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Fri, 2 May 2025 21:31:40 +0200 Subject: [PATCH] PaperToken: add CollectorNumber and update Downloader --- forge-core/src/main/java/forge/ImageKeys.java | 51 ++++----- .../src/main/java/forge/card/CardDb.java | 4 +- .../src/main/java/forge/card/CardEdition.java | 2 +- .../src/main/java/forge/item/PaperToken.java | 106 ++++++------------ .../src/main/java/forge/token/TokenDb.java | 67 ++++++++--- .../java/forge/util/SwingImageFetcher.java | 2 +- .../src/forge/util/LibGDXImageFetcher.java | 2 +- .../java/forge/gamemodes/quest/QuestUtil.java | 2 +- .../gamemodes/quest/bazaar/QuestPetStats.java | 3 +- .../gui/download/GuiDownloadPicturesHQ.java | 3 +- .../main/java/forge/util/ImageFetcher.java | 23 +++- 11 files changed, 134 insertions(+), 131 deletions(-) diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java index 4a575dffe3a..ed9b245b03f 100644 --- a/forge-core/src/main/java/forge/ImageKeys.java +++ b/forge-core/src/main/java/forge/ImageKeys.java @@ -103,7 +103,13 @@ public final class ImageKeys { final String dir; final String filename; if (key.startsWith(ImageKeys.TOKEN_PREFIX)) { - filename = key.substring(ImageKeys.TOKEN_PREFIX.length()); + String[] tempdata = key.substring(ImageKeys.TOKEN_PREFIX.length()).split("\\|"); + String tokenname = tempdata[0] + "_" + tempdata[1]; + if (tempdata.length > 2) { + tokenname += "_" + tempdata[2]; + } + filename = tokenname; + dir = CACHE_TOKEN_PICS_DIR; } else if (key.startsWith(ImageKeys.ICON_PREFIX)) { filename = key.substring(ImageKeys.ICON_PREFIX.length()); @@ -226,36 +232,27 @@ public final class ImageKeys { } } if (dir.equals(CACHE_TOKEN_PICS_DIR)) { - int index = filename.lastIndexOf('_'); - if (index != -1) { - String setlessFilename = filename.substring(0, index); - String setCode = filename.substring(index + 1); - // try with upper case set - file = findFile(dir, setlessFilename + "_" + setCode.toUpperCase()); + String[] tempdata = key.substring(ImageKeys.TOKEN_PREFIX.length()).split("\\|"); + String setlessFilename = tempdata[0]; + String setCode = tempdata[1]; + String collectorNumber = tempdata.length > 2 ? tempdata[2] : ""; + + if (!collectorNumber.isEmpty()) { + file = findFile(dir, setlessFilename + "_" + setCode + "_" + collectorNumber); if (file != null) { cachedCards.put(filename, file); return file; } - // try with lower case set - file = findFile(dir, setlessFilename + "_" + setCode.toLowerCase()); - if (file != null) { - cachedCards.put(filename, file); - return file; - } - // try without set name - file = findFile(dir, setlessFilename); - if (file != null) { - cachedCards.put(filename, file); - return file; - } - // if there's an art variant try without it - if (setlessFilename.matches(".*[0-9]*$")) { - file = findFile(dir, setlessFilename.replaceAll("[0-9]*$", "")); - if (file != null) { - cachedCards.put(filename, file); - return file; - } - } + } + file = findFile(dir, setlessFilename + "_" + setCode); + if (file != null) { + cachedCards.put(filename, file); + return file; + } + file = findFile(dir, setlessFilename); + if (file != null) { + cachedCards.put(filename, file); + return file; } } else if (filename.contains("/")) { String setlessFilename = filename.substring(filename.indexOf('/') + 1); diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index cbc6498a1fd..a5904977187 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -1043,7 +1043,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { public static final Predicate EDITION_NON_PROMO = paperCard -> { String code = paperCard.getEdition(); CardEdition edition = StaticData.instance().getCardEdition(code); - if(edition == null && code.equals("???")) + if(edition == null && code.equals(CardEdition.UNKNOWN_CODE)) return true; return edition != null && edition.getType() != Type.PROMO; }; @@ -1051,7 +1051,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { public static final Predicate EDITION_NON_REPRINT = paperCard -> { String code = paperCard.getEdition(); CardEdition edition = StaticData.instance().getCardEdition(code); - if(edition == null && code.equals("???")) + if(edition == null && code.equals(CardEdition.UNKNOWN_CODE)) return true; return edition != null && Type.REPRINT_SET_TYPES.contains(edition.getType()); }; diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index a21b956ef0f..316b7f9951f 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -870,7 +870,7 @@ public final class CardEdition implements Comparable { public CardEdition getEditionByCodeOrThrow(final String code) { final CardEdition set = this.get(code); - if (null == set && code.equals("???")) //Hardcoded set ??? is not with the others, needs special check. + if (null == set && code.equals(UNKNOWN_CODE)) //Hardcoded set ??? is not with the others, needs special check. return UNKNOWN; if (null == set) { throw new RuntimeException("Edition with code '" + code + "' not found"); diff --git a/forge-core/src/main/java/forge/item/PaperToken.java b/forge-core/src/main/java/forge/item/PaperToken.java index 8facd4baa43..dc6b647aecd 100644 --- a/forge-core/src/main/java/forge/item/PaperToken.java +++ b/forge-core/src/main/java/forge/item/PaperToken.java @@ -11,6 +11,8 @@ import java.util.Locale; public class PaperToken implements InventoryItemFromSet, IPaperCard { private static final long serialVersionUID = 1L; private String name; + private String collectorNumber; + private String artist; private transient CardEdition edition; private ArrayList imageFileName = new ArrayList<>(); private transient CardRules cardRules; @@ -53,75 +55,36 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard { return makeTokenFileName(fileName); } - public static String makeTokenFileName(final CardRules rules, CardEdition edition) { - ArrayList build = new ArrayList<>(); - - String subtypes = StringUtils.join(rules.getType().getSubtypes(), " "); - if (!rules.getName().equals(subtypes)) { - return makeTokenFileName(rules.getName()); - } - - ColorSet colors = rules.getColor(); - - if (colors.isColorless()) { - build.add("C"); - } else { - String color = ""; - if (colors.hasWhite()) color += "W"; - if (colors.hasBlue()) color += "U"; - if (colors.hasBlack()) color += "B"; - if (colors.hasRed()) color += "R"; - if (colors.hasGreen()) color += "G"; - - build.add(color); - } - - if (rules.getPower() != null && rules.getToughness() != null) { - build.add(rules.getPower()); - build.add(rules.getToughness()); - } - - String cardTypes = ""; - if (rules.getType().isArtifact()) cardTypes += "A"; - if (rules.getType().isEnchantment()) cardTypes += "E"; - - if (!cardTypes.isEmpty()) { - build.add(cardTypes); - } - - build.add(subtypes); - - // Are these keywords sorted? - for (String keyword : rules.getMainPart().getKeywords()) { - build.add(keyword); - } - - if (edition != null) { - build.add(edition.getCode()); - } - - return StringUtils.join(build, "_").replace('*', 'x').toLowerCase(); - } - - public PaperToken(final CardRules c, CardEdition edition0, String imageFileName) { + public PaperToken(final CardRules c, CardEdition edition0, String imageFileName, String collectorNumber, String artist) { this.cardRules = c; this.name = c.getName(); this.edition = edition0; + this.collectorNumber = collectorNumber; + this.artist = artist; if (edition != null && edition.getTokens().containsKey(imageFileName)) { - this.artIndex = edition.getTokens().get(imageFileName).size(); - } - - if (imageFileName == null) { - // This shouldn't really happen. We can just use the normalized name again for the base image name - this.imageFileName.add(makeTokenFileName(c, edition0)); - } else { + if (collectorNumber != null && !collectorNumber.isEmpty()) { + int idx = 0; + // count the one with the same collectorNumber + for (CardEdition.TokenInSet t : edition.getTokens().get(imageFileName)) { + ++idx; + if (!t.collectorNumber.equals(collectorNumber)) { + continue; + } + // TODO make better image file names when collector number is known + // for the right index, we need to count the ones with wrong collector number too + this.imageFileName.add(String.format("%s|%s|%s|%d", imageFileName, edition.getCode().toLowerCase(), collectorNumber, idx)); + } + this.artIndex = this.imageFileName.size(); + } + } else if (imageFileName != null) { String formatEdition = null == edition || CardEdition.UNKNOWN == edition ? "" : "_" + edition.getCode().toLowerCase(); - this.imageFileName.add(String.format("%s%s", imageFileName, formatEdition)); - for (int idx = 2; idx <= this.artIndex; idx++) { - this.imageFileName.add(String.format("%s%d%s", imageFileName, idx, formatEdition)); + if (null == edition || CardEdition.UNKNOWN == edition) { + this.imageFileName.add(imageFileName); } + // Fallback for if the Edition doesn't know about the Token + this.imageFileName.add(String.format("%s|%s", imageFileName, formatEdition)); } } @@ -137,12 +100,14 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard { @Override public String getEdition() { - return edition != null ? edition.getCode() : "???"; + return edition != null ? edition.getCode() : CardEdition.UNKNOWN_CODE; } @Override public String getCollectorNumber() { - return IPaperCard.NO_COLLECTOR_NUMBER; + if (collectorNumber.isEmpty()) + return IPaperCard.NO_COLLECTOR_NUMBER; + return collectorNumber; } @Override @@ -177,13 +142,8 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard { } @Override - public String getArtist() { /*TODO*/ - return ""; - } - - // Unfortunately this is a property of token, cannot move it outside of class - public String getImageFilename() { - return getImageFilename(1); + public String getArtist() { + return artist; } public String getImageFilename(int idx) { @@ -261,11 +221,11 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard { if (hasBackFace()) { String edCode = edition != null ? "_" + edition.getCode().toLowerCase() : ""; if (altState) { - String name = ImageKeys.TOKEN_PREFIX + cardRules.getOtherPart().getName().toLowerCase().replace(" token", ""); + String name = ImageKeys.getTokenKey(cardRules.getOtherPart().getName().toLowerCase().replace(" token", "")); name.replace(" ", "_"); return name + edCode; } else { - String name = ImageKeys.TOKEN_PREFIX + cardRules.getMainPart().getName().toLowerCase().replace(" token", ""); + String name = ImageKeys.getTokenKey(cardRules.getMainPart().getName().toLowerCase().replace(" token", "")); name.replace(" ", "_"); return name + edCode; } @@ -275,7 +235,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard { } public String getImageKey(int artIndex) { - return ImageKeys.TOKEN_PREFIX + imageFileName.get(artIndex).replace(" ", "_"); + return ImageKeys.getTokenKey(imageFileName.get(artIndex).replace(" ", "_")); } public boolean isRebalanced() { diff --git a/forge-core/src/main/java/forge/token/TokenDb.java b/forge-core/src/main/java/forge/token/TokenDb.java index 0ca8b826fc0..7f8051b18f4 100644 --- a/forge-core/src/main/java/forge/token/TokenDb.java +++ b/forge-core/src/main/java/forge/token/TokenDb.java @@ -1,10 +1,15 @@ package forge.token; +import com.google.common.collect.HashMultimap; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + import forge.card.CardDb; import forge.card.CardEdition; import forge.card.CardRules; +import forge.item.IPaperCard; import forge.item.PaperToken; +import forge.util.Aggregates; import java.util.*; import java.util.function.Predicate; @@ -23,8 +28,8 @@ public class TokenDb implements ITokenDatabase { // The image names should be the same as the script name + _set // If that isn't found, consider falling back to the original token - - private final Map tokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); + private final Multimap allTokenByName = HashMultimap.create(); + private final Map extraTokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); private final CardEdition.Collection editions; private final Map rulesByName; @@ -38,38 +43,64 @@ public class TokenDb implements ITokenDatabase { return this.rulesByName.containsKey(rule); } - @Override - public PaperToken getToken(String tokenName) { - return getToken(tokenName, CardEdition.UNKNOWN.getName()); - } public void preloadTokens() { for (CardEdition edition : this.editions) { - for (String name : edition.getTokens().keySet()) { - try { - getToken(name, edition.getCode()); - } catch(Exception e) { - System.out.println(name + "_" + edition.getCode() + " defined in Edition file, but not defined as a token script."); + for (Map.Entry> inSet : edition.getTokens().asMap().entrySet()) { + String name = inSet.getKey(); + String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase()); + for (CardEdition.TokenInSet t : inSet.getValue()) { + allTokenByName.put(fullName, addTokenInSet(edition, name, t)); } } } } + protected boolean loadTokenFromSet(CardEdition edition, String name) { + String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase()); + if (allTokenByName.containsKey(fullName)) { + return true; + } + if (!edition.getTokens().containsKey(name)) { + return false; + } + + for (CardEdition.TokenInSet t : edition.getTokens().get(name)) { + allTokenByName.put(fullName, addTokenInSet(edition, name, t)); + } + return true; + } + + protected PaperToken addTokenInSet(CardEdition edition, String name, CardEdition.TokenInSet t) { + return new PaperToken(rulesByName.get(name), edition, name, t.collectorNumber, t.artistName); + } + + @Override + public PaperToken getToken(String tokenName) { + return getToken(tokenName, CardEdition.UNKNOWN.getCode()); + } + @Override public PaperToken getToken(String tokenName, String edition) { - String fullName = String.format("%s_%s", tokenName, edition.toLowerCase()); + CardEdition realEdition = editions.getEditionByCodeOrThrow(edition); + String fullName = String.format("%s_%s", tokenName, realEdition.getCode().toLowerCase()); - if (!tokensByName.containsKey(fullName)) { + // token exist in Set, return one at random + if (loadTokenFromSet(realEdition, tokenName)) { + return Aggregates.random(allTokenByName.get(fullName)); + } + + if (!extraTokensByName.containsKey(fullName)) { try { - PaperToken pt = new PaperToken(rulesByName.get(tokenName), editions.get(edition), tokenName); - tokensByName.put(fullName, pt); + PaperToken pt = new PaperToken(rulesByName.get(tokenName), realEdition, tokenName, "", IPaperCard.NO_ARTIST_NAME); + extraTokensByName.put(fullName, pt); return pt; } catch(Exception e) { throw e; } } - return tokensByName.get(fullName); + return extraTokensByName.get(fullName); } @Override @@ -119,7 +150,7 @@ public class TokenDb implements ITokenDatabase { @Override public List getAllTokens() { - return new ArrayList<>(tokensByName.values()); + return new ArrayList<>(allTokenByName.values()); } @Override @@ -139,6 +170,6 @@ public class TokenDb implements ITokenDatabase { @Override public Iterator iterator() { - return tokensByName.values().iterator(); + return allTokenByName.values().iterator(); } } diff --git a/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java b/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java index 9aa52794271..d8fb622e71e 100644 --- a/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java +++ b/forge-gui-desktop/src/main/java/forge/util/SwingImageFetcher.java @@ -36,7 +36,7 @@ public class SwingImageFetcher extends ImageFetcher { String newdespath = urlToDownload.contains(".fullborder.jpg") || urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) ? TextUtil.fastReplace(destPath, ".full.jpg", ".fullborder.jpg") : destPath; - if (!newdespath.contains(".full") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD)) + if (!newdespath.contains(".full") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) && !destPath.startsWith(ForgeConstants.CACHE_TOKEN_PICS_DIR)) newdespath = newdespath.replace(".jpg", ".fullborder.jpg"); //fix planes/phenomenon for round border options URL url = new URL(urlToDownload); System.out.println("Attempting to fetch: " + url); diff --git a/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java b/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java index 89c511100e4..549f3ceb0c6 100644 --- a/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java +++ b/forge-gui-mobile/src/forge/util/LibGDXImageFetcher.java @@ -37,7 +37,7 @@ public class LibGDXImageFetcher extends ImageFetcher { String newdespath = urlToDownload.contains(".fullborder.") || urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) ? TextUtil.fastReplace(destPath, ".full.", ".fullborder.") : destPath; - if (!newdespath.contains(".full") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD)) + if (!newdespath.contains(".full") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) && !destPath.startsWith(ForgeConstants.CACHE_TOKEN_PICS_DIR)) newdespath = newdespath.replace(".jpg", ".fullborder.jpg"); //fix planes/phenomenon for round border options URL url = new URL(urlToDownload); System.out.println("Attempting to fetch: " + url); diff --git a/forge-gui/src/main/java/forge/gamemodes/quest/QuestUtil.java b/forge-gui/src/main/java/forge/gamemodes/quest/QuestUtil.java index 4894cde58e6..b81e1daf273 100644 --- a/forge-gui/src/main/java/forge/gamemodes/quest/QuestUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/quest/QuestUtil.java @@ -156,7 +156,7 @@ public class QuestUtil { script.add("Types:" + properties[5].replace(';', ' ')); script.add("Oracle:"); // tokens don't have texts yet final String fileName = PaperToken.makeTokenFileName(properties[1], properties[2], properties[3], properties[4]); - return new PaperToken(CardRules.fromScript(script), CardEdition.UNKNOWN, fileName); + return new PaperToken(CardRules.fromScript(script), CardEdition.UNKNOWN, fileName, "", IPaperCard.NO_ARTIST_NAME); } /** diff --git a/forge-gui/src/main/java/forge/gamemodes/quest/bazaar/QuestPetStats.java b/forge-gui/src/main/java/forge/gamemodes/quest/bazaar/QuestPetStats.java index a2e7fcc681b..4981add2d17 100644 --- a/forge-gui/src/main/java/forge/gamemodes/quest/bazaar/QuestPetStats.java +++ b/forge-gui/src/main/java/forge/gamemodes/quest/bazaar/QuestPetStats.java @@ -8,6 +8,7 @@ import com.thoughtworks.xstream.annotations.XStreamAsAttribute; import forge.card.CardEdition; import forge.card.CardRules; +import forge.item.IPaperCard; import forge.item.PaperToken; import forge.localinstance.properties.ForgeConstants; import forge.util.FileUtil; @@ -59,7 +60,7 @@ public class QuestPetStats { if (null == petCard) { List cardLines = FileUtil.readFile(new File(ForgeConstants.BAZAAR_DIR, cardFile)); CardRules rules = CardRules.fromScript(cardLines); - petCard = new PaperToken(rules, CardEdition.UNKNOWN, picture); + petCard = new PaperToken(rules, CardEdition.UNKNOWN, picture, "", IPaperCard.NO_ARTIST_NAME); } return petCard; } diff --git a/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java b/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java index 62885cb93ab..c5816dce702 100644 --- a/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java +++ b/forge-gui/src/main/java/forge/gui/download/GuiDownloadPicturesHQ.java @@ -17,6 +17,7 @@ */ package forge.gui.download; +import forge.card.CardEdition; import forge.item.PaperCard; import forge.localinstance.properties.ForgeConstants; import forge.model.FModel; @@ -93,7 +94,7 @@ public class GuiDownloadPicturesHQ extends GuiDownloadService { cardname = cardname.replace(" ", "+"); cardname = cardname.replace("'", ""); String scryfallurl = ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + "named?fuzzy=" + cardname; - if(!setCode.equals("???")) scryfallurl += "&set=" + setCode.toLowerCase(); + if(!setCode.equals(CardEdition.UNKNOWN_CODE)) scryfallurl += "&set=" + setCode.toLowerCase(); if(face.equals("back")) scryfallurl += "&face=back"; scryfallurl += "&format=image"; diff --git a/forge-gui/src/main/java/forge/util/ImageFetcher.java b/forge-gui/src/main/java/forge/util/ImageFetcher.java index 7af1e8bab97..fb5e42168fa 100644 --- a/forge-gui/src/main/java/forge/util/ImageFetcher.java +++ b/forge-gui/src/main/java/forge/util/ImageFetcher.java @@ -228,21 +228,29 @@ public abstract class ImageFetcher { this.getScryfallDownloadURL(paperCard, face, useArtCrop, hasSetLookup, filename, downloadUrls); } } else if (prefix.equals(ImageKeys.TOKEN_PREFIX)) { - final String filename = imageKey.substring(2) + ".jpg"; + String[] tempdata = imageKey.substring(2).split("\\|"); //We want to check the edition first. + String tokenName = tempdata[0]; + String setCode = tempdata[1]; + + StringBuilder sb = new StringBuilder(tokenName); + sb.append("_").append(setCode); + if (tempdata.length > 2) { + sb.append("_").append(tempdata[2]); + } + sb.append(".jpg"); + + final String filename = sb.toString(); if (ImageKeys.missingCards.contains(filename)) return; if (filename.equalsIgnoreCase("null.jpg")) return; - String[] tempdata = imageKey.substring(2).split("[_](?=[^_]*$)"); //We want to check the edition first. if (tempdata.length < 2) { System.err.println("Token image key is malformed: " + imageKey); return; } - String tokenName = tempdata[0]; - String setCode = tempdata[1]; // Load the paper token from filename + edition CardEdition edition = StaticData.instance().getEditions().get(setCode); @@ -251,7 +259,12 @@ public abstract class ImageFetcher { //PaperToken pt = StaticData.instance().getAllTokens().getToken(tokenName, setCode); Collection allTokens = edition.getTokens().get(tokenName); - if (!allTokens.isEmpty()) { + if (tempdata.length > 2) { + String tokenCode = edition.getTokensCode(); + String langCode = edition.getCardsLangCode(); + // just assume the CNr from the token image is valid + downloadUrls.add(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD + ImageUtil.getScryfallTokenDownloadUrl(tempdata[2], tokenCode, langCode)); + } else if (!allTokens.isEmpty()) { // This loop is going to try to download all the arts until it finds one // This is a bit wrong since it _should_ just be trying to get the one with the appropriate collector number // Since we don't have that for tokens, we'll just take the first one