Eliminate redundant protocol round-trip: cache UI skip-phase prefs#10536
Merged
tool4ever merged 4 commits intoCard-Forge:masterfrom Apr 27, 2026
Merged
Conversation
…-trip PlayerControllerHuman.getActivePlayerInput called getGui().isUiSetToSkipPhase on every priority transition; for remote players that routed through RemoteClientGuiGame as a sendAndWait — a full TCP round-trip per check. Each PlayerControllerHuman now keeps a per-(turnPlayer, phase) EnumSet seeded once at match start from the client's UI label state and updated via a new Mode.CLIENT setUiShouldSkipPhase. Reads hit the host's own map. Local play and hotseat fall through unchanged — gui delegate on read, no-op on write. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tool4ever
reviewed
Apr 27, 2026
Move pushSkipPhaseToControllers and the host-cache seeding loop into AbstractGuiGame so desktop and mobile share one implementation. While unifying, mobile's isUiSetToSkipPhase gained the same mindslave AND-combine desktop has — toggling a master's row now correctly invalidates the cached skip value for every player they control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tool4ever
approved these changes
Apr 27, 2026
RafaelHGOliveira
added a commit
to RafaelHGOliveira/forge
that referenced
this pull request
Apr 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's the issue
In network multiplayer, the host pings the remote client over the wire on every priority transition to ask "is your UI configured to skip this phase?". Each call goes through
RemoteClientGuiGame.isUiSetToSkipPhase→sendAndWait(ProtocolMethod.isUiSetToSkipPhase, …)— a full TCP round-trip. The host's game thread parks until the reply lands.PlayerControllerHuman.getActivePlayerInputmakes this call every time a remote human gets priority and the stack is empty — order-of-magnitude ~13 calls per turn for one remote human, ~26 for two.The data being asked for is static across most of a match — just the client's UI label state for "stop at this phase". There's no reason to round-trip it every time.
What's the fix
The client tells the host its full skip-phase state once at match start, and pushes per-toggle deltas thereafter. The host caches it per
PlayerControllerHumanand answersisUiSetToSkipPhasefrom local memory — zero round-trips on the hot path.This is the same pattern recently established for auto-yield prefs (the controller-owns-prefs refactor): per-player preference state lives on
IGameController, the host'sPlayerControllerHumankeeps a remote-mirror map gated onisRemoteClient(), andNetGameControllerships mutations via 1:1-namedMode.CLIENTprotocol methods. Initial state is replayed at game start by the client (replayActiveYieldsfor yields;replayUiSkipPhasesfor skip-phase here).The only structural difference: yield prefs live in stores loaded before the game (so the seed fires from
FGameClient.setGameControllers), whereas skip-phase prefs live in UI label state, which is populated byactuateMatchPreferencesafter game start. The seed therefore fires from the end ofactuateMatchPreferences— the first point at which all label state and player views are settled.How it works
Three logical pieces:
Host-side cache.
PlayerControllerHumangainsMap<PlayerView, EnumSet<PhaseType>> remoteSkipPhases. NewIGameController.isUiSetToSkipPhasereads from it when serving a remote (gui instanceof RemoteClientGuiGame); the empty-cache default returnsfalse(don't skip), matching a fresh UI's conservative default. The twogetActivePlayerInputcall sites swap fromgetGui().isUiSetToSkipPhase(…)tothis.isUiSetToSkipPhase(…).Client-side push. New
Mode.CLIENTProtocolMethod.setUiShouldSkipPhase(PlayerView, PhaseType, Boolean)carries a single(player, phase, shouldSkip)mutation.NetGameController.replayUiSkipPhases(allPlayers, isSkipped)is a one-shot helper that pushes the full snapshot — onlyshouldSkip == trueentries, since the host's empty cache already representsfalse.UI wiring.
PhaseLabel(desktop) andVPhaseIndicator.PhaseLabel(mobile) gainsetOnToggled(Runnable), fired post-flip.CMatchUI.actuateMatchPreferences(desktop) andMatchController.actuateMatchPreferences(mobile) wire per-(player, phase)callbacks at the end of label initialization, then fire the initialreplayUiSkipPhasesfor any localNetGameController. Mid-game toggles ship one ~80-byte message each.The old
Mode.SERVER ProtocolMethod.isUiSetToSkipPhaseis deleted. TheIGuiGame.isUiSetToSkipPhasemethod stays — still used locally byFControlGamePlayback, and by the local-fallback branch insidePlayerControllerHuman.isUiSetToSkipPhase. All are local reads with no network cost.Why it's safe
isRemoteClient()is false,isUiSetToSkipPhasedelegates togetGui()(label state, exactly as today) andsetUiShouldSkipPhaseis a no-op. The on-toggle push and the seed are gated oninstanceof NetGameController, so local controllers (PlayerControllerHuman) skip them entirely. Existing hotseat semantics — both rows seeded fromPHASES_HUMAN, independent toggles mid-match, last-write-wins on save — are preserved exactly.CMatchUI.isUiSetToSkipPhase(turnPlayer, phase)AND-combines the turn player's row with the master's row. When the master toggles their label, the cached answer for every player they control changes too.pushSkipPhaseToControllersiterates players and re-pushes any dependent — narrow edge case (Mindslaver, Sorin Markov ult) but the cache stays consistent with what the GUI would return locally.🤖 Generated with Claude Code