Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b6a8bcb
Refactor yield state to per-PCH YieldController with unified wire env…
MostCromulent Apr 28, 2026
e2e55ef
Remove dead setUiShouldSkipPhase from IGameController/PCH
MostCromulent Apr 28, 2026
5f0e49d
Lift PhaseLabel right-click yield-marker handling into match controller
MostCromulent Apr 29, 2026
64ead6f
Clean up
Apr 29, 2026
d4f7a48
Move auto-yield/trigger query+mutation into YieldController
MostCromulent Apr 29, 2026
2cbe5b2
Fix yield marker firing immediately when set on a past phase
MostCromulent Apr 29, 2026
94c6a76
Refactor desktop PhaseIndicator to use EnumMap of PhaseType
MostCromulent Apr 29, 2026
fc8dc86
Extract clearActiveYields helper and apply on mobile ESC
MostCromulent Apr 29, 2026
80856fb
Use controller.getMatchUI() in VPrompt ESC handler
MostCromulent Apr 29, 2026
e850a98
Test android build
Apr 30, 2026
90eee4d
Start of clean up
Apr 30, 2026
0a0d704
Drop server→client applyYieldUpdate; client derives marker fire locally
MostCromulent Apr 30, 2026
f3a25ea
Drop dead skip-phase cache write in NetGameController
MostCromulent Apr 30, 2026
bba8b0c
ESC: contextual cancel takes precedence over yield clear
MostCromulent Apr 30, 2026
05a5615
Fix marker chevron bugs surfaced by network testing of round-2 changes
MostCromulent Apr 30, 2026
48166cd
Make user-cancel of yields work end-to-end on remote client
MostCromulent May 1, 2026
7583f4a
Consolidate per-phase tooltip strings into one format key
MostCromulent May 1, 2026
c5ac787
fix CI failure
MostCromulent May 1, 2026
6bede57
Refactor yield cancellation and server→client protocol
MostCromulent May 2, 2026
7ec1c7c
Merge branch 'master' into YieldControllerMvp
tool4ever May 2, 2026
95edf0d
Clean up
May 3, 2026
9483328
Clean up
May 3, 2026
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
6 changes: 0 additions & 6 deletions forge-game/src/main/java/forge/game/GameView.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,6 @@ void updateStack(final MagicStack stack) {
set(TrackableProperty.StormCount, stack.getSpellsCastThisTurn().size());
}

public boolean isFirstGameInMatch() {
return getNumPlayedGamesInMatch() == 0;
}

public int getNumPlayedGamesInMatch() {
return get(TrackableProperty.NumPlayedGamesInMatch);
}
Expand All @@ -154,8 +150,6 @@ public boolean isMatchOver() {
}

public boolean isMulligan() {
if (get(TrackableProperty.Mulligan) == null)
return false;
return get(TrackableProperty.Mulligan);
}

Expand Down
74 changes: 66 additions & 8 deletions forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JOptionPane;
Expand Down Expand Up @@ -67,7 +66,11 @@
import forge.game.spellability.StackItemView;
import forge.game.zone.ZoneType;
import forge.util.IHasForgeLog;
import forge.gamemodes.match.YieldController;
import forge.gamemodes.match.YieldMarker;
import forge.gamemodes.match.YieldUpdate;
import forge.gamemodes.net.NetworkGuiGame;
import forge.interfaces.IGameController;
import forge.gui.FNetOverlay;
import forge.gui.FThreads;
import forge.gui.GuiBase;
Expand Down Expand Up @@ -129,6 +132,7 @@
import forge.view.FView;
import forge.view.arcane.CardPanel;
import forge.view.arcane.FloatingZone;

import net.miginfocom.layout.LinkHandler;
import net.miginfocom.swing.MigLayout;

Expand Down Expand Up @@ -180,7 +184,7 @@ public CMatchUI() {
this.myDocs.put(EDocID.CARD_PICTURE, cDetailPicture.getCPicture().getView());
this.myDocs.put(EDocID.CARD_DETAIL, cDetailPicture.getCDetail().getView());
// only create an ante doc if playing for ante
if (isPreferenceEnabled(FPref.UI_ANTE)) {
if (FModel.getPreferences().getPrefBoolean(FPref.UI_ANTE)) {
this.myDocs.put(EDocID.CARD_ANTES, cAntes.getView());
} else {
this.myDocs.put(EDocID.CARD_ANTES, null);
Expand All @@ -200,10 +204,6 @@ private void registerDocs() {
}
}

private static boolean isPreferenceEnabled(final ForgePreferences.FPref preferenceName) {
return FModel.getPreferences().getPrefBoolean(preferenceName);
}

FScreen getScreen() {
return this.screen;
}
Expand Down Expand Up @@ -235,6 +235,35 @@ protected void afterDeltaApplied() {
refreshAllViews();
}

@Override
public void refreshYieldUi(final PlayerView player) {
FThreads.invokeInEdtNowOrLater(() -> {
// Marker is rendered only on the local player's view of the targeted phase indicator.
PlayerView local = getCurrentPlayer();
if (!player.equals(local)) {
return;
}
for (final VField f : getFieldViews()) {
for (PhaseLabel l : f.getPhaseIndicator().allLabels()) {
l.setYieldMarked(false);
}
}
IGameController controller = getGameController(local);
YieldMarker marker = controller != null ? controller.getYieldController().getAutoPassUntilMarker() : null;
if (marker == null) {
return;
}
VField markedField = getFieldViewFor(marker.getPhaseOwner());
if (markedField == null) {
return;
}
PhaseLabel target = markedField.getPhaseIndicator().getLabelFor(marker.getPhase());
if (target != null) {
target.setYieldMarked(true);
}
});
}

private void refreshAllViews() {
if (sortedPlayers != null) {
FThreads.invokeInEdtNowOrLater(() -> {
Expand Down Expand Up @@ -430,7 +459,7 @@ else if (selectedDocBeforeCombat != null) { //re-select doc that was selected be

// Combat pairings changed — rebuild layout so grouping reflects them
if (!"default".equals(FModel.getPreferences().getPref(FPref.UI_GROUP_PERMANENTS))
|| isPreferenceEnabled(FPref.UI_SEPARATE_COMBAT_STACKS)) {
|| FModel.getPreferences().getPrefBoolean(FPref.UI_SEPARATE_COMBAT_STACKS)) {
FThreads.invokeInEdtNowOrLater(() -> {
for (final VField f : getFieldViews()) {
f.getTabletop().doLayout();
Expand Down Expand Up @@ -1315,10 +1344,39 @@ private void actuateMatchPreferences() {
final PhaseLabel label = pi.getLabelFor(phase);
label.setEnabled(prefs.getPrefBoolean(keys[p - 1]));
label.setOnToggled(() -> pushSkipPhaseToControllers(player, phase));
label.setOnRightClick(() -> handleYieldMarkerToggle(label, player, phase));
}
}

seedSkipPhaseCache();
seedYieldStateOnHost();
}

private void handleYieldMarkerToggle(final PhaseLabel label, final PlayerView phaseOwner, final PhaseType phase) {
PlayerView local = getCurrentPlayer();
if (local == null) {
return;
}
IGameController controller = getGameController(local);
if (controller == null) {
return;
}
YieldMarker existing = controller.getYieldController().getAutoPassUntilMarker();
boolean clickedSameLabel = existing != null
&& phaseOwner.equals(existing.getPhaseOwner())
&& phase == existing.getPhase();
if (clickedSameLabel) {
controller.sendYieldUpdate(new YieldUpdate.ClearMarker(local));
} else {
// Setting a marker implies we want to stop here — un-skip the cell
// so the marker can fire (skip-phase pref + marker would skip past).
label.setEnabled(true);
label.repaintOnlyThisLabel();
boolean atOrPast = YieldController.isPriorityAtOrPastMarker(getGameView(), phaseOwner, phase);
controller.sendYieldUpdate(new YieldUpdate.SetMarker(phaseOwner, phase, atOrPast));
// Pass current priority so the marker takes effect immediately.
controller.selectButtonOk();
}
refreshYieldUi(local);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public VAutoYields(final CMatchUI matchUI) {
setTitle(Localizer.getInstance().getMessage("lblAutoYields"));

autoYields = new ArrayList<>();
for (final String autoYield : matchUI.getGameController().getAutoYields()) {
for (final String autoYield : matchUI.getGameController().getYieldController().getAutoYields()) {
autoYields.add(autoYield);
}
lstAutoYields = new FList<>(new AutoYieldsListModel());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,37 +65,35 @@ public class VPrompt implements IVDoc<CPrompt> {
private final FScrollPane messageScroller = new FScrollPane(tarMessage, false,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
private final JLabel lblGames;
private CardView card = null ;
private CardView card = null;

public void setCardView(final CardView card) {
this.card = card ;
this.card = card;
}

private KeyAdapter buttonKeyAdapter = new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
if (btnCancel.isEnabled()) {
if (FModel.getPreferences().getPrefBoolean(FPref.UI_ALLOW_ESC_TO_END_TURN) || !btnCancel.getText().equals("End Turn")) {
btnCancel.doClick();
}
}
if (e.getKeyCode() == KeyEvent.VK_ESCAPE
&& btnCancel.isEnabled()
&& (FModel.getPreferences().getPrefBoolean(FPref.UI_ALLOW_ESC_TO_END_TURN)
|| !btnCancel.getText().equals(Localizer.getInstance().getMessage("lblEndTurn")))) {
btnCancel.doClick();
}
}
};

private final CPrompt controller;

//========= Constructor
public VPrompt(final CPrompt controller) {
this.controller = controller;

lblGames = new FLabel.Builder()
.fontSize(12)
.fontStyle(Font.PLAIN)
.fontAlign(SwingConstants.CENTER)
.opaque()
.build();
.fontSize(12)
.fontStyle(Font.PLAIN)
.fontAlign(SwingConstants.CENTER)
.opaque()
.build();

btnOK.addKeyListener(buttonKeyAdapter);
btnCancel.addKeyListener(buttonKeyAdapter);
Expand All @@ -107,15 +105,13 @@ public VPrompt(final CPrompt controller) {
messageScroller.getViewport().getView().addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
if ( card != null ) {
controller.getMatchUI().setCard(card);
if (card != null) {
controller.getMatchUI().setCard(card);
}
}
});
}

//========== Overridden methods

/* (non-Javadoc)
* @see forge.gui.framework.IVDoc#populate()
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package forge.screens.match.views;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
Expand All @@ -27,6 +28,7 @@
import java.awt.image.BufferedImage;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
Expand All @@ -35,7 +37,9 @@
import forge.CachedCardImage;
import forge.game.GameView;
import forge.game.card.CardView.CardStateView;
import forge.game.player.PlayerView;
import forge.game.spellability.StackItemView;
import forge.gamemodes.match.YieldUpdate;
import forge.gui.card.CardDetailUtil;
import forge.gui.card.CardDetailUtil.DetailColors;
import forge.gui.framework.DragCell;
Expand Down Expand Up @@ -233,22 +237,30 @@ public void mouseClicked(final MouseEvent e) {
}
});

if (item.isAbility()) {
addMouseListener(new FMouseAdapter() {
@Override
public void onLeftClick(final MouseEvent e) {
onClick(e);
}
@Override
public void onRightClick(final MouseEvent e) {
onClick(e);
addMouseListener(new FMouseAdapter() {
@Override
public void onLeftClick(final MouseEvent e) {
onClick(e);
}
@Override
public void onRightClick(final MouseEvent e) {
onClick(e);
}
private void onClick(final MouseEvent e) {
abilityMenu.setStackInstance(item);
boolean hasVisibleItem = false;
for (Component c : abilityMenu.getComponents()) {
if (c.isVisible()) {
hasVisibleItem = true;
break;
}
}
private void onClick(final MouseEvent e) {
abilityMenu.setStackInstance(item);
abilityMenu.show(e.getComponent(), e.getX(), e.getY());
if (!hasVisibleItem) {
return;
}
});
}
abilityMenu.show(e.getComponent(), e.getX(), e.getY());
}
});

// TODO: A hacky workaround is currently used to make the game not leak the color information for Morph cards.
final CardStateView curState = item.getSourceCard().getCurrentState();
Expand Down Expand Up @@ -286,6 +298,7 @@ private final class AbilityMenu extends JPopupMenu {
private final JCheckBoxMenuItem jmiAutoYield;
private final JCheckBoxMenuItem jmiAlwaysYes;
private final JCheckBoxMenuItem jmiAlwaysNo;
private final JMenuItem jmiYieldToEntireStack;
private StackItemView item;

private Integer triggerID = 0;
Expand Down Expand Up @@ -326,13 +339,24 @@ public AbilityMenu(){
}
});
add(jmiAlwaysNo);

jmiYieldToEntireStack = new JMenuItem(Localizer.getInstance().getMessage("lblYieldToEntireStack"));
jmiYieldToEntireStack.addActionListener(arg0 -> {
final PlayerView local = controller.getMatchUI().getCurrentPlayer();
if (local == null) return;
controller.getMatchUI().getGameController().sendYieldUpdate(new YieldUpdate.StackYield(local, true));
controller.getMatchUI().getGameController().passPriority();
});
add(jmiYieldToEntireStack);
}

public void setStackInstance(final StackItemView item0) {
item = item0;
triggerID = item.getSourceTrigger();

jmiAutoYield.setSelected(controller.getMatchUI().getGameController().shouldAutoYield(item.getKey()));
jmiAutoYield.setVisible(item.isAbility());
jmiAutoYield.setSelected(item.isAbility()
&& controller.getMatchUI().getGameController().shouldAutoYield(item.getKey()));

if (item.isOptionalTrigger() && controller.getMatchUI().isLocalPlayer(item.getActivatingPlayer())) {
jmiAlwaysYes.setSelected(controller.getMatchUI().getGameController().shouldAlwaysAcceptTrigger(triggerID));
Expand Down
Loading
Loading