diff --git a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java index f520efcde2b..fabde6e55e8 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/AbstractGuiGame.java @@ -352,12 +352,16 @@ public boolean concede() { } if (hasLocalPlayers()) { boolean concedeNeeded = false; - // check if anyone still needs to confirm + // check if anyone still needs to concede for (final IGameController c : getOriginalGameControllers()) { - if (c instanceof PlayerControllerHuman) { - if (((PlayerControllerHuman) c).getPlayer().getOutcome() == null) { + if (c instanceof PlayerControllerHuman pch) { + if (pch.getPlayer().getOutcome() == null) { concedeNeeded = true; } + } else { + // Network client — no access to Player outcome, but game + // is still in progress (isGameOver checked above) + concedeNeeded = true; } } if (concedeNeeded) { @@ -376,6 +380,11 @@ public boolean concede() { } else { return !ignoreConcedeChain; } + if (isNetGame()) { + // Network: concede was sent to server asynchronously. + // Let the server drive game-end flow — don't send nextGameDecision here. + return false; + } if (gameView.isGameOver()) { // Don't immediately close, wait for win/lose screen return false; diff --git a/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java b/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java index ada27f0713d..ddd5f92dbec 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/GameLobby.java @@ -527,6 +527,7 @@ else if (autoGenerateVariant != null) { //if above checks succeed, return runnable that can be used to finish starting game return () -> { hostedMatch = GuiBase.getInterface().hostMatch(); + hostedMatch.setOnMatchOver(this::onMatchOver); hostedMatch.startMatch(GameType.Constructed, variantTypes, players, guis); for (final Player p : hostedMatch.getGame().getPlayers()) { @@ -542,6 +543,12 @@ else if (autoGenerateVariant != null) { }; } + protected void onMatchOver() { + hostedMatch = null; + gameControllers.clear(); + updateView(true); + } + public final static class GameLobbyData implements Serializable { private static final long serialVersionUID = 9184758307999646864L; diff --git a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java index 7d31c9383bf..b5819cbf4d2 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/HostedMatch.java @@ -56,6 +56,7 @@ public class HostedMatch { public HashMap gameControllers = null; private Runnable startGameHook = null; private Runnable endGameHook = null; + private Runnable onMatchOver = null; private final List humanControllers = Lists.newArrayList(); private Map guis; private int humanCount; @@ -81,6 +82,7 @@ public void setStartGameHook(Runnable hook) { startGameHook = hook; } public void setEndGameHook(Runnable hook) { endGameHook = hook; } + public void setOnMatchOver(Runnable callback) { onMatchOver = callback; } private static GameRules getDefaultRules(final GameType gameType) { final GameRules gameRules = new GameRules(gameType); @@ -303,6 +305,17 @@ public void startGame() { endGameHook.run(); } + // Flush any buffered game events to remote clients so they receive + // GameEventGameOutcome and GameEventGameFinished before we proceed. + for (PlayerControllerHuman hc : humanControllers) { + if (hc.getGui() instanceof forge.gamemodes.net.server.RemoteClientGuiGame ngg) { + forge.gui.control.GameEventForwarder fwd = ngg.getForwarder(); + if (fwd != null) { + fwd.flush(); + } + } + } + // After game is over... isMatchOver = match.isMatchOver(); if (humanCount == 0) { @@ -366,6 +379,12 @@ public void endCurrentGame() { for (final PlayerControllerHuman humanController : humanControllers) { if (humanController.getGui() instanceof forge.gamemodes.net.server.RemoteClientGuiGame ngg) { + forge.gui.control.GameEventForwarder fwd = ngg.getForwarder(); + if (fwd != null) { + for (PlayerControllerHuman allHc : humanControllers) { + allHc.getInputQueue().deleteObserver(fwd); + } + } ngg.shutdownForwarder(); } humanController.getGui().setGameSpeed(PlaybackSpeed.NORMAL); @@ -520,6 +539,9 @@ private void addNextGameDecision(final PlayerControllerHuman controller, final N FThreads.invokeInEdtNowOrLater(() -> { endCurrentGame(); isMatchOver = true; + if (onMatchOver != null) { + onMatchOver.run(); + } }); return; // if any player chooses quit, quit the match } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/GameProtocolHandler.java b/forge-gui/src/main/java/forge/gamemodes/net/GameProtocolHandler.java index 3e0e64d3810..2d92d1f1d1b 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/GameProtocolHandler.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/GameProtocolHandler.java @@ -47,6 +47,18 @@ public final void channelRead(final ChannelHandlerContext ctx, final Object msg) protocolMethod.checkArgs(args); final Object toInvoke = getToInvoke(ctx); + if (toInvoke == null) { + netLog.info("Ignoring {} — controller no longer available (game ended)", methodName); + // For methods expecting a reply, send null so the client doesn't hang + final Class earlyReturnType = protocolMethod.getReturnType(); + if (!earlyReturnType.equals(Void.TYPE)) { + final IRemote remote = getRemote(ctx); + if (remote != null) { + remote.send(new ReplyEvent(event.getId(), null)); + } + } + return; + } // Pre-call actions (runs on IO thread — blocks all subsequent messages) final long beforeCallStart = System.currentTimeMillis(); diff --git a/forge-gui/src/main/java/forge/gamemodes/net/server/GameServerHandler.java b/forge-gui/src/main/java/forge/gamemodes/net/server/GameServerHandler.java index 5d7fad56e06..f9d2ec0ef4d 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/server/GameServerHandler.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/server/GameServerHandler.java @@ -33,7 +33,8 @@ protected IRemote getRemote(final ChannelHandlerContext ctx) { @Override protected IGameController getToInvoke(final ChannelHandlerContext ctx) { - return server.getController(getClient(ctx).getIndex()); + final RemoteClient client = getClient(ctx); + return client != null ? server.getController(client.getIndex()) : null; } @Override 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 cf2fdd6787f..872ad23eeeb 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 @@ -82,4 +82,17 @@ protected IGuiGame getGui(final int index) { @Override protected void onGameStarted() { } + + @Override + protected void onMatchOver() { + for (int i = 0; i < getNumberOfSlots(); i++) { + final LobbySlot slot = getSlot(i); + if (slot != null) { + slot.setIsReady(false); + } + } + super.onMatchOver(); + FServerManager.getInstance().clearPlayerGuis(); + FServerManager.getInstance().updateLobbyState(); + } } diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index bb1764ec01b..198e0562fe6 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -3344,6 +3344,15 @@ public void concede() { if (player != null) { player.concede(); getGame().getAction().checkGameOverCondition(); + if (getGame().isGameOver()) { + // Remote-client controllers on the server have no FControlGameEventHandler, + // so their input queues won't be released by the normal event path + for (Player p : getGame().getPlayers()) { + if (p.getController() instanceof PlayerControllerHuman pch) { + pch.getInputQueue().onGameOver(true); + } + } + } } }