package games.strategy.engine.framework.startup.mc; import java.awt.Component; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.Preferences; import javax.swing.JOptionPane; import games.strategy.debug.ClientLogger; import games.strategy.engine.ClientContext; import games.strategy.engine.chat.Chat; import games.strategy.engine.chat.ChatController; import games.strategy.engine.chat.ChatPanel; import games.strategy.engine.chat.HeadlessChat; import games.strategy.engine.chat.IChatPanel; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.properties.GameProperties; import games.strategy.engine.data.properties.IEditableProperty; import games.strategy.engine.framework.GameDataManager; import games.strategy.engine.framework.GameObjectStreamFactory; import games.strategy.engine.framework.GameRunner; import games.strategy.engine.framework.headlessGameServer.HeadlessGameServer; import games.strategy.engine.framework.message.PlayerListing; import games.strategy.engine.framework.startup.launcher.ILauncher; import games.strategy.engine.framework.startup.launcher.ServerLauncher; import games.strategy.engine.framework.startup.login.ClientLoginValidator; import games.strategy.engine.framework.startup.ui.ServerOptions; import games.strategy.engine.framework.ui.SaveGameFileChooser; import games.strategy.engine.lobby.server.NullModeratorController; import games.strategy.engine.message.ChannelMessenger; import games.strategy.engine.message.IChannelMessenger; import games.strategy.engine.message.IRemoteMessenger; import games.strategy.engine.message.RemoteMessenger; import games.strategy.engine.message.RemoteName; import games.strategy.engine.message.unifiedmessenger.UnifiedMessenger; import games.strategy.net.IConnectionChangeListener; import games.strategy.net.IMessenger; import games.strategy.net.IMessengerErrorListener; import games.strategy.net.INode; import games.strategy.net.IServerMessenger; import games.strategy.net.ServerMessenger; import games.strategy.util.Version; public class ServerModel extends Observable implements IMessengerErrorListener, IConnectionChangeListener { public static final RemoteName SERVER_REMOTE_NAME = new RemoteName("games.strategy.engine.framework.ui.ServerStartup.SERVER_REMOTE", IServerStartupRemote.class); public enum InteractionMode { HEADLESS, SWING_CLIENT_UI } static final String CHAT_NAME = "games.strategy.engine.framework.ui.ServerStartup.CHAT_NAME"; static final String PLAYERNAME = "PlayerName"; static RemoteName getObserverWaitingToStartName(final INode node) { return new RemoteName("games.strategy.engine.framework.startup.mc.ServerModel.OBSERVER" + node.getName(), IObserverWaitingToJoin.class); } private static Logger logger = Logger.getLogger(ServerModel.class.getName()); private final GameObjectStreamFactory objectStreamFactory = new GameObjectStreamFactory(null); private final SetupPanelModel typePanelModel; private final boolean headless; private IServerMessenger serverMessenger; private IRemoteMessenger remoteMessenger; private IChannelMessenger channelMessenger; private GameData data; private Map<String, String> playersToNodeListing = new HashMap<>(); private Map<String, Boolean> playersEnabledListing = new HashMap<>(); private Collection<String> playersAllowedToBeDisabled = new HashSet<>(); private Map<String, Collection<String>> playerNamesAndAlliancesInTurnOrder = new LinkedHashMap<>(); private IRemoteModelListener remoteModelListener = IRemoteModelListener.NULL_LISTENER; private final GameSelectorModel gameSelectorModel; private Component ui; private IChatPanel chatPanel; private ChatController chatController; private final Map<String, String> localPlayerTypes = new HashMap<>(); // while our server launcher is not null, delegate new/lost connections to it private volatile ServerLauncher serverLauncher; private CountDownLatch removeConnectionsLatch = null; private final Observer gameSelectorObserver = (observable, value) -> gameDataChanged(); ServerModel(final GameSelectorModel gameSelectorModel, final SetupPanelModel typePanelModel) { this(gameSelectorModel, typePanelModel, InteractionMode.SWING_CLIENT_UI); } public ServerModel(final GameSelectorModel gameSelectorModel, final SetupPanelModel typePanelModel, final InteractionMode interactionMode) { this.gameSelectorModel = gameSelectorModel; this.typePanelModel = typePanelModel; this.gameSelectorModel.addObserver(gameSelectorObserver); headless = (interactionMode == InteractionMode.HEADLESS); } public void shutDown() { gameSelectorModel.deleteObserver(gameSelectorObserver); if (serverMessenger != null) { chatController.deactivate(); serverMessenger.shutDown(); serverMessenger.removeErrorListener(this); chatPanel.shutDown(); } } public void cancel() { gameSelectorModel.deleteObserver(gameSelectorObserver); if (serverMessenger != null) { chatController.deactivate(); serverMessenger.shutDown(); serverMessenger.removeErrorListener(this); chatPanel.setChat(null); } } public void setRemoteModelListener(IRemoteModelListener listener) { if (listener == null) { listener = IRemoteModelListener.NULL_LISTENER; } remoteModelListener = listener; } public void setLocalPlayerType(final String player, final String type) { synchronized (this) { localPlayerTypes.put(player, type); } } 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 PlayerID player : data.getPlayerList().getPlayers()) { final String name = player.getName(); if (headless) { if (player.getIsDisabled()) { playersToNodeListing.put(name, serverMessenger.getLocalNode().getName()); // the 2nd in the list should be Weak AI final int indexPosition = Math.max(0, Math.min(data.getGameLoader().getServerPlayerTypes().length - 1, 1)); localPlayerTypes.put(name, data.getGameLoader().getServerPlayerTypes()[indexPosition]); } 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 { playersToNodeListing.put(name, serverMessenger.getLocalNode().getName()); } playerNamesAndAlliancesInTurnOrder.put(name, data.getAllianceTracker().getAlliancesPlayerIsIn(player)); playersEnabledListing.put(name, !player.getIsDisabled()); } } objectStreamFactory.setData(data); localPlayerTypes.clear(); } notifyChanellPlayersChanged(); remoteModelListener.playerListChanged(); } private ServerProps getServerProps(final Component ui) { if (System.getProperties().getProperty(GameRunner.TRIPLEA_SERVER_PROPERTY, "false").equals("true") && System.getProperties().getProperty(GameRunner.TRIPLEA_STARTED, "").equals("")) { final ServerProps props = new ServerProps(); props.setName(System.getProperty(GameRunner.TRIPLEA_NAME_PROPERTY)); props.setPort(Integer.parseInt(System.getProperty(GameRunner.TRIPLEA_PORT_PROPERTY))); if (System.getProperty(GameRunner.TRIPLEA_SERVER_PASSWORD_PROPERTY) != null) { props.setPassword(System.getProperty(GameRunner.TRIPLEA_SERVER_PASSWORD_PROPERTY)); } System.setProperty(GameRunner.TRIPLEA_STARTED, "true"); return props; } final Preferences prefs = Preferences.userNodeForPackage(this.getClass()); final String playername = prefs.get(PLAYERNAME, System.getProperty("user.name")); final ServerOptions options = new ServerOptions(ui, playername, GameRunner.PORT, false); options.setLocationRelativeTo(ui); options.setVisible(true); options.dispose(); if (!options.getOKPressed()) { return null; } final String name = options.getName(); logger.log(Level.FINE, "Server playing as:" + name); // save the name! -- lnxduk prefs.put(PLAYERNAME, name); final int port = options.getPort(); if (port >= 65536 || port == 0) { if (headless) { System.out.println("Invalid Port: " + port); } else { JOptionPane.showMessageDialog(ui, "Invalid Port: " + port, "Error", JOptionPane.ERROR_MESSAGE); } return null; } final ServerProps props = new ServerProps(); props.setName(options.getName()); props.setPort(options.getPort()); props.setPassword(options.getPassword()); return props; } /** * UI can be null. We use it as the parent for message dialogs we show. * If you have a component displayed, use it. */ public boolean createServerMessenger(Component ui) { ui = ui == null ? null : JOptionPane.getFrameForComponent(ui); this.ui = ui; final ServerProps props = getServerProps(ui); if (props == null) { return false; } try { serverMessenger = new ServerMessenger(props.getName(), props.getPort(), objectStreamFactory); final ClientLoginValidator clientLoginValidator = new ClientLoginValidator(serverMessenger); clientLoginValidator.setGamePassword(props.getPassword()); serverMessenger.setLoginValidator(clientLoginValidator); serverMessenger.addErrorListener(this); serverMessenger.addConnectionChangeListener(this); final UnifiedMessenger unifiedMessenger = new UnifiedMessenger(serverMessenger); remoteMessenger = new RemoteMessenger(unifiedMessenger); remoteMessenger.registerRemote(m_serverStartupRemote, SERVER_REMOTE_NAME); channelMessenger = new ChannelMessenger(unifiedMessenger); final NullModeratorController moderatorController = new NullModeratorController(serverMessenger, null); moderatorController.register(remoteMessenger); chatController = new ChatController(CHAT_NAME, serverMessenger, remoteMessenger, channelMessenger, moderatorController); if (ui == null && headless) { chatPanel = new HeadlessChat(serverMessenger, channelMessenger, remoteMessenger, CHAT_NAME, Chat.CHAT_SOUND_PROFILE.GAME_CHATROOM); } else { chatPanel = new ChatPanel(serverMessenger, channelMessenger, remoteMessenger, CHAT_NAME, Chat.CHAT_SOUND_PROFILE.GAME_CHATROOM); } serverMessenger.setAcceptNewConnections(true); gameDataChanged(); return true; } catch (final IOException ioe) { ioe.printStackTrace(System.out); if (headless) { System.out.println("Unable to create server socket:" + ioe.getMessage()); } else { JOptionPane.showMessageDialog(ui, "Unable to create server socket:" + ioe.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } return false; } } private final IServerStartupRemote m_serverStartupRemote = new IServerStartupRemote() { @Override public PlayerListing getPlayerListing() { return getPlayerListingInternal(); } @Override public void takePlayer(final INode who, final String playerName) { takePlayerInternal(who, true, playerName); } @Override public void releasePlayer(final INode who, final String playerName) { takePlayerInternal(who, false, playerName); } @Override public void disablePlayer(final String playerName) { if (!headless) { return; } // we don't want the client's changing stuff for anyone but a bot setPlayerEnabled(playerName, false); } @Override public void enablePlayer(final String playerName) { if (!headless) { return; } // we don't want the client's changing stuff for anyone but a bot setPlayerEnabled(playerName, true); } @Override public boolean isGameStarted(final INode newNode) { if (serverLauncher != null) { final RemoteName remoteName = getObserverWaitingToStartName(newNode); final IObserverWaitingToJoin observerWaitingToJoinBlocking = (IObserverWaitingToJoin) remoteMessenger.getRemote(remoteName); final IObserverWaitingToJoin observerWaitingToJoinNonBlocking = (IObserverWaitingToJoin) remoteMessenger.getRemote(remoteName, true); serverLauncher.addObserver(observerWaitingToJoinBlocking, observerWaitingToJoinNonBlocking, newNode); return true; } else { return false; } } @Override public boolean getIsServerHeadless() { return HeadlessGameServer.headless(); } /** * This should not be called from within game, only from the game setup screen, while everyone is waiting for game * to start. */ @Override public byte[] getSaveGame() { System.out.println("Sending save game"); byte[] bytes = null; try (final ByteArrayOutputStream sink = new ByteArrayOutputStream(5000)) { new GameDataManager().saveGame(sink, data); bytes = sink.toByteArray(); } catch (final IOException e) { ClientLogger.logQuietly(e); throw new IllegalStateException(e); } return bytes; } @Override public byte[] getGameOptions() { byte[] bytes = null; if (data == null || data.getProperties() == null || data.getProperties().getEditableProperties() == null || data.getProperties().getEditableProperties().isEmpty()) { return bytes; } final List<IEditableProperty> currentEditableProperties = data.getProperties().getEditableProperties(); try (final ByteArrayOutputStream sink = new ByteArrayOutputStream(1000)) { GameProperties.toOutputStream(sink, currentEditableProperties); bytes = sink.toByteArray(); } catch (final IOException e) { ClientLogger.logQuietly(e); } return bytes; } @Override public Set<String> getAvailableGames() { final HeadlessGameServer headless = HeadlessGameServer.getInstance(); if (headless == null) { return null; } return headless.getAvailableGames(); } @Override public void changeServerGameTo(final String gameName) { final HeadlessGameServer headless = HeadlessGameServer.getInstance(); if (headless == null) { return; } System.out.println("Changing to game map: " + gameName); headless.setGameMapTo(gameName); } @Override public void changeToLatestAutosave(final SaveGameFileChooser.AUTOSAVE_TYPE typeOfAutosave) { final HeadlessGameServer headless = HeadlessGameServer.getInstance(); if (headless == null) { return; } final File save; if (SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE.equals(typeOfAutosave)) { save = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveFileName()); } else if (SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE2.equals(typeOfAutosave)) { save = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSave2FileName()); } else if (SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE_ODD.equals(typeOfAutosave)) { save = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveOddFileName()); } else if (SaveGameFileChooser.AUTOSAVE_TYPE.AUTOSAVE_EVEN.equals(typeOfAutosave)) { save = new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveEvenFileName()); } else { return; } if (save == null || !save.exists()) { return; } System.out.println("Changing to autosave of type: " + typeOfAutosave.toString()); headless.loadGameSave(save); } @Override public void changeToGameSave(final byte[] bytes, final String fileName) { // TODO: change to a string message return, so we can tell the user/requestor if it was successful or not, and why // if not. final HeadlessGameServer headless = HeadlessGameServer.getInstance(); if (headless == null || bytes == null) { return; } System.out.println("Changing to user savegame: " + fileName); try (ByteArrayInputStream input = new ByteArrayInputStream(bytes); InputStream oinput = new BufferedInputStream(input);) { headless.loadGameSave(oinput, fileName); } catch (final Exception e) { ClientLogger.logQuietly(e); } } @Override public void changeToGameOptions(final byte[] bytes) { // TODO: change to a string message return, so we can tell the user/requestor if it was successful or not, and why // if not. final HeadlessGameServer headless = HeadlessGameServer.getInstance(); if (headless == null || bytes == null) { return; } System.out.println("Changing to user game options."); try { headless.loadGameOptions(bytes); } catch (final Exception e) { ClientLogger.logQuietly(e); } } }; private PlayerListing getPlayerListingInternal() { synchronized (this) { if (data == null) { return new PlayerListing(new HashMap<>(), new HashMap<>(playersEnabledListing), getLocalPlayerTypes(), new Version(0, 0), gameSelectorModel.getGameName(), gameSelectorModel.getGameRound(), new HashSet<>(playersAllowedToBeDisabled), new LinkedHashMap<>()); } else { return new PlayerListing(new HashMap<>(playersToNodeListing), new HashMap<>(playersEnabledListing), getLocalPlayerTypes(), data.getGameVersion(), data.getGameName(), data.getSequence().getRound() + "", new HashSet<>(playersAllowedToBeDisabled), playerNamesAndAlliancesInTurnOrder); } } } private void takePlayerInternal(final INode from, final boolean take, final String playerName) { // synchronize to make sure two adds arent executed at once synchronized (this) { if (!playersToNodeListing.containsKey(playerName)) { return; } if (take) { playersToNodeListing.put(playerName, from.getName()); } else { playersToNodeListing.put(playerName, null); } } notifyChanellPlayersChanged(); remoteModelListener.playersTakenChanged(); } private void setPlayerEnabled(final String playerName, final boolean enabled) { takePlayerInternal(serverMessenger.getLocalNode(), true, playerName); // synchronize synchronized (this) { if (!playersEnabledListing.containsKey(playerName)) { return; } playersEnabledListing.put(playerName, enabled); if (headless) { // we do not want the host bot to actually play, so set to null if enabled, and set to weak ai if disabled if (enabled) { playersToNodeListing.put(playerName, null); } else { localPlayerTypes.put(playerName, data.getGameLoader().getServerPlayerTypes()[Math.max(0, // the 2nd in the list should be Weak AI Math.min(data.getGameLoader().getServerPlayerTypes().length - 1, 1))]); } } } notifyChanellPlayersChanged(); remoteModelListener.playersTakenChanged(); } public void setAllPlayersToNullNodes() { if (playersToNodeListing != null) { for (final String p : playersToNodeListing.keySet()) { playersToNodeListing.put(p, null); } } } private void notifyChanellPlayersChanged() { final IClientChannel channel = (IClientChannel) channelMessenger.getChannelBroadcastor(IClientChannel.CHANNEL_NAME); channel.playerListingChanged(getPlayerListingInternal()); } public void takePlayer(final String playerName) { takePlayerInternal(serverMessenger.getLocalNode(), true, playerName); } public void releasePlayer(final String playerName) { takePlayerInternal(serverMessenger.getLocalNode(), false, playerName); } public void disablePlayer(final String playerName) { setPlayerEnabled(playerName, false); } public void enablePlayer(final String playerName) { setPlayerEnabled(playerName, true); } public IServerMessenger getMessenger() { return serverMessenger; } public Map<String, String> getPlayersToNodeListing() { synchronized (this) { return new HashMap<>(playersToNodeListing); } } public Map<String, Boolean> getPlayersEnabledListing() { synchronized (this) { return new HashMap<>(playersEnabledListing); } } public Collection<String> getPlayersAllowedToBeDisabled() { synchronized (this) { return new HashSet<>(playersAllowedToBeDisabled); } } public Map<String, Collection<String>> getPlayerNamesAndAlliancesInTurnOrderLinkedHashMap() { synchronized (this) { return new LinkedHashMap<>(playerNamesAndAlliancesInTurnOrder); } } @Override public void messengerInvalid(final IMessenger messenger, final Exception reason) { if (headless) { System.out.println("Connection Lost"); if (typePanelModel != null) { typePanelModel.showSelectType(); } } else { JOptionPane.showMessageDialog(ui, "Connection lost", "Error", JOptionPane.ERROR_MESSAGE); typePanelModel.showSelectType(); } } @Override public void connectionAdded(final INode to) {} @Override public void connectionRemoved(final INode node) { if (removeConnectionsLatch != null) { try { removeConnectionsLatch.await(6, TimeUnit.SECONDS); } catch (final InterruptedException e) { // no worries } } // will be handled elsewhere if (serverLauncher != null) { serverLauncher.connectionLost(node); return; } // we lost a node. Remove the players he plays. final List<String> free = new ArrayList<>(); synchronized (this) { for (final String player : playersToNodeListing.keySet()) { final String playedBy = playersToNodeListing.get(player); if (playedBy != null && playedBy.equals(node.getName())) { free.add(player); } } } for (final String player : free) { takePlayerInternal(node, false, player); } } public IChatPanel getChatPanel() { return chatPanel; } public void disallowRemoveConnections() { while (removeConnectionsLatch != null && removeConnectionsLatch.getCount() > 0) { removeConnectionsLatch.countDown(); } removeConnectionsLatch = new CountDownLatch(1); } public void allowRemoveConnections() { while (removeConnectionsLatch != null && removeConnectionsLatch.getCount() > 0) { removeConnectionsLatch.countDown(); } removeConnectionsLatch = null; } public Map<String, String> getLocalPlayerTypes() { final Map<String, String> localPlayerMappings = new HashMap<>(); if (data == null) { return localPlayerMappings; } // local player default = humans (for bots = weak ai) final String defaultLocalType = headless ? data.getGameLoader().getServerPlayerTypes()[Math.max(0, Math.min(data.getGameLoader().getServerPlayerTypes().length - 1, 1))] : data.getGameLoader().getServerPlayerTypes()[0]; for (final String player : playersToNodeListing.keySet()) { final String playedBy = playersToNodeListing.get(player); if (playedBy == null) { continue; } if (playedBy.equals(serverMessenger.getLocalNode().getName())) { String type = defaultLocalType; if (localPlayerTypes.containsKey(player)) { type = localPlayerTypes.get(player); } localPlayerMappings.put(player, type); } } return localPlayerMappings; } public ILauncher getLauncher() { synchronized (this) { disallowRemoveConnections(); // -1 since we dont count outselves final int clientCount = serverMessenger.getNodes().size() - 1; final Map<String, INode> remotePlayers = new HashMap<>(); for (final String player : playersToNodeListing.keySet()) { final String playedBy = playersToNodeListing.get(player); if (playedBy == null) { return null; } if (!playedBy.equals(serverMessenger.getLocalNode().getName())) { final Set<INode> nodes = serverMessenger.getNodes(); for (final INode node : nodes) { if (node.getName().equals(playedBy)) { remotePlayers.put(player, node); break; } } } } final ServerLauncher launcher = new ServerLauncher(clientCount, remoteMessenger, channelMessenger, serverMessenger, gameSelectorModel, getPlayerListingInternal(), remotePlayers, this, headless); return launcher; } } public void newGame() { serverMessenger.setAcceptNewConnections(true); final IClientChannel channel = (IClientChannel) channelMessenger.getChannelBroadcastor(IClientChannel.CHANNEL_NAME); notifyChanellPlayersChanged(); channel.gameReset(); } public void setServerLauncher(final ServerLauncher launcher) { serverLauncher = launcher; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("ServerModel GameData:").append(data == null ? "null" : data.getGameName()).append("\n"); sb.append("Connected:").append(serverMessenger == null ? "null" : serverMessenger.isConnected()).append("\n"); sb.append(serverMessenger); sb.append("\n"); sb.append(remoteMessenger); sb.append("\n"); sb.append(channelMessenger); return sb.toString(); } } class ServerProps { private String name; private int port; private String password; public String getPassword() { return password; } public void setPassword(final String password) { this.password = password; } public String getName() { return name; } public void setName(final String name) { this.name = name; } public int getPort() { return port; } public void setPort(final int port) { this.port = port; } }