From 39ccce2d8de984d95bf26c3e3375b43802111ad2 Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Sat, 23 May 2026 10:07:32 +0930 Subject: [PATCH] Alert when mobile users are in a Limited lobby Online draft/sealed is desktop-only. Mobile users joining a Limited lobby didn't realise their client couldn't participate. Show a modal on the host and all clients when a libgdx client joins a Limited lobby, or when the host switches to Limited while libgdx clients are connected. Infrastructure: new LobbyAlertEvent + ILobbyListener.lobbyAlert with a cross-platform SOptionPane modal. TODO comments mark the mobile-specific trigger sites for removal once mobile supports online draft/sealed. Co-Authored-By: Claude Opus 4.7 (1M context) --- forge-gui/res/languages/en-US.properties | 2 ++ .../forge/gamemodes/net/NetConnectUtil.java | 6 +++++ .../gamemodes/net/client/FGameClient.java | 5 ++++ .../gamemodes/net/event/LobbyAlertEvent.java | 21 ++++++++++++++++ .../gamemodes/net/server/FServerManager.java | 24 +++++++++++++++++++ .../gamemodes/net/server/ServerGameLobby.java | 5 ++++ .../java/forge/interfaces/ILobbyListener.java | 1 + 7 files changed, 64 insertions(+) create mode 100644 forge-gui/src/main/java/forge/gamemodes/net/event/LobbyAlertEvent.java diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 9bc3a508bac..b14499fbf76 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -3031,6 +3031,8 @@ lblNetworkPickTimerPrompt=Pick timer (seconds per pick): lblNetworkGraceTimerPromptLine1=Grace period on disconnect lblNetworkGraceTimerPromptLine2=(seconds, 0 to disable): lblNetworkDraftTimersTitle=Draft Timers +lblMobileLimitedUnsupportedTitle=Mobile client joined Limited lobby +lblMobileLimitedUnsupportedMessage=Draft and Sealed are not yet supported on network play for mobile. {0} will not be able to participate in this lobby. lblDraftCompletePoolSaved=Draft complete! Your pool has been saved as ''{0}''. #FDraftOverlay.java lblDraftOverlayPackOfN=Pack {0} of {1} diff --git a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java index 98f3d01da89..8f47ee4932d 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/NetConnectUtil.java @@ -10,6 +10,7 @@ import forge.gamemodes.net.server.FServerManager; import forge.gamemodes.net.server.ServerGameLobby; import forge.localinstance.properties.ForgeNetPreferences; +import forge.gui.FThreads; import forge.gui.GuiBase; import forge.gui.interfaces.IGuiGame; import forge.gui.interfaces.ILobbyView; @@ -58,6 +59,11 @@ public void draftAutoPicked(int seatIndex, forge.item.PaperCard card, int packNu public void receiveEventPool(String eventId, forge.deck.Deck pool) { view.onReceiveEventPool(eventId, pool); } + @Override + public void lobbyAlert(final String title, final String message) { + FThreads.invokeInBackgroundThread(() -> + SOptionPane.showMessageDialog(message, title, SOptionPane.WARNING_ICON)); + } } /** 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 1b30307cdc5..66aea32f310 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 @@ -213,6 +213,11 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throw listener.draftSeatPicked(event.getSeatIndex(), event.getSeatQueueDepths()); } return; + } else if (msg instanceof LobbyAlertEvent event) { + for (final ILobbyListener listener : lobbyListeners) { + listener.lobbyAlert(event.getTitle(), event.getMessage()); + } + return; } super.channelRead(ctx, msg); } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/event/LobbyAlertEvent.java b/forge-gui/src/main/java/forge/gamemodes/net/event/LobbyAlertEvent.java new file mode 100644 index 00000000000..6a23a467670 --- /dev/null +++ b/forge-gui/src/main/java/forge/gamemodes/net/event/LobbyAlertEvent.java @@ -0,0 +1,21 @@ +package forge.gamemodes.net.event; + +public final class LobbyAlertEvent implements NetEvent { + private static final long serialVersionUID = 1L; + + private final String title; + private final String message; + + public LobbyAlertEvent(final String title, final String message) { + this.title = title; + this.message = message; + } + + public String getTitle() { + return title; + } + + public String getMessage() { + return message; + } +} 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 8e64b771d97..fc1708f3509 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 @@ -285,6 +285,21 @@ public void broadcast(final NetEvent event) { broadcastTo(event, clients.values()); } + // TODO: remove when mobile supports online draft/sealed + public void broadcastMobileLimitedAlert() { + final List mobileNames = new ArrayList<>(); + for (final RemoteClient client : clients.values()) { + if (client.isLibgdx()) { + mobileNames.add(client.getUsername()); + } + } + if (mobileNames.isEmpty()) return; + final Localizer loc = Localizer.getInstance(); + broadcast(new LobbyAlertEvent( + loc.getMessage("lblMobileLimitedUnsupportedTitle"), + loc.getMessage("lblMobileLimitedUnsupportedMessage", String.join(", ", mobileNames)))); + } + /** * Dispatch a broadcast event to the host's local listener — the host does * not receive its own broadcasts over the network, so we mirror them here. @@ -304,6 +319,8 @@ private void dispatchToLocalListener(final NetEvent event) { e.getPackNumber(), e.getPickNumber(), e.getTimerDurationSeconds()); } else if (event instanceof DraftSeatPickedEvent e) { lobbyListener.draftSeatPicked(e.getSeatIndex(), e.getSeatQueueDepths()); + } else if (event instanceof LobbyAlertEvent e) { + lobbyListener.lobbyAlert(e.getTitle(), e.getMessage()); } } @@ -1006,6 +1023,13 @@ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throw broadcastTo(new MessageEvent(formatAfkTimeoutMessage()), Collections.singleton(client)); } + // TODO: remove when mobile supports online draft/sealed + if (event.isLibgdx() && localLobby.getData().isLimitedMode()) { + final Localizer loc = Localizer.getInstance(); + broadcast(new LobbyAlertEvent( + loc.getMessage("lblMobileLimitedUnsupportedTitle"), + loc.getMessage("lblMobileLimitedUnsupportedMessage", event.getUsername()))); + } // Warn if client version differs from host final String clientVersion = event.getVersion(); final String hostVersion = BuildInfo.getVersionString(); diff --git a/forge-gui/src/main/java/forge/gamemodes/net/server/ServerGameLobby.java b/forge-gui/src/main/java/forge/gamemodes/net/server/ServerGameLobby.java index a75a5b4c97f..d2bfbb168df 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/server/ServerGameLobby.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/server/ServerGameLobby.java @@ -50,8 +50,13 @@ protected void updateView(boolean fullUpdate) { /** Set the lobby's declared mode (Constructed / Limited) and broadcast to clients. */ public void setLimitedMode(boolean limited) { + final boolean wasLimited = getData().isLimitedMode(); getData().setLimitedMode(limited); updateView(true); + // TODO: remove when mobile supports online draft/sealed + if (limited && !wasLimited) { + FServerManager.getInstance().broadcastMobileLimitedAlert(); + } } public ServerGameLobby() { diff --git a/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java b/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java index 43cb4397456..feba41bf044 100644 --- a/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java +++ b/forge-gui/src/main/java/forge/interfaces/ILobbyListener.java @@ -19,4 +19,5 @@ default void draftPackArrived(int seatIndex, List pack, default void draftSeatPicked(int seatIndex, int[] seatQueueDepths) { } default void draftAutoPicked(int seatIndex, PaperCard card, int packNumber, int pickInPack) { } default void receiveEventPool(String eventId, Deck pool) { } + default void lobbyAlert(String title, String message) { } }