diff --git a/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/launcher/ServerLauncher.java b/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/launcher/ServerLauncher.java index 7ab8f2926bb..0f3666eb451 100644 --- a/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/launcher/ServerLauncher.java +++ b/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/launcher/ServerLauncher.java @@ -255,10 +255,10 @@ public void addObserver( * example, a disconnected participant will cause the game to be stopped, while a disconnected * observer will have no effect. */ - public void connectionLost(final INode node) { + public void connectionLost(final INode droppedNode) { if (isLaunching) { // this is expected, we told the observer he couldn't join, so now we lose the connection - if (observersThatTriedToJoinDuringStartup.remove(node)) { + if (observersThatTriedToJoinDuringStartup.remove(droppedNode)) { return; } // a player has dropped out, abort @@ -268,9 +268,15 @@ public void connectionLost(final INode node) { } // if we lose a connection to a player, shut down the game (after saving) and go back to the // main screen - if (serverGame.getPlayerManager().isPlaying(node)) { + if (serverGame.getPlayerManager().isPlaying(droppedNode)) { if (serverGame.isGameSequenceRunning()) { - saveAndEndGame(node); + saveAndEndGame(droppedNode); + // Release any countries played by the player that dropped. The other seating assignments + // are preserved for the game lobby that will be shown. + for (final String countryName : serverGame.getPlayerManager().getPlayedBy(droppedNode)) { + serverModel.releasePlayer(countryName); + } + serverModel.persistPlayersToNodesMapping(); } else { stopGame(); } @@ -294,6 +300,7 @@ private void saveAndEndGame(final INode node) { final Path f = launchAction.getAutoSaveFile(); try { serverGame.saveGame(f); + gameSelectorModel.setSaveGameFileToLoad(f); } catch (final Exception e) { log.error("Failed to save game: " + f.toAbsolutePath(), e); } diff --git a/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/mc/ServerModel.java b/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/mc/ServerModel.java index 53591f37880..bcafff9b77f 100644 --- a/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/mc/ServerModel.java +++ b/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/mc/ServerModel.java @@ -61,6 +61,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.swing.JFrame; import javax.swing.JOptionPane; @@ -99,6 +100,7 @@ public class ServerModel extends Observable implements IConnectionChangeListener private Messengers messengers; private GameData data; private Map playersToNodeListing = new HashMap<>(); + private boolean playersToNodesMappingPersisted = false; private Map playersEnabledListing = new HashMap<>(); private Collection playersAllowedToBeDisabled = new HashSet<>(); private Map> playerNamesAndAlliancesInTurnOrder = @@ -313,36 +315,19 @@ public synchronized void setLocalPlayerType(final String player, final PlayerTyp localPlayerTypes.put(player, type); } + /** + * Persists the players mappings to re-use upon a game data change. Used to persist the previous + * game's player setting for the game restart if a connection is lost. + */ + public void persistPlayersToNodesMapping() { + this.playersToNodesMappingPersisted = true; + } + private void gameDataChanged() { synchronized (this) { data = gameSelectorModel.getGameData(); if (data != null) { - playersToNodeListing = new HashMap<>(); - playersEnabledListing = new HashMap<>(); - playersAllowedToBeDisabled = - new HashSet<>(data.getPlayerList().getPlayersThatMayBeDisabled()); - playerNamesAndAlliancesInTurnOrder = new LinkedHashMap<>(); - for (final GamePlayer player : data.getPlayerList().getPlayers()) { - final String name = player.getName(); - if (HeadlessGameServer.headless()) { - if (player.getIsDisabled()) { - playersToNodeListing.put(name, messengers.getLocalNode().getName()); - localPlayerTypes.put(name, PlayerTypes.WEAK_AI); - } else { - // we generally do not want a headless host bot to be doing any AI turns, since that - // is taxing on the system - playersToNodeListing.put(name, null); - } - } else { - Optional.ofNullable(messengers) - .ifPresent( - messenger -> - playersToNodeListing.put(name, messenger.getLocalNode().getName())); - } - playerNamesAndAlliancesInTurnOrder.put( - name, data.getAllianceTracker().getAlliancesPlayerIsIn(player)); - playersEnabledListing.put(name, !player.getIsDisabled()); - } + updatePlayersOnGameDataChanged(data); } objectStreamFactory.setData(data); localPlayerTypes.clear(); @@ -351,6 +336,47 @@ private void gameDataChanged() { remoteModelListener.playerListChanged(); } + private void updatePlayersOnGameDataChanged(final GameData data) { + // If specified, keep the previous player data. + if (playersToNodesMappingPersisted) { + playersToNodesMappingPersisted = false; + final Set dataPlayers = + data.getPlayerList().stream().map(GamePlayer::getName).collect(Collectors.toSet()); + // Sanity check that the list of countries matches. + if (dataPlayers.equals(playersToNodeListing.keySet())) { + // Don't regenerate the mappings, persist existing ones. + return; + } + throw new IllegalStateException("Expected countries to match when persisting seatings"); + } + + // Reset setting based on game data. + playersToNodeListing = new HashMap<>(); + playersEnabledListing = new HashMap<>(); + playersAllowedToBeDisabled = new HashSet<>(data.getPlayerList().getPlayersThatMayBeDisabled()); + playerNamesAndAlliancesInTurnOrder = new LinkedHashMap<>(); + for (final GamePlayer player : data.getPlayerList().getPlayers()) { + final String name = player.getName(); + if (HeadlessGameServer.headless()) { + if (player.getIsDisabled()) { + playersToNodeListing.put(name, messengers.getLocalNode().getName()); + localPlayerTypes.put(name, PlayerTypes.WEAK_AI); + } else { + // we generally do not want a headless host bot to be doing any AI turns, since that + // is taxing on the system + playersToNodeListing.put(name, null); + } + } else { + Optional.ofNullable(messengers) + .ifPresent( + messenger -> playersToNodeListing.put(name, messenger.getLocalNode().getName())); + } + playerNamesAndAlliancesInTurnOrder.put( + name, data.getAllianceTracker().getAlliancesPlayerIsIn(player)); + playersEnabledListing.put(name, !player.getIsDisabled()); + } + } + private Optional getServerProps() { if (System.getProperty(TRIPLEA_SERVER, "false").equals("true") && GameState.notStarted()) { GameState.setStarted(); diff --git a/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/ui/panels/main/game/selector/GameSelectorModel.java b/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/ui/panels/main/game/selector/GameSelectorModel.java index bb9bfa347d4..61977ceb7f7 100644 --- a/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/ui/panels/main/game/selector/GameSelectorModel.java +++ b/game-app/game-core/src/main/java/games/strategy/engine/framework/startup/ui/panels/main/game/selector/GameSelectorModel.java @@ -44,6 +44,7 @@ public class GameSelectorModel extends Observable implements GameSelector { // just for host bots, so we can get the actions for loading/saving games on the bots from this // model @Setter @Getter private ClientModel clientModelForHostBots = null; + private Optional saveGameToLoad = Optional.empty(); public GameSelectorModel() { this.gameParser = GameParser::parse; @@ -173,13 +174,24 @@ public void onGameEnded() { ThreadRunner.runInNewThread(this::loadDefaultGameSameThread); } + /** Sets the path of a save file that should be loaded. */ + public void setSaveGameFileToLoad(final Path filePath) { + saveGameToLoad = Optional.of(filePath.toAbsolutePath().toString()); + } + /** * Runs the load default game logic in same thread. Default game is the one that we loaded on * startup. */ public void loadDefaultGameSameThread() { - ClientSetting.defaultGameUri - .getValue() + final Optional gameUri; + if (saveGameToLoad.isPresent()) { + gameUri = saveGameToLoad; + saveGameToLoad = Optional.empty(); + } else { + gameUri = ClientSetting.defaultGameUri.getValue(); + } + gameUri .filter(Predicate.not(String::isBlank)) .filter(GameSelectorModel::gameUriExistsOnFileSystem) .map(Path::of) diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/ui/ActionButtons.java b/game-app/game-core/src/main/java/games/strategy/triplea/ui/ActionButtons.java index 3da65017ca9..e5ba52bffda 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/ui/ActionButtons.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/ui/ActionButtons.java @@ -46,22 +46,23 @@ public class ActionButtons extends JPanel { private static final long serialVersionUID = 2175685892863042399L; private final CardLayout layout = new CardLayout(); - @Getter private BattlePanel battlePanel; + @Getter private @Nullable BattlePanel battlePanel; - private MovePanel movePanel; + private @Nullable MovePanel movePanel; - private PurchasePanel purchasePanel; - private RepairPanel repairPanel; + private @Nullable PurchasePanel purchasePanel; + private @Nullable RepairPanel repairPanel; @Getter private PlacePanel placePanel; - private TechPanel techPanel; - private EndTurnPanel endTurnPanel; - private MoveForumPosterPanel moveForumPosterPanel; + private @Nullable TechPanel techPanel; + private @Nullable EndTurnPanel endTurnPanel; + private @Nullable MoveForumPosterPanel moveForumPosterPanel; + private @Nullable PoliticsPanel politicsPanel; + private @Nullable UserActionPanel userActionPanel; + private @Nullable PickTerritoryAndUnitsPanel pickTerritoryAndUnitsPanel; + private @Nullable ActionPanel actionPanel; - private PoliticsPanel politicsPanel; - private UserActionPanel userActionPanel; - private PickTerritoryAndUnitsPanel pickTerritoryAndUnitsPanel; public ActionButtons( final GameData data, @@ -128,11 +129,13 @@ public ActionButtons( } void changeToMove(final GamePlayer gamePlayer, final boolean nonCombat, final String stepName) { - movePanel.setNonCombat(nonCombat); - final boolean airBorne = stepName.endsWith("AirborneCombatMove"); - final String displayText = (airBorne ? " Airborne" : (nonCombat ? " Non" : "")); - movePanel.setDisplayText(displayText + " Combat Move"); - movePanel.setMoveType(airBorne ? MoveType.SPECIAL : MoveType.DEFAULT); + if (movePanel != null) { + movePanel.setNonCombat(nonCombat); + final boolean airBorne = stepName.endsWith("AirborneCombatMove"); + final String displayText = (airBorne ? " Airborne" : (nonCombat ? " Non" : "")); + movePanel.setDisplayText(displayText + " Combat Move"); + movePanel.setMoveType(airBorne ? MoveType.SPECIAL : MoveType.DEFAULT); + } changeTo(gamePlayer, movePanel); } @@ -150,7 +153,9 @@ public void changeToPlace(final GamePlayer gamePlayer) { public void changeToBattle( final GamePlayer gamePlayer, final Map> battles) { - battlePanel.setBattlesAndBombing(battles); + if (battlePanel != null) { + battlePanel.setBattlesAndBombing(battles); + } changeTo(gamePlayer, battlePanel); } @@ -182,9 +187,9 @@ private void changeTo(final GamePlayer gamePlayer, final @Nullable ActionPanel n actionPanel = newCurrent; // newCurrent might be null if we are shutting down - if (actionPanel != null) { - actionPanel.display(gamePlayer); - SwingUtilities.invokeLater(() -> layout.show(ActionButtons.this, actionPanel.toString())); + if (newCurrent != null) { + newCurrent.display(gamePlayer); + SwingUtilities.invokeLater(() -> layout.show(ActionButtons.this, newCurrent.toString())); } } @@ -198,6 +203,9 @@ public void changeToPickTerritoryAndUnits(final GamePlayer gamePlayer) { * @return null if no move was made. */ public IntegerMap waitForPurchase(final boolean bid) { + if (purchasePanel == null) { + return null; + } return purchasePanel.waitForPurchase(bid); } @@ -208,6 +216,9 @@ public IntegerMap waitForPurchase(final boolean bid) { */ public Map> waitForRepair( final boolean bid, final Collection allowedPlayersToRepair) { + if (repairPanel == null) { + return null; + } return repairPanel.waitForRepair(bid, allowedPlayersToRepair); } @@ -217,6 +228,9 @@ public Map> waitForRepair( * @return null if no move was made. */ public MoveDescription waitForMove(final IPlayerBridge bridge) { + if (movePanel == null) { + return null; + } return movePanel.waitForMove(bridge); } @@ -226,6 +240,9 @@ public MoveDescription waitForMove(final IPlayerBridge bridge) { * @return null if no tech roll was made. */ public TechRoll waitForTech() { + if (techPanel == null) { + return null; + } return techPanel.waitForTech(); } @@ -236,6 +253,9 @@ public TechRoll waitForTech() { */ public PoliticalActionAttachment waitForPoliticalAction( final boolean firstRun, final IPoliticsDelegate politicsDelegate) { + if (politicsPanel == null) { + return null; + } return politicsPanel.waitForPoliticalAction(firstRun, politicsDelegate); } @@ -246,6 +266,9 @@ public PoliticalActionAttachment waitForPoliticalAction( */ public UserActionAttachment waitForUserActionAction( final boolean firstRun, final IUserActionDelegate userActionDelegate) { + if (userActionPanel == null) { + return null; + } return userActionPanel.waitForUserActionAction(firstRun, userActionDelegate); } @@ -255,20 +278,30 @@ public UserActionAttachment waitForUserActionAction( * @return null if no placement was made. */ public PlaceData waitForPlace(final boolean bid, final IPlayerBridge bridge) { + if (placePanel == null) { + return null; + } return placePanel.waitForPlace(bid, bridge); } /** Blocks until the user selects an end-of-turn action. */ public void waitForEndTurn(final TripleAFrame frame, final IPlayerBridge bridge) { - endTurnPanel.waitForDone(frame, bridge); + if (endTurnPanel != null) { + endTurnPanel.waitForDone(frame, bridge); + } } public void waitForMoveForumPosterPanel(final TripleAFrame frame, final IPlayerBridge bridge) { - moveForumPosterPanel.waitForDone(frame, bridge); + if (moveForumPosterPanel != null) { + moveForumPosterPanel.waitForDone(frame, bridge); + } } /** Blocks until the user selects a battle to fight. */ public FightBattleDetails waitForBattleSelection() { + if (battlePanel == null) { + return null; + } return battlePanel.waitForBattleSelection(); } @@ -276,6 +309,9 @@ public Tuple> waitForPickTerritoryAndUnits( final List territoryChoices, final List unitChoices, final int unitsPerPick) { + if (pickTerritoryAndUnitsPanel == null) { + return Tuple.of(null, Set.of()); + } return pickTerritoryAndUnitsPanel.waitForPickTerritoryAndUnits( territoryChoices, unitChoices, unitsPerPick); }