Skip to content

Commit

Permalink
Keep player lists and select save if a game ends due to a disconnect. (
Browse files Browse the repository at this point in the history
…triplea-game#10142)

With this change, if a networked game ends because a joined player disconnects, the
game lobby dialog that will be re-opened for the server is now improved in these ways:
  - The save game that was produced on disconnect will now be selected.
  - The countries / players set up from the ended game will be kept.
     This includes which players have selected which countries and the AI settings
     for the host. Countries that were played by the disconnected player will be cleared
     and be opened to be chosen.
  • Loading branch information
asvitkine authored Mar 25, 2022
1 parent b9ab1d2 commit 742c76a
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +100,7 @@ public class ServerModel extends Observable implements IConnectionChangeListener
private Messengers messengers;
private GameData data;
private Map<String, String> playersToNodeListing = new HashMap<>();
private boolean playersToNodesMappingPersisted = false;
private Map<String, Boolean> playersEnabledListing = new HashMap<>();
private Collection<String> playersAllowedToBeDisabled = new HashSet<>();
private Map<String, Collection<String>> playerNamesAndAlliancesInTurnOrder =
Expand Down Expand Up @@ -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();
Expand All @@ -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<String> 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<ServerConnectionProps> getServerProps() {
if (System.getProperty(TRIPLEA_SERVER, "false").equals("true") && GameState.notStarted()) {
GameState.setStarted();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> saveGameToLoad = Optional.empty();

public GameSelectorModel() {
this.gameParser = GameParser::parse;
Expand Down Expand Up @@ -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<String> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand All @@ -150,7 +153,9 @@ public void changeToPlace(final GamePlayer gamePlayer) {

public void changeToBattle(
final GamePlayer gamePlayer, final Map<BattleType, Collection<Territory>> battles) {
battlePanel.setBattlesAndBombing(battles);
if (battlePanel != null) {
battlePanel.setBattlesAndBombing(battles);
}
changeTo(gamePlayer, battlePanel);
}

Expand Down Expand Up @@ -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()));
}
}

Expand All @@ -198,6 +203,9 @@ public void changeToPickTerritoryAndUnits(final GamePlayer gamePlayer) {
* @return null if no move was made.
*/
public IntegerMap<ProductionRule> waitForPurchase(final boolean bid) {
if (purchasePanel == null) {
return null;
}
return purchasePanel.waitForPurchase(bid);
}

Expand All @@ -208,6 +216,9 @@ public IntegerMap<ProductionRule> waitForPurchase(final boolean bid) {
*/
public Map<Unit, IntegerMap<RepairRule>> waitForRepair(
final boolean bid, final Collection<GamePlayer> allowedPlayersToRepair) {
if (repairPanel == null) {
return null;
}
return repairPanel.waitForRepair(bid, allowedPlayersToRepair);
}

Expand All @@ -217,6 +228,9 @@ public Map<Unit, IntegerMap<RepairRule>> waitForRepair(
* @return null if no move was made.
*/
public MoveDescription waitForMove(final IPlayerBridge bridge) {
if (movePanel == null) {
return null;
}
return movePanel.waitForMove(bridge);
}

Expand All @@ -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();
}

Expand All @@ -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);
}

Expand All @@ -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);
}

Expand All @@ -255,27 +278,40 @@ 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();
}

public Tuple<Territory, Set<Unit>> waitForPickTerritoryAndUnits(
final List<Territory> territoryChoices,
final List<Unit> unitChoices,
final int unitsPerPick) {
if (pickTerritoryAndUnitsPanel == null) {
return Tuple.of(null, Set.of());
}
return pickTerritoryAndUnitsPanel.waitForPickTerritoryAndUnits(
territoryChoices, unitChoices, unitsPerPick);
}
Expand Down

0 comments on commit 742c76a

Please sign in to comment.