Skip to content

Yield Rework V2 — configurable interrupts, APINA, suggestions, settings UI#10606

Open
MostCromulent wants to merge 13 commits intoCard-Forge:masterfrom
MostCromulent:YieldReworkV2
Open

Yield Rework V2 — configurable interrupts, APINA, suggestions, settings UI#10606
MostCromulent wants to merge 13 commits intoCard-Forge:masterfrom
MostCromulent:YieldReworkV2

Conversation

@MostCromulent
Copy link
Copy Markdown
Contributor

@MostCromulent MostCromulent commented May 4, 2026

Refactor of #9643 (Yield Rework v1) on top of the per-PCH YieldController MVP shipped in #10555.

Re-implements the deferred YieldRework feature set against the MVP's architecture (single YieldUpdate wire envelope, per-PCH controller, atomic seed) without adding new ProtocolMethod entries.

Architectural reference doc attached separately covers the design in detail.

Deferred features included

  • Configurable interrupt system — six interrupt prefs governing when an active autopass cancels: OPPONENT_SPELL, ATTACKERS, TARGETING, MASS_REMOVAL, TRIGGERS, REVEAL. First two default ON to preserve master's existing hardcoded interrupts; the rest default OFF.
  • APINA (Auto-Pass If No Actions) — persistent toggle that auto-passes priority whenever no playable actions exist. Backed by a new forge.ai.AvailableActions heuristic with a per-PCH timeout budget. Separate RESPECTS_INTERRUPTS opt-in (default OFF).
  • Smart suggestionsInputPassPriority offers Accept/Decline prompts when conditions match: stack-yield when there's no playable response to the stack, marker-on-upkeep when no actions exist on your turn. Each suggestion type has a configurable decline scope (NEVER/ALWAYS/STACK/TURN).
  • Speed settings — opt-in skip of the inter-phase pause and the post-resolve pause during sustained auto-passes.
  • Per-PCH preference sync — every yield FPref is overridable per-controller, propagated via two new YieldUpdate envelope variants. Atomic seed at game start carries the full overlay map in a single message.
  • Settings UIs — desktop VYieldSettings dialog + VYield dock-tab panel (Auto-Pass toggle button + settings shortcut), mobile VYieldOptions scrollable dialog. Game-menu entries on both platforms. Three configurable desktop shortcuts (Ctrl+Y / F2 / ESC).

Hardcoded autoPassCancel() sites in MagicStack, PCH.declareAttackers, and HostedMatch deleted — the event-driven handler covers them, governed by per-PCH prefs. Default-ON pref values keep observable behavior identical for unconfigured users.


🤖 Generated with Claude Code

@MostCromulent MostCromulent requested a review from tool4ever May 4, 2026 22:39
@MostCromulent MostCromulent added Enhancement New feature or request QOL Quality of Life labels May 4, 2026
…ttings UI

Builds on the per-PCH YieldController MVP shipped in master. Adds the
deferred YieldRework feature set on top of that architecture without
introducing new ProtocolMethod entries — everything rides the existing
YieldUpdate envelope.

Features
- Six configurable interrupt prefs that control when an active autopass
  (EOT, marker) is cancelled: OPPONENT_SPELL, ATTACKERS, TARGETING,
  MASS_REMOVAL, TRIGGERS, REVEAL. First two default ON to preserve
  master's hardcoded behavior; the rest default OFF.
- APINA (Auto-Pass If No Actions) — per-tick predicate gated by the
  AvailableActions heuristic in forge-ai, with configurable timeout.
- Smart suggestions — InputPassPriority offers a stack-yield prompt when
  there are spells on the stack and no playable response, and a no-actions
  marker prompt on the player's own turn with no playable cards. Each has
  a configurable decline scope (NEVER / ALWAYS / STACK / TURN).
- Speed settings — opt-in skip of inter-phase and post-resolve delays.
- Desktop UI: VYield dock-tab panel (Auto-Pass toggle + Settings buttons)
  and VYieldSettings dialog. Three configurable shortcuts: Ctrl+Y opens
  settings, F2 toggles APINA, ESC clears the active yield.
- Mobile UI: VYieldOptions scrollable settings dialog + VGameMenu entries
  for Yield Options and the Auto-Pass toggle.

Architecture
- All new state lives on per-PCH YieldController; each client's
  preferences govern only that client's host-side proxy.
- Three new YieldUpdate variants (SetAutoPassUntilEndOfTurn,
  SetYieldBoolPref, SetYieldStringPref) plus pref-overlay extensions to
  the SeedFromClient snapshot. Zero new ProtocolMethod entries.
- Single-yield invariant: at most one of EOT/marker/stack-yield is active
  at a time (setters enforce). Stack-yield is deliberately immune to
  event-driven interrupts — only stack-empty turns it off, since its
  whole point is "ride through stack additions."
- YieldController.apply(YieldUpdate) is the single dispatch point for
  wire envelopes; PCH and NetGameController applyYieldUpdate are thin
  delegators.
- Type-safe domain enums: DeclineScope (NEVER/ALWAYS/STACK/TURN) and
  SuggestionType (STACK_YIELD/NO_ACTIONS, owns allowed scopes + scope
  FPref) replace the previous stringly-typed code.
- PCH.tryAutoPassNow runs after every yield-state change and
  re-evaluates mayAutoPass at the current input, so toggles fire on the
  current prompt (no priority-window lag).

Hardcoded interrupt sites deleted from MagicStack, PCH.declareAttackers,
and HostedMatch — the event-driven handler covers the same conditions,
now governed by per-PCH prefs.

Also lower DragCell minimum height to 50px so the dragged-out
yield-options tab can shrink to fit a single row of buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@autumnmyst
Copy link
Copy Markdown
Contributor

autumnmyst commented May 5, 2026

I notice you still have something checking state despite the yield interrupts. I don't think this is necessary and will just cause the state-scanner to re-prompt on every priority pass while the interrupt state persists. shouldInterruptYield(player) shouldn't need to check the state of the game to decide whether to yield, since we have event driven interrupts now. Also, should make the stack yield respect interrupts, so if things are added that should interrupt, they indeed interrupt. I don't think isInterruptibleYieldActive() is necessary, all yields should be interruptible.

@autumnmyst
Copy link
Copy Markdown
Contributor

Also, I recall we talked about removing the smart suggestion prompts? Are they actually useful enough to justify as a feature?

Previously APINA + RESPECTS_INTERRUPTS evaluated `shouldInterruptYield` on
every priority tick — meaning a Wrath sitting on the stack caused APINA to
re-prompt every priority window for as long as the spell stayed there.
Yields had the right semantic (event-driven, one prompt per event); APINA
duplicated the classifiers in a per-tick state-scanner that produced the
wrong UX.

APINA now shares the yield event path. Each event handler (`onSpellAbilityCast`,
`onAttackersDeclared`, `maybeInterruptOnReveal`) calls a unified
`applyInterrupt()` that clears any interruptible yield and sets a transient
`autoPassInterrupted` flag when APINA + RESPECTS_INTERRUPTS are on. The flag
is cleared by `noteMayAutoPassResult` once the user has been prompted, so
APINA resumes on the next priority window without re-prompting.

`shouldInterruptYield` and `hasMassRemovalOnStack` are deleted — both were
only reachable from the APINA path. Side effect: REVEAL now applies to
APINA. The state-scanner had no REVEAL check, so users with INTERRUPT_ON_REVEAL
enabled were silently missing that interrupt under APINA; the event handler
covers it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MostCromulent
Copy link
Copy Markdown
Contributor Author

@autumnmyst

I notice you still have something checking state despite the yield interrupts. I don't think this is necessary and will just cause the state-scanner to re-prompt on every priority pass while the interrupt state persists.

this was for APINA, have updated so interrupts apply on event driven basis there too. much cleaner architecture now

Also, should make the stack yield respect interrupts, so if things are added that should interrupt, they indeed interrupt. I don't think isInterruptibleYieldActive() is necessary, all yields should be interruptible.

i didn't think we want stack yield to be interrupted? i.e. when you click 'yield to entire stack' you're saying "I don't want to interact with this stack in any way / I have no instants I can play, don't bother me again".

e.g. you're in a commander game and your opponents are interacting on the stack but you don't want/cant participate. if stack yield interrupts you get bothered every time opponents interact when you've already opted out.

if we really we want this i'd suggest we add a 'Stack yield respects interrupt' option to the settings, same as the APINA respects, so at least its optional

Also, I recall we talked about removing the smart suggestion prompts? Are they actually useful enough to justify as a feature?

in discussion on Discord, TRTs view was worth keeping because its useful for players who don't want full automation and want to use Forge to eg. train for playing paper games

@autumnmyst
Copy link
Copy Markdown
Contributor

I think we still want stack interrupts, since yielding the stack is saying “I don’t want to interact with the stack in its current state” but you yield and then say, a mass removal spell is added, you might want to reconsider. You can always just turn off interrupts too. I don’t really see stack yield as “special” in that it should ignore interruption.

Example: play a spell, 10 prowess triggers appear, you have a counterspell but you don’t want to counter your own spell, but that means it doesn’t autopass. You don’t want to click through each trigger, so you yield the stack. The next player plays their own counterspell on your spell. You had targeting interruptions on, but it was ignored and you never had a chance to respond to them countering your spell.

MostCromulent and others added 6 commits May 6, 2026 20:49
The dedicated yield options panel was a tab in the prompt cell hosting
just two controls (Auto-Pass toggle + Settings dialog launcher). Fold
both into the existing dock — Auto-Pass becomes an icon with a goldenrod-
highlighted toggled state, and the previously-dormant cog button now
opens VYieldSettings. Promote BUTTON_DOCK to its own match.xml cell
directly above the prompt panel, aligning the prompt top with the hand
top. Drop the in-dock Revert/Open/Save Layout buttons (still available
from the menu bar's Layout menu). Update the Advanced Yield Options
wiki page to match the new access points.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The stack-item context menu now offers two choices on desktop and mobile. "Yield to stack" auto-passes until the stack empties but backs off when any of the existing yield-interrupt prefs trip (opponent spell, targeting, mass removal, triggers, reveal). "Resolve entire stack" preserves today's fire-and-forget behavior — only stack-empty turns it off. The "you cannot respond to the stack" suggestion defaults to the interruptible variant.

Implemented as a single boolean flag (stackYieldRespectsInterrupts) paired with the existing autoPassUntilStackEmpty state on YieldController, with the flag carried over the wire as a third component on YieldUpdate.StackYield. Reuses the existing applyInterrupt() path — no new prefs introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread forge-gui/src/main/java/forge/gamemodes/match/YieldUpdate.java
Comment thread forge-gui/src/main/java/forge/gamemodes/match/YieldController.java
Comment thread forge-gui/src/main/java/forge/player/PlayerControllerHuman.java Outdated
tool4EvEr and others added 3 commits May 6, 2026 20:49
Collapse the dedicated passPriorityUntilEndOfTurn ProtocolMethod into the
existing YieldUpdate envelope so all yield-related wire traffic rides one
ProtocolMethod pair, matching the V2 single-surface principle. The four
call sites (CDock, GameMenu, KeyboardShortcuts, MatchScreen) now go
through a YieldController.endTurn helper that mirrors the existing
toggleAutoPassNoActions shape.

Removing the isYieldActive early-return in tryAutoPassNow is safe because
the mayAutoPass check below already encompasses it — mayAutoPass is
shouldAutoYield || isAutoPassingNoActions, and shouldAutoYield is true
exactly when a yield is active. The early-return only diverged from
mayAutoPass in one direction: when a yield had just been activated, it
blocked the OK click that the original passPriority(true) always made
unconditionally. Dropping it lets the envelope path reproduce that
behavior on the current InputPassPriority instead of waiting for the next
cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot building (snapshotBoolPrefs / snapshotStringPrefs) was returning a
copy of the local override map, which is empty at game start because the
override map only fills when the user toggles a setting mid-game. The
host's proxy of a remote player would receive an empty seed, then fall
through to the host's FModel for every read — silently substituting the
host's preferences for the client's. Defaults-only players didn't notice
because the two default-on interrupts matched between unconfigured host
and client; anyone who customized their settings would see them ignored
until they re-toggled each pref mid-match. Fix: enumerate the synced
yield FPrefs (SYNCED_BOOL_PREFS / SYNCED_STRING_PREFS) and read each
effective value from FModel when building the snapshot, so the proxy is
seeded with the client's actual values from turn 1.

This makes the override map's role coherent: populated only on the host's
proxy of a remote player, via applyClientSeed and the
SetYieldBoolPref/SetYieldStringPref envelopes. Local controllers (host's
own PCH and client's NetGameController) always read through FModel
fallback — the dialog already writes there, so the parallel local store
was dead weight. Drop the yieldController.setBoolPref/setStringPref calls
from PCH.setYieldBoolPref/StringPref and the equivalent in
NetGameController; PCH.setYieldBoolPref still fires tryAutoPassNow because
toggling APINA may flip mayAutoPass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread forge-gui/src/main/java/forge/player/PlayerControllerHuman.java Outdated
Addresses review comment on <!-- -->Card-Forge#10606. When a yield is active,
mayAutoPass() short-circuits via shouldAutoYield() and never consults the
available-actions field, so computing it is wasted work. Match the gate
already used by chooseSpellAbilityToPlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request QOL Quality of Life

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants