mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 20:28:00 +00:00
Support canceling when selecting block or custom draft format when setting up a new draft or sealed deck
Prevent crash if you select a block that has no sets
This commit is contained in:
@@ -150,11 +150,14 @@ public enum CSubmenuDraft implements ICDoc {
|
|||||||
final LimitedPoolType poolType = GuiChoose.oneOrNone("Choose Draft Format", LimitedPoolType.values());
|
final LimitedPoolType poolType = GuiChoose.oneOrNone("Choose Draft Format", LimitedPoolType.values());
|
||||||
if (poolType == null) { return; }
|
if (poolType == null) { return; }
|
||||||
|
|
||||||
final CEditorDraftingProcess draft = new CEditorDraftingProcess();
|
BoosterDraft draft = BoosterDraft.createDraft(poolType);
|
||||||
draft.showGui(new BoosterDraft(poolType));
|
if (draft == null) { return; }
|
||||||
|
|
||||||
|
final CEditorDraftingProcess draftController = new CEditorDraftingProcess();
|
||||||
|
draftController.showGui(draft);
|
||||||
|
|
||||||
Singletons.getControl().setCurrentScreen(FScreen.DRAFTING_PROCESS);
|
Singletons.getControl().setCurrentScreen(FScreen.DRAFTING_PROCESS);
|
||||||
CDeckEditorUI.SINGLETON_INSTANCE.setEditorController(draft);
|
CDeckEditorUI.SINGLETON_INSTANCE.setEditorController(draftController);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (non-Javadoc)
|
/* (non-Javadoc)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import forge.Command;
|
import forge.Command;
|
||||||
import forge.Singletons;
|
import forge.Singletons;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
|
import forge.deck.CardPool;
|
||||||
import forge.deck.Deck;
|
import forge.deck.Deck;
|
||||||
import forge.deck.DeckBase;
|
import forge.deck.DeckBase;
|
||||||
import forge.deck.DeckGroup;
|
import forge.deck.DeckGroup;
|
||||||
@@ -36,7 +37,6 @@ import forge.limited.ReadDraftRankings;
|
|||||||
import forge.limited.SealedCardPoolGenerator;
|
import forge.limited.SealedCardPoolGenerator;
|
||||||
import forge.limited.SealedDeckBuilder;
|
import forge.limited.SealedDeckBuilder;
|
||||||
import forge.properties.ForgePreferences.FPref;
|
import forge.properties.ForgePreferences.FPref;
|
||||||
import forge.util.ItemPool;
|
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.storage.IStorage;
|
import forge.util.storage.IStorage;
|
||||||
|
|
||||||
@@ -151,7 +151,8 @@ public enum CSubmenuSealed implements ICDoc {
|
|||||||
SealedCardPoolGenerator sd = new SealedCardPoolGenerator(poolType);
|
SealedCardPoolGenerator sd = new SealedCardPoolGenerator(poolType);
|
||||||
if (sd.isEmpty()) { return; }
|
if (sd.isEmpty()) { return; }
|
||||||
|
|
||||||
final ItemPool<PaperCard> humanPool = sd.getCardpool(true);
|
final CardPool humanPool = sd.getCardPool(true);
|
||||||
|
if (humanPool == null) { return; }
|
||||||
|
|
||||||
// System.out.println(humanPool);
|
// System.out.println(humanPool);
|
||||||
|
|
||||||
@@ -208,7 +209,10 @@ public enum CSubmenuSealed implements ICDoc {
|
|||||||
sealed.setHumanDeck(deck);
|
sealed.setHumanDeck(deck);
|
||||||
for (int i = 0; i < rounds; i++) {
|
for (int i = 0; i < rounds; i++) {
|
||||||
// Generate other decks for next N opponents
|
// Generate other decks for next N opponents
|
||||||
sealed.addAiDeck(new SealedDeckBuilder(sd.getCardpool(false).toFlatList()).buildDeck());
|
final CardPool aiPool = sd.getCardPool(false);
|
||||||
|
if (aiPool == null) { return; }
|
||||||
|
|
||||||
|
sealed.addAiDeck(new SealedDeckBuilder(aiPool.toFlatList()).buildDeck());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rank the AI decks
|
// Rank the AI decks
|
||||||
|
|||||||
@@ -71,6 +71,95 @@ public final class BoosterDraft implements IBoosterDraft {
|
|||||||
|
|
||||||
private final List<Supplier<List<PaperCard>>> product = new ArrayList<Supplier<List<PaperCard>>>();
|
private final List<Supplier<List<PaperCard>>> product = new ArrayList<Supplier<List<PaperCard>>>();
|
||||||
|
|
||||||
|
public static BoosterDraft createDraft(final LimitedPoolType draftType) {
|
||||||
|
BoosterDraft draft = new BoosterDraft(draftType);
|
||||||
|
|
||||||
|
switch (draftType) {
|
||||||
|
case Full: // Draft from all cards in Forge
|
||||||
|
Supplier<List<PaperCard>> s = new UnOpenedProduct(SealedProduct.Template.genericBooster);
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
draft.product.add(s);
|
||||||
|
}
|
||||||
|
IBoosterDraft.LAND_SET_CODE[0] = CardEdition.Predicates.getRandomSetWithAllBasicLands(Singletons.getMagicDb().getEditions());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Block: // Draft from cards by block or set
|
||||||
|
case FantasyBlock:
|
||||||
|
List<CardBlock> blocks = new ArrayList<CardBlock>();
|
||||||
|
IStorage<CardBlock> storage = draftType == LimitedPoolType.Block
|
||||||
|
? Singletons.getModel().getBlocks() : Singletons.getModel().getFantasyBlocks();
|
||||||
|
|
||||||
|
for (CardBlock b : storage) {
|
||||||
|
if (b.getCntBoostersDraft() > 0) {
|
||||||
|
blocks.add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final CardBlock block = GuiChoose.oneOrNone("Choose Block", blocks);
|
||||||
|
if (block == null) { return null; }
|
||||||
|
|
||||||
|
final CardEdition[] cardSets = block.getSets();
|
||||||
|
if (cardSets.length == 0) {
|
||||||
|
FOptionPane.showErrorDialog(block.toString() + " does not contain any set combinations.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Stack<String> sets = new Stack<String>();
|
||||||
|
for (int k = cardSets.length - 1; k >= 0; k--) {
|
||||||
|
sets.add(cardSets[k].getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String setCode : block.getMetaSetNames()) {
|
||||||
|
if (block.getMetaSet(setCode).isDraftable()) {
|
||||||
|
sets.push(setCode); // to the beginning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int nPacks = block.getCntBoostersDraft();
|
||||||
|
|
||||||
|
if (sets.size() > 1) {
|
||||||
|
final Object p = GuiChoose.oneOrNone("Choose Set Combination", getSetCombos(sets));
|
||||||
|
if (p == null) { return null; }
|
||||||
|
|
||||||
|
final String[] pp = p.toString().split("/");
|
||||||
|
for (int i = 0; i < nPacks; i++) {
|
||||||
|
draft.product.add(block.getBooster(pp[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
IUnOpenedProduct product1 = block.getBooster(sets.get(0));
|
||||||
|
|
||||||
|
for (int i = 0; i < nPacks; i++) {
|
||||||
|
draft.product.add(product1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IBoosterDraft.LAND_SET_CODE[0] = block.getLandSet();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Custom:
|
||||||
|
final List<CustomLimited> myDrafts = draft.loadCustomDrafts("res/draft/", ".draft");
|
||||||
|
|
||||||
|
if (myDrafts.isEmpty()) {
|
||||||
|
FOptionPane.showMessageDialog("No custom draft files found.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final CustomLimited customDraft = GuiChoose.oneOrNone("Choose Custom Draft", myDrafts);
|
||||||
|
if (customDraft == null) { return null; }
|
||||||
|
|
||||||
|
draft.setupCustomDraft(customDraft);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new NoSuchElementException("Draft for mode " + draftType + " has not been set up!");
|
||||||
|
}
|
||||||
|
|
||||||
|
draft.pack = draft.get8BoosterPack();
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* Constructor for BoosterDraft_1.
|
* Constructor for BoosterDraft_1.
|
||||||
@@ -79,77 +168,9 @@ public final class BoosterDraft implements IBoosterDraft {
|
|||||||
* @param draftType
|
* @param draftType
|
||||||
* a {@link java.lang.String} object.
|
* a {@link java.lang.String} object.
|
||||||
*/
|
*/
|
||||||
public BoosterDraft(final LimitedPoolType draftType) {
|
private BoosterDraft(final LimitedPoolType draftType) {
|
||||||
this.draftAI.setBd(this);
|
this.draftAI.setBd(this);
|
||||||
this.draftFormat = draftType;
|
this.draftFormat = draftType;
|
||||||
|
|
||||||
switch (draftType) {
|
|
||||||
case Full: // Draft from all cards in Forge
|
|
||||||
Supplier<List<PaperCard>> s = new UnOpenedProduct(SealedProduct.Template.genericBooster);
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) this.product.add(s);
|
|
||||||
IBoosterDraft.LAND_SET_CODE[0] = CardEdition.Predicates.getRandomSetWithAllBasicLands(Singletons.getMagicDb().getEditions());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Block: case FantasyBlock: // Draft from cards by block or set
|
|
||||||
|
|
||||||
List<CardBlock> blocks = new ArrayList<CardBlock>();
|
|
||||||
IStorage<CardBlock> storage = draftType == LimitedPoolType.Block
|
|
||||||
? Singletons.getModel().getBlocks() : Singletons.getModel().getFantasyBlocks();
|
|
||||||
|
|
||||||
for (CardBlock b : storage) {
|
|
||||||
if( b.getCntBoostersDraft() > 0)
|
|
||||||
blocks.add(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
final CardBlock block = GuiChoose.one("Choose Block", blocks);
|
|
||||||
|
|
||||||
final CardEdition[] cardSets = block.getSets();
|
|
||||||
final Stack<String> sets = new Stack<String>();
|
|
||||||
for (int k = cardSets.length - 1; k >= 0; --k) {
|
|
||||||
sets.add(cardSets[k].getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
for(String setCode : block.getMetaSetNames() ) {
|
|
||||||
if ( block.getMetaSet(setCode).isDraftable() )
|
|
||||||
sets.push(setCode); // to the beginning
|
|
||||||
}
|
|
||||||
|
|
||||||
final int nPacks = block.getCntBoostersDraft();
|
|
||||||
|
|
||||||
if (sets.size() > 1) {
|
|
||||||
final Object p = GuiChoose.one("Choose Set Combination", getSetCombos(sets));
|
|
||||||
final String[] pp = p.toString().split("/");
|
|
||||||
for (int i = 0; i < nPacks; i++) {
|
|
||||||
this.product.add(block.getBooster(pp[i]));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
IUnOpenedProduct product1 = block.getBooster(sets.get(0));
|
|
||||||
|
|
||||||
for (int i = 0; i < nPacks; i++) {
|
|
||||||
this.product.add(product1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IBoosterDraft.LAND_SET_CODE[0] = block.getLandSet();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Custom:
|
|
||||||
final List<CustomLimited> myDrafts = this.loadCustomDrafts("res/draft/", ".draft");
|
|
||||||
|
|
||||||
if (myDrafts.isEmpty()) {
|
|
||||||
FOptionPane.showMessageDialog("No custom draft files found.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
final CustomLimited draft = GuiChoose.one("Choose Custom Draft", myDrafts);
|
|
||||||
this.setupCustomDraft(draft);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new NoSuchElementException("Draft for mode " + draftType + " has not been set up!");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pack = this.get8BoosterPack();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupCustomDraft(final CustomLimited draft) {
|
private void setupCustomDraft(final CustomLimited draft) {
|
||||||
@@ -252,11 +273,9 @@ public final class BoosterDraft implements IBoosterDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void computerChoose() {
|
private void computerChoose() {
|
||||||
|
|
||||||
final int iHumansBooster = this.getCurrentBoosterIndex();
|
final int iHumansBooster = this.getCurrentBoosterIndex();
|
||||||
int iPlayer = 0;
|
int iPlayer = 0;
|
||||||
for (int i = 1; i < this.pack.size(); i++) {
|
for (int i = 1; i < this.pack.size(); i++) {
|
||||||
|
|
||||||
final List<Card> forAi = new ArrayList<Card>();
|
final List<Card> forAi = new ArrayList<Card>();
|
||||||
final List<PaperCard> booster = this.pack.get((iHumansBooster + i) % this.pack.size());
|
final List<PaperCard> booster = this.pack.get((iHumansBooster + i) % this.pack.size());
|
||||||
for (final IPaperCard cr : booster) {
|
for (final IPaperCard cr : booster) {
|
||||||
@@ -311,13 +330,15 @@ public final class BoosterDraft implements IBoosterDraft {
|
|||||||
if (cc.equals(c)) {
|
if (cc.equals(c)) {
|
||||||
pickValue = thisBooster.size()
|
pickValue = thisBooster.size()
|
||||||
* (1f - (((float) this.currentBoosterPick / this.currentBoosterSize) * 2f));
|
* (1f - (((float) this.currentBoosterPick / this.currentBoosterSize) * 2f));
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
pickValue = 0;
|
pickValue = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.draftPicks.containsKey(cnBk)) {
|
if (!this.draftPicks.containsKey(cnBk)) {
|
||||||
this.draftPicks.put(cnBk, pickValue);
|
this.draftPicks.put(cnBk, pickValue);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
final float curValue = this.draftPicks.get(cnBk);
|
final float curValue = this.draftPicks.get(cnBk);
|
||||||
final float newValue = (curValue + pickValue) / 2;
|
final float newValue = (curValue + pickValue) / 2;
|
||||||
this.draftPicks.put(cnBk, newValue);
|
this.draftPicks.put(cnBk, newValue);
|
||||||
@@ -344,7 +365,7 @@ public final class BoosterDraft implements IBoosterDraft {
|
|||||||
HttpUtil.upload(NewConstants.URL_DRAFT_UPLOAD + "?fmt=" + draftFormat, outDraftData);
|
HttpUtil.upload(NewConstants.URL_DRAFT_UPLOAD + "?fmt=" + draftFormat, outDraftData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getSetCombos(final List<String> setz) {
|
private static List<String> getSetCombos(final List<String> setz) {
|
||||||
String[] sets = setz.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
|
String[] sets = setz.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
|
||||||
List<String> setCombos = new ArrayList<String>();
|
List<String> setCombos = new ArrayList<String>();
|
||||||
if (sets.length >= 2) {
|
if (sets.length >= 2) {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import forge.item.SealedProduct;
|
|||||||
import forge.model.CardBlock;
|
import forge.model.CardBlock;
|
||||||
import forge.model.UnOpenedMeta;
|
import forge.model.UnOpenedMeta;
|
||||||
import forge.util.FileUtil;
|
import forge.util.FileUtil;
|
||||||
import forge.util.ItemPool;
|
|
||||||
import forge.util.TextUtil;
|
import forge.util.TextUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,7 +154,8 @@ public class SealedCardPoolGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CustomLimited draft = GuiChoose.one("Choose Custom Sealed Pool", customs);
|
final CustomLimited draft = GuiChoose.oneOrNone("Choose Custom Sealed Pool", customs);
|
||||||
|
if (draft == null) { return; }
|
||||||
|
|
||||||
UnOpenedProduct toAdd = new UnOpenedProduct(draft.getSealedProductTemplate(), draft.getCardPool());
|
UnOpenedProduct toAdd = new UnOpenedProduct(draft.getSealedProductTemplate(), draft.getCardPool());
|
||||||
toAdd.setLimitedPool(draft.isSingleton());
|
toAdd.setLimitedPool(draft.isSingleton());
|
||||||
@@ -185,7 +185,7 @@ public class SealedCardPoolGenerator {
|
|||||||
*
|
*
|
||||||
* @return an ArrayList of the set choices.
|
* @return an ArrayList of the set choices.
|
||||||
*/
|
*/
|
||||||
private ArrayList<String> getSetCombos(final List<String> setz, final int nPacks) {
|
private static ArrayList<String> getSetCombos(final List<String> setz, final int nPacks) {
|
||||||
String[] sets = setz.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
|
String[] sets = setz.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
|
||||||
ArrayList<String> setCombos = new ArrayList<String>();
|
ArrayList<String> setCombos = new ArrayList<String>();
|
||||||
|
|
||||||
@@ -335,12 +335,16 @@ public class SealedCardPoolGenerator {
|
|||||||
* boolean, get pool for human (possible choices)
|
* boolean, get pool for human (possible choices)
|
||||||
* @return a {@link forge.CardList} object.
|
* @return a {@link forge.CardList} object.
|
||||||
*/
|
*/
|
||||||
public ItemPool<PaperCard> getCardpool(final boolean isHuman) {
|
public CardPool getCardPool(final boolean isHuman) {
|
||||||
final CardPool pool = new CardPool();
|
final CardPool pool = new CardPool();
|
||||||
|
|
||||||
for (IUnOpenedProduct prod : product) {
|
for (IUnOpenedProduct prod : product) {
|
||||||
if (prod instanceof UnOpenedMeta) {
|
if (prod instanceof UnOpenedMeta) {
|
||||||
pool.addAllFlat(((UnOpenedMeta) prod).open(isHuman));
|
List<PaperCard> cards = ((UnOpenedMeta) prod).open(isHuman, true);
|
||||||
|
if (cards == null) {
|
||||||
|
return null; //return null if user canceled
|
||||||
|
}
|
||||||
|
pool.addAllFlat(cards);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pool.addAllFlat(prod.get());
|
pool.addAllFlat(prod.get());
|
||||||
|
|||||||
@@ -61,14 +61,13 @@ public class UnOpenedMeta implements IUnOpenedProduct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the booster pack, return contents.
|
* Open the booster pack, return contents.
|
||||||
* @return List, list of cards.
|
* @return List, list of cards.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<PaperCard> get() {
|
public List<PaperCard> get() {
|
||||||
return this.open(true);
|
return this.open(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,8 +78,7 @@ public class UnOpenedMeta implements IUnOpenedProduct {
|
|||||||
* known partialities for the AI.
|
* known partialities for the AI.
|
||||||
* @return List, list of cards.
|
* @return List, list of cards.
|
||||||
*/
|
*/
|
||||||
public List<PaperCard> open(final boolean isHuman) {
|
public List<PaperCard> open(final boolean isHuman, final boolean allowCancel) {
|
||||||
|
|
||||||
if (metaSets.isEmpty()) {
|
if (metaSets.isEmpty()) {
|
||||||
throw new RuntimeException("Empty UnOpenedMetaset, cannot generate booster.");
|
throw new RuntimeException("Empty UnOpenedMetaset, cannot generate booster.");
|
||||||
}
|
}
|
||||||
@@ -88,7 +86,16 @@ public class UnOpenedMeta implements IUnOpenedProduct {
|
|||||||
switch (operation) {
|
switch (operation) {
|
||||||
case ChooseOne:
|
case ChooseOne:
|
||||||
if (isHuman) {
|
if (isHuman) {
|
||||||
final MetaSet ms = GuiChoose.one("Choose booster:", metaSets);
|
final MetaSet ms;
|
||||||
|
if (allowCancel) {
|
||||||
|
ms = GuiChoose.oneOrNone("Choose Booster", metaSets);
|
||||||
|
if (ms == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ms = GuiChoose.one("Choose Booster", metaSets);
|
||||||
|
}
|
||||||
return ms.getBooster().get();
|
return ms.getBooster().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,5 +123,4 @@ public class UnOpenedMeta implements IUnOpenedProduct {
|
|||||||
public static UnOpenedMeta selectAll(String desc) {
|
public static UnOpenedMeta selectAll(String desc) {
|
||||||
return new UnOpenedMeta(desc, JoinOperation.SelectAll);
|
return new UnOpenedMeta(desc, JoinOperation.SelectAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ public class BoosterDraft1Test {
|
|||||||
*/
|
*/
|
||||||
@Test(groups = { "UnitTest", "fast" }, timeOut = 1000, enabled = false)
|
@Test(groups = { "UnitTest", "fast" }, timeOut = 1000, enabled = false)
|
||||||
public void boosterDraft1Test1() throws Exception {
|
public void boosterDraft1Test1() throws Exception {
|
||||||
final BoosterDraft draft = new BoosterDraft(LimitedPoolType.Full);
|
final BoosterDraft draft = BoosterDraft.createDraft(LimitedPoolType.Full);
|
||||||
|
if (draft == null) { return; }
|
||||||
|
|
||||||
while (draft.hasNextChoice()) {
|
while (draft.hasNextChoice()) {
|
||||||
final CardPool list = draft.nextChoice();
|
final CardPool list = draft.nextChoice();
|
||||||
System.out.println(list.countAll());
|
System.out.println(list.countAll());
|
||||||
|
|||||||
Reference in New Issue
Block a user