diff --git a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java index dba0a92f38c..526d8d93d46 100644 --- a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java +++ b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java @@ -159,7 +159,9 @@ public void actionPerformed(final ActionEvent e) { if (matchUI == null) { return; } StackItemView si = matchUI.getGameView().peekStack(); if (si != null && si.isAbility()) { - matchUI.getGameController().setShouldAutoYield(si.getKey(), true); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + matchUI.getGameController().setShouldAutoYield(si.getKey(), true, abilityScope); int triggerID = si.getSourceTrigger(); if (si.isOptionalTrigger() && matchUI.isLocalPlayer(si.getActivatingPlayer())) { matchUI.getGameController().setShouldAlwaysAcceptTrigger(triggerID); @@ -177,7 +179,9 @@ public void actionPerformed(final ActionEvent e) { if (matchUI == null) { return; } StackItemView si = matchUI.getGameView().peekStack(); if (si != null && si.isAbility()) { - matchUI.getGameController().setShouldAutoYield(si.getKey(), true); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + matchUI.getGameController().setShouldAutoYield(si.getKey(), true, abilityScope); int triggerID = si.getSourceTrigger(); if (si.isOptionalTrigger() && matchUI.isLocalPlayer(si.getActivatingPlayer())) { matchUI.getGameController().setShouldAlwaysDeclineTrigger(triggerID); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java index 2b4db5b6760..5fbec6209ce 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/CSubmenuPreferences.java @@ -568,7 +568,12 @@ private void initializeSwitchStatesCombobox() { } private void initializeAutoYieldModeComboBox() { - final String[] elems = {ForgeConstants.AUTO_YIELD_PER_ABILITY, ForgeConstants.AUTO_YIELD_PER_CARD}; + final String[] elems = { + ForgeConstants.AUTO_YIELD_PER_CARD, + ForgeConstants.AUTO_YIELD_PER_ABILITY, + ForgeConstants.AUTO_YIELD_PER_ABILITY_SESSION, + ForgeConstants.AUTO_YIELD_PER_ABILITY_INSTALL, + }; final FPref userSetting = FPref.UI_AUTO_YIELD_MODE; final FComboBoxPanel panel = this.view.getAutoYieldModeComboBoxPanel(); final FComboBox comboBox = createComboBox(elems, userSetting); diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java b/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java index e27710722b5..1863f9e8e46 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/VAutoYields.java @@ -57,7 +57,9 @@ public VAutoYields(final CMatchUI matchUI) { if (selected != null) { autoYields.remove(selected); btnRemove.setEnabled(autoYields.size() > 0); - matchUI.getGameController().setShouldAutoYield(selected, false); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + matchUI.getGameController().setShouldAutoYield(selected, false, abilityScope); VAutoYields.this.revalidate(); lstAutoYields.repaint(); } diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java b/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java index 024c9ba74af..6467ac8c23b 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/views/VStack.java @@ -293,7 +293,9 @@ public AbilityMenu(){ jmiAutoYield.addActionListener(arg0 -> { final String key = item.getKey(); final boolean autoYield = controller.getMatchUI().getGameController().shouldAutoYield(key); - controller.getMatchUI().getGameController().setShouldAutoYield(key, !autoYield); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + controller.getMatchUI().getGameController().setShouldAutoYield(key, !autoYield, abilityScope); if (!autoYield && controller.getMatchUI().getGameView().peekStack() == item) { //auto-pass priority if ability is on top of stack controller.getMatchUI().getGameController().passPriority(); diff --git a/forge-gui-mobile/src/forge/screens/match/MatchScreen.java b/forge-gui-mobile/src/forge/screens/match/MatchScreen.java index da89a41d7ff..160846df951 100644 --- a/forge-gui-mobile/src/forge/screens/match/MatchScreen.java +++ b/forge-gui-mobile/src/forge/screens/match/MatchScreen.java @@ -686,7 +686,9 @@ public boolean keyDown(int keyCode) { } final String key = stackInstance.getKey(); - controller.setShouldAutoYield(key, true); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + controller.setShouldAutoYield(key, true, abilityScope); if (stackInstance.equals(gameView.peekStack())) { //auto-pass priority if ability is on top of stack controller.passPriority(); @@ -718,7 +720,9 @@ public boolean keyDown(int keyCode) { } final String key = stackInstance.getKey(); - controller.setShouldAutoYield(key, true); + boolean abilityScope2 = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + controller.setShouldAutoYield(key, true, abilityScope2); if (stackInstance.equals(gameView.peekStack())) { //auto-pass priority if ability is on top of stack controller.passPriority(); diff --git a/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java b/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java index d6307f177ab..f4bda9b8938 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VAutoYields.java @@ -39,7 +39,9 @@ protected boolean allowDefaultItemWrap() { String selected = lstAutoYields.getSelectedItem(); if (selected != null) { lstAutoYields.removeItem(selected); - MatchController.instance.getGameController().setShouldAutoYield(selected, false); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + MatchController.instance.getGameController().setShouldAutoYield(selected, false, abilityScope); setButtonEnabled(1, lstAutoYields.getCount() > 0); lstAutoYields.cleanUpSelections(); VAutoYields.this.revalidate(); diff --git a/forge-gui-mobile/src/forge/screens/match/views/VGameMenu.java b/forge-gui-mobile/src/forge/screens/match/views/VGameMenu.java index fdf73c452f1..d0fb7c4470a 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VGameMenu.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VGameMenu.java @@ -47,7 +47,9 @@ public void setVisible(boolean b0) { if (MatchController.instance.getGameView().peekStack() != null) { final String key = MatchController.instance.getGameView().peekStack().getKey(); final boolean autoYield = MatchController.instance.getGameController().shouldAutoYield(key); - MatchController.instance.getGameController().setShouldAutoYield(key, !autoYield); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + MatchController.instance.getGameController().setShouldAutoYield(key, !autoYield, abilityScope); if (!autoYield && MatchController.instance.getGameController().shouldAutoYield(key)) { //auto-pass priority if ability is on top of stack MatchController.instance.getGameController().passPriority(); diff --git a/forge-gui-mobile/src/forge/screens/match/views/VStack.java b/forge-gui-mobile/src/forge/screens/match/views/VStack.java index b24a0baa3e7..5d24df53ccc 100644 --- a/forge-gui-mobile/src/forge/screens/match/views/VStack.java +++ b/forge-gui-mobile/src/forge/screens/match/views/VStack.java @@ -292,7 +292,9 @@ protected void buildMenu() { final boolean autoYield = controller.shouldAutoYield(key); addItem(new FCheckBoxMenuItem(Forge.getLocalizer().getMessage("cbpAutoYieldMode"), autoYield, e -> { - controller.setShouldAutoYield(key, !autoYield); + boolean abilityScope = !forge.localinstance.properties.ForgeConstants.AUTO_YIELD_PER_CARD.equals( + forge.model.FModel.getPreferences().getPref(forge.localinstance.properties.ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + controller.setShouldAutoYield(key, !autoYield, abilityScope); if (!autoYield && stackInstance.equals(gameView.peekStack())) { //auto-pass priority if ability is on top of stack controller.passPriority(); diff --git a/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java b/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java index 7ff87e3c130..e7a660a59fd 100644 --- a/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java +++ b/forge-gui-mobile/src/forge/screens/settings/SettingsPage.java @@ -262,7 +262,12 @@ public void valueChanged(String newValue) { lstSettings.addItem(new CustomSelectSetting(FPref.UI_AUTO_YIELD_MODE, Forge.getLocalizer().getMessage("lblAutoYields"), Forge.getLocalizer().getMessage("nlpAutoYieldMode"), - new String[] { ForgeConstants.AUTO_YIELD_PER_ABILITY, ForgeConstants.AUTO_YIELD_PER_CARD }), 1); + new String[] { + ForgeConstants.AUTO_YIELD_PER_CARD, + ForgeConstants.AUTO_YIELD_PER_ABILITY, + ForgeConstants.AUTO_YIELD_PER_ABILITY_SESSION, + ForgeConstants.AUTO_YIELD_PER_ABILITY_INSTALL, + }), 1); lstSettings.addItem(new BooleanSetting(FPref.UI_ALLOW_ESC_TO_END_TURN, Forge.getLocalizer().getMessage("cbEscapeEndsTurn"), Forge.getLocalizer().getMessage("nlEscapeEndsTurn")), 1); diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index c023cb23cd3..c1bb6e3d7be 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -219,7 +219,7 @@ nlShowStormCount=When enabled, displays the current storm count in the prompt pa nlRemindOnPriority=When enabled, flashes the player choice area upon receiving priority. nlPreselectPrevAbOrder=When enabled, preselects the last defined simultaneous ability order in the ordering dialog. nlpGraveyardOrdering=Determines when to let the player choose the order of cards simultaneously put in graveyard (never, always, or only when playing with cards for which it matters, for example, Volrath''s Shapeshifter). -nlpAutoYieldMode=Defines the granularity level of auto-yields (per unique ability or per unique card). +nlpAutoYieldMode=Auto-yield scope: per card (one game), or per ability (this match, this session, or all sessions). RandomDeckGeneration=Random Deck Generation nlRemoveSmall=Disables 1/1 and 0/X creatures in generated decks nlSingletons=Disables non-land duplicates in generated decks diff --git a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java index 24ef91e5068..adaf086b601 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java @@ -30,7 +30,6 @@ import forge.gui.events.*; import forge.gui.interfaces.IGuiGame; import forge.interfaces.IGameController; -import forge.localinstance.properties.ForgeConstants; import forge.localinstance.properties.ForgePreferences; import forge.localinstance.properties.ForgePreferences.FPref; import forge.model.FModel; @@ -388,10 +387,7 @@ public void endCurrentGame() { ngg.shutdownForwarder(); } humanController.getGui().setGameSpeed(PlaybackSpeed.NORMAL); - if (FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE).equals(ForgeConstants.AUTO_YIELD_PER_CARD) || isMatchOver()) { - // when autoyielding per card, we need to clear auto yields between games since card IDs change - humanController.clearAutoYields(); - } + humanController.clearAutoYields(); if (humanCount > 0) //conceded humanController.getGui().afterGameEnd(); diff --git a/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java b/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java index 2c8a826dcc5..95c1d61e41d 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java @@ -95,7 +95,7 @@ public enum ProtocolMethod implements IHasForgeLog { alphaStrike (Mode.CLIENT, Void.TYPE), reorderHand (Mode.CLIENT, Void.TYPE, CardView.class, Integer.TYPE), requestResync (Mode.CLIENT, Void.TYPE), - setShouldAutoYield (Mode.CLIENT, Void.TYPE, String.class, Boolean.TYPE), + setShouldAutoYield (Mode.CLIENT, Void.TYPE, String.class, Boolean.TYPE, Boolean.TYPE), setShouldAlwaysAcceptTrigger (Mode.CLIENT, Void.TYPE, Integer.TYPE), setShouldAlwaysDeclineTrigger (Mode.CLIENT, Void.TYPE, Integer.TYPE), setShouldAlwaysAskTrigger (Mode.CLIENT, Void.TYPE, Integer.TYPE); diff --git a/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java b/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java index 5d6e36b98f1..488e3768e1b 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/client/FGameClient.java @@ -153,7 +153,9 @@ public void addLobbyListener(final ILobbyListener listener) { void setGameControllers(final Iterable myPlayers) { for (final PlayerView p : myPlayers) { - clientGui.setOriginalGameController(p, new NetGameController(this)); + NetGameController controller = new NetGameController(this); + clientGui.setOriginalGameController(p, controller); + controller.replayActiveYields(); } } 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 0044ede05e4..0ccfb889ba2 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 @@ -1,7 +1,5 @@ package forge.gamemodes.net.client; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import forge.game.card.CardView; import forge.game.player.PlayerView; import forge.game.player.actions.PlayerAction; @@ -15,20 +13,17 @@ import forge.localinstance.properties.ForgeConstants; import forge.localinstance.properties.ForgePreferences; import forge.model.FModel; +import forge.player.AutoYieldStore; +import forge.player.PersistentYieldStore; import forge.util.ITriggerEvent; import java.util.List; -import java.util.Map; -import java.util.Set; public class NetGameController implements IGameController { private final GameProtocolSender sender; - // Local mirror of yield state for UI display - private final Set autoYields = Sets.newHashSet(); - private final Map triggersAlwaysAccept = Maps.newTreeMap(); - private boolean disableAutoYields; + private final AutoYieldStore yieldStore = new AutoYieldStore(); public NetGameController(final IToServer server) { this.sender = new GameProtocolSender(server); @@ -139,76 +134,97 @@ public void requestResync() { send(ProtocolMethod.requestResync); } + private boolean activeModeIsInstall() { + return ForgeConstants.AUTO_YIELD_PER_ABILITY_INSTALL.equals( + FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + } + + private boolean activeModeIsAbilityScope() { + return !ForgeConstants.AUTO_YIELD_PER_CARD.equals( + FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE)); + } + + private AutoYieldStore.Tier activeTier() { + String mode = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE); + if (ForgeConstants.AUTO_YIELD_PER_CARD.equals(mode)) return AutoYieldStore.Tier.GAME; + if (ForgeConstants.AUTO_YIELD_PER_ABILITY_SESSION.equals(mode)) return AutoYieldStore.Tier.SESSION; + return AutoYieldStore.Tier.MATCH; + } + @Override public boolean shouldAutoYield(final String key) { - String abilityKey = key.contains("): ") ? key.substring(key.indexOf("): ") + 3) : key; - boolean yieldPerAbility = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE) - .equals(ForgeConstants.AUTO_YIELD_PER_ABILITY); - return !disableAutoYields && autoYields.contains(yieldPerAbility ? abilityKey : key); + if (yieldStore.isDisabled()) return false; + if (activeModeIsInstall()) { + return PersistentYieldStore.get().contains(AutoYieldStore.abilitySuffix(key)); + } + String storageKey = activeModeIsAbilityScope() ? AutoYieldStore.abilitySuffix(key) : key; + return yieldStore.shouldYield(activeTier(), storageKey); } @Override - public void setShouldAutoYield(final String key, final boolean autoYield) { - String abilityKey = key.contains("): ") ? key.substring(key.indexOf("): ") + 3) : key; - boolean yieldPerAbility = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_AUTO_YIELD_MODE) - .equals(ForgeConstants.AUTO_YIELD_PER_ABILITY); - if (autoYield) { - autoYields.add(yieldPerAbility ? abilityKey : key); + public void setShouldAutoYield(final String key, final boolean autoYield, final boolean isAbilityScope) { + String storageKey = isAbilityScope ? AutoYieldStore.abilitySuffix(key) : key; + if (activeModeIsInstall()) { + PersistentYieldStore.get().setYield(storageKey, autoYield); } else { - autoYields.remove(yieldPerAbility ? abilityKey : key); + yieldStore.setYield(activeTier(), storageKey, autoYield); } - send(ProtocolMethod.setShouldAutoYield, key, autoYield); + send(ProtocolMethod.setShouldAutoYield, storageKey, autoYield, isAbilityScope); } @Override public Iterable getAutoYields() { - return autoYields; + return activeModeIsInstall() + ? PersistentYieldStore.get().getYields() + : yieldStore.getYields(activeTier()); } @Override public void clearAutoYields() { - autoYields.clear(); - triggersAlwaysAccept.clear(); + // No-op locally: tier lifecycle is driven separately. Server-side mirror is cleared by HostedMatch. } @Override - public boolean getDisableAutoYields() { - return disableAutoYields; - } + public boolean getDisableAutoYields() { return yieldStore.isDisabled(); } @Override - public void setDisableAutoYields(final boolean disable) { - disableAutoYields = disable; - } + public void setDisableAutoYields(final boolean disable) { yieldStore.setDisabled(disable); } @Override public boolean shouldAlwaysAcceptTrigger(final int trigger) { - return Boolean.TRUE.equals(triggersAlwaysAccept.get(trigger)); + return yieldStore.getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.ACCEPT; } @Override public boolean shouldAlwaysDeclineTrigger(final int trigger) { - return Boolean.FALSE.equals(triggersAlwaysAccept.get(trigger)); + return yieldStore.getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.DECLINE; } @Override public void setShouldAlwaysAcceptTrigger(final int trigger) { - triggersAlwaysAccept.put(trigger, Boolean.TRUE); + yieldStore.setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT); send(ProtocolMethod.setShouldAlwaysAcceptTrigger, trigger); } @Override public void setShouldAlwaysDeclineTrigger(final int trigger) { - triggersAlwaysAccept.put(trigger, Boolean.FALSE); + yieldStore.setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE); send(ProtocolMethod.setShouldAlwaysDeclineTrigger, trigger); } @Override public void setShouldAlwaysAskTrigger(final int trigger) { - triggersAlwaysAccept.remove(trigger); + yieldStore.setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK); send(ProtocolMethod.setShouldAlwaysAskTrigger, trigger); } + public void replayActiveYields() { + boolean abilityScope = activeModeIsAbilityScope(); + for (String key : getAutoYields()) { + send(ProtocolMethod.setShouldAutoYield, key, Boolean.TRUE, abilityScope); + } + } + private IMacroSystem macros; @Override public IMacroSystem macros() { diff --git a/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java b/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java index e98076fba71..177bea2e619 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/server/FServerManager.java @@ -519,9 +519,6 @@ public static LinkedHashMap getAllLocalAddresses() { result.putAll(sorted); } catch (final SocketException e) { netLog.error(e, "Failed to enumerate network interfaces"); - if (result.isEmpty()) { - result.put("Default", routableAddress); - } } if (result.isEmpty()) { diff --git a/forge-gui/src/main/java/forge/interfaces/IGameController.java b/forge-gui/src/main/java/forge/interfaces/IGameController.java index d13992a9dc0..fa31f4ba6c6 100644 --- a/forge-gui/src/main/java/forge/interfaces/IGameController.java +++ b/forge-gui/src/main/java/forge/interfaces/IGameController.java @@ -54,7 +54,12 @@ public interface IGameController { // --- Auto-yield preferences (per-player) --- boolean shouldAutoYield(String key); - void setShouldAutoYield(String key, boolean autoYield); + /** + * @param isAbilityScope true if {@code key} is an ability suffix (Per Ability * modes); + * false if {@code key} is the full raw key (Per Card mode). Server-side handlers + * route storage by this flag instead of consulting the host's own UI_AUTO_YIELD_MODE. + */ + void setShouldAutoYield(String key, boolean autoYield, boolean isAbilityScope); Iterable getAutoYields(); void clearAutoYields(); boolean getDisableAutoYields(); diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java index 7fb977880d9..733f6c25330 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgeConstants.java @@ -348,6 +348,8 @@ public final class ForgeConstants { // Constants for Auto-Yield Mode public static final String AUTO_YIELD_PER_CARD = "Per Card (Each Game)"; public static final String AUTO_YIELD_PER_ABILITY = "Per Ability (Each Match)"; + public static final String AUTO_YIELD_PER_ABILITY_SESSION = "Per Ability (Each Session)"; + public static final String AUTO_YIELD_PER_ABILITY_INSTALL = "Per Ability (Each Install)"; // Constants for Graveyard Ordering public static final String GRAVEYARD_ORDERING_NEVER = "Never"; diff --git a/forge-gui/src/main/java/forge/player/AutoYieldStore.java b/forge-gui/src/main/java/forge/player/AutoYieldStore.java new file mode 100644 index 00000000000..28ad9f2ff0d --- /dev/null +++ b/forge-gui/src/main/java/forge/player/AutoYieldStore.java @@ -0,0 +1,57 @@ +package forge.player; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +import java.util.EnumMap; +import java.util.Map; +import java.util.Set; + +public class AutoYieldStore { + public enum Tier { GAME, MATCH, SESSION } + public enum TriggerDecision { ASK, ACCEPT, DECLINE } + + private final EnumMap> yieldsByTier = new EnumMap<>(Tier.class); + private final Map triggerDecisions = Maps.newTreeMap(); + private boolean disabled; + + public AutoYieldStore() { + for (Tier t : Tier.values()) yieldsByTier.put(t, Sets.newHashSet()); + } + + public boolean shouldYield(Tier tier, String key) { + return !disabled && yieldsByTier.get(tier).contains(key); + } + + public void setYield(Tier tier, String key, boolean autoYield) { + if (autoYield) yieldsByTier.get(tier).add(key); + else yieldsByTier.get(tier).remove(key); + } + + public Iterable getYields(Tier tier) { return yieldsByTier.get(tier); } + public boolean isDisabled() { return disabled; } + public void setDisabled(boolean disabled) { this.disabled = disabled; } + + public TriggerDecision getTriggerDecision(int triggerId) { + TriggerDecision d = triggerDecisions.get(triggerId); + return d == null ? TriggerDecision.ASK : d; + } + + public void setTriggerDecision(int triggerId, TriggerDecision decision) { + if (decision == TriggerDecision.ASK) triggerDecisions.remove(triggerId); + else triggerDecisions.put(triggerId, decision); + } + + public void onGameEnd(boolean matchOver) { + triggerDecisions.clear(); + yieldsByTier.get(Tier.GAME).clear(); + if (matchOver) { + yieldsByTier.get(Tier.MATCH).clear(); + } + } + + /** Strips the "Card (id=N): " prefix to derive the ability-scope key, or returns the input unchanged. */ + public static String abilitySuffix(String rawKey) { + return rawKey.contains("): ") ? rawKey.substring(rawKey.indexOf("): ") + 3) : rawKey; + } +} diff --git a/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java b/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java index e5882646851..1a894b74a03 100644 --- a/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java +++ b/forge-gui/src/main/java/forge/player/LobbyPlayerHuman.java @@ -8,6 +8,10 @@ import forge.util.GuiDisplayUtil; public class LobbyPlayerHuman extends LobbyPlayer implements IGameEntitiesFactory { + private final AutoYieldStore yieldStore = new AutoYieldStore(); + + public AutoYieldStore getYieldStore() { return yieldStore; } + public LobbyPlayerHuman(final String name) { this(name, -1, -1); } diff --git a/forge-gui/src/main/java/forge/player/PersistentYieldStore.java b/forge-gui/src/main/java/forge/player/PersistentYieldStore.java new file mode 100644 index 00000000000..6b1b524a9c0 --- /dev/null +++ b/forge-gui/src/main/java/forge/player/PersistentYieldStore.java @@ -0,0 +1,93 @@ +package forge.player; + +import forge.localinstance.properties.ForgeConstants; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +/** + * File-backed singleton holding INSTALL-tier auto-yield keys. Shared across all + * local controllers on this install of Forge. Trigger accept/decline decisions + * are not persisted: trigger IDs are not stable across games. + * + * Key stability: ability keys derive from SpellAbility.toUnsuppressedString() and + * are not guaranteed stable across Forge versions or card text edits. A stale + * persisted key silently fails to match — acceptable; users can re-add as needed. + */ +public class PersistentYieldStore { + private static final String YIELD_PREFIX = "yield."; + private static volatile PersistentYieldStore instance; + + public static PersistentYieldStore get() { + PersistentYieldStore local = instance; + if (local != null) return local; + synchronized (PersistentYieldStore.class) { + if (instance == null) { + instance = new PersistentYieldStore(Paths.get(ForgeConstants.USER_DIR, "auto-yields.dat")); + } + return instance; + } + } + + private final Path persistFile; + private final Set yields = new HashSet<>(); + + PersistentYieldStore(Path persistFile) { + this.persistFile = persistFile; + load(); + } + + public synchronized boolean contains(String key) { return yields.contains(key); } + + public synchronized void setYield(String key, boolean autoYield) { + boolean changed = autoYield ? yields.add(key) : yields.remove(key); + if (changed) save(); + } + + public synchronized Iterable getYields() { + return Collections.unmodifiableSet(new HashSet<>(yields)); + } + + private void load() { + if (persistFile == null || !Files.exists(persistFile)) return; + try (BufferedReader r = Files.newBufferedReader(persistFile)) { + Properties p = new Properties(); + p.load(r); + for (String name : p.stringPropertyNames()) { + if (name.startsWith(YIELD_PREFIX) && "true".equals(p.getProperty(name))) { + yields.add(URLDecoder.decode(name.substring(YIELD_PREFIX.length()), "UTF-8")); + } + } + } catch (IOException ignored) { + // UnsupportedEncodingException is unreachable ("UTF-8" is always supported). + // IOException: stale or corrupt file — treat as empty; next save overwrites. + } + } + + private void save() { + if (persistFile == null) return; + try { + Properties p = new Properties(); + for (String key : yields) { + p.setProperty(YIELD_PREFIX + URLEncoder.encode(key, "UTF-8"), "true"); + } + Path parent = persistFile.getParent(); + if (parent != null) Files.createDirectories(parent); + try (BufferedWriter w = Files.newBufferedWriter(persistFile)) { + p.store(w, "Forge auto-yield persistent store"); + } + } catch (IOException ignored) { + // Best-effort persistence; in-memory state remains correct for this session. + } + } +} diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index dc6cab9bc04..cd97f8408e1 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -98,9 +98,14 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont private IGuiGame gui; - private final Set autoYields = Sets.newHashSet(); - private final Map triggersAlwaysAccept = Maps.newTreeMap(); - private boolean disableAutoYields; + // Inlined server-side mirror used only when this controller serves a remote network + // player (gui instanceof RemoteClientGuiGame). Two scope buckets so the host doesn't + // need to know the client's UI_AUTO_YIELD_MODE: the client tells us which bucket via + // the isAbilityScope flag on setShouldAutoYield. + private final Set remoteCardYields = Sets.newHashSet(); + private final Set remoteAbilityYields = Sets.newHashSet(); + private final Map remoteTriggerDecisions = Maps.newTreeMap(); + private boolean remoteAutoYieldsDisabled; protected final InputQueue inputQueue; protected final InputProxy inputProxy; @@ -3470,69 +3475,115 @@ public void requestResync() { // No-op for local games - resync is only used for network play } + private boolean isRemoteClient() { + return gui instanceof forge.gamemodes.net.server.RemoteClientGuiGame; + } + + private AutoYieldStore localStore() { + return ((LobbyPlayerHuman) getLobbyPlayer()).getYieldStore(); + } + + private boolean activeModeIsInstall() { + return ForgeConstants.AUTO_YIELD_PER_ABILITY_INSTALL.equals( + FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE)); + } + + private AutoYieldStore.Tier activeTier() { + String mode = FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE); + if (ForgeConstants.AUTO_YIELD_PER_CARD.equals(mode)) return AutoYieldStore.Tier.GAME; + if (ForgeConstants.AUTO_YIELD_PER_ABILITY_SESSION.equals(mode)) return AutoYieldStore.Tier.SESSION; + return AutoYieldStore.Tier.MATCH; + } + @Override public boolean shouldAutoYield(final String key) { - String abilityKey = key.contains("): ") ? key.substring(key.indexOf("): ") + 3) : key; - boolean yieldPerAbility = FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE) - .equals(ForgeConstants.AUTO_YIELD_PER_ABILITY); - return !disableAutoYields && autoYields.contains(yieldPerAbility ? abilityKey : key); + if (isRemoteClient()) { + if (remoteAutoYieldsDisabled) return false; + if (remoteCardYields.contains(key)) return true; + return remoteAbilityYields.contains(AutoYieldStore.abilitySuffix(key)); + } + if (localStore().isDisabled()) return false; + if (activeModeIsInstall()) { + return PersistentYieldStore.get().contains(AutoYieldStore.abilitySuffix(key)); + } + boolean abilityScope = activeTier() != AutoYieldStore.Tier.GAME; + String storageKey = abilityScope ? AutoYieldStore.abilitySuffix(key) : key; + return localStore().shouldYield(activeTier(), storageKey); } @Override - public void setShouldAutoYield(final String key, final boolean autoYield) { - String abilityKey = key.contains("): ") ? key.substring(key.indexOf("): ") + 3) : key; - boolean yieldPerAbility = FModel.getPreferences().getPref(FPref.UI_AUTO_YIELD_MODE) - .equals(ForgeConstants.AUTO_YIELD_PER_ABILITY); - if (autoYield) { - autoYields.add(yieldPerAbility ? abilityKey : key); - } else { - autoYields.remove(yieldPerAbility ? abilityKey : key); + public void setShouldAutoYield(final String key, final boolean autoYield, final boolean isAbilityScope) { + if (isRemoteClient()) { + Set bucket = isAbilityScope ? remoteAbilityYields : remoteCardYields; + if (autoYield) bucket.add(key); else bucket.remove(key); + return; + } + String storageKey = isAbilityScope ? AutoYieldStore.abilitySuffix(key) : key; + if (activeModeIsInstall()) { + PersistentYieldStore.get().setYield(storageKey, autoYield); + return; } + localStore().setYield(activeTier(), storageKey, autoYield); } @Override public Iterable getAutoYields() { - return autoYields; + if (isRemoteClient()) { + return com.google.common.collect.Iterables.concat(remoteCardYields, remoteAbilityYields); + } + if (activeModeIsInstall()) return PersistentYieldStore.get().getYields(); + return localStore().getYields(activeTier()); } @Override public void clearAutoYields() { - autoYields.clear(); - triggersAlwaysAccept.clear(); + if (isRemoteClient()) { + remoteCardYields.clear(); + remoteAbilityYields.clear(); + remoteTriggerDecisions.clear(); + return; + } + localStore().onGameEnd(getGame() == null || getGame().getView().isMatchOver()); } @Override public boolean getDisableAutoYields() { - return disableAutoYields; + return isRemoteClient() ? remoteAutoYieldsDisabled : localStore().isDisabled(); } @Override public void setDisableAutoYields(final boolean disable) { - disableAutoYields = disable; + if (isRemoteClient()) remoteAutoYieldsDisabled = disable; + else localStore().setDisabled(disable); } @Override public boolean shouldAlwaysAcceptTrigger(final int trigger) { - return Boolean.TRUE.equals(triggersAlwaysAccept.get(trigger)); + if (isRemoteClient()) return Boolean.TRUE.equals(remoteTriggerDecisions.get(trigger)); + return localStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.ACCEPT; } @Override public boolean shouldAlwaysDeclineTrigger(final int trigger) { - return Boolean.FALSE.equals(triggersAlwaysAccept.get(trigger)); + if (isRemoteClient()) return Boolean.FALSE.equals(remoteTriggerDecisions.get(trigger)); + return localStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.DECLINE; } @Override public void setShouldAlwaysAcceptTrigger(final int trigger) { - triggersAlwaysAccept.put(trigger, Boolean.TRUE); + if (isRemoteClient()) remoteTriggerDecisions.put(trigger, Boolean.TRUE); + else localStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT); } @Override public void setShouldAlwaysDeclineTrigger(final int trigger) { - triggersAlwaysAccept.put(trigger, Boolean.FALSE); + if (isRemoteClient()) remoteTriggerDecisions.put(trigger, Boolean.FALSE); + else localStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE); } @Override public void setShouldAlwaysAskTrigger(final int trigger) { - triggersAlwaysAccept.remove(trigger); + if (isRemoteClient()) remoteTriggerDecisions.remove(trigger); + else localStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK); } }