mirror of
https://github.com/correl/mage.git
synced 2024-12-25 03:00:15 +00:00
GUI: fixed that user can target/click on second side of mdf card instead the main side (related to #7297);
This commit is contained in:
parent
5731360f1d
commit
767644f1c8
2 changed files with 157 additions and 102 deletions
|
@ -1,18 +1,5 @@
|
|||
package mage.player.human;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.stream.Collectors;
|
||||
import mage.MageObject;
|
||||
import mage.abilities.*;
|
||||
import mage.abilities.costs.VariableCost;
|
||||
|
@ -26,12 +13,11 @@ import mage.abilities.mana.ActivatedManaAbilityImpl;
|
|||
import mage.abilities.mana.ManaAbility;
|
||||
import mage.cards.Card;
|
||||
import mage.cards.Cards;
|
||||
import mage.cards.ModalDoubleFacesCardHalf;
|
||||
import mage.cards.decks.Deck;
|
||||
import mage.choices.Choice;
|
||||
import mage.choices.ChoiceImpl;
|
||||
import mage.constants.*;
|
||||
import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL;
|
||||
import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.filter.common.FilterAttackingCreature;
|
||||
import mage.filter.common.FilterBlockingCreature;
|
||||
|
@ -62,6 +48,15 @@ import mage.util.ManaUtil;
|
|||
import mage.util.MessageToClient;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static mage.constants.PlayerAction.REQUEST_AUTO_ANSWER_RESET_ALL;
|
||||
import static mage.constants.PlayerAction.TRIGGER_AUTO_ORDER_RESET_ALL;
|
||||
|
||||
/**
|
||||
* @author BetaSteward_at_googlemail.com
|
||||
*/
|
||||
|
@ -504,22 +499,23 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
// selected some target
|
||||
|
||||
// remove selected
|
||||
if (target.getTargets().contains(response.getUUID())) {
|
||||
target.remove(response.getUUID());
|
||||
if (target.getTargets().contains(responseId)) {
|
||||
target.remove(responseId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!targetIds.contains(response.getUUID())) {
|
||||
if (!targetIds.contains(responseId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target instanceof TargetPermanent) {
|
||||
if (((TargetPermanent) target).canTarget(abilityControllerId, response.getUUID(), sourceId, game, false)) {
|
||||
target.add(response.getUUID(), game);
|
||||
if (((TargetPermanent) target).canTarget(abilityControllerId, responseId, sourceId, game, false)) {
|
||||
target.add(responseId, game);
|
||||
if (target.doneChosing()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -527,21 +523,21 @@ public class HumanPlayer extends PlayerImpl {
|
|||
} else {
|
||||
MageObject object = game.getObject(sourceId);
|
||||
if (object instanceof Ability) {
|
||||
if (target.canTarget(response.getUUID(), (Ability) object, game)) {
|
||||
if (target.getTargets().contains(response.getUUID())) { // if already included remove it with
|
||||
target.remove(response.getUUID());
|
||||
if (target.canTarget(responseId, (Ability) object, game)) {
|
||||
if (target.getTargets().contains(responseId)) { // if already included remove it with
|
||||
target.remove(responseId);
|
||||
} else {
|
||||
target.addTarget(response.getUUID(), (Ability) object, game);
|
||||
target.addTarget(responseId, (Ability) object, game);
|
||||
if (target.doneChosing()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (target.canTarget(response.getUUID(), game)) {
|
||||
if (target.getTargets().contains(response.getUUID())) { // if already included remove it with
|
||||
target.remove(response.getUUID());
|
||||
} else if (target.canTarget(responseId, game)) {
|
||||
if (target.getTargets().contains(responseId)) { // if already included remove it with
|
||||
target.remove(responseId);
|
||||
} else {
|
||||
target.addTarget(response.getUUID(), null, game);
|
||||
target.addTarget(responseId, null, game);
|
||||
if (target.doneChosing()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -594,16 +590,17 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
// remove selected
|
||||
if (target.getTargets().contains(response.getUUID())) {
|
||||
target.remove(response.getUUID());
|
||||
if (target.getTargets().contains(responseId)) {
|
||||
target.remove(responseId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (possibleTargets.contains(response.getUUID())) {
|
||||
if (target.canTarget(abilityControllerId, response.getUUID(), source, game)) {
|
||||
target.addTarget(response.getUUID(), source, game);
|
||||
if (possibleTargets.contains(responseId)) {
|
||||
if (target.canTarget(abilityControllerId, responseId, source, game)) {
|
||||
target.addTarget(responseId, source, game);
|
||||
if (target.doneChosing()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -684,12 +681,13 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
if (target.getTargets().contains(response.getUUID())) { // if already included remove it with
|
||||
target.remove(response.getUUID());
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
if (target.getTargets().contains(responseId)) { // if already included remove it with
|
||||
target.remove(responseId);
|
||||
} else {
|
||||
if (target.canTarget(abilityControllerId, response.getUUID(), null, cards, game)) {
|
||||
target.add(response.getUUID(), game);
|
||||
if (target.canTarget(abilityControllerId, responseId, null, cards, game)) {
|
||||
target.add(responseId, game);
|
||||
if (target.doneChosing()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -758,11 +756,12 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
if (target.getTargets().contains(response.getUUID())) { // if already included remove it
|
||||
target.remove(response.getUUID());
|
||||
} else if (target.canTarget(abilityControllerId, response.getUUID(), source, cards, game)) {
|
||||
target.addTarget(response.getUUID(), source, game);
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
if (target.getTargets().contains(responseId)) { // if already included remove it
|
||||
target.remove(responseId);
|
||||
} else if (target.canTarget(abilityControllerId, responseId, source, cards, game)) {
|
||||
target.addTarget(responseId, source, game);
|
||||
if (target.doneChosing()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -814,9 +813,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
if (target.canTarget(abilityControllerId, response.getUUID(), source, game)) {
|
||||
UUID targetId = response.getUUID();
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
if (target.canTarget(abilityControllerId, responseId, source, game)) {
|
||||
UUID targetId = responseId;
|
||||
MageObject targetObject = game.getObject(targetId);
|
||||
|
||||
boolean removeMode = target.getTargets().contains(targetId)
|
||||
|
@ -1044,12 +1044,13 @@ public class HumanPlayer extends PlayerImpl {
|
|||
break;
|
||||
}
|
||||
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (response.getString() != null
|
||||
&& response.getString().equals("special")) {
|
||||
activateSpecialAction(game, null);
|
||||
} else if (response.getUUID() != null) {
|
||||
} else if (responseId != null) {
|
||||
boolean result = false;
|
||||
MageObject object = game.getObject(response.getUUID());
|
||||
MageObject object = game.getObject(responseId);
|
||||
if (object != null) {
|
||||
Zone zone = game.getState().getZone(object.getId());
|
||||
if (zone != null) {
|
||||
|
@ -1106,6 +1107,21 @@ public class HumanPlayer extends PlayerImpl {
|
|||
return false;
|
||||
}
|
||||
|
||||
private UUID getFixedResponseUUID(Game game) {
|
||||
// user can clicks on any side of multi/double faces card, but game must process click to main card all the time
|
||||
MageObject object = game.getObject(response.getUUID());
|
||||
|
||||
// mdf cards
|
||||
if (object instanceof ModalDoubleFacesCardHalf) {
|
||||
if (!Zone.BATTLEFIELD.equals(game.getState().getZone(object.getId()))
|
||||
&& !Zone.STACK.equals(game.getState().getZone(object.getId()))) {
|
||||
return ((ModalDoubleFacesCardHalf) object).getMainCard().getId();
|
||||
}
|
||||
}
|
||||
|
||||
return response.getUUID();
|
||||
}
|
||||
|
||||
private boolean checkPassStep(Game game, HumanPlayer controllingPlayer) {
|
||||
try {
|
||||
|
||||
|
@ -1191,11 +1207,12 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
for (TriggeredAbility ability : abilitiesWithNoOrderSet) {
|
||||
if (ability.getId().equals(response.getUUID())
|
||||
if (ability.getId().equals(responseId)
|
||||
|| (!macroTriggeredSelectionFlag
|
||||
&& ability.getSourceId().equals(response.getUUID()))) {
|
||||
&& ability.getSourceId().equals(responseId))) {
|
||||
if (recordingMacro) {
|
||||
PlayerResponse tResponse = new PlayerResponse();
|
||||
tResponse.setUUID(ability.getSourceId());
|
||||
|
@ -1239,10 +1256,11 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (response.getBoolean() != null) {
|
||||
return false;
|
||||
} else if (response.getUUID() != null) {
|
||||
playManaAbilities(abilityToCast, unpaid, game);
|
||||
} else if (responseId != null) {
|
||||
playManaAbilities(responseId, abilityToCast, unpaid, game);
|
||||
} else if (response.getString() != null
|
||||
&& response.getString().equals("special")) {
|
||||
if (unpaid instanceof ManaCostsImpl) {
|
||||
|
@ -1355,9 +1373,9 @@ public class HumanPlayer extends PlayerImpl {
|
|||
return xValue;
|
||||
}
|
||||
|
||||
protected void playManaAbilities(Ability abilityToCast, ManaCost unpaid, Game game) {
|
||||
protected void playManaAbilities(UUID objectId, Ability abilityToCast, ManaCost unpaid, Game game) {
|
||||
updateGameStatePriority("playManaAbilities", game);
|
||||
MageObject object = game.getObject(response.getUUID());
|
||||
MageObject object = game.getObject(objectId);
|
||||
if (object == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -1453,6 +1471,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (response.getString() != null
|
||||
&& response.getString().equals("special")) { // All attack
|
||||
setStoredBookmark(game.bookmarkState());
|
||||
|
@ -1486,8 +1505,8 @@ public class HumanPlayer extends PlayerImpl {
|
|||
if (checkIfAttackersValid(game)) {
|
||||
return;
|
||||
}
|
||||
} else if (response.getUUID() != null) {
|
||||
Permanent attacker = game.getPermanent(response.getUUID());
|
||||
} else if (responseId != null) {
|
||||
Permanent attacker = game.getPermanent(responseId);
|
||||
if (attacker != null) {
|
||||
if (filterCreatureForCombat.match(attacker, null, playerId, game)) {
|
||||
selectDefender(game.getCombat().getDefenders(), attacker.getId(), game);
|
||||
|
@ -1631,13 +1650,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
}
|
||||
if (chooseTarget(Outcome.Damage, target, null, game)) {
|
||||
UUID defenderId = response.getUUID();
|
||||
for (Player player : game.getPlayers().values()) {
|
||||
if (player.getId().equals(response.getUUID())) {
|
||||
defenderId = player.getId(); // get the correct player object
|
||||
break;
|
||||
}
|
||||
}
|
||||
UUID defenderId = getFixedResponseUUID(game);
|
||||
declareAttacker(attackerId, defenderId, game, true);
|
||||
return true;
|
||||
}
|
||||
|
@ -1649,7 +1662,7 @@ public class HumanPlayer extends PlayerImpl {
|
|||
TargetDefender target = new TargetDefender(defenders, null);
|
||||
target.setNotTarget(true); // player or planswalker hexproof does not prevent attacking a player
|
||||
if (chooseTarget(Outcome.Damage, target, null, game)) {
|
||||
return response.getUUID();
|
||||
return getFixedResponseUUID(game);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1692,12 +1705,13 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (response.getBoolean() != null) {
|
||||
return;
|
||||
} else if (response.getInteger() != null) {
|
||||
return;
|
||||
} else if (response.getUUID() != null) {
|
||||
Permanent blocker = game.getPermanent(response.getUUID());
|
||||
} else if (responseId != null) {
|
||||
Permanent blocker = game.getPermanent(responseId);
|
||||
if (blocker != null) {
|
||||
boolean removeBlocker = false;
|
||||
// does not block yet and can block or can block more attackers
|
||||
|
@ -1730,9 +1744,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
for (Permanent perm : attackers) {
|
||||
if (perm.getId().equals(response.getUUID())) {
|
||||
if (perm.getId().equals(responseId)) {
|
||||
return perm.getId();
|
||||
}
|
||||
}
|
||||
|
@ -1755,9 +1770,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
for (Permanent perm : blockers) {
|
||||
if (perm.getId().equals(response.getUUID())) {
|
||||
if (perm.getId().equals(responseId)) {
|
||||
return perm.getId();
|
||||
}
|
||||
}
|
||||
|
@ -1795,14 +1811,15 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (response.getBoolean() != null) {
|
||||
// do nothing
|
||||
} else if (response.getUUID() != null) {
|
||||
CombatGroup group = game.getCombat().findGroup(response.getUUID());
|
||||
} else if (responseId != null) {
|
||||
CombatGroup group = game.getCombat().findGroup(responseId);
|
||||
if (group != null) {
|
||||
// check if already blocked, if not add
|
||||
if (!group.getBlockers().contains(blockerId)) {
|
||||
declareBlocker(defenderId, blockerId, response.getUUID(), game);
|
||||
declareBlocker(defenderId, blockerId, responseId, game);
|
||||
} else { // else remove from block
|
||||
game.getCombat().removeBlockerGromGroup(blockerId, group, game);
|
||||
}
|
||||
|
@ -1904,9 +1921,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
if (specialActions.containsKey(response.getUUID())) {
|
||||
SpecialAction specialAction = specialActions.get(response.getUUID());
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
if (specialActions.containsKey(responseId)) {
|
||||
SpecialAction specialAction = specialActions.get(responseId);
|
||||
if (unpaidForManaAction != null) {
|
||||
specialAction.setUnpaidMana(unpaidForManaAction);
|
||||
}
|
||||
|
@ -1966,9 +1984,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
if (abilities.containsKey(response.getUUID())) {
|
||||
activateAbility(abilities.get(response.getUUID()), game);
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
if (abilities.containsKey(responseId)) {
|
||||
activateAbility(abilities.get(responseId), game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2015,9 +2034,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
if (useableAbilities.containsKey(response.getUUID())) {
|
||||
return (SpellAbility) useableAbilities.get(response.getUUID());
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
if (useableAbilities.containsKey(responseId)) {
|
||||
return (SpellAbility) useableAbilities.get(responseId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2096,9 +2116,10 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
waitForResponse(game);
|
||||
|
||||
if (response.getUUID() != null) {
|
||||
UUID responseId = getFixedResponseUUID(game);
|
||||
if (responseId != null) {
|
||||
for (Mode mode : modes.getAvailableModes(source, game)) {
|
||||
if (mode.getId().equals(response.getUUID())) {
|
||||
if (mode.getId().equals(responseId)) {
|
||||
// TODO: add checks on 2x selects (cheaters can rewrite client side code and select same mode multiple times)
|
||||
// reason: wrong setup eachModeMoreThanOnce and eachModeOnlyOnce in many cards
|
||||
return mode;
|
||||
|
@ -2106,12 +2127,12 @@ public class HumanPlayer extends PlayerImpl {
|
|||
}
|
||||
|
||||
// end choice by done option in ability pickup dialog
|
||||
if (canEndChoice && Modes.CHOOSE_OPTION_DONE_ID.equals(response.getUUID())) {
|
||||
if (canEndChoice && Modes.CHOOSE_OPTION_DONE_ID.equals(responseId)) {
|
||||
done = true;
|
||||
}
|
||||
|
||||
// cancel choice (remove all selections)
|
||||
if (Modes.CHOOSE_OPTION_CANCEL_ID.equals(response.getUUID())) {
|
||||
if (Modes.CHOOSE_OPTION_CANCEL_ID.equals(responseId)) {
|
||||
modes.clearSelectedModes();
|
||||
}
|
||||
} else if (canEndChoice) {
|
||||
|
|
|
@ -314,7 +314,7 @@ public class ModalDoubleFacesCardsTest extends CardTestPlayerBase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void test_Zones_AfterCast() {
|
||||
public void test_Zones_AfterCast_1() {
|
||||
// Akoum Warrior {5}{R} - creature
|
||||
// Akoum Teeth - land
|
||||
addCard(Zone.HAND, playerA, "Akoum Warrior");
|
||||
|
@ -342,6 +342,40 @@ public class ModalDoubleFacesCardsTest extends CardTestPlayerBase {
|
|||
Assert.assertEquals("main card must be on battlefield", Zone.BATTLEFIELD, currentGame.getState().getZone(((PermanentCard) card).getCard().getMainCard().getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Zones_AfterCast_2() {
|
||||
removeAllCardsFromHand(playerA);
|
||||
// possible bug: if you click on mdf card second side then keep in hand after cast (sorcery + land)
|
||||
// P.S. it works in GUI only, reason: user can sends UUID from wrong card side
|
||||
|
||||
// Ondu Inversion {6}{W}{W} - sorcery
|
||||
// Ondu Skyruins - land
|
||||
addCard(Zone.HAND, playerA, "Ondu Inversion");
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Plains", 8);
|
||||
|
||||
// prepare mdf permanent
|
||||
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ondu Skyruins");
|
||||
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
|
||||
checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ondu Skyruins", 1);
|
||||
|
||||
setStrictChooseMode(true);
|
||||
setStopAt(1, PhaseStep.END_TURN);
|
||||
execute();
|
||||
assertAllCommandsUsed();
|
||||
|
||||
assertHandCount(playerA, 0);
|
||||
Card card = currentGame.getState().getBattlefield().getAllPermanents()
|
||||
.stream()
|
||||
.filter(p -> CardUtil.haveSameNames(p, "Ondu Skyruins", currentGame))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
Assert.assertNotNull(card);
|
||||
Assert.assertEquals("permanent card must be on battlefield", Zone.BATTLEFIELD, currentGame.getState().getZone(card.getId()));
|
||||
Assert.assertEquals("main permanent card must be on battlefield", Zone.BATTLEFIELD, currentGame.getState().getZone(card.getMainCard().getId()));
|
||||
Assert.assertEquals("half card must be on battlefield", Zone.BATTLEFIELD, currentGame.getState().getZone(((PermanentCard) card).getCard().getId()));
|
||||
Assert.assertEquals("main card must be on battlefield", Zone.BATTLEFIELD, currentGame.getState().getZone(((PermanentCard) card).getCard().getMainCard().getId()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Zones_AfterExile() {
|
||||
// {2}, {tap}: Exile target permanent you control.
|
||||
|
|
Loading…
Reference in a new issue