Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion forge-game/src/main/java/forge/game/trigger/Trigger.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ public final String toString() {
}

public String toString(boolean active) {
return toString(active, true);
}

/**
* @param includeRemembered append the per-game-state "triggerRemembered" suffix.
* Pass false to obtain a stable key for persisting Always-Yes / Always-No
* decisions across game instances. Staleness across Forge versions or
* card-text edits is acceptable -- stale keys silently fail to match.
*/
public String toString(boolean active, boolean includeRemembered) {
if (hasParam("TriggerDescription") && !this.isSuppressed()) {
StringBuilder sb = new StringBuilder();
ITranslatable nameSource = getHostName(this);
Expand All @@ -140,7 +150,7 @@ public String toString(boolean active) {
desc = TextUtil.fastReplace(desc, "EFFECTSOURCE", getHostCard().getEffectSource().getDisplayName());
}
sb.append(desc);
if (!this.triggerRemembered.isEmpty()) {
if (includeRemembered && !this.triggerRemembered.isEmpty()) {
Comment thread
tool4ever marked this conversation as resolved.
Outdated
sb.append(" (").append(this.triggerRemembered).append(")");
}
return sb.toString();
Expand All @@ -149,6 +159,13 @@ public String toString(boolean active) {
}
}

/** Composed per-card-instance key, mirroring SpellAbility.yieldKey(). Empty if no stable key. */
public final String getYieldKey() {
String stable = toString(false, false);
if (stable.isEmpty()) return "";
return getHostCard() != null ? getHostCard().toString() + ": " + stable : stable;
}

public final String replaceAbilityText(final String desc, final CardState state) {
// this function is for ABILITY
if (!desc.contains("ABILITY")) {
Expand Down
112 changes: 89 additions & 23 deletions forge-gui/src/main/java/forge/gamemodes/match/YieldController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import forge.model.FModel;
import forge.player.AutoYieldStore;
import forge.player.LobbyPlayerHuman;
import forge.player.PersistentYieldStore;
import forge.player.PersistentAutoDecisionStore;
import forge.player.PlayerControllerHuman;

import java.util.EnumSet;
Expand Down Expand Up @@ -139,7 +139,7 @@ public boolean shouldAutoYield(String key) {
|| store.shouldYield(AutoYieldStore.Tier.GAME, AutoYieldStore.abilitySuffix(key));
}
if (activeModeIsInstall()) {
return PersistentYieldStore.get().contains(AutoYieldStore.abilitySuffix(key));
return PersistentAutoDecisionStore.get().contains(AutoYieldStore.abilitySuffix(key));
}
AutoYieldStore.Tier tier = activeTier();
boolean abilityScope = tier != AutoYieldStore.Tier.GAME;
Expand All @@ -151,7 +151,7 @@ public boolean shouldAutoYield(String key) {
public String setShouldAutoYield(String key, boolean autoYield, boolean abilityScope) {
String storageKey = abilityScope ? AutoYieldStore.abilitySuffix(key) : key;
if (activeModeIsInstall()) {
PersistentYieldStore.get().setYield(storageKey, autoYield);
PersistentAutoDecisionStore.get().setYield(storageKey, autoYield);
} else {
activeStore().setYield(activeTier(), storageKey, autoYield);
}
Expand Down Expand Up @@ -193,7 +193,7 @@ public void applyAutoYieldFromWire(String storageKey, boolean active) {
public Iterable<String> getAutoYields() {
AutoYieldStore store = activeStore();
if (!tierAware()) return store.getYields(AutoYieldStore.Tier.GAME);
if (activeModeIsInstall()) return PersistentYieldStore.get().getYields();
if (activeModeIsInstall()) return PersistentAutoDecisionStore.get().getYields();
return store.getYields(activeTier());
}

Expand All @@ -213,50 +213,116 @@ public void setDisableAutoYields(boolean disable) {
activeStore().setDisabled(disable);
}

public boolean shouldAlwaysAcceptTrigger(int trigger) {
return activeStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.ACCEPT;
public boolean shouldAlwaysAcceptTrigger(String key) {
return readTriggerDecision(key) == AutoYieldStore.TriggerDecision.ACCEPT;
}
public boolean shouldAlwaysDeclineTrigger(int trigger) {
return activeStore().getTriggerDecision(trigger) == AutoYieldStore.TriggerDecision.DECLINE;
public boolean shouldAlwaysDeclineTrigger(String key) {
return readTriggerDecision(key) == AutoYieldStore.TriggerDecision.DECLINE;
}

public void setAlwaysAcceptTrigger(int trigger) {
activeStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT);
/** Tier-aware user-initiated set. Returns the storage key (stripped if ability-scope) for wire propagation. */
public String setAlwaysAcceptTrigger(String key, boolean abilityScope) {
return writeTriggerDecision(key, abilityScope, AutoYieldStore.TriggerDecision.ACCEPT);
}
public String setAlwaysDeclineTrigger(String key, boolean abilityScope) {
return writeTriggerDecision(key, abilityScope, AutoYieldStore.TriggerDecision.DECLINE);
}
public void setAlwaysDeclineTrigger(int trigger) {
activeStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE);
public String setAlwaysAskTrigger(String key, boolean abilityScope) {
return writeTriggerDecision(key, abilityScope, AutoYieldStore.TriggerDecision.ASK);
}
public void setAlwaysAskTrigger(int trigger) {
activeStore().setTriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK);

/** Cache-mode write of a wire-received trigger decision. Storage key is already at the right shape. */
public void applyTriggerDecisionFromWire(String storageKey, AutoYieldStore.TriggerDecision decision) {
activeStore().setTriggerDecision(AutoYieldStore.Tier.GAME, storageKey, decision);
}

public void setTriggerDecision(int trigger, AutoYieldStore.TriggerDecision decision) {
activeStore().setTriggerDecision(trigger, decision);
public Iterable<java.util.Map.Entry<String, AutoYieldStore.TriggerDecision>> getAutoTriggers() {
if (!tierAware()) return activeStore().getAutoTriggers(AutoYieldStore.Tier.GAME);
if (activeTriggerModeIsInstall()) return PersistentAutoDecisionStore.get().getAutoTriggers();
return activeStore().getAutoTriggers(activeTriggerTier());
}

public boolean getDisableAutoTriggers() { return activeStore().isTriggerDecisionsDisabled(); }
public void setDisableAutoTriggers(boolean disable) { activeStore().setTriggerDecisionsDisabled(disable); }

private AutoYieldStore.TriggerDecision readTriggerDecision(String key) {
if (key == null || key.isEmpty()) return AutoYieldStore.TriggerDecision.ASK;
AutoYieldStore store = activeStore();
if (store.isTriggerDecisionsDisabled()) return AutoYieldStore.TriggerDecision.ASK;
if (!tierAware()) {
// Cache mode: keys stored at storageKey shape (full or stripped).
AutoYieldStore.TriggerDecision d = store.getTriggerDecision(AutoYieldStore.Tier.GAME, key);
if (d != AutoYieldStore.TriggerDecision.ASK) return d;
return store.getTriggerDecision(AutoYieldStore.Tier.GAME, AutoYieldStore.abilitySuffix(key));
}
if (activeTriggerModeIsInstall()) {
return PersistentAutoDecisionStore.get().getTriggerDecision(AutoYieldStore.abilitySuffix(key));
}
AutoYieldStore.Tier tier = activeTriggerTier();
boolean abilityScope = tier != AutoYieldStore.Tier.GAME;
String storageKey = abilityScope ? AutoYieldStore.abilitySuffix(key) : key;
return store.getTriggerDecision(tier, storageKey);
}

private String writeTriggerDecision(String key, boolean abilityScope, AutoYieldStore.TriggerDecision decision) {
String storageKey = abilityScope ? AutoYieldStore.abilitySuffix(key) : key;
if (activeTriggerModeIsInstall()) {
PersistentAutoDecisionStore.get().setTriggerDecision(storageKey, decision);
} else {
activeStore().setTriggerDecision(activeTriggerTier(), storageKey, decision);
}
return storageKey;
}

private static boolean activeTriggerModeIsInstall() {
return ForgeConstants.AUTO_TRIGGER_PER_ABILITY_INSTALL.equals(FModel.getPreferences().getPref(FPref.UI_AUTO_TRIGGER_MODE));
}

private static AutoYieldStore.Tier activeTriggerTier() {
String mode = FModel.getPreferences().getPref(FPref.UI_AUTO_TRIGGER_MODE);
if (ForgeConstants.AUTO_TRIGGER_PER_CARD.equals(mode)) return AutoYieldStore.Tier.GAME;
if (ForgeConstants.AUTO_TRIGGER_PER_ABILITY_SESSION.equals(mode)) return AutoYieldStore.Tier.SESSION;
return AutoYieldStore.Tier.MATCH;
}

/** Build the seed payload from this controller's authoritative store. */
public YieldStateSnapshot buildClientSnapshot(Map<PlayerView, EnumSet<PhaseType>> skipPhases) {
Set<String> cardYields = new HashSet<>();
Set<String> abilityYields = new HashSet<>();
boolean abilityScope = activeModeIsInstall() || activeTier() != AutoYieldStore.Tier.GAME;
boolean yieldAbilityScope = activeModeIsInstall() || activeTier() != AutoYieldStore.Tier.GAME;
for (String key : getAutoYields()) {
if (abilityScope) abilityYields.add(key);
if (yieldAbilityScope) abilityYields.add(key);
else cardYields.add(key);
}
// Trigger decisions are per-game; deltas flow during play.
Map<Integer, AutoYieldStore.TriggerDecision> triggers = new HashMap<>();
return new YieldStateSnapshot(cardYields, abilityYields, triggers, getDisableAutoYields(), skipPhases);

Map<String, AutoYieldStore.TriggerDecision> cardTriggers = new HashMap<>();
Map<String, AutoYieldStore.TriggerDecision> abilityTriggers = new HashMap<>();
boolean trigAbilityScope = activeTriggerModeIsInstall() || activeTriggerTier() != AutoYieldStore.Tier.GAME;
for (Map.Entry<String, AutoYieldStore.TriggerDecision> e : getAutoTriggers()) {
if (trigAbilityScope) abilityTriggers.put(e.getKey(), e.getValue());
else cardTriggers.put(e.getKey(), e.getValue());
}

return new YieldStateSnapshot(
cardYields, abilityYields,
cardTriggers, abilityTriggers,
getDisableAutoYields(), getDisableAutoTriggers(),
skipPhases);
}

/** Atomic seed of client-persistent state at game start or reconnection. Cache mode only. */
public void applyClientSeed(YieldStateSnapshot snap) {
localStore.clear();
for (String k : snap.cardYields()) localStore.setYield(AutoYieldStore.Tier.GAME, k, true);
for (String k : snap.abilityYields()) localStore.setYield(AutoYieldStore.Tier.GAME, k, true);
for (Map.Entry<Integer, AutoYieldStore.TriggerDecision> e : snap.triggerDecisions().entrySet()) {
localStore.setTriggerDecision(e.getKey(), e.getValue());
for (Map.Entry<String, AutoYieldStore.TriggerDecision> e : snap.cardTriggerDecisions().entrySet()) {
localStore.setTriggerDecision(AutoYieldStore.Tier.GAME, e.getKey(), e.getValue());
}
for (Map.Entry<String, AutoYieldStore.TriggerDecision> e : snap.abilityTriggerDecisions().entrySet()) {
localStore.setTriggerDecision(AutoYieldStore.Tier.GAME, e.getKey(), e.getValue());
}
localStore.setDisabled(snap.autoYieldsDisabled());
localStore.setTriggerDecisionsDisabled(snap.autoTriggersDisabled());
skipPhases.clear();
skipPhases.putAll(snap.skipPhases());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@
import java.util.Set;

/**
* Atomic snapshot of a client's persistent yield state, transmitted at
* game start (and on reconnection) so the host's PCH proxy is fully
* seeded in one wire message instead of N.
* Atomic snapshot of a client's persistent yield + trigger state, transmitted at
* game start (and on reconnection) so the host's PCH proxy is fully seeded in
* one wire message instead of N.
*
* Trigger decisions split by scope (parallel to yields) so the host's remote
* cache can match incoming game-time keys against either bucket.
*/
public record YieldStateSnapshot(
Set<String> cardYields,
Set<String> abilityYields,
Map<Integer, AutoYieldStore.TriggerDecision> triggerDecisions,
Map<String, AutoYieldStore.TriggerDecision> cardTriggerDecisions,
Map<String, AutoYieldStore.TriggerDecision> abilityTriggerDecisions,
boolean autoYieldsDisabled,
boolean autoTriggersDisabled,
Map<PlayerView, EnumSet<PhaseType>> skipPhases
) implements Serializable {}
13 changes: 11 additions & 2 deletions forge-gui/src/main/java/forge/gamemodes/match/YieldUpdate.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.io.Serializable;

/**
* Unified envelope for all yield-related sync between client and host.
* Unified envelope for all yield- and trigger-related sync between client and host.
*
* Receiver dispatches via exhaustive switch in PlayerControllerHuman
* (host-side) and NetworkGuiGame (client-side).
Expand All @@ -19,6 +19,8 @@ public sealed interface YieldUpdate extends Serializable
YieldUpdate.TriggerDecision,
YieldUpdate.CardAutoYield,
YieldUpdate.SkipPhase,
YieldUpdate.SetDisableYields,
YieldUpdate.SetDisableTriggers,
YieldUpdate.SeedFromClient {

/** {@code atOrPastAtClick}: priority was at-or-past target on owner's turn when the user clicked — computed by the UI so client cache and host PCH initialize identically. */
Expand All @@ -28,11 +30,18 @@ record ClearMarker(PlayerView player) implements YieldUpdate {}

record StackYield(PlayerView player, boolean active) implements YieldUpdate {}

record TriggerDecision(int trigId, AutoYieldStore.TriggerDecision decision) implements YieldUpdate {}
/** Param order mirrors {@link CardAutoYield} (key, value, scope). */
record TriggerDecision(String storageKey, AutoYieldStore.TriggerDecision decision, boolean abilityScope) implements YieldUpdate {}

record CardAutoYield(String cardKey, boolean active, boolean abilityScope) implements YieldUpdate {}

record SkipPhase(PlayerView turnPlayer, PhaseType phase, boolean skip) implements YieldUpdate {}

/** Runtime toggle of the global auto-yield disable flag — host applies to its remote-cache, client to its local controller. */
record SetDisableYields(boolean disabled) implements YieldUpdate {}

/** Runtime toggle of the global auto-trigger disable flag — host applies to its remote-cache, client to its local controller. */
record SetDisableTriggers(boolean disabled) implements YieldUpdate {}

record SeedFromClient(YieldStateSnapshot snapshot) implements YieldUpdate {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,31 +155,43 @@ public void setShouldAutoYield(final String key, final boolean autoYield, final
@Override
public boolean getDisableAutoYields() { return yieldController.getDisableAutoYields(); }
@Override
public void setDisableAutoYields(final boolean disable) { yieldController.setDisableAutoYields(disable); }
public void setDisableAutoYields(final boolean disable) {
yieldController.setDisableAutoYields(disable);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.SetDisableYields(disable));
}

@Override
public boolean shouldAlwaysAcceptTrigger(final int trigger) {
return yieldController.shouldAlwaysAcceptTrigger(trigger);
public boolean shouldAlwaysAcceptTrigger(final String key) {
return yieldController.shouldAlwaysAcceptTrigger(key);
}
@Override
public boolean shouldAlwaysDeclineTrigger(final int trigger) {
return yieldController.shouldAlwaysDeclineTrigger(trigger);
public boolean shouldAlwaysDeclineTrigger(final String key) {
return yieldController.shouldAlwaysDeclineTrigger(key);
}

@Override
public void setShouldAlwaysAcceptTrigger(final int trigger) {
yieldController.setAlwaysAcceptTrigger(trigger);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(trigger, AutoYieldStore.TriggerDecision.ACCEPT));
public void setShouldAlwaysAcceptTrigger(final String key, final boolean isAbilityScope) {
String storageKey = yieldController.setAlwaysAcceptTrigger(key, isAbilityScope);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(storageKey, AutoYieldStore.TriggerDecision.ACCEPT, isAbilityScope));
}
@Override
public void setShouldAlwaysDeclineTrigger(final String key, final boolean isAbilityScope) {
String storageKey = yieldController.setAlwaysDeclineTrigger(key, isAbilityScope);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(storageKey, AutoYieldStore.TriggerDecision.DECLINE, isAbilityScope));
}
@Override
public void setShouldAlwaysDeclineTrigger(final int trigger) {
yieldController.setAlwaysDeclineTrigger(trigger);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(trigger, AutoYieldStore.TriggerDecision.DECLINE));
public void setShouldAlwaysAskTrigger(final String key, final boolean isAbilityScope) {
String storageKey = yieldController.setAlwaysAskTrigger(key, isAbilityScope);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(storageKey, AutoYieldStore.TriggerDecision.ASK, isAbilityScope));
}

@Override
public boolean getDisableAutoTriggers() { return yieldController.getDisableAutoTriggers(); }

@Override
public void setShouldAlwaysAskTrigger(final int trigger) {
yieldController.setAlwaysAskTrigger(trigger);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.TriggerDecision(trigger, AutoYieldStore.TriggerDecision.ASK));
public void setDisableAutoTriggers(final boolean disable) {
yieldController.setDisableAutoTriggers(disable);
send(ProtocolMethod.sendYieldUpdate, new YieldUpdate.SetDisableTriggers(disable));
}

public void setUiShouldSkipPhase(final PlayerView turnPlayer, final PhaseType phase, final boolean shouldSkip) {
Expand Down
12 changes: 7 additions & 5 deletions forge-gui/src/main/java/forge/interfaces/IGameController.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ public interface IGameController {
void setDisableAutoYields(boolean disable);

// Trigger accept/decline preferences
boolean shouldAlwaysAcceptTrigger(int trigger);
boolean shouldAlwaysDeclineTrigger(int trigger);
void setShouldAlwaysAcceptTrigger(int trigger);
void setShouldAlwaysDeclineTrigger(int trigger);
void setShouldAlwaysAskTrigger(int trigger);
boolean shouldAlwaysAcceptTrigger(String key);
Comment thread
tool4ever marked this conversation as resolved.
Outdated
boolean shouldAlwaysDeclineTrigger(String key);
void setShouldAlwaysAcceptTrigger(String key, boolean isAbilityScope);
void setShouldAlwaysDeclineTrigger(String key, boolean isAbilityScope);
void setShouldAlwaysAskTrigger(String key, boolean isAbilityScope);
boolean getDisableAutoTriggers();
void setDisableAutoTriggers(boolean disable);

/** Apply a unified yield update envelope to this controller's YieldController. */
void applyYieldUpdate(YieldUpdate update);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,11 @@ public final class ForgeConstants {
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)";

public static final String AUTO_TRIGGER_PER_CARD = "Per Card (Each Game)";
Comment thread
tool4ever marked this conversation as resolved.
Outdated
public static final String AUTO_TRIGGER_PER_ABILITY = "Per Ability (Each Match)";
public static final String AUTO_TRIGGER_PER_ABILITY_SESSION = "Per Ability (Each Session)";
public static final String AUTO_TRIGGER_PER_ABILITY_INSTALL = "Per Ability (Each Install)";

// Constants for Graveyard Ordering
public static final String GRAVEYARD_ORDERING_NEVER = "Never";
public static final String GRAVEYARD_ORDERING_OWN_CARDS = "With Relevant Cards";
Expand Down
Loading