Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions forge-core/src/main/java/forge/token/TokenDb.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,32 @@ public class TokenDb implements ITokenDatabase {
private final CardEdition.Collection editions;
private final Map<String, CardRules> rulesByName;

// null preserves first-alphabetical match; adventure pushes a filter here.
private Predicate<CardEdition> defaultEditionFilter = null;
// Blocklist of "{EDITION_CODE}/{tokenScript}" pairs; skipped in fallback.
private Set<String> 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<String, CardRules> rules, CardEdition.Collection editions) {
this.rulesByName = rules;
this.editions = editions;
}

public void setDefaultEditionFilter(Predicate<CardEdition> filter) {
this.defaultEditionFilter = filter;
}

public void setRestrictedTokenEntries(Set<String> 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);
}
Expand Down Expand Up @@ -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<CardEdition> 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<CardEdition> 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
Expand Down Expand Up @@ -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;
}
Expand Down
16 changes: 16 additions & 0 deletions forge-game/src/main/java/forge/game/card/token/TokenInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Game, Map<String, String>> TOKEN_EDITION_PINS =
java.util.Collections.synchronizedMap(new WeakHashMap<>());

private static Map<String, String> getPinsFor(Game game) {
return TOKEN_EDITION_PINS.computeIfAbsent(game, g -> new ConcurrentHashMap<>());
}

final String name;
final String imageName;
final String manaCost;
Expand Down Expand Up @@ -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<String, String> 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;
Expand Down
1 change: 1 addition & 0 deletions forge-gui-mobile/src/forge/adventure/data/ConfigData.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions forge-gui-mobile/src/forge/adventure/data/SettingData.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ public class SettingData {
public boolean generateLDADecks;
public boolean bindEquipmentLoadoutsToDecks;
public boolean drawChevronsToHiddenEnemiesInClearQuest;
public boolean preferEraMatchedTokenArt;
}
9 changes: 9 additions & 0 deletions forge-gui-mobile/src/forge/adventure/scene/SettingsScene.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions forge-gui-mobile/src/forge/adventure/util/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> allowed = (allowedArr == null || allowedArr.length == 0)
? null : new HashSet<>(Arrays.asList(allowedArr));
Set<String> restricted = (restrictedArr == null || restrictedArr.length == 0)
? Collections.emptySet() : new HashSet<>(Arrays.asList(restrictedArr));
Set<String> 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 {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion forge-gui-mobile/src/forge/assets/ImageCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion forge-gui/res/adventure/Shandalar Old Border/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
9 changes: 9 additions & 0 deletions forge-gui/res/editions/Astral Cards.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Date=1997-04-01
Name=Astral Cards
Type=Funny
ScryfallCode=PAST
TokensCode=PAST

[cards]
1 C Aswan Jaguar @Pat Lewis
Expand All @@ -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
14 changes: 14 additions & 0 deletions forge-gui/res/editions/Duelist Magazine.txt
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions forge-gui/res/editions/Magic Player Rewards 2002.txt
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions forge-gui/res/editions/Old Border Custom.txt
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions forge-gui/res/editions/Unglued.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions forge-gui/res/languages/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading