package games.strategy.engine.framework.startup.mc;
import java.awt.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
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.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.Action;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import games.strategy.debug.ClientLogger;
import games.strategy.engine.chat.Chat;
import games.strategy.engine.chat.ChatPanel;
import games.strategy.engine.chat.IChatPanel;
import games.strategy.engine.data.GameData;
import games.strategy.engine.framework.ClientGame;
import games.strategy.engine.framework.GameDataManager;
import games.strategy.engine.framework.GameObjectStreamFactory;
import games.strategy.engine.framework.GameRunner;
import games.strategy.engine.framework.IGameLoader;
import games.strategy.engine.framework.message.PlayerListing;
import games.strategy.engine.framework.networkMaintenance.ChangeGameOptionsClientAction;
import games.strategy.engine.framework.networkMaintenance.ChangeGameToSaveGameClientAction;
import games.strategy.engine.framework.networkMaintenance.ChangeToAutosaveClientAction;
import games.strategy.engine.framework.networkMaintenance.GetGameSaveClientAction;
import games.strategy.engine.framework.networkMaintenance.SetMapClientAction;
import games.strategy.engine.framework.startup.launcher.IServerReady;
import games.strategy.engine.framework.startup.login.ClientLogin;
import games.strategy.engine.framework.startup.ui.ClientOptions;
import games.strategy.engine.framework.startup.ui.MainFrame;
import games.strategy.engine.framework.ui.SaveGameFileChooser;
import games.strategy.engine.framework.ui.background.WaitWindow;
import games.strategy.engine.gamePlayer.IGamePlayer;
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.ClientMessenger;
import games.strategy.net.CouldNotLogInException;
import games.strategy.net.IClientMessenger;
import games.strategy.net.IMessenger;
import games.strategy.net.IMessengerErrorListener;
import games.strategy.net.INode;
import games.strategy.net.MacFinder;
import games.strategy.net.Messengers;
import games.strategy.ui.SwingAction;
import games.strategy.util.CountDownLatchHandler;
import games.strategy.util.EventThreadJOptionPane;
public class ClientModel implements IMessengerErrorListener {
public static final RemoteName CLIENT_READY_CHANNEL =
new RemoteName("games.strategy.engine.framework.startup.mc.ClientModel.CLIENT_READY_CHANNEL", IServerReady.class);
private static Logger s_logger = Logger.getLogger(ClientModel.class.getName());
private IRemoteModelListener m_listener = IRemoteModelListener.NULL_LISTENER;
private IChannelMessenger m_channelMessenger;
private IRemoteMessenger m_remoteMessenger;
private IClientMessenger m_messenger;
private final GameObjectStreamFactory m_objectStreamFactory = new GameObjectStreamFactory(null);
private final GameSelectorModel m_gameSelectorModel;
private final SetupPanelModel m_typePanelModel;
private Component m_ui;
private IChatPanel m_chatPanel;
private ClientGame m_game;
private boolean m_hostIsHeadlessBot = false;
private final WaitWindow m_gameLoadingWindow = new WaitWindow();
// we set the game data to be null, since we
// are a client game, and the game data lives on the server
// however, if we cancel, we want to restore the old game data.
private GameData m_gameDataOnStartup;
private Map<String, String> m_playersToNodes = new HashMap<>();
private Map<String, Boolean> m_playersEnabledListing = new HashMap<>();
private Collection<String> m_playersAllowedToBeDisabled = new HashSet<>();
private Map<String, Collection<String>> m_playerNamesAndAlliancesInTurnOrder =
new LinkedHashMap<>();
ClientModel(final GameSelectorModel gameSelectorModel, final SetupPanelModel typePanelModel) {
m_typePanelModel = typePanelModel;
m_gameSelectorModel = gameSelectorModel;
}
public void setRemoteModelListener(IRemoteModelListener listener) {
if (listener == null) {
listener = IRemoteModelListener.NULL_LISTENER;
}
m_listener = listener;
}
private ClientProps getProps(final Component ui) {
if (System.getProperties().getProperty(GameRunner.TRIPLEA_CLIENT_PROPERTY, "false").equals("true")
&& System.getProperties().getProperty(GameRunner.TRIPLEA_STARTED, "").equals("")) {
final ClientProps props = new ClientProps();
props.setHost(System.getProperty(GameRunner.TRIPLEA_HOST_PROPERTY));
props.setName(System.getProperty(GameRunner.TRIPLEA_NAME_PROPERTY));
props.setPort(Integer.parseInt(System.getProperty(GameRunner.TRIPLEA_PORT_PROPERTY)));
System.setProperty(GameRunner.TRIPLEA_STARTED, "true");
return props;
}
// load in the saved name!
final Preferences prefs = Preferences.userNodeForPackage(this.getClass());
final String playername = prefs.get(ServerModel.PLAYERNAME, System.getProperty("user.name"));
final ClientOptions options = new ClientOptions(ui, playername, GameRunner.PORT, "127.0.0.1");
options.setLocationRelativeTo(ui);
options.setVisible(true);
options.dispose();
if (!options.getOKPressed()) {
return null;
}
final ClientProps props = new ClientProps();
props.setHost(options.getAddress());
props.setName(options.getName());
props.setPort(options.getPort());
return props;
}
public boolean createClientMessenger(Component ui) {
m_gameDataOnStartup = m_gameSelectorModel.getGameData();
m_gameSelectorModel.setCanSelect(false);
ui = JOptionPane.getFrameForComponent(ui);
m_ui = ui;
// load in the saved name!
final Preferences prefs = Preferences.userNodeForPackage(this.getClass());
final ClientProps props = getProps(ui);
if (props == null) {
m_gameSelectorModel.setCanSelect(true);
cancel();
return false;
}
final String name = props.getName();
s_logger.log(Level.FINE, "Client playing as:" + name);
// save the name! -- lnxduk
prefs.put(ServerModel.PLAYERNAME, name);
final int port = props.getPort();
if (port >= 65536 || port <= 0) {
EventThreadJOptionPane.showMessageDialog(ui, "Invalid Port: " + port, "Error", JOptionPane.ERROR_MESSAGE,
new CountDownLatchHandler(true));
return false;
}
final String address = props.getHost();
try {
final String mac = MacFinder.getHashedMacAddress();
m_messenger = new ClientMessenger(address, port, name, mac, m_objectStreamFactory, new ClientLogin(m_ui));
} catch (final CouldNotLogInException ioe) {
// an error message should have already been reported
return false;
} catch (final Exception ioe) {
ioe.printStackTrace(System.out);
EventThreadJOptionPane.showMessageDialog(ui, "Unable to connect:" + ioe.getMessage(), "Error",
JOptionPane.ERROR_MESSAGE, new CountDownLatchHandler(true));
return false;
}
m_messenger.addErrorListener(this);
final UnifiedMessenger unifiedMessenger = new UnifiedMessenger(m_messenger);
m_channelMessenger = new ChannelMessenger(unifiedMessenger);
m_remoteMessenger = new RemoteMessenger(unifiedMessenger);
m_channelMessenger.registerChannelSubscriber(m_channelListener, IClientChannel.CHANNEL_NAME);
m_chatPanel = new ChatPanel(m_messenger, m_channelMessenger, m_remoteMessenger, ServerModel.CHAT_NAME,
Chat.CHAT_SOUND_PROFILE.GAME_CHATROOM);
if (getIsServerHeadlessTest()) {
m_gameSelectorModel.setClientModelForHostBots(this);
((ChatPanel) m_chatPanel).getChatMessagePanel()
.addServerMessage("Welcome to an automated dedicated host service (a host bot). "
+ "\nIf anyone disconnects, the autosave will be reloaded (a save might be loaded right now). "
+ "\nYou can get the current save, or you can load a save (only saves that it has the map for).");
}
m_remoteMessenger.registerRemote(m_observerWaitingToJoin,
ServerModel.getObserverWaitingToStartName(m_messenger.getLocalNode()));
// save this, it will be cleared later
m_gameDataOnStartup = m_gameSelectorModel.getGameData();
final IServerStartupRemote serverStartup = getServerStartup();
final PlayerListing players = serverStartup.getPlayerListing();
internalPlayerListingChanged(players);
if (!serverStartup.isGameStarted(m_messenger.getLocalNode())) {
m_remoteMessenger.unregisterRemote(ServerModel.getObserverWaitingToStartName(m_messenger.getLocalNode()));
}
m_gameSelectorModel.setIsHostHeadlessBot(m_hostIsHeadlessBot);
return true;
}
private IServerStartupRemote getServerStartup() {
return (IServerStartupRemote) m_remoteMessenger.getRemote(ServerModel.SERVER_REMOTE_NAME);
}
public List<String> getAvailableServerGames() {
final Set<String> games = getServerStartup().getAvailableGames();
if (games == null) {
return new ArrayList<>();
}
return new ArrayList<>(games);
}
public void shutDown() {
if (m_messenger == null) {
return;
}
m_objectStreamFactory.setData(null);
m_messenger.shutDown();
m_chatPanel.shutDown();
m_gameSelectorModel.setGameData(null);
m_gameSelectorModel.setCanSelect(false);
m_hostIsHeadlessBot = false;
m_gameSelectorModel.setIsHostHeadlessBot(false);
m_gameSelectorModel.setClientModelForHostBots(null);
m_messenger.removeErrorListener(this);
}
public void cancel() {
if (m_messenger == null) {
return;
}
m_objectStreamFactory.setData(null);
m_messenger.shutDown();
m_chatPanel.setChat(null);
m_gameSelectorModel.setGameData(m_gameDataOnStartup);
m_gameSelectorModel.setCanSelect(true);
m_hostIsHeadlessBot = false;
m_gameSelectorModel.setIsHostHeadlessBot(false);
m_gameSelectorModel.setClientModelForHostBots(null);
m_messenger.removeErrorListener(this);
}
private final IClientChannel m_channelListener = new IClientChannel() {
@Override
public void playerListingChanged(final PlayerListing listing) {
internalPlayerListingChanged(listing);
}
@Override
public void gameReset() {
m_objectStreamFactory.setData(null);
SwingAction.invokeAndWait(() -> MainFrame.getInstance().setVisible(true));
}
@Override
public void doneSelectingPlayers(final byte[] gameData, final Map<String, INode> players) {
final CountDownLatch latch = new CountDownLatch(1);
startGame(gameData, players, latch, false);
try {
latch.await(GameRunner.MINIMUM_CLIENT_GAMEDATA_LOAD_GRACE_TIME, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
ClientLogger.logQuietly(e);
}
}
};
IObserverWaitingToJoin m_observerWaitingToJoin = new IObserverWaitingToJoin() {
@Override
public void joinGame(final byte[] gameData, final Map<String, INode> players) {
m_remoteMessenger.unregisterRemote(ServerModel.getObserverWaitingToStartName(m_messenger.getLocalNode()));
final CountDownLatch latch = new CountDownLatch(1);
startGame(gameData, players, latch, true);
try {
latch.await(GameRunner.MINIMUM_CLIENT_GAMEDATA_LOAD_GRACE_TIME, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
ClientLogger.logQuietly(e);
}
}
@Override
public void cannotJoinGame(final String reason) {
SwingUtilities.invokeLater(() -> {
m_typePanelModel.showSelectType();
EventThreadJOptionPane.showMessageDialog(m_ui, "Could not join game: " + reason,
new CountDownLatchHandler(true));
});
}
};
private void startGame(final byte[] gameData, final Map<String, INode> players, final CountDownLatch onDone,
final boolean gameRunning) {
SwingUtilities.invokeLater(() -> {
m_gameLoadingWindow.setVisible(true);
m_gameLoadingWindow.setLocationRelativeTo(JOptionPane.getFrameForComponent(m_ui));
m_gameLoadingWindow.showWait();
});
try {
startGameInNewThread(gameData, players, gameRunning);
} catch (final RuntimeException e) {
m_gameLoadingWindow.doneWait();
throw e;
} finally {
if (onDone != null) {
onDone.countDown();
}
}
}
private void startGameInNewThread(final byte[] gameData, final Map<String, INode> players,
final boolean gameRunning) {
final GameData data;
try {
// this normally takes a couple seconds, but can take
// up to 60 seconds for a freaking huge game
data = new GameDataManager().loadGame(new ByteArrayInputStream(gameData), null);
} catch (final IOException ex) {
ClientLogger.logQuietly(ex);
return;
}
m_objectStreamFactory.setData(data);
final Map<String, String> playerMapping = new HashMap<>();
for (final String player : m_playersToNodes.keySet()) {
final String playedBy = m_playersToNodes.get(player);
if (playedBy.equals(m_messenger.getLocalNode().getName())) {
playerMapping.put(player, IGameLoader.CLIENT_PLAYER_TYPE);
}
}
final Set<IGamePlayer> playerSet = data.getGameLoader().createPlayers(playerMapping);
final Messengers messengers = new Messengers(m_messenger, m_remoteMessenger, m_channelMessenger);
m_game = new ClientGame(data, playerSet, players, messengers);
new Thread(() -> {
SwingUtilities.invokeLater(() -> JOptionPane.getFrameForComponent(m_ui).setVisible(false));
try {
// game will be null if we loose the connection
if (m_game != null) {
try {
data.getGameLoader().startGame(m_game, playerSet, false);
data.testLocksOnRead();
} catch (final Exception e) {
ClientLogger.logError("Failed to start Game", e);
m_game.shutDown();
m_messenger.shutDown();
m_gameLoadingWindow.doneWait();
// an ugly hack, we need a better
// way to get the main frame
MainFrame.getInstance().clientLeftGame();
}
}
if (!gameRunning) {
((IServerReady) m_remoteMessenger.getRemote(CLIENT_READY_CHANNEL)).clientReady();
}
} finally {
m_gameLoadingWindow.doneWait();
}
}, "Client Game Launcher").start();
}
public void takePlayer(final String playerName) {
getServerStartup().takePlayer(m_messenger.getLocalNode(), playerName);
}
public void releasePlayer(final String playerName) {
getServerStartup().releasePlayer(m_messenger.getLocalNode(), playerName);
}
public void disablePlayer(final String playerName) {
getServerStartup().disablePlayer(playerName);
}
public void enablePlayer(final String playerName) {
getServerStartup().enablePlayer(playerName);
}
private void internalPlayerListingChanged(final PlayerListing listing) {
SwingUtilities
.invokeLater(() -> m_gameSelectorModel.clearDataButKeepGameInfo(listing.getGameName(), listing.getGameRound(),
listing.getGameVersion().toString()));
synchronized (this) {
m_playersToNodes = listing.getPlayerToNodeListing();
m_playersEnabledListing = listing.getPlayersEnabledListing();
m_playersAllowedToBeDisabled = listing.getPlayersAllowedToBeDisabled();
m_playerNamesAndAlliancesInTurnOrder = listing.getPlayerNamesAndAlliancesInTurnOrderLinkedHashMap();
}
SwingUtilities.invokeLater(() -> m_listener.playerListChanged());
}
public Map<String, String> getPlayerToNodesMapping() {
synchronized (this) {
return new HashMap<>(m_playersToNodes);
}
}
public Map<String, Boolean> getPlayersEnabledListing() {
synchronized (this) {
return new HashMap<>(m_playersEnabledListing);
}
}
public Collection<String> getPlayersAllowedToBeDisabled() {
synchronized (this) {
return new HashSet<>(m_playersAllowedToBeDisabled);
}
}
public Map<String, Collection<String>> getPlayerNamesAndAlliancesInTurnOrderLinkedHashMap() {
synchronized (this) {
return new LinkedHashMap<>(m_playerNamesAndAlliancesInTurnOrder);
}
}
public IClientMessenger getMessenger() {
return m_messenger;
}
public IServerStartupRemote getServerStartupRemote() {
return getServerStartup();
}
@Override
public void messengerInvalid(final IMessenger messenger, final Exception reason) {
// The self chat disconnect notification is simply so we have an on-screen notification of the disconnect.
// In case for example there are many game windows open, it may not be clear which game disconnected.
MainFrame.getInstance().getChat().sendMessage("*** Was Disconnected ***", false);
EventThreadJOptionPane.showMessageDialog(m_ui, "Connection to game host lost.\nPlease save and restart.",
"Connection Lost!", JOptionPane.ERROR_MESSAGE, new CountDownLatchHandler(true));
}
public IChatPanel getChatPanel() {
return m_chatPanel;
}
public boolean getIsServerHeadlessTest() {
final IServerStartupRemote serverRemote = getServerStartup();
if (serverRemote != null) {
m_hostIsHeadlessBot = serverRemote.getIsServerHeadless();
} else {
m_hostIsHeadlessBot = false;
}
return m_hostIsHeadlessBot;
}
public boolean getIsServerHeadlessCached() {
return m_hostIsHeadlessBot;
}
public Action getHostBotSetMapClientAction(final Component parent) {
return new SetMapClientAction(parent, getMessenger(), getAvailableServerGames());
}
public Action getHostBotChangeGameOptionsClientAction(final Component parent) {
return new ChangeGameOptionsClientAction(parent, getServerStartupRemote());
}
public Action getHostBotChangeGameToSaveGameClientAction(final Component parent) {
return new ChangeGameToSaveGameClientAction(parent, getMessenger());
}
public Action getHostBotChangeToAutosaveClientAction(final Component parent,
final SaveGameFileChooser.AUTOSAVE_TYPE autosaveType) {
return new ChangeToAutosaveClientAction(parent, getMessenger(), autosaveType);
}
public Action getHostBotGetGameSaveClientAction(final Component parent) {
return new GetGameSaveClientAction(parent, getServerStartupRemote());
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append("ClientModel GameData:").append(m_gameDataOnStartup == null ? "null" : m_gameDataOnStartup.getGameName())
.append("\n");
sb.append("Connected:").append(m_messenger == null ? "null" : m_messenger.isConnected()).append("\n");
sb.append(m_messenger);
sb.append("\n");
sb.append(m_remoteMessenger);
sb.append("\n");
sb.append(m_channelMessenger);
return sb.toString();
}
}
class ClientProps {
private int port;
private String name;
private String host;
public String getHost() {
return host;
}
public void setHost(final String host) {
this.host = host;
}
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;
}
}