From b8899535043ce2ccfb28dd441694b05039c32ac3 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 26 May 2019 13:29:54 -0400 Subject: [PATCH 1/3] Implemented Winds of Change --- .../src/mage/cards/w/WindsOfAbandon.java | 152 ++++++++++++++++++ Mage.Sets/src/mage/sets/ModernHorizons.java | 1 + 2 files changed, 153 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/w/WindsOfAbandon.java diff --git a/Mage.Sets/src/mage/cards/w/WindsOfAbandon.java b/Mage.Sets/src/mage/cards/w/WindsOfAbandon.java new file mode 100644 index 0000000000..53b3b02ea2 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WindsOfAbandon.java @@ -0,0 +1,152 @@ +package mage.cards.w; + +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.OverloadAbility; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetCardInLibrary; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WindsOfAbandon extends CardImpl { + + public WindsOfAbandon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{W}"); + + // Exile target creature you don't control. For each creature exiled this way, its controller searches their library for a basic land card. Those players put those cards onto the battlefield tapped, then shuffle their libraries. + this.getSpellAbility().addEffect(new WindsOfAbandonEffect()); + this.getSpellAbility().addTarget(new TargetPermanent(WindsOfAbandonOverloadEffect.filter)); + + // Overload {4}{W}{W} + this.addAbility(new OverloadAbility( + this, new WindsOfAbandonOverloadEffect(), new ManaCostsImpl("{4}{W}{W}") + )); + } + + private WindsOfAbandon(final WindsOfAbandon card) { + super(card); + } + + @Override + public WindsOfAbandon copy() { + return new WindsOfAbandon(this); + } +} + +class WindsOfAbandonEffect extends OneShotEffect { + + WindsOfAbandonEffect() { + super(Outcome.Exile); + staticText = "Exile target creature you don't control. For each creature exiled this way, " + + "its controller searches their library for a basic land card. " + + "Those players put those cards onto the battlefield tapped, then shuffle their libraries."; + } + + private WindsOfAbandonEffect(final WindsOfAbandonEffect effect) { + super(effect); + } + + @Override + public WindsOfAbandonEffect copy() { + return new WindsOfAbandonEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (controller == null || permanent == null) { + return false; + } + Player player = game.getPlayer(permanent.getControllerId()); + // if the zone change to exile gets replaced does not prevent the target controller to be able to search + controller.moveCardToExileWithInfo(permanent, null, "", source.getSourceId(), game, Zone.BATTLEFIELD, true); + if (!player.chooseUse(Outcome.PutCardInPlay, "Search your library for a basic land card?", source, game)) { + return true; + } + TargetCardInLibrary target = new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND); + if (player.searchLibrary(target, source, game)) { + Card card = player.getLibrary().getCard(target.getFirstTarget(), game); + if (card != null) { + player.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null); + } + } + player.shuffleLibrary(source, game); + return true; + } +} + +class WindsOfAbandonOverloadEffect extends OneShotEffect { + + static final FilterPermanent filter = new FilterCreaturePermanent("creature you don't control"); + + static { + filter.add(new ControllerPredicate(TargetController.NOT_YOU)); + } + + WindsOfAbandonOverloadEffect() { + super(Outcome.Exile); + staticText = "Exile each creature you don't control. For each creature exiled this way, " + + "its controller searches their library for a basic land card. " + + "Those players put those cards onto the battlefield tapped, then shuffle their libraries."; + } + + private WindsOfAbandonOverloadEffect(final WindsOfAbandonOverloadEffect effect) { + super(effect); + } + + @Override + public WindsOfAbandonOverloadEffect copy() { + return new WindsOfAbandonOverloadEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Map playerMap = new HashMap(); + Cards cards = new CardsImpl(); + for (Permanent permanent : game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source.getSourceId(), game)) { + int count = playerMap.getOrDefault(permanent.getControllerId(), 0); + playerMap.put(permanent.getControllerId(), count + 1); + cards.add(permanent); + } + controller.moveCards(cards, Zone.EXILED, source, game); + for (UUID playerId : game.getOpponents(source.getControllerId())) { + Player player = game.getPlayer(playerId); + int count = playerMap.getOrDefault(playerId, 0); + if (player == null || count == 0) { + continue; + } + TargetCardInLibrary target = new TargetCardInLibrary(0, count, StaticFilters.FILTER_CARD_BASIC_LAND); + if (player.searchLibrary(target, source, game)) { + Card card = player.getLibrary().getCard(target.getFirstTarget(), game); + if (card != null) { + player.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null); + } + } + player.shuffleLibrary(source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/ModernHorizons.java b/Mage.Sets/src/mage/sets/ModernHorizons.java index abc65b42c1..e2be95ef4a 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons.java @@ -133,6 +133,7 @@ public final class ModernHorizons extends ExpansionSet { cards.add(new SetCardInfo("Venomous Changeling", 114, Rarity.COMMON, mage.cards.v.VenomousChangeling.class)); cards.add(new SetCardInfo("Wall of One Thousand Cuts", 36, Rarity.COMMON, mage.cards.w.WallOfOneThousandCuts.class)); cards.add(new SetCardInfo("Waterlogged Grove", 249, Rarity.RARE, mage.cards.w.WaterloggedGrove.class)); + cards.add(new SetCardInfo("Winds of Abandon", 37, Rarity.RARE, mage.cards.w.WindsOfAbandon.class)); cards.add(new SetCardInfo("Wing Shards", 38, Rarity.UNCOMMON, mage.cards.w.WingShards.class)); cards.add(new SetCardInfo("Wrenn and Six", 217, Rarity.MYTHIC, mage.cards.w.WrennAndSix.class)); cards.add(new SetCardInfo("Zhalfirin Decoy", 39, Rarity.UNCOMMON, mage.cards.z.ZhalfirinDecoy.class)); From bf528dfc59ca82ab4c8216d83cb466589b22e2aa Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sun, 26 May 2019 17:18:18 -0400 Subject: [PATCH 2/3] Implemented Splicer's Skill --- Mage.Sets/src/mage/cards/s/SplicersSkill.java | 35 ++++ Mage.Sets/src/mage/sets/ModernHorizons.java | 1 + .../SpliceOntoInstantOrSorceryAbility.java | 182 ++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SplicersSkill.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/SpliceOntoInstantOrSorceryAbility.java diff --git a/Mage.Sets/src/mage/cards/s/SplicersSkill.java b/Mage.Sets/src/mage/cards/s/SplicersSkill.java new file mode 100644 index 0000000000..36522d00df --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SplicersSkill.java @@ -0,0 +1,35 @@ +package mage.cards.s; + +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.SpliceOntoInstantOrSorceryAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.permanent.token.GolemToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SplicersSkill extends CardImpl { + + public SplicersSkill(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{W}"); + + // Create a 3/3 colorless Golem artifact creature token. + this.getSpellAbility().addEffect(new CreateTokenEffect(new GolemToken())); + + // Splice onto instant or sorcery {3}{W} + this.addAbility(new SpliceOntoInstantOrSorceryAbility("{3}{W}")); + } + + private SplicersSkill(final SplicersSkill card) { + super(card); + } + + @Override + public SplicersSkill copy() { + return new SplicersSkill(this); + } +} diff --git a/Mage.Sets/src/mage/sets/ModernHorizons.java b/Mage.Sets/src/mage/sets/ModernHorizons.java index e2be95ef4a..208398cc2f 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons.java @@ -117,6 +117,7 @@ public final class ModernHorizons extends ExpansionSet { cards.add(new SetCardInfo("Snow-Covered Mountain", 253, Rarity.LAND, mage.cards.s.SnowCoveredMountain.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Snow-Covered Plains", 250, Rarity.LAND, mage.cards.s.SnowCoveredPlains.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Snow-Covered Swamp", 252, Rarity.LAND, mage.cards.s.SnowCoveredSwamp.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Splicer's Skill", 31, Rarity.UNCOMMON, mage.cards.s.SplicersSkill.class)); cards.add(new SetCardInfo("Spore Frog", 180, Rarity.COMMON, mage.cards.s.SporeFrog.class)); cards.add(new SetCardInfo("Springbloom Druid", 181, Rarity.COMMON, mage.cards.s.SpringbloomDruid.class)); cards.add(new SetCardInfo("Squirrel Nest", 182, Rarity.UNCOMMON, mage.cards.s.SquirrelNest.class)); diff --git a/Mage/src/main/java/mage/abilities/keyword/SpliceOntoInstantOrSorceryAbility.java b/Mage/src/main/java/mage/abilities/keyword/SpliceOntoInstantOrSorceryAbility.java new file mode 100644 index 0000000000..52136ebe7f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/SpliceOntoInstantOrSorceryAbility.java @@ -0,0 +1,182 @@ +package mage.abilities.keyword; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.costs.CostsImpl; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.SpliceCardEffectImpl; +import mage.cards.Card; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SpellAbilityType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.stack.Spell; +import mage.players.Player; + +import java.util.Iterator; + +/** + * 702.45. Splice + *

+ * 702.45a Splice is a static ability that functions while a card is in your + * hand. "Splice onto [subtype] [cost]" means "You may reveal this card from + * your hand as you cast a [subtype] spell. If you do, copy this card's text box + * onto that spell and pay [cost] as an additional cost to cast that spell." + * Paying a card's splice cost follows the rules for paying additional costs in + * rules 601.2b and 601.2e-g. + *

+ * Example: Since the card with splice remains in the player's hand, it can + * later be cast normally or spliced onto another spell. It can even be + * discarded to pay a "discard a card" cost of the spell it's spliced onto. + *

+ * 702.45b You can't choose to use a splice ability if you can't make the + * required choices (targets, etc.) for that card's instructions. You can't + * splice any one card onto the same spell more than once. If you're splicing + * more than one card onto a spell, reveal them all at once and choose the order + * in which their instructions will be followed. The instructions on the main + * spell have to be followed first. + *

+ * 702.45c The spell has the characteristics of the main spell, plus the text + * boxes of each of the spliced cards. The spell doesn't gain any other + * characteristics (name, mana cost, color, supertypes, card types, subtypes, + * etc.) of the spliced cards. Text copied onto the spell that refers to a card + * by name refers to the spell on the stack, not the card from which the text + * was copied. + *

+ * Example: Glacial Ray is a red card with splice onto Arcane that reads, + * "Glacial Ray deals 2 damage to any target." Suppose Glacial Ray is spliced + * onto Reach Through Mists, a blue spell. The spell is still blue, and Reach + * Through Mists deals the damage. This means that the ability can target a + * creature with protection from red and deal 2 damage to that creature. + *

+ * 702.45d Choose targets for the added text normally (see rule 601.2c). Note + * that a spell with one or more targets will be countered if all of its targets + * are illegal on resolution. + *

+ * 702.45e The spell loses any splice changes once it leaves the stack (for + * example, when it's countered, it's exiled, or it resolves). + *

+ * Rulings + *

+ * You must reveal all of the cards you intend to splice at the same time. Each + * individual card can only be spliced once onto a spell. If you have more than + * one card with the same name in your hand, you may splice both of them onto + * the spell. A card with a splice ability can't be spliced onto itself because + * the spell is on the stack (and not in your hand) when you reveal the cards + * you want to splice onto it. The target for a card that's spliced onto a spell + * may be the same as the target chosen for the original spell or for another + * spliced-on card. (A recent change to the targeting rules allows this, but + * most other cards are unaffected by the change.) If you splice a targeted card + * onto an untargeted spell, the entire spell will be countered if the target + * isn't legal when the spell resolves. If you splice an untargeted card onto a + * targeted spell, the entire spell will be countered if the target isn't legal + * when the spell resolves. A spell is countered on resolution only if *all* of + * its targets are illegal (or the spell is countered by an effect). + * + * @author LevelX2 + */ +public class SpliceOntoInstantOrSorceryAbility extends SimpleStaticAbility { + + private static final String KEYWORD_TEXT = "Splice onto instant or sorcery"; + private Costs spliceCosts = new CostsImpl<>(); + private boolean nonManaCosts = false; + + public SpliceOntoInstantOrSorceryAbility(String manaString) { + super(Zone.HAND, new SpliceOntoInstantOrSorceryEffect()); + spliceCosts.add(new ManaCostsImpl<>(manaString)); + } + + public SpliceOntoInstantOrSorceryAbility(Cost cost) { + super(Zone.HAND, new SpliceOntoInstantOrSorceryEffect()); + spliceCosts.add(cost); + nonManaCosts = true; + } + + private SpliceOntoInstantOrSorceryAbility(final SpliceOntoInstantOrSorceryAbility ability) { + super(ability); + this.spliceCosts = ability.spliceCosts.copy(); + this.nonManaCosts = ability.nonManaCosts; + } + + @Override + public SimpleStaticAbility copy() { + return new SpliceOntoInstantOrSorceryAbility(this); + } + + Costs getSpliceCosts() { + return spliceCosts; + } + + @Override + public String getRule() { + StringBuilder sb = new StringBuilder(); + sb.append(KEYWORD_TEXT).append(nonManaCosts ? "—" : " "); + sb.append(spliceCosts.getText()).append(nonManaCosts ? ". " : " "); + sb.append("(As you cast an instant or sorcery spell, you may reveal this card from your hand and pay its splice cost. If you do, add this card's effects to that spell.)"); + return sb.toString(); + } +} + +class SpliceOntoInstantOrSorceryEffect extends SpliceCardEffectImpl { + + SpliceOntoInstantOrSorceryEffect() { + super(Duration.WhileOnBattlefield, Outcome.Copy); + staticText = "Splice onto Instant or Sorcery"; + } + + private SpliceOntoInstantOrSorceryEffect(final SpliceOntoInstantOrSorceryEffect effect) { + super(effect); + } + + @Override + public SpliceOntoInstantOrSorceryEffect copy() { + return new SpliceOntoInstantOrSorceryEffect(this); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + Player controller = game.getPlayer(source.getControllerId()); + Card spliceCard = game.getCard(source.getSourceId()); + if (spliceCard != null && controller != null) { + Spell spell = game.getStack().getSpell(abilityToModify.getId()); + if (spell != null) { + SpellAbility splicedAbility = spliceCard.getSpellAbility().copy(); + splicedAbility.setSpellAbilityType(SpellAbilityType.SPLICE); + splicedAbility.setSourceId(abilityToModify.getSourceId()); + spell.addSpellAbility(splicedAbility); + for (Iterator it = ((SpliceOntoInstantOrSorceryAbility) source).getSpliceCosts().iterator(); it.hasNext(); ) { + spell.getSpellAbility().getCosts().add(((Cost) it.next()).copy()); + } + } + return true; + } + return false; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + MageObject object = game.getObject(abilityToModify.getSourceId()); + if (object != null && object.isInstantOrSorcery()) { + return spliceSpellCanBeActivated(source, game); + } + return false; + } + + private boolean spliceSpellCanBeActivated(Ability source, Game game) { + // check if spell can be activated (protection problem not solved because effect will be used from the base spell?) + Card card = game.getCard(source.getSourceId()); + if (card != null) { + if (card.getManaCost().isEmpty()) { // e.g. Evermind + return card.getSpellAbility().spellCanBeActivatedRegularlyNow(source.getControllerId(), game); + } else { + return card.getSpellAbility().canActivate(source.getControllerId(), game).canActivate(); + } + } + return false; + } +} From adb666587bd8bb0d00f053e567f233f8cef6b498 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 27 May 2019 10:19:47 +0400 Subject: [PATCH 3/3] Refactor images download for #5814 --- .../plugins/card/dl/DownloadServiceInfo.java | 19 +++ .../sources/AltMtgOnlTokensImageSource.java | 7 + .../card/dl/sources/CardImageSource.java | 4 + .../card/dl/sources/CopyPasteImageSource.java | 12 +- .../card/dl/sources/GrabbagImageSource.java | 11 +- .../card/dl/sources/MagidexImageSource.java | 6 + .../card/dl/sources/MtgImageSource.java | 7 + .../dl/sources/MtgOnlTokensImageSource.java | 7 + .../dl/sources/MythicspoilerComSource.java | 6 + .../card/dl/sources/ScryfallImageSource.java | 41 ++++- .../card/dl/sources/TokensMtgImageSource.java | 13 +- .../dl/sources/WizardCardsImageSource.java | 6 + .../card/images/DownloadPicturesService.java | 148 ++++++++++-------- 13 files changed, 204 insertions(+), 83 deletions(-) create mode 100644 Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java new file mode 100644 index 0000000000..d125845bce --- /dev/null +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/DownloadServiceInfo.java @@ -0,0 +1,19 @@ +package org.mage.plugins.card.dl; + +import java.net.Proxy; + +/** + * @author JayDi85 + */ +public interface DownloadServiceInfo { + + Proxy getProxy(); + + boolean isNeedCancel(); + + void incErrorCount(); + + void updateMessage(String text); + + void showDownloadControls(boolean needToShow); +} diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/AltMtgOnlTokensImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/AltMtgOnlTokensImageSource.java index f8216028ba..2819de9b4e 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/AltMtgOnlTokensImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/AltMtgOnlTokensImageSource.java @@ -1,10 +1,12 @@ package org.mage.plugins.card.dl.sources; import org.apache.log4j.Logger; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import java.io.IOException; import java.util.HashMap; +import java.util.List; /** * @author spjspj @@ -57,6 +59,11 @@ public enum AltMtgOnlTokensImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { return null; diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java index 758f38881e..1106da877e 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CardImageSource.java @@ -1,9 +1,11 @@ package org.mage.plugins.card.dl.sources; import mage.client.util.CardLanguage; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import java.util.ArrayList; +import java.util.List; /** * @author North, JayDi85 @@ -14,6 +16,8 @@ public interface CardImageSource { CardImageUrls generateTokenUrl(CardDownloadData card) throws Exception; + boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList); + String getNextHttpImageUrl(); String getFileForHttpImage(String httpImageUrl); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CopyPasteImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CopyPasteImageSource.java index 766208bdf1..f930bafeed 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CopyPasteImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/CopyPasteImageSource.java @@ -1,6 +1,7 @@ package org.mage.plugins.card.dl.sources; import mage.cards.Sets; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import javax.swing.*; @@ -8,10 +9,8 @@ import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.List; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -73,6 +72,11 @@ public enum CopyPasteImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { if (singleLinks == null) { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GrabbagImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GrabbagImageSource.java index b8c9e2b65b..0bc422ef18 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GrabbagImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/GrabbagImageSource.java @@ -1,13 +1,11 @@ package org.mage.plugins.card.dl.sources; import org.apache.log4j.Logger; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -46,6 +44,11 @@ public enum GrabbagImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { if (singleLinks == null) { diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MagidexImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MagidexImageSource.java index 4bd2c49433..098ceecf02 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MagidexImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MagidexImageSource.java @@ -1,5 +1,6 @@ package org.mage.plugins.card.dl.sources; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import java.net.URI; @@ -227,6 +228,11 @@ public enum MagidexImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { String cardDownloadName = card.getDownloadName().toLowerCase(Locale.ENGLISH); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgImageSource.java index 7c8a368579..8509e95730 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgImageSource.java @@ -1,7 +1,9 @@ package org.mage.plugins.card.dl.sources; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; +import java.util.List; import java.util.Locale; /** @@ -28,6 +30,11 @@ public enum MtgImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { String collectorId = card.getCollectorId(); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgOnlTokensImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgOnlTokensImageSource.java index f43baec92c..ed7f174664 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgOnlTokensImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MtgOnlTokensImageSource.java @@ -1,10 +1,12 @@ package org.mage.plugins.card.dl.sources; import org.apache.log4j.Logger; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import java.io.IOException; import java.util.HashMap; +import java.util.List; /** * @author spjspj @@ -57,6 +59,11 @@ public enum MtgOnlTokensImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { return null; diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MythicspoilerComSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MythicspoilerComSource.java index 1c0ecaf857..c8b30400d3 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MythicspoilerComSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/MythicspoilerComSource.java @@ -7,6 +7,7 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import java.io.BufferedReader; @@ -381,6 +382,11 @@ public enum MythicspoilerComSource implements CardImageSource { return pageLinks; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { String collectorId = card.getCollectorId(); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java index 133bebf5b0..82dd5aeecf 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSource.java @@ -1,12 +1,12 @@ package org.mage.plugins.card.dl.sources; import mage.client.util.CardLanguage; +import org.apache.log4j.Logger; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; +import java.net.Proxy; +import java.util.*; /** * @author JayDi85 @@ -15,8 +15,11 @@ public enum ScryfallImageSource implements CardImageSource { instance; + private static final Logger logger = Logger.getLogger(ScryfallImageSource.class); + private final Map languageAliases; private CardLanguage currentLanguage = CardLanguage.ENGLISH; // working language + private Map preparedUrls = new HashMap<>(); ScryfallImageSource() { // LANGUAGES @@ -36,6 +39,11 @@ public enum ScryfallImageSource implements CardImageSource { } private CardImageUrls innerGenerateURL(CardDownloadData card, boolean isToken) { + String prepared = preparedUrls.getOrDefault(card, null); + if (prepared != null) { + return new CardImageUrls(prepared, null); + } + String defaultCode = CardLanguage.ENGLISH.getCode(); String localizedCode = languageAliases.getOrDefault(this.getCurrentLanguage(), defaultCode); // loc example: https://api.scryfall.com/cards/xln/121/ru?format=image @@ -99,6 +107,31 @@ public enum ScryfallImageSource implements CardImageSource { return new CardImageUrls(baseUrl, alternativeUrl); } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + // prepare download list example ( + Proxy proxy = downloadServiceInfo.getProxy(); + + preparedUrls.clear(); + for (CardDownloadData card : downloadList) { + // need cancel + if (downloadServiceInfo.isNeedCancel()) { + return false; + } + + // TODO: download faces info here + if (card.isTwoFacedCard()) { + String url = null; + preparedUrls.put(card, url); + } + + // inc error count to stop on too many errors + // downloadServiceInfo.incErrorCount(); + } + + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { return innerGenerateURL(card, false); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/TokensMtgImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/TokensMtgImageSource.java index 5ac33a0b0a..ad8f654f47 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/TokensMtgImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/TokensMtgImageSource.java @@ -2,6 +2,7 @@ package org.mage.plugins.card.dl.sources; import mage.constants.SubType; import org.apache.log4j.Logger; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import org.mage.plugins.card.images.DownloadPicturesService; import org.mage.plugins.card.utils.CardImageUtils; @@ -50,6 +51,11 @@ public enum TokensMtgImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { return null; @@ -180,7 +186,7 @@ public enum TokensMtgImageSource implements CardImageSource { private HashMap> getTokensData() throws IOException { synchronized (tokensDataSync) { if (tokensData == null) { - DownloadPicturesService.getInstance().updateAndViewMessage("Find tokens data..."); + DownloadPicturesService.getInstance().updateMessage("Find tokens data..."); tokensData = new HashMap<>(); // get tokens data from resource file @@ -231,10 +237,11 @@ public enum TokensMtgImageSource implements CardImageSource { } } } - DownloadPicturesService.getInstance().updateAndViewMessage(""); + DownloadPicturesService.getInstance().updateMessage(""); + DownloadPicturesService.getInstance().showDownloadControls(true); } catch (Exception ex) { logger.warn("Failed to get tokens description from tokens.mtg.onl", ex); - DownloadPicturesService.getInstance().updateAndViewMessage(ex.getMessage()); + DownloadPicturesService.getInstance().updateMessage(ex.getMessage()); } } } diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java index 0d434ec1c0..3ee788d1e5 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/WizardCardsImageSource.java @@ -9,6 +9,7 @@ import org.apache.log4j.Logger; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.images.CardDownloadData; import org.mage.plugins.card.utils.CardImageUtils; @@ -451,6 +452,11 @@ public enum WizardCardsImageSource implements CardImageSource { return null; } + @Override + public boolean prepareDownloadList(DownloadServiceInfo downloadServiceInfo, List downloadList) { + return true; + } + @Override public CardImageUrls generateCardUrl(CardDownloadData card) throws Exception { String collectorId = card.getCollectorId(); diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java index 2ffc11bad1..13dd0a9883 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/images/DownloadPicturesService.java @@ -16,6 +16,7 @@ import net.java.truevfs.access.TFileOutputStream; import net.java.truevfs.access.TVFS; import net.java.truevfs.kernel.spec.FsSyncException; import org.apache.log4j.Logger; +import org.mage.plugins.card.dl.DownloadServiceInfo; import org.mage.plugins.card.dl.sources.*; import org.mage.plugins.card.utils.CardImageUtils; @@ -37,7 +38,7 @@ import static org.mage.plugins.card.utils.CardImageUtils.getImagesDir; /** * @author JayDi85 */ -public class DownloadPicturesService extends DefaultBoundedRangeModel implements Runnable { +public class DownloadPicturesService extends DefaultBoundedRangeModel implements DownloadServiceInfo, Runnable { // don't forget to remove new sets from ignore.urls to download (properties file in resources) private static DownloadPicturesService instance; @@ -65,7 +66,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements private static CardImageSource selectedSource; private final Object sync = new Object(); - private Proxy p = Proxy.NO_PROXY; + private Proxy proxy = Proxy.NO_PROXY; enum DownloadSources { WIZARDS("1. wizards.com - low quality CARDS, multi-language, slow download", WizardCardsImageSource.instance), @@ -118,7 +119,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements instance.setNeedCancel(true); } - private boolean getNeedCancel() { + public boolean isNeedCancel() { return this.needCancel || (this.errorCount > MAX_ERRORS_COUNT_BEFORE_CANCEL); } @@ -126,7 +127,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements this.needCancel = needCancel; } - private void incErrorCount() { + public void incErrorCount() { this.errorCount = this.errorCount + 1; } @@ -196,23 +197,24 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements } public void findMissingCards() { - updateAndViewMessage("Loading..."); + updateMessage("Loading..."); this.cardsAll.clear(); this.cardsMissing.clear(); this.cardsDownloadQueue.clear(); - updateAndViewMessage("Loading cards list..."); + updateMessage("Loading cards list..."); this.cardsAll = Collections.synchronizedList(CardRepository.instance.findCards(new CardCriteria())); - updateAndViewMessage("Finding missing images..."); + updateMessage("Finding missing images..."); this.cardsMissing = prepareMissingCards(this.cardsAll, uiDialog.getRedownloadCheckbox().isSelected()); - updateAndViewMessage("Finding available sets from selected source..."); + updateMessage("Finding available sets from selected source..."); this.uiDialog.getSetsCombo().setModel(new DefaultComboBoxModel<>(getSetsForCurrentImageSource())); reloadCardsToDownload(this.uiDialog.getSetsCombo().getSelectedItem().toString()); this.uiDialog.showDownloadControls(true); - updateAndViewMessage(""); + updateMessage(""); + showDownloadControls(true); } private void reloadLanguagesForSelectedSource() { @@ -237,13 +239,13 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements } } - public void updateAndViewMessage(String text) { + public void updateMessage(String text) { this.uiDialog.setGlobalInfo(text); + } - // auto-size on empty message (on complete) - if (text.isEmpty()) { - this.uiDialog.showDownloadControls(true); - } + public void showDownloadControls(boolean needToShow) { + // auto-size form on show + this.uiDialog.showDownloadControls(needToShow); } private String getSetNameWithYear(ExpansionSet exp) { @@ -587,7 +589,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements break; case NONE: default: - p = Proxy.NO_PROXY; + proxy = Proxy.NO_PROXY; break; } @@ -595,67 +597,69 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements try { String address = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_ADDRESS, ""); Integer port = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_PROXY_PORT, "80")); - p = new Proxy(type, new InetSocketAddress(address, port)); + proxy = new Proxy(type, new InetSocketAddress(address, port)); } catch (Exception ex) { throw new RuntimeException("Gui_DownloadPicturesService : error 1 - " + ex); } } - if (p != null) { - update(0, cardsDownloadQueue.size()); + if (proxy != null) { logger.info("Started download of " + cardsDownloadQueue.size() + " images" + " from source: " + selectedSource.getSourceName() + ", language: " + selectedSource.getCurrentLanguage().getCode()); + uiDialog.getProgressBar().setString("Preparing download list..."); + if (selectedSource.prepareDownloadList(this, cardsDownloadQueue)) { + update(0, cardsDownloadQueue.size()); + int numberOfThreads = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_THREADS, "10")); + ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); + for (int i = 0; i < cardsDownloadQueue.size() && !this.isNeedCancel(); i++) { + try { + CardDownloadData card = cardsDownloadQueue.get(i); - int numberOfThreads = Integer.parseInt(PreferencesDialog.getCachedValue(PreferencesDialog.KEY_CARD_IMAGES_THREADS, "10")); - ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); - for (int i = 0; i < cardsDownloadQueue.size() && !this.getNeedCancel(); i++) { - try { - CardDownloadData card = cardsDownloadQueue.get(i); + logger.debug("Downloading image: " + card.getName() + " (" + card.getSet() + ')'); - logger.debug("Downloading image: " + card.getName() + " (" + card.getSet() + ')'); - - CardImageUrls urls; - if (card.isToken()) { - if (!"0".equals(card.getCollectorId())) { - continue; - } - urls = selectedSource.generateTokenUrl(card); - } else { - urls = selectedSource.generateCardUrl(card); - } - - if (urls == null) { - String imageRef = selectedSource.getNextHttpImageUrl(); - String fileName = selectedSource.getFileForHttpImage(imageRef); - if (imageRef != null && fileName != null) { - imageRef = selectedSource.getSourceName() + imageRef; - try { - card.setToken(selectedSource.isTokenSource()); - Runnable task = new DownloadTask(card, imageRef, fileName, selectedSource.getTotalImages()); - executor.execute(task); - } catch (Exception ex) { - } - } else if (selectedSource.getTotalImages() == -1) { - logger.info("Image not available on " + selectedSource.getSourceName() + ": " + card.getName() + " (" + card.getSet() + ')'); - synchronized (sync) { - update(cardIndex + 1, cardsDownloadQueue.size()); + CardImageUrls urls; + if (card.isToken()) { + if (!"0".equals(card.getCollectorId())) { + continue; } + urls = selectedSource.generateTokenUrl(card); + } else { + urls = selectedSource.generateCardUrl(card); } - } else { - Runnable task = new DownloadTask(card, urls, cardsDownloadQueue.size()); - executor.execute(task); + + if (urls == null) { + String imageRef = selectedSource.getNextHttpImageUrl(); + String fileName = selectedSource.getFileForHttpImage(imageRef); + if (imageRef != null && fileName != null) { + imageRef = selectedSource.getSourceName() + imageRef; + try { + card.setToken(selectedSource.isTokenSource()); + Runnable task = new DownloadTask(card, imageRef, fileName, selectedSource.getTotalImages()); + executor.execute(task); + } catch (Exception ex) { + } + } else if (selectedSource.getTotalImages() == -1) { + logger.info("Image not available on " + selectedSource.getSourceName() + ": " + card.getName() + " (" + card.getSet() + ')'); + synchronized (sync) { + update(cardIndex + 1, cardsDownloadQueue.size()); + } + } + } else { + Runnable task = new DownloadTask(card, urls, cardsDownloadQueue.size()); + executor.execute(task); + } + } catch (Exception ex) { + logger.error(ex, ex); } - } catch (Exception ex) { - logger.error(ex, ex); } - } - executor.shutdown(); - while (!executor.isTerminated()) { - try { - TimeUnit.SECONDS.sleep(1); - } catch (InterruptedException ie) { + executor.shutdown(); + while (!executor.isTerminated()) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException ie) { + } } } } @@ -671,6 +675,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements // stop reloadCardsToDownload(uiDialog.getSetsCombo().getSelectedItem().toString()); + enableDialogButtons(); // reset images cache ImageCache.clearCache(); @@ -707,7 +712,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements @Override public void run() { - if (DownloadPicturesService.getInstance().getNeedCancel()) { + if (DownloadPicturesService.getInstance().isNeedCancel()) { synchronized (sync) { update(cardIndex + 1, count); } @@ -791,14 +796,14 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements URL url = new URL(currentUrl); // on download cancel need to stop - if (DownloadPicturesService.getInstance().getNeedCancel()) { + if (DownloadPicturesService.getInstance().isNeedCancel()) { return; } // download selectedSource.doPause(url.getPath()); - httpConn = url.openConnection(p); + httpConn = url.openConnection(proxy); if (httpConn != null) { httpConn.setRequestProperty("User-Agent", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2"); @@ -847,7 +852,7 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements int len; while ((len = in.read(buf)) != -1) { // user cancelled - if (DownloadPicturesService.getInstance().getNeedCancel()) { + if (DownloadPicturesService.getInstance().isNeedCancel()) { // stop download, save current state and exit TFile archive = destFile.getTopLevelArchive(); ///* not need to unmout/close - it's auto action @@ -934,14 +939,21 @@ public class DownloadPicturesService extends DefaultBoundedRangeModel implements // try download again } - this.uiDialog.getRedownloadCheckbox().setSelected(false); - uiDialog.enableActionControls(true); - uiDialog.getStartButton().setEnabled(true); + enableDialogButtons(); } } private static final long serialVersionUID = 1L; + private void enableDialogButtons() { + uiDialog.getRedownloadCheckbox().setSelected(false); // reset re-download button after finished + uiDialog.enableActionControls(true); + uiDialog.getStartButton().setEnabled(true); + } + + public Proxy getProxy() { + return proxy; + } } class LoadMissingCardDataNew implements Runnable {