From 03725d204972ef3808149977149c8bcef84fd276 Mon Sep 17 00:00:00 2001 From: Chris H Date: Thu, 7 Dec 2023 12:36:13 -0500 Subject: [PATCH] Macro system based on recording clicks rather than typing (#4006) * Add a new macro system that tracks clicks * Old macro system is still around, but currently inaccessible. Maybe give an option if people rebel against that. --- .../player/actions/ActivateAbilityAction.java | 10 + .../game/player/actions/CastSpellAction.java | 10 + .../player/actions/FinishTargetingAction.java | 9 + .../player/actions/PassPriorityAction.java | 8 + .../game/player/actions/PayCostAction.java | 11 + .../player/actions/PayManaFromPoolAction.java | 16 ++ .../game/player/actions/PlayerAction.java | 22 ++ .../game/player/actions/SelectCardAction.java | 12 + .../player/actions/SelectPlayerAction.java | 11 + .../player/actions/TargetEntityAction.java | 11 + .../gamemodes/match/input/InputBase.java | 18 ++ .../match/input/InputPassPriority.java | 2 + .../gamemodes/match/input/InputPayMana.java | 3 + .../match/input/InputSelectTargets.java | 1 + .../net/client/NetGameController.java | 20 +- .../java/forge/interfaces/IMacroSystem.java | 5 + .../java/forge/player/BasicMacroSystem.java | 206 ++++++++++++++++++ .../forge/player/PlayerControllerHuman.java | 172 +-------------- .../player/RecordActionsMacroSystem.java | 179 +++++++++++++++ 19 files changed, 563 insertions(+), 163 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/PayCostAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/PlayerAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java create mode 100644 forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java create mode 100644 forge-gui/src/main/java/forge/player/BasicMacroSystem.java create mode 100644 forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java diff --git a/forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java b/forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java new file mode 100644 index 00000000000..c0b0ead9e17 --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/ActivateAbilityAction.java @@ -0,0 +1,10 @@ +package forge.game.player.actions; + +import forge.game.GameEntityView; + +public class ActivateAbilityAction extends PlayerAction{ + public ActivateAbilityAction(GameEntityView cardView) { + super(cardView); + name = "Activate ability"; + } +} diff --git a/forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java b/forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java new file mode 100644 index 00000000000..861f451f223 --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/CastSpellAction.java @@ -0,0 +1,10 @@ +package forge.game.player.actions; + +import forge.game.GameEntityView; + +public class CastSpellAction extends PlayerAction { + public CastSpellAction(GameEntityView cardView) { + super(cardView); + name = "Cast spell"; + } +} diff --git a/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java b/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java new file mode 100644 index 00000000000..f17b3c9020d --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/FinishTargetingAction.java @@ -0,0 +1,9 @@ +package forge.game.player.actions; + +public class FinishTargetingAction extends PlayerAction{ + public FinishTargetingAction() { + super(null); + + name = "Finish game entity"; + } +} diff --git a/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java new file mode 100644 index 00000000000..edde010cf5c --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/PassPriorityAction.java @@ -0,0 +1,8 @@ +package forge.game.player.actions; + +public class PassPriorityAction extends PlayerAction { + public PassPriorityAction() { + super(null); + name = "Pass Priority"; + } +} diff --git a/forge-game/src/main/java/forge/game/player/actions/PayCostAction.java b/forge-game/src/main/java/forge/game/player/actions/PayCostAction.java new file mode 100644 index 00000000000..824b5232c66 --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/PayCostAction.java @@ -0,0 +1,11 @@ +package forge.game.player.actions; + +import forge.game.GameEntityView; + +public class PayCostAction extends PlayerAction { + public PayCostAction(GameEntityView cardView) { + super(cardView); + name = "Pay cost"; + gameEntityView = cardView; + } +} diff --git a/forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java b/forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java new file mode 100644 index 00000000000..12cd6a31111 --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/PayManaFromPoolAction.java @@ -0,0 +1,16 @@ +package forge.game.player.actions; + + +public class PayManaFromPoolAction extends PlayerAction{ + private byte colorSelected; + public PayManaFromPoolAction(byte colorCode) { + super(null); + + name = "Pay mana"; + colorSelected = colorCode; + } + + public byte getSelectedColor() { + return colorSelected; + } +} diff --git a/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java b/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java new file mode 100644 index 00000000000..0a3e754a098 --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/PlayerAction.java @@ -0,0 +1,22 @@ +package forge.game.player.actions; + +import forge.game.GameEntityView; +import forge.game.player.PlayerController; + +public abstract class PlayerAction { + protected String name; + protected GameEntityView gameEntityView = null; + + public PlayerAction(GameEntityView cardView) { + gameEntityView = cardView; + } + + public void run(PlayerController controller) { + // Turn this abstract soon + // This should try to replicate the recorded macro action + } + + public GameEntityView getGameEntityView() { + return gameEntityView; + } +} diff --git a/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java b/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java new file mode 100644 index 00000000000..a6087f26a89 --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/SelectCardAction.java @@ -0,0 +1,12 @@ +package forge.game.player.actions; + +import forge.game.GameEntityView; + +public class SelectCardAction extends PlayerAction{ + public SelectCardAction(GameEntityView cardView) { + super(cardView); + name = "Select card"; + } + + +} diff --git a/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java b/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java new file mode 100644 index 00000000000..c255f1bdca5 --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/SelectPlayerAction.java @@ -0,0 +1,11 @@ +package forge.game.player.actions; + +import forge.game.GameEntityView; + +public class SelectPlayerAction extends PlayerAction { + public SelectPlayerAction(GameEntityView playerView) { + super(playerView); + name = "Select player"; + } + +} \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java b/forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java new file mode 100644 index 00000000000..b53e2a182bf --- /dev/null +++ b/forge-game/src/main/java/forge/game/player/actions/TargetEntityAction.java @@ -0,0 +1,11 @@ +package forge.game.player.actions; + +import forge.game.GameEntityView; + +public class TargetEntityAction extends PlayerAction { + // TODO Add distribution damage/counters + public TargetEntityAction(GameEntityView cardView) { + super(cardView); + name = "Target game entity"; + } +} diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java index 9041edf7849..2ae2db4175e 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java @@ -166,6 +166,24 @@ protected String getTurnPhasePriorityMessage(final Game game) { sb.append("\n").append(localizer.getMessage("lblStormCount")).append(": ").append(stormCount); } } + + if (controller.macros() != null) { + boolean isRecording = controller.macros().isRecording(); + String pbText = controller.macros().playbackText(); + if (pbText != null) { + sb.append("\n"); + if (isRecording) { + sb.append("Macro Recording -- "); + } else { + sb.append("Macro Playback -- "); + } + + sb.append(pbText); + } else if (isRecording) { + sb.append("\n").append("Macro Recording -- "); + } + } + return sb.toString(); } } diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPassPriority.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPassPriority.java index dd256c88166..115c474e493 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPassPriority.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPassPriority.java @@ -24,6 +24,7 @@ import forge.game.card.Card; import forge.game.player.Player; import forge.game.player.PlayerController; +import forge.game.player.actions.PassPriorityAction; import forge.game.spellability.LandAbility; import forge.game.spellability.SpellAbility; import forge.localinstance.properties.ForgePreferences.FPref; @@ -74,6 +75,7 @@ protected final void onOk() { passPriority(new Runnable() { @Override public void run() { + getController().macros().addRememberedAction(new PassPriorityAction()); stop(); } }); diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java index 9366b825749..c68784b7da9 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputPayMana.java @@ -20,6 +20,7 @@ import forge.game.mana.ManaCostBeingPaid; import forge.game.player.Player; import forge.game.player.PlayerView; +import forge.game.player.actions.PayManaFromPoolAction; import forge.game.spellability.AbilityManaPart; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityView; @@ -200,6 +201,8 @@ public List getUsefulManaAbilities(Card card) { public void useManaFromPool(byte colorCode) { // find the matching mana in pool. if (player.getManaPool().tryPayCostWithColor(colorCode, saPaidFor, manaCost, saPaidFor.getPayingMana())) { + // Record paying mana from pool here + getController().macros().addRememberedAction(new PayManaFromPoolAction(colorCode)); onManaAbilityPaid(); showMessage(); } diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java index 473e23fbf12..08896bd481b 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectTargets.java @@ -408,6 +408,7 @@ else if (ge instanceof Player) { private void done() { for (final GameEntity c : targets) { + //getController().macros().addRememberedAction(new TargetEntityAction(c.getView())); if (c instanceof Card) { getController().getGui().setUsedToPay(CardView.get((Card) c), false); } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java b/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java index 5123c058e6f..00943f8625c 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/client/NetGameController.java @@ -4,6 +4,7 @@ import forge.game.card.CardView; import forge.game.player.PlayerView; +import forge.game.player.actions.PlayerAction; import forge.game.spellability.SpellAbilityView; import forge.gamemodes.match.NextGameDecision; import forge.gamemodes.net.GameProtocolSender; @@ -124,11 +125,16 @@ public void reorderHand(final CardView card, final int index) { @Override public IMacroSystem macros() { if (macros == null) { - macros = new MacroSystem(); + macros = new NetMacroSystem(); } return macros; } - public class MacroSystem implements IMacroSystem { + public class NetMacroSystem implements IMacroSystem { + @Override + public void addRememberedAction(PlayerAction action) { + // DO i need to send this? + } + @Override public void setRememberedActions() { send(ProtocolMethod.setRememberedActions); @@ -138,5 +144,15 @@ public void setRememberedActions() { public void nextRememberedAction() { send(ProtocolMethod.nextRememberedAction); } + + @Override + public boolean isRecording() { + return false; + } + + @Override + public String playbackText() { + return null; + } } } diff --git a/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java b/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java index a460e9aef9b..fdbd2a25831 100644 --- a/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java +++ b/forge-gui/src/main/java/forge/interfaces/IMacroSystem.java @@ -1,6 +1,11 @@ package forge.interfaces; +import forge.game.player.actions.PlayerAction; + public interface IMacroSystem { + void addRememberedAction(PlayerAction action); void setRememberedActions(); void nextRememberedAction(); + boolean isRecording(); + String playbackText(); } diff --git a/forge-gui/src/main/java/forge/player/BasicMacroSystem.java b/forge-gui/src/main/java/forge/player/BasicMacroSystem.java new file mode 100644 index 00000000000..c9832f85838 --- /dev/null +++ b/forge-gui/src/main/java/forge/player/BasicMacroSystem.java @@ -0,0 +1,206 @@ +package forge.player; + +import com.google.common.collect.Lists; +import forge.game.GameEntityView; +import forge.game.card.Card; +import forge.game.card.CardCollectionView; +import forge.game.card.CardView; +import forge.game.player.Player; +import forge.game.player.PlayerView; +import forge.game.player.actions.PlayerAction; +import forge.game.zone.ZoneType; +import forge.gamemodes.match.input.Input; +import forge.gamemodes.match.input.InputPassPriority; +import forge.interfaces.IMacroSystem; +import forge.localinstance.skin.FSkinProp; +import forge.util.ITriggerEvent; +import forge.util.Localizer; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.Arrays; +import java.util.List; + +// Simple macro system implementation. Its goal is to simulate "clicking" +// on cards/players in an automated way, to reduce mechanical overhead of +// situations like repeated combo activation. +public class BasicMacroSystem implements IMacroSystem { + private final PlayerControllerHuman playerControllerHuman; + // Position in the macro "sequence". + private int sequenceIndex = 0; + // "Actions" are stored as a pair of the "action" recipient (the entity + // to "click") and a boolean representing whether the entity is a player. + private final List> rememberedActions = Lists.newArrayList(); + private String rememberedSequenceText = ""; + + private final Localizer localizer = Localizer.getInstance(); + + public BasicMacroSystem(PlayerControllerHuman playerControllerHuman) { + this.playerControllerHuman = playerControllerHuman; + } + + @Override + public void addRememberedAction(PlayerAction action) { + // No-op this isn't really used for the old macro system + } + + @Override + public void setRememberedActions() { + final String dialogTitle = localizer.getMessage("lblRememberActionSequence"); + // Not sure if this priority guard is really needed, but it seems + // like an alright idea. + final Input input = playerControllerHuman.inputQueue.getInput(); + if (!(input instanceof InputPassPriority)) { + playerControllerHuman.getGui().message(localizer.getMessage("lblYouMustHavePrioritytoUseThisFeature"), dialogTitle); + return; + } + + int currentIndex = sequenceIndex; + sequenceIndex = 0; + // Use a Pair so we can keep a flag for isPlayer + final List> entityInfo = Lists.newArrayList(); + final int playerID = playerControllerHuman.getPlayer().getId(); + // Only support 1 opponent for now. There are some ideas about + // supporting multiplayer games in the future, but for now it would complicate + // the parsing process, and this implementation is still a "proof of concept". + int opponentID = 0; + for (final Player player : playerControllerHuman.getGame().getPlayers()) { + if (player.getId() != playerID) { + opponentID = player.getId(); + break; + } + } + + // A more informative prompt would be useful, but the dialog seems + // to like to clip text in long messages... + final String prompt = localizer.getMessage("lblEnterASequence"); + String textSequence = playerControllerHuman.getGui().showInputDialog(prompt, dialogTitle, FSkinProp.ICO_QUEST_NOTES, + rememberedSequenceText); + if (textSequence == null || textSequence.trim().isEmpty()) { + rememberedActions.clear(); + if (!rememberedSequenceText.isEmpty()) { + rememberedSequenceText = ""; + playerControllerHuman.getGui().message(localizer.getMessage("lblActionSequenceCleared"), dialogTitle); + } + return; + } + // If they haven't changed the sequence, inform them the index is + // reset but don't change rememberedActions. + if (textSequence.equals(rememberedSequenceText)) { + if (currentIndex > 0 && currentIndex < rememberedActions.size()) { + playerControllerHuman.getGui().message(localizer.getMessage("lblRestartingActionSequence"), dialogTitle); + } + return; + } + rememberedSequenceText = textSequence; + rememberedActions.clear(); + + // Clean up input + textSequence = textSequence.trim().toLowerCase().replaceAll("[@%]", ""); + // Replace "opponent" and "me" with symbols to ease following replacements + textSequence = textSequence.replaceAll("\\bopponent\\b", "%").replaceAll("\\bme\\b", "@"); + // Strip user input of anything that's not a + // digit/comma/whitespace/special symbol + textSequence = textSequence.replaceAll("[^\\d\\s,@%]", ""); + // Now change various allowed delimiters to something neutral + textSequence = textSequence.replaceAll("(,\\s+|,|\\s+)", "_"); + final String[] splitSequence = textSequence.split("_"); + for (final String textID : splitSequence) { + if (StringUtils.isNumeric(textID)) { + entityInfo.add(Pair.of(Integer.valueOf(textID), false)); + } else if (textID.equals("%")) { + entityInfo.add(Pair.of(opponentID, true)); + } else if (textID.equals("@")) { + entityInfo.add(Pair.of(playerID, true)); + } + } + if (entityInfo.isEmpty()) { + playerControllerHuman.getGui().message(localizer.getMessage("lblErrorPleaseCheckID"), dialogTitle); + return; + } + + // Fetch cards and players specified by the user input + final ZoneType[] zones = {ZoneType.Battlefield, ZoneType.Hand, ZoneType.Graveyard, ZoneType.Exile, + ZoneType.Command}; + final CardCollectionView cards = playerControllerHuman.getGame().getCardsIn(Arrays.asList(zones)); + for (final Pair entity : entityInfo) { + boolean found = false; + // Nested loops are no fun; however, seems there's no better way + // to get stuff by ID + boolean isPlayer = entity.getValue(); + if (isPlayer) { + for (final Player player : playerControllerHuman.getGame().getPlayers()) { + if (player.getId() == entity.getKey()) { + found = true; + rememberedActions.add(Pair.of(player.getView(), true)); + break; + } + } + } else { + for (final Card card : cards) { + if (card.getId() == entity.getKey()) { + found = true; + rememberedActions.add(Pair.of(card.getView(), false)); + break; + } + } + } + if (!found) { + playerControllerHuman.getGui().message(localizer.getMessage("lblErrorEntityWithId") + " " + entity.getKey() + " " + localizer.getMessage("lblNotFound") + ".", dialogTitle); + rememberedActions.clear(); + return; + } + } + } + + @Override + public void nextRememberedAction() { + final String dialogTitle = localizer.getMessage("lblDoNextActioninSequence"); + if (rememberedActions.isEmpty()) { + playerControllerHuman.getGui().message(localizer.getMessage("lblPleaseDefineanActionSequenceFirst"), dialogTitle); + return; + } + if (sequenceIndex >= rememberedActions.size()) { + // Wrap around to repeat the sequence + sequenceIndex = 0; + } + final Pair action = rememberedActions.get(sequenceIndex); + final boolean isPlayer = action.getValue(); + if (isPlayer) { + playerControllerHuman.selectPlayer((PlayerView) action.getKey(), new DummyTriggerEvent()); + } else { + playerControllerHuman.selectCard((CardView) action.getKey(), null, new DummyTriggerEvent()); + } + sequenceIndex++; + } + + @Override + public boolean isRecording() { + return false; + } + + @Override + public String playbackText() { + if (!"".equals(rememberedSequenceText)) { + return new StringBuilder().append(sequenceIndex).append(" / ").append(rememberedActions.size()).toString(); + } + return null; + } + + private class DummyTriggerEvent implements ITriggerEvent { + @Override + public int getButton() { + return 1; // Emulate left mouse button + } + + @Override + public int getX() { + return 0; // Hopefully this doesn't do anything wonky! + } + + @Override + public int getY() { + return 0; + } + } +} diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index 0a411cc5464..39785c721d1 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -19,7 +19,11 @@ import java.util.stream.Collectors; import java.util.TreeSet; + +import forge.game.player.actions.SelectCardAction; +import forge.game.player.actions.SelectPlayerAction; import forge.game.trigger.TriggerType; + import forge.trackable.TrackableCollection; import forge.util.ImageUtil; import org.apache.commons.lang3.ObjectUtils; @@ -136,7 +140,6 @@ import forge.localinstance.achievements.AchievementCollection; import forge.localinstance.properties.ForgeConstants; import forge.localinstance.properties.ForgePreferences.FPref; -import forge.localinstance.skin.FSkinProp; import forge.model.FModel; import forge.util.CardTranslation; import forge.util.DeckAIUtils; @@ -2341,12 +2344,17 @@ public void useMana(final byte mana) { @Override public void selectPlayer(final PlayerView playerView, final ITriggerEvent triggerEvent) { + // TODO Also record input type and wait for that input to be present before sending select player + macros().addRememberedAction(new SelectPlayerAction(playerView)); + inputProxy.selectPlayer(playerView, triggerEvent); } @Override public boolean selectCard(final CardView cardView, final List otherCardViewsToSelect, final ITriggerEvent triggerEvent) { + macros().addRememberedAction(new SelectCardAction(cardView)); + return inputProxy.selectCard(cardView, otherCardViewsToSelect, triggerEvent); } @@ -3124,170 +3132,12 @@ public void askAI(boolean useSimulation) { @Override public IMacroSystem macros() { if (macros == null) { - macros = new MacroSystem(); + //macros = new BasicMacroSystem(this); + macros = new RecordActionsMacroSystem(this); } return macros; } - // Simple macro system implementation. Its goal is to simulate "clicking" - // on cards/players in an automated way, to reduce mechanical overhead of - // situations like repeated combo activation. - public class MacroSystem implements IMacroSystem { - // Position in the macro "sequence". - private int sequenceIndex = 0; - // "Actions" are stored as a pair of the "action" recipient (the entity - // to "click") and a boolean representing whether the entity is a player. - private final List> rememberedActions = Lists.newArrayList(); - private String rememberedSequenceText = ""; - - @Override - public void setRememberedActions() { - final String dialogTitle = localizer.getMessage("lblRememberActionSequence"); - // Not sure if this priority guard is really needed, but it seems - // like an alright idea. - final Input input = inputQueue.getInput(); - if (!(input instanceof InputPassPriority)) { - getGui().message(localizer.getMessage("lblYouMustHavePrioritytoUseThisFeature"), dialogTitle); - return; - } - - int currentIndex = sequenceIndex; - sequenceIndex = 0; - // Use a Pair so we can keep a flag for isPlayer - final List> entityInfo = Lists.newArrayList(); - final int playerID = getPlayer().getId(); - // Only support 1 opponent for now. There are some ideas about - // supporting multiplayer games in the future, but for now it would complicate - // the parsing process, and this implementation is still a "proof of concept". - int opponentID = 0; - for (final Player player : getGame().getPlayers()) { - if (player.getId() != playerID) { - opponentID = player.getId(); - break; - } - } - - // A more informative prompt would be useful, but the dialog seems - // to like to clip text in long messages... - final String prompt = localizer.getMessage("lblEnterASequence"); - String textSequence = getGui().showInputDialog(prompt, dialogTitle, FSkinProp.ICO_QUEST_NOTES, - rememberedSequenceText); - if (textSequence == null || textSequence.trim().isEmpty()) { - rememberedActions.clear(); - if (!rememberedSequenceText.isEmpty()) { - rememberedSequenceText = ""; - getGui().message(localizer.getMessage("lblActionSequenceCleared"), dialogTitle); - } - return; - } - // If they haven't changed the sequence, inform them the index is - // reset but don't change rememberedActions. - if (textSequence.equals(rememberedSequenceText)) { - if (currentIndex > 0 && currentIndex < rememberedActions.size()) { - getGui().message(localizer.getMessage("lblRestartingActionSequence"), dialogTitle); - } - return; - } - rememberedSequenceText = textSequence; - rememberedActions.clear(); - - // Clean up input - textSequence = textSequence.trim().toLowerCase().replaceAll("[@%]", ""); - // Replace "opponent" and "me" with symbols to ease following replacements - textSequence = textSequence.replaceAll("\\bopponent\\b", "%").replaceAll("\\bme\\b", "@"); - // Strip user input of anything that's not a - // digit/comma/whitespace/special symbol - textSequence = textSequence.replaceAll("[^\\d\\s,@%]", ""); - // Now change various allowed delimiters to something neutral - textSequence = textSequence.replaceAll("(,\\s+|,|\\s+)", "_"); - final String[] splitSequence = textSequence.split("_"); - for (final String textID : splitSequence) { - if (StringUtils.isNumeric(textID)) { - entityInfo.add(Pair.of(Integer.valueOf(textID), false)); - } else if (textID.equals("%")) { - entityInfo.add(Pair.of(opponentID, true)); - } else if (textID.equals("@")) { - entityInfo.add(Pair.of(playerID, true)); - } - } - if (entityInfo.isEmpty()) { - getGui().message(localizer.getMessage("lblErrorPleaseCheckID"), dialogTitle); - return; - } - - // Fetch cards and players specified by the user input - final ZoneType[] zones = {ZoneType.Battlefield, ZoneType.Hand, ZoneType.Graveyard, ZoneType.Exile, - ZoneType.Command}; - final CardCollectionView cards = getGame().getCardsIn(Arrays.asList(zones)); - for (final Pair entity : entityInfo) { - boolean found = false; - // Nested loops are no fun; however, seems there's no better way - // to get stuff by ID - boolean isPlayer = entity.getValue(); - if (isPlayer) { - for (final Player player : getGame().getPlayers()) { - if (player.getId() == entity.getKey()) { - found = true; - rememberedActions.add(Pair.of(player.getView(), true)); - break; - } - } - } else { - for (final Card card : cards) { - if (card.getId() == entity.getKey()) { - found = true; - rememberedActions.add(Pair.of(card.getView(), false)); - break; - } - } - } - if (!found) { - getGui().message(localizer.getMessage("lblErrorEntityWithId") + " " + entity.getKey() + " " + localizer.getMessage("lblNotFound") + ".", dialogTitle); - rememberedActions.clear(); - return; - } - } - } - - @Override - public void nextRememberedAction() { - final String dialogTitle = localizer.getMessage("lblDoNextActioninSequence"); - if (rememberedActions.isEmpty()) { - getGui().message(localizer.getMessage("lblPleaseDefineanActionSequenceFirst"), dialogTitle); - return; - } - if (sequenceIndex >= rememberedActions.size()) { - // Wrap around to repeat the sequence - sequenceIndex = 0; - } - final Pair action = rememberedActions.get(sequenceIndex); - final boolean isPlayer = action.getValue(); - if (isPlayer) { - selectPlayer((PlayerView) action.getKey(), new DummyTriggerEvent()); - } else { - selectCard((CardView) action.getKey(), null, new DummyTriggerEvent()); - } - sequenceIndex++; - } - - private class DummyTriggerEvent implements ITriggerEvent { - @Override - public int getButton() { - return 1; // Emulate left mouse button - } - - @Override - public int getX() { - return 0; // Hopefully this doesn't do anything wonky! - } - - @Override - public int getY() { - return 0; - } - } - } - @Override public void concede() { if (player != null) { diff --git a/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java new file mode 100644 index 00000000000..b82b783fe0e --- /dev/null +++ b/forge-gui/src/main/java/forge/player/RecordActionsMacroSystem.java @@ -0,0 +1,179 @@ +package forge.player; + +import com.google.common.collect.Lists; +import forge.game.GameEntityView; +import forge.game.card.CardView; +import forge.game.player.PlayerView; +import forge.game.player.actions.FinishTargetingAction; +import forge.game.player.actions.PassPriorityAction; +import forge.game.player.actions.PayManaFromPoolAction; +import forge.game.player.actions.PlayerAction; +import forge.gamemodes.match.input.*; +import forge.interfaces.IMacroSystem; +import forge.util.ITriggerEvent; +import forge.util.Localizer; + +import java.util.List; + +// Iteration on the current limited macro system. Instead of asking for IDs to click on +// Instead we wrap the input queue in a way that we can record what the player is doing and +// try to play it back as much as possible + +public class RecordActionsMacroSystem implements IMacroSystem { + private final PlayerControllerHuman playerControllerHuman; + private final Localizer localizer = Localizer.getInstance(); + + private final List actions = Lists.newArrayList(); + private final List playbackActions = Lists.newArrayList(); + + + private boolean recording = false; + + public RecordActionsMacroSystem(PlayerControllerHuman playerControllerHuman) { + this.playerControllerHuman = playerControllerHuman; + } + + @Override + public boolean isRecording() { return recording; } + + @Override + public String playbackText() { + if (playbackActions.isEmpty()) { + return null; + } + + return new StringBuilder().append(actions.size() - playbackActions.size()).append(" / ").append(actions.size()).toString(); + } + + public boolean startRecording() { + if (recording) { + return false; + } + + recording = true; + actions.clear(); + playbackActions.clear(); + playerControllerHuman.getInputQueue().updateObservers(); + + return true; + } + + public boolean finishRecording() { + if (!recording) { + return false; + } + + recording = false; + playerControllerHuman.getInputQueue().updateObservers(); + + return true; + } + + @Override + public void addRememberedAction(PlayerAction action) { + if (!recording) { + return; + } + + actions.add(action); + playbackActions.add(action); + } + + @Override + public void setRememberedActions() { + if (recording) { + finishRecording(); + } else { + startRecording(); + } + } + + public void runFullMacro() { + if (actions.isEmpty() || recording) { + return; + } + + if (playbackActions.isEmpty()) { + playbackActions.addAll(actions); + } else if (actions.size() != playbackActions.size()) { + // Not at the beginning of the loop + return; + } + + while(!playbackActions.isEmpty()) { + runFirstAction(); + } + } + + @Override + public void nextRememberedAction() { + if (actions.isEmpty()) { + // Didn't record anything. We should warn the user. + // playerControllerHuman.getGui().message(localizer.getMessage("lblPleaseDefineanActionSequenceFirst"), dialogTitle); + return; + } + + if (recording) { + // In the middle of a recording can't run macros + System.out.println("Make sure macros are finished recording before running them..."); + return; + } + + if (playbackActions.isEmpty()) { + playbackActions.addAll(actions); + // Finished a loop. Reset loop + System.out.println("Finished macro loop. Restarting at the beginning..."); + // playerControllerHuman.getGui().message(localizer.getMessage("lblPleaseDefineanActionSequenceFirst"), dialogTitle); + // return; + } + + runFirstAction(); + } + + private void runFirstAction() { + PlayerAction action = playbackActions.remove(0); + processAction(action); + } + + public void processAction(PlayerAction action) { + // TODO Add Actions that haven't been covered yet + final Input inp = playerControllerHuman.getInputProxy().getInput(); + if (action instanceof PassPriorityAction) { + if (inp instanceof InputPassPriority) { + inp.selectButtonOK(); + } + } else if (action instanceof FinishTargetingAction) { + if (inp instanceof InputSelectTargets) { + inp.selectButtonOK(); + } + } else if (action instanceof PayManaFromPoolAction) { + if (inp instanceof InputPayMana) { + ((InputPayMana) inp).useManaFromPool(((PayManaFromPoolAction) action).getSelectedColor()); + } + } else { + GameEntityView gev = action.getGameEntityView(); + if (gev instanceof CardView) { + playerControllerHuman.selectCard((CardView)gev, null, new DummyTriggerEvent()); + } else if (gev instanceof PlayerView) { + playerControllerHuman.selectPlayer((PlayerView)gev, null); + } + } + } + + private class DummyTriggerEvent implements ITriggerEvent { + @Override + public int getButton() { + return 1; // Emulate left mouse button + } + + @Override + public int getX() { + return 0; // Hopefully this doesn't do anything wonky! + } + + @Override + public int getY() { + return 0; + } + } +}