diff --git a/forge-core/src/main/java/forge/token/TokenDb.java b/forge-core/src/main/java/forge/token/TokenDb.java index 9cfc5e44949..31b2ed944c8 100644 --- a/forge-core/src/main/java/forge/token/TokenDb.java +++ b/forge-core/src/main/java/forge/token/TokenDb.java @@ -35,11 +35,32 @@ public class TokenDb implements ITokenDatabase { private final CardEdition.Collection editions; private final Map rulesByName; + // null preserves first-alphabetical match; adventure pushes a filter here. + private Predicate defaultEditionFilter = null; + // Blocklist of "{EDITION_CODE}/{tokenScript}" pairs; skipped in fallback. + private Set restrictedTokenEntries = Collections.emptySet(); + // When true and a host-card date is known, pick the legal edition whose + // release date is closest to the host's, so eras match (e.g. a 1999 card + // gets a 1998 Unglued token rather than a 2002 Player Rewards print). + private boolean preferEraMatchedArt = false; + public TokenDb(Map rules, CardEdition.Collection editions) { this.rulesByName = rules; this.editions = editions; } + public void setDefaultEditionFilter(Predicate filter) { + this.defaultEditionFilter = filter; + } + + public void setRestrictedTokenEntries(Set entries) { + this.restrictedTokenEntries = entries != null ? entries : Collections.emptySet(); + } + + public void setPreferEraMatchedArt(boolean flag) { + this.preferEraMatchedArt = flag; + } + public boolean containsRule(String rule) { return this.rulesByName.containsKey(rule); } @@ -83,15 +104,53 @@ protected PaperToken addTokenInSet(CardEdition edition, String name, CardEdition return new PaperToken(rules, edition, name, t.collectorNumber(), t.artistName()); } - // try all editions to find token - protected PaperToken fallbackToken(String name) { + // Null filter: historical first-alphabetical match. Non-null: random among + // editions that register the token and pass the filter, or null if none. + // When preferEraMatchedArt is on and hostDate != null, instead picks the + // legal edition whose release date is closest to hostDate. + public PaperToken getTokenFromEditions(String tokenName, Predicate editionFilter, Date hostDate) { + if (editionFilter == null) { + for (CardEdition edition : this.editions) { + if (restrictedTokenEntries.contains(edition.getCode() + "/" + tokenName)) continue; + String fullName = String.format("%s_%s", tokenName, edition.getCode().toLowerCase()); + if (loadTokenFromSet(edition, tokenName)) { + return Aggregates.random(allTokenByName.get(fullName)); + } + } + return null; + } + List legal = new ArrayList<>(); for (CardEdition edition : this.editions) { - String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase()); - if (loadTokenFromSet(edition, name)) { - return Aggregates.random(allTokenByName.get(fullName)); + if (!loadTokenFromSet(edition, tokenName)) continue; + if (restrictedTokenEntries.contains(edition.getCode() + "/" + tokenName)) continue; + if (editionFilter.test(edition)) legal.add(edition); + } + if (legal.isEmpty()) return null; + CardEdition pick; + if (preferEraMatchedArt && hostDate != null) { + pick = legal.get(0); + long best = Math.abs(pick.getDate().getTime() - hostDate.getTime()); + for (int i = 1; i < legal.size(); i++) { + long delta = Math.abs(legal.get(i).getDate().getTime() - hostDate.getTime()); + if (delta < best) { + best = delta; + pick = legal.get(i); + } } + } else { + pick = Aggregates.random(legal); } - return null; + String fullName = String.format("%s_%s", tokenName, pick.getCode().toLowerCase()); + return Aggregates.random(allTokenByName.get(fullName)); + } + + protected PaperToken fallbackToken(String name, String hostEditionCode) { + Date hostDate = null; + if (hostEditionCode != null) { + CardEdition host = this.editions.get(hostEditionCode); + if (host != null) hostDate = host.getDate(); + } + return getTokenFromEditions(name, defaultEditionFilter, hostDate); } @Override @@ -119,7 +178,7 @@ public PaperToken getToken(String tokenName, String edition, int artIndex) { return Iterables.get(collection, artIndex - 1); } - PaperToken fallback = this.fallbackToken(tokenName); + PaperToken fallback = this.fallbackToken(tokenName, edition); if (fallback != null) { return fallback; } diff --git a/forge-game/src/main/java/forge/game/card/token/TokenInfo.java b/forge-game/src/main/java/forge/game/card/token/TokenInfo.java index 012257c0f46..da0698aff56 100644 --- a/forge-game/src/main/java/forge/game/card/token/TokenInfo.java +++ b/forge-game/src/main/java/forge/game/card/token/TokenInfo.java @@ -25,8 +25,18 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; public class TokenInfo { + // Per-game pin so same-type tokens share art. Weak keys GC finished games. + private static final Map> TOKEN_EDITION_PINS = + java.util.Collections.synchronizedMap(new WeakHashMap<>()); + + private static Map getPinsFor(Game game) { + return TOKEN_EDITION_PINS.computeIfAbsent(game, g -> new ConcurrentHashMap<>()); + } + final String name; final String imageName; final String manaCost; @@ -289,7 +299,13 @@ static public Card getProtoType(final String script, final SpellAbility sa, fina } String edition = Objects.requireNonNullElse(editionHost, host).getSetCode(); edition = Objects.requireNonNullElse(StaticData.instance().getCardEdition(edition).getTokenSet(script), edition); + Map pins = getPinsFor(game); + String pinned = pins.get(script); + if (pinned != null) edition = pinned; PaperToken token = StaticData.instance().getAllTokens().getToken(script, edition); + if (token != null && pinned == null) { + pins.put(script, token.getEdition()); + } if (token == null) { return null; diff --git a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java index 79318f9ecf1..d2a06a10676 100644 --- a/forge-gui-mobile/src/forge/adventure/data/ConfigData.java +++ b/forge-gui-mobile/src/forge/adventure/data/ConfigData.java @@ -26,6 +26,7 @@ public class ConfigData { public String[] restrictedCards; public String[] restrictedEditions; public String[] restrictedBlocks; + public String[] restrictedTokens; public String[] allowedEditions; public boolean vintageOnlyEditions = false; public String[] restrictedEvents; diff --git a/forge-gui-mobile/src/forge/adventure/data/SettingData.java b/forge-gui-mobile/src/forge/adventure/data/SettingData.java index bfacffaacc9..df6dbb842a7 100644 --- a/forge-gui-mobile/src/forge/adventure/data/SettingData.java +++ b/forge-gui-mobile/src/forge/adventure/data/SettingData.java @@ -28,4 +28,5 @@ public class SettingData { public boolean generateLDADecks; public boolean bindEquipmentLoadoutsToDecks; public boolean drawChevronsToHiddenEnemiesInClearQuest; + public boolean preferEraMatchedTokenArt; } diff --git a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java index e753a40135f..304594ea2b7 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java @@ -239,6 +239,15 @@ public void changed(ChangeEvent event, Actor actor) { Config.instance().saveSettings(); } }); + addSettingField(Forge.getLocalizer().getMessage("lblPreferEraMatchedTokenArt"), Config.instance().getSettingData().preferEraMatchedTokenArt, new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + boolean enabled = ((CheckBox) actor).isChecked(); + Config.instance().getSettingData().preferEraMatchedTokenArt = enabled; + forge.model.FModel.getMagicDb().getAllTokens().setPreferEraMatchedArt(enabled); + Config.instance().saveSettings(); + } + }); addSettingField(Forge.getLocalizer().getMessage("lblExcludeAlchemyVariants"), Config.instance().getSettingData().excludeAlchemyVariants, new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { diff --git a/forge-gui-mobile/src/forge/adventure/util/Config.java b/forge-gui-mobile/src/forge/adventure/util/Config.java index 63eec6d4f20..1c209233ba3 100644 --- a/forge-gui-mobile/src/forge/adventure/util/Config.java +++ b/forge-gui-mobile/src/forge/adventure/util/Config.java @@ -140,6 +140,32 @@ public ConfigData getConfigData() { return configData; } + // Push the plane's allowed/restricted editions and restricted token pairs into TokenDb. + private void applyTokenEditionFilter() { + if (configData == null) return; + String[] allowedArr = configData.allowedEditions; + String[] restrictedArr = configData.restrictedEditions; + String[] restrictedTokensArr = configData.restrictedTokens; + Set allowed = (allowedArr == null || allowedArr.length == 0) + ? null : new HashSet<>(Arrays.asList(allowedArr)); + Set restricted = (restrictedArr == null || restrictedArr.length == 0) + ? Collections.emptySet() : new HashSet<>(Arrays.asList(restrictedArr)); + Set restrictedTokens = (restrictedTokensArr == null || restrictedTokensArr.length == 0) + ? Collections.emptySet() : new HashSet<>(Arrays.asList(restrictedTokensArr)); + FModel.getMagicDb().getAllTokens().setRestrictedTokenEntries(restrictedTokens); + FModel.getMagicDb().getAllTokens().setPreferEraMatchedArt( + settingsData != null && settingsData.preferEraMatchedTokenArt); + if (allowed == null && restricted.isEmpty()) { + FModel.getMagicDb().getAllTokens().setDefaultEditionFilter(null); + return; + } + FModel.getMagicDb().getAllTokens().setDefaultEditionFilter(edition -> { + String code = edition.getCode(); + if (restricted.contains(code)) return false; + return allowed == null || allowed.contains(code); + }); + } + public int getBlurDivisor() { int val = 1; try { @@ -555,6 +581,7 @@ public String getCommanderPreconDeckPath(int deckIndex) { public void loadResources() { AdventureOverrides.instance().load(prefix, FModel.getMagicDb().getEditions(), configData); + applyTokenEditionFilter(); RewardData.getAllCards();//initialize before loading custom cards final CardRules.Reader rulesReader = new CardRules.Reader(); ImageKeys.ADVENTURE_CARD_PICS_DIR = Config.currentConfig.getCommonFilePath(forge.adventure.util.Paths.CUSTOM_CARDS_PICS);// not the cleanest solution diff --git a/forge-gui-mobile/src/forge/assets/ImageCache.java b/forge-gui-mobile/src/forge/assets/ImageCache.java index 704b18b7a25..4047d93e64e 100644 --- a/forge-gui-mobile/src/forge/assets/ImageCache.java +++ b/forge-gui-mobile/src/forge/assets/ImageCache.java @@ -450,7 +450,7 @@ public void preloadCache(Deck deck) { } public TextureRegion croppedBorderImage(Texture image) { - if (!image.toString().contains(".fullborder.")) + if (!image.toString().contains(".fullborder.") && !image.toString().contains("tokens")) return new TextureRegion(image); float rscale = 0.96f; int rw = Math.round(image.getWidth() * rscale); diff --git a/forge-gui/res/adventure/Shandalar Old Border/config.json b/forge-gui/res/adventure/Shandalar Old Border/config.json index 6250383c602..ec0c14a04af 100644 --- a/forge-gui/res/adventure/Shandalar Old Border/config.json +++ b/forge-gui/res/adventure/Shandalar Old Border/config.json @@ -115,13 +115,14 @@ ], "restrictedEditions": ["8ED", "P8ED", "MRD"], "restrictedBlocks": ["Ice Age"], + "restrictedTokens": ["P03/r_4_4_bird_flying", "P03/b_x_x_demon_flying"], "allowedEditions": [ "LEA", "LEB", "2ED", "3ED", "ARN", "ATQ", "LEG", "DRK", "FEM", "4ED", "ICE", "CHR", "HML", "ALL", "MIR", "VIS", "5ED", "POR", "WTH", "TMP", "STH", "EXO", "PO2", "USG", "ATH", "ULG", "UDS", "6ED", "PTK", "S99", "MMQ", "BRB", "NMS", "S00", "PCY", "INV", "BTD", "PLS", "APC", "7ED", "ODY", "DKM", "TOR", "JUD", "ONS", "LGN", "SCG", "PHPR", "PAST", "DRC94", - "O90P", "PALP", "PELP", "PGRU", "UGL" + "O90P", "PALP", "PELP", "PGRU", "UGL", "MPR", "PR2", "P03", "TDLS", "OBC" ], "vintageOnlyEditions": true, "allowedJumpstart": [], diff --git a/forge-gui/res/editions/Astral Cards.txt b/forge-gui/res/editions/Astral Cards.txt index 4927943c554..bfc682f0dca 100644 --- a/forge-gui/res/editions/Astral Cards.txt +++ b/forge-gui/res/editions/Astral Cards.txt @@ -4,6 +4,7 @@ Date=1997-04-01 Name=Astral Cards Type=Funny ScryfallCode=PAST +TokensCode=PAST [cards] 1 C Aswan Jaguar @Pat Lewis @@ -18,3 +19,11 @@ ScryfallCode=PAST 10 C Whimsy @Anson Maddocks 11 C Pandora's Box @Amy Weber 12 C Gem Bazaar @Liz Danforth + +[tokens] +13 wasp @Sandra Everingham +14 c_5_5_a_djinn_flying @Randy Gallegos +15 c_1_1_a_tetravite_flying_noenchant @Mark Tedin +16 c_1_1_a_snake_poison @Mark Tedin +17 spawn_of_azar @Randy Gallegos +18 r_4_4_bird_flying @Christopher Rush diff --git a/forge-gui/res/editions/Duelist Magazine.txt b/forge-gui/res/editions/Duelist Magazine.txt new file mode 100644 index 00000000000..b561f8d4f63 --- /dev/null +++ b/forge-gui/res/editions/Duelist Magazine.txt @@ -0,0 +1,14 @@ +[metadata] +Code=TDLS +Date=1995-03-01 +Name=Duelist Magazine Tokens +Type=Promo +ScryfallCode=TDLS +TokensCode=TDLS + +[tokens] +1 b_0_1_thrull @Anson Maddocks +2 g_1_1_saproling @Daniel Gelon +3 w_1_1_citizen @Tom Wanerstrand +4 u_1_1_camarid @Douglas Shuler +5 r_1_1_goblin @Christopher Rush diff --git a/forge-gui/res/editions/Magic Player Rewards 2002.txt b/forge-gui/res/editions/Magic Player Rewards 2002.txt new file mode 100644 index 00000000000..0254878ba7e --- /dev/null +++ b/forge-gui/res/editions/Magic Player Rewards 2002.txt @@ -0,0 +1,15 @@ +[metadata] +Code=PR2 +Date=2002-01-01 +Name=Magic Player Rewards 2002 +Type=Promo +ScryfallCode=PR2 +TokensCode=PR2 + +[tokens] +3 g_1_1_squirrel @Ron Spencer +4 b_2_2_zombie @Dana Knutson +5 g_3_3_elephant @Arnie Swekel +6 g_6_6_wurm @Alan Pollack +7 r_5_5_dragon_flying @Glen Angus +8 w_1_1_soldier @Ron Spencer diff --git a/forge-gui/res/editions/Old Border Custom.txt b/forge-gui/res/editions/Old Border Custom.txt new file mode 100644 index 00000000000..12fdeca4ab2 --- /dev/null +++ b/forge-gui/res/editions/Old Border Custom.txt @@ -0,0 +1,58 @@ +[metadata] +Code=OBC +Date=2003-05-26 +Name=Old Border Custom Tokens +Type=Promo +ScryfallCode=OBC +TokensCode=OBC + +[tokens] +1 b_0_1_insect @Geofrey Darrow +2 b_1_1_minion @Ron Spencer +3 b_1_1_rat @DiTerlizzi +4 b_1_1_skeleton_regenerate @NéNé Thomas +5 b_2_1_cat @Heather Hudson +6 b_3_3_angel_flying @Anson Maddocks +7 b_3_3_kavu @Tony Szczudlo +8 b_6_6_wurm_trample @Jeff Easley +9 b_x_x_phyrexian_minion @Sam Wood +10 b_x_x_spirit @Jesper Myrfors +11 br_3_1_graveborn_haste @Mike Kimble +12 butterfly @Carl Critchlow +13 c_0_1_a_prism @Ron Spencer +14 c_0_2_a_wall_defender @Dan Frazier +15 c_1_1_a_gnome @Jeff Laubenstein +16 c_1_1_a_thopter_flying @Doug Chaffee +17 carnivore @Val Mayerik +18 g_1_1_cat @Susan Van Camp +19 g_1_1_dog @Jeff Miracola +20 g_1_1_hippo @Amy Weber +21 g_1_1_snake @Roger Raupp +22 g_1_1_spike @Adam Rex +23 g_1_1_splinter_flying_cum @Ron Spencer +24 g_2_2_beast @Carl Critchlow +25 g_2_2_monkey @Carl Critchlow +26 g_3_3_beast @Paolo Parente +27 g_3_3_centaur @Alex Horley-Orlandelli +28 g_7_7_elemental_trample @rk post +29 hornet @Ron Spencer +30 kelp @Rob Alexander +31 minor_demon @Quinton Hoover +32 r_1_1_elemental_cat_haste @David Martin +33 r_1_1_goblin_scout_mountainwalk @Geofrey Darrow +34 rgw_1_1_sand_warrior @Richard Kane Ferguson +35 stangg_twin @Mark Poole +36 tombspawn @Dom! +37 u_0_1_starfish @Alan Rabinowitz +38 u_5_5_wall_defender @Brian Snõddy +39 u_x_x_orb_flying @Mark Tedin +40 w_0_1_caribou @Ruth Thompson +41 w_0_1_deserter @Cecil Fernando +42 w_1_1_bird_flying @Mark Poole +43 w_1_1_knight_banding @Ruth Thompson +44 w_2_2_knight @Greg Staples +45 w_2_2_reflection @D. Alexander Gregory +46 w_4_4_angel_flying @Paolo Parente +47 w_x_x_reflection @Scott M. Fischer +48 wolves_of_the_hunt @Jeff A. Menges +49 wood @Mark Tedin diff --git a/forge-gui/res/editions/Unglued.txt b/forge-gui/res/editions/Unglued.txt index 3a31a0fbfff..44a637c9f54 100644 --- a/forge-gui/res/editions/Unglued.txt +++ b/forge-gui/res/editions/Unglued.txt @@ -100,9 +100,9 @@ ScryfallCode=UGL 88 L Forest @Terese Nielsen [tokens] -1 w_1_1_pegasus_flying @Mark Zug -2 w_1_1_soldier @Daren Bader -3 b_2_2_zombie @Christopher Rush -4 r_1_1_goblin @Pete Venters -5 g_2_2_sheep @Kev Walker -6 g_1_1_squirrel @Ron Spencer +89 w_1_1_pegasus_flying @Mark Zug +90 w_1_1_soldier @Daren Bader +91 b_2_2_zombie @Christopher Rush +92 r_1_1_goblin @Pete Venters +93 g_2_2_sheep @Kev Walker +94 g_1_1_squirrel @Ron Spencer diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index f04b2d7cbcc..d7660d94a7e 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -3264,6 +3264,7 @@ lblDisableWinLose=Disable WinLose Overlay lblDisableNotForSaleOverlay=Disable Not For Sale Overlay lblShowShopOverlay=Display Shop Item names lblUseAllCardVariants=Use Card Variants from All Sets (Restart Required) +lblPreferEraMatchedTokenArt=Pick Token Art from Edition Closest to Producing Card's Release Date lblExcludeAlchemyVariants=Exclude variants rebalanced for Arena's Alchemy and Historic formats lblGenerateLDADecks=Generate Archetype Decks instead of Genetic AI Decks lblBindEquipmentLoadoutsToDecks=Bind equipment loadouts to decks diff --git a/forge-gui/res/lists/token-images.txt b/forge-gui/res/lists/token-images.txt index 213dc91de77..10734bbf15f 100644 --- a/forge-gui/res/lists/token-images.txt +++ b/forge-gui/res/lists/token-images.txt @@ -1,5 +1,70 @@ #Tokens +# Shandalar Old Border: Card Conjurer TokenOld renders hosted on oldborder-shandalar.net +# TDLS (Duelist Magazine #4, 1995) +TDLS/1_b_0_1_thrull.jpg https://oldborder-shandalar.net/tokens/TDLS/1_b_0_1_thrull.jpg +TDLS/2_g_1_1_saproling.jpg https://oldborder-shandalar.net/tokens/TDLS/2_g_1_1_saproling.jpg +TDLS/3_w_1_1_citizen.jpg https://oldborder-shandalar.net/tokens/TDLS/3_w_1_1_citizen.jpg +TDLS/4_u_1_1_camarid.jpg https://oldborder-shandalar.net/tokens/TDLS/4_u_1_1_camarid.jpg +TDLS/5_r_1_1_goblin.jpg https://oldborder-shandalar.net/tokens/TDLS/5_r_1_1_goblin.jpg +# PAST (Astral Cards, MicroProse Shandalar 1997) +PAST/13_wasp.jpg https://oldborder-shandalar.net/tokens/PAST/13_wasp.jpg +PAST/14_c_5_5_a_djinn_flying.jpg https://oldborder-shandalar.net/tokens/PAST/14_c_5_5_a_djinn_flying.jpg +PAST/15_c_1_1_a_tetravite_flying_noenchant.jpg https://oldborder-shandalar.net/tokens/PAST/15_c_1_1_a_tetravite_flying_noenchant.jpg +PAST/16_c_1_1_a_snake_poison.jpg https://oldborder-shandalar.net/tokens/PAST/16_c_1_1_a_snake_poison.jpg +PAST/17_spawn_of_azar.jpg https://oldborder-shandalar.net/tokens/PAST/17_spawn_of_azar.jpg +PAST/18_r_4_4_bird_flying.jpg https://oldborder-shandalar.net/tokens/PAST/18_r_4_4_bird_flying.jpg +# OBC (Old Border Custom Tokens) +OBC/1_b_0_1_insect.jpg https://oldborder-shandalar.net/tokens/OBC/1_b_0_1_insect.jpg +OBC/2_b_1_1_minion.jpg https://oldborder-shandalar.net/tokens/OBC/2_b_1_1_minion.jpg +OBC/3_b_1_1_rat.jpg https://oldborder-shandalar.net/tokens/OBC/3_b_1_1_rat.jpg +OBC/4_b_1_1_skeleton_regenerate.jpg https://oldborder-shandalar.net/tokens/OBC/4_b_1_1_skeleton_regenerate.jpg +OBC/5_b_2_1_cat.jpg https://oldborder-shandalar.net/tokens/OBC/5_b_2_1_cat.jpg +OBC/6_b_3_3_angel_flying.jpg https://oldborder-shandalar.net/tokens/OBC/6_b_3_3_angel_flying.jpg +OBC/7_b_3_3_kavu.jpg https://oldborder-shandalar.net/tokens/OBC/7_b_3_3_kavu.jpg +OBC/8_b_6_6_wurm_trample.jpg https://oldborder-shandalar.net/tokens/OBC/8_b_6_6_wurm_trample.jpg +OBC/9_b_x_x_phyrexian_minion.jpg https://oldborder-shandalar.net/tokens/OBC/9_b_x_x_phyrexian_minion.jpg +OBC/10_b_x_x_spirit.jpg https://oldborder-shandalar.net/tokens/OBC/10_b_x_x_spirit.jpg +OBC/11_br_3_1_graveborn_haste.jpg https://oldborder-shandalar.net/tokens/OBC/11_br_3_1_graveborn_haste.jpg +OBC/12_butterfly.jpg https://oldborder-shandalar.net/tokens/OBC/12_butterfly.jpg +OBC/13_c_0_1_a_prism.jpg https://oldborder-shandalar.net/tokens/OBC/13_c_0_1_a_prism.jpg +OBC/14_c_0_2_a_wall_defender.jpg https://oldborder-shandalar.net/tokens/OBC/14_c_0_2_a_wall_defender.jpg +OBC/15_c_1_1_a_gnome.jpg https://oldborder-shandalar.net/tokens/OBC/15_c_1_1_a_gnome.jpg +OBC/16_c_1_1_a_thopter_flying.jpg https://oldborder-shandalar.net/tokens/OBC/16_c_1_1_a_thopter_flying.jpg +OBC/17_carnivore.jpg https://oldborder-shandalar.net/tokens/OBC/17_carnivore.jpg +OBC/18_g_1_1_cat.jpg https://oldborder-shandalar.net/tokens/OBC/18_g_1_1_cat.jpg +OBC/19_g_1_1_dog.jpg https://oldborder-shandalar.net/tokens/OBC/19_g_1_1_dog.jpg +OBC/20_g_1_1_hippo.jpg https://oldborder-shandalar.net/tokens/OBC/20_g_1_1_hippo.jpg +OBC/21_g_1_1_snake.jpg https://oldborder-shandalar.net/tokens/OBC/21_g_1_1_snake.jpg +OBC/22_g_1_1_spike.jpg https://oldborder-shandalar.net/tokens/OBC/22_g_1_1_spike.jpg +OBC/23_g_1_1_splinter_flying_cum.jpg https://oldborder-shandalar.net/tokens/OBC/23_g_1_1_splinter_flying_cum.jpg +OBC/24_g_2_2_beast.jpg https://oldborder-shandalar.net/tokens/OBC/24_g_2_2_beast.jpg +OBC/25_g_2_2_monkey.jpg https://oldborder-shandalar.net/tokens/OBC/25_g_2_2_monkey.jpg +OBC/26_g_3_3_beast.jpg https://oldborder-shandalar.net/tokens/OBC/26_g_3_3_beast.jpg +OBC/27_g_3_3_centaur.jpg https://oldborder-shandalar.net/tokens/OBC/27_g_3_3_centaur.jpg +OBC/28_g_7_7_elemental_trample.jpg https://oldborder-shandalar.net/tokens/OBC/28_g_7_7_elemental_trample.jpg +OBC/29_hornet.jpg https://oldborder-shandalar.net/tokens/OBC/29_hornet.jpg +OBC/30_kelp.jpg https://oldborder-shandalar.net/tokens/OBC/30_kelp.jpg +OBC/31_minor_demon.jpg https://oldborder-shandalar.net/tokens/OBC/31_minor_demon.jpg +OBC/32_r_1_1_elemental_cat_haste.jpg https://oldborder-shandalar.net/tokens/OBC/32_r_1_1_elemental_cat_haste.jpg +OBC/33_r_1_1_goblin_scout_mountainwalk.jpg https://oldborder-shandalar.net/tokens/OBC/33_r_1_1_goblin_scout_mountainwalk.jpg +OBC/34_rgw_1_1_sand_warrior.jpg https://oldborder-shandalar.net/tokens/OBC/34_rgw_1_1_sand_warrior.jpg +OBC/35_stangg_twin.jpg https://oldborder-shandalar.net/tokens/OBC/35_stangg_twin.jpg +OBC/36_tombspawn.jpg https://oldborder-shandalar.net/tokens/OBC/36_tombspawn.jpg +OBC/37_u_0_1_starfish.jpg https://oldborder-shandalar.net/tokens/OBC/37_u_0_1_starfish.jpg +OBC/38_u_5_5_wall_defender.jpg https://oldborder-shandalar.net/tokens/OBC/38_u_5_5_wall_defender.jpg +OBC/39_u_x_x_orb_flying.jpg https://oldborder-shandalar.net/tokens/OBC/39_u_x_x_orb_flying.jpg +OBC/40_w_0_1_caribou.jpg https://oldborder-shandalar.net/tokens/OBC/40_w_0_1_caribou.jpg +OBC/41_w_0_1_deserter.jpg https://oldborder-shandalar.net/tokens/OBC/41_w_0_1_deserter.jpg +OBC/42_w_1_1_bird_flying.jpg https://oldborder-shandalar.net/tokens/OBC/42_w_1_1_bird_flying.jpg +OBC/43_w_1_1_knight_banding.jpg https://oldborder-shandalar.net/tokens/OBC/43_w_1_1_knight_banding.jpg +OBC/44_w_2_2_knight.jpg https://oldborder-shandalar.net/tokens/OBC/44_w_2_2_knight.jpg +OBC/45_w_2_2_reflection.jpg https://oldborder-shandalar.net/tokens/OBC/45_w_2_2_reflection.jpg +OBC/46_w_4_4_angel_flying.jpg https://oldborder-shandalar.net/tokens/OBC/46_w_4_4_angel_flying.jpg +OBC/47_w_x_x_reflection.jpg https://oldborder-shandalar.net/tokens/OBC/47_w_x_x_reflection.jpg +OBC/48_wolves_of_the_hunt.jpg https://oldborder-shandalar.net/tokens/OBC/48_wolves_of_the_hunt.jpg +OBC/49_wood.jpg https://oldborder-shandalar.net/tokens/OBC/49_wood.jpg + Arco-Flagellant.jpg https://downloads.cardforge.org/images/tokens/Arco-Flagellant.jpg On an Adventure.jpg https://downloads.cardforge.org/images/tokens/On an Adventure.jpg Sicarian Infiltrator.jpg https://downloads.cardforge.org/images/tokens/Sicarian%20Infiltrator.jpg @@ -2522,4 +2587,4 @@ plantwall_lvl2.jpg https://raw.githubusercontent.com/Card-Fo plantwall_lvl3.jpg https://raw.githubusercontent.com/Card-Forge/forge-extras/refs/heads/main/images/pets/plantwall_lvl3.jpg plantwall_lvl4.jpg https://raw.githubusercontent.com/Card-Forge/forge-extras/refs/heads/main/images/pets/plantwall_lvl4.jpg plantwall_lvl5.jpg https://raw.githubusercontent.com/Card-Forge/forge-extras/refs/heads/main/images/pets/plantwall_lvl5.jpg -plantwall_lvl6.jpg https://raw.githubusercontent.com/Card-Forge/forge-extras/refs/heads/main/images/pets/plantwall_lvl6.jpg \ No newline at end of file +plantwall_lvl6.jpg https://raw.githubusercontent.com/Card-Forge/forge-extras/refs/heads/main/images/pets/plantwall_lvl6.jpg diff --git a/forge-gui/src/main/java/forge/util/ImageFetcher.java b/forge-gui/src/main/java/forge/util/ImageFetcher.java index 925456a050b..40a8bfd3d85 100644 --- a/forge-gui/src/main/java/forge/util/ImageFetcher.java +++ b/forge-gui/src/main/java/forge/util/ImageFetcher.java @@ -280,6 +280,15 @@ public void fetchImage(final String imageKey, final Callback callback) { || destFile.exists()) return; + // token-images.txt lets mods route specific tokens to hosted URLs. + for (org.apache.commons.lang3.tuple.Pair pair : + FileUtil.readNameUrlFile(ForgeConstants.IMAGE_LIST_TOKENS_FILE)) { + if (filename.equalsIgnoreCase(pair.getLeft())) { + downloadUrls.add(pair.getRight()); + break; + } + } + if (tempdata.length < 2) { if (!"planechase".equals(tempdata[0])) System.err.println("Token image key is malformed: " + imageKey); @@ -289,9 +298,13 @@ public void fetchImage(final String imageKey, final Callback callback) { // Load the paper token from filename + edition CardEdition edition = StaticData.instance().getEditions().get(setCode); - if (edition == null || edition.getType() == CardEdition.Type.CUSTOM_SET) - return; //Custom set token, skip fetching. - + if (edition == null || edition.getType() == CardEdition.Type.CUSTOM_SET) { + // Custom/unknown set (e.g. TDLS/Duelist): rely on the URL map above. + if (downloadUrls.isEmpty()) return; + ImageKeys.missingCards.add(filename); + setupObserver(destFile.getAbsolutePath(), callback, downloadUrls); + return; + } // PaperToken pt = StaticData.instance().getAllTokens().getToken(tokenName, setCode); Collection allTokens = edition.getTokens().get(tokenName);