package com.faforever.client.remote; import com.faforever.client.config.CacheNames; import com.faforever.client.connectivity.ConnectivityState; import com.faforever.client.game.Faction; import com.faforever.client.game.NewGameInfo; import com.faforever.client.i18n.I18n; import com.faforever.client.legacy.UidService; import com.faforever.client.login.LoginFailedException; import com.faforever.client.net.ConnectionState; import com.faforever.client.notification.ImmediateNotification; import com.faforever.client.notification.NotificationService; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.rankedmatch.SearchRanked1V1ClientMessage; import com.faforever.client.rankedmatch.StopSearchRanked1V1ClientMessage; import com.faforever.client.relay.GpgClientMessage; import com.faforever.client.relay.GpgClientMessageSerializer; import com.faforever.client.relay.GpgServerMessageType; import com.faforever.client.remote.domain.AddFoeMessage; import com.faforever.client.remote.domain.AddFriendMessage; import com.faforever.client.remote.domain.AuthenticationFailedMessage; import com.faforever.client.remote.domain.Avatar; import com.faforever.client.remote.domain.AvatarMessage; import com.faforever.client.remote.domain.ClientMessage; import com.faforever.client.remote.domain.ClientMessageType; import com.faforever.client.remote.domain.FafServerMessageType; import com.faforever.client.remote.domain.GameAccess; import com.faforever.client.remote.domain.GameLaunchMessage; import com.faforever.client.remote.domain.GameState; import com.faforever.client.remote.domain.HostGameMessage; import com.faforever.client.remote.domain.InitSessionMessage; import com.faforever.client.remote.domain.JoinGameMessage; import com.faforever.client.remote.domain.ListPersonalAvatarsMessage; import com.faforever.client.remote.domain.LoginClientMessage; import com.faforever.client.remote.domain.LoginMessage; import com.faforever.client.remote.domain.MessageTarget; import com.faforever.client.remote.domain.NoticeMessage; import com.faforever.client.remote.domain.RatingRange; import com.faforever.client.remote.domain.RemoveFoeMessage; import com.faforever.client.remote.domain.RemoveFriendMessage; import com.faforever.client.remote.domain.SelectAvatarMessage; import com.faforever.client.remote.domain.SerializableMessage; import com.faforever.client.remote.domain.ServerCommand; import com.faforever.client.remote.domain.ServerMessage; import com.faforever.client.remote.domain.SessionMessage; import com.faforever.client.remote.domain.VictoryCondition; import com.faforever.client.remote.gson.ClientMessageTypeTypeAdapter; import com.faforever.client.remote.gson.ConnectivityStateTypeAdapter; import com.faforever.client.remote.gson.GameAccessTypeAdapter; import com.faforever.client.remote.gson.GameStateTypeAdapter; import com.faforever.client.remote.gson.GpgServerMessageTypeTypeAdapter; import com.faforever.client.remote.gson.InetSocketAddressTypeAdapter; import com.faforever.client.remote.gson.InitConnectivityTestMessage; import com.faforever.client.remote.gson.MessageTargetTypeAdapter; import com.faforever.client.remote.gson.RatingRangeTypeAdapter; import com.faforever.client.remote.gson.ServerMessageTypeAdapter; import com.faforever.client.remote.gson.ServerMessageTypeTypeAdapter; import com.faforever.client.remote.gson.VictoryConditionTypeAdapter; import com.faforever.client.update.ClientUpdateService; import com.github.nocatch.NoCatch; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Task; import org.apache.commons.compress.utils.IOUtils; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import javax.annotation.PreDestroy; import javax.annotation.Resource; import java.io.IOException; import java.io.OutputStream; import java.lang.invoke.MethodHandles; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URL; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static com.faforever.client.util.ConcurrentUtil.executeInBackground; public class FafServerAccessorImpl extends AbstractServerAccessor implements FafServerAccessor { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final long RECONNECT_DELAY = 3000; private final Gson gson; private final HashMap<Class<? extends ServerMessage>, Collection<Consumer<ServerMessage>>> messageListeners; @Resource PreferencesService preferencesService; @Resource UidService uidService; @Resource ClientUpdateService clientUpdateService; @Resource NotificationService notificationService; @Resource I18n i18n; @Value("${lobby.host}") String lobbyHost; @Value("${lobby.port}") int lobbyPort; private Task<Void> fafConnectionTask; private String localIp; private ServerWriter serverWriter; private CompletableFuture<LoginMessage> loginFuture; private CompletableFuture<SessionMessage> sessionFuture; private CompletableFuture<GameLaunchMessage> gameLaunchFuture; private ObjectProperty<Long> sessionId; private StringProperty login; private String username; private String password; private ObjectProperty<ConnectionState> connectionState; private Socket fafServerSocket; private CompletableFuture<List<Avatar>> avatarsFuture; public FafServerAccessorImpl() { messageListeners = new HashMap<>(); connectionState = new SimpleObjectProperty<>(); sessionId = new SimpleObjectProperty<>(); login = new SimpleStringProperty(); // TODO note to myself; seriously, create a single gson instance (or builder) and put it all there gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(VictoryCondition.class, VictoryConditionTypeAdapter.INSTANCE) .registerTypeAdapter(GameState.class, GameStateTypeAdapter.INSTANCE) .registerTypeAdapter(GameAccess.class, GameAccessTypeAdapter.INSTANCE) .registerTypeAdapter(ClientMessageType.class, ClientMessageTypeTypeAdapter.INSTANCE) .registerTypeAdapter(FafServerMessageType.class, ServerMessageTypeTypeAdapter.INSTANCE) .registerTypeAdapter(GpgServerMessageType.class, GpgServerMessageTypeTypeAdapter.INSTANCE) .registerTypeAdapter(MessageTarget.class, MessageTargetTypeAdapter.INSTANCE) .registerTypeAdapter(ServerMessage.class, ServerMessageTypeAdapter.INSTANCE) .registerTypeAdapter(ConnectivityState.class, ConnectivityStateTypeAdapter.INSTANCE) .registerTypeAdapter(InetSocketAddress.class, InetSocketAddressTypeAdapter.INSTANCE) .registerTypeAdapter(RatingRange.class, RatingRangeTypeAdapter.INSTANCE) .create(); addOnMessageListener(NoticeMessage.class, this::onNotice); addOnMessageListener(SessionMessage.class, this::onSessionInitiated); addOnMessageListener(LoginMessage.class, this::onFafLoginSucceeded); addOnMessageListener(GameLaunchMessage.class, this::onGameLaunchInfo); addOnMessageListener(AuthenticationFailedMessage.class, this::dispatchAuthenticationFailed); addOnMessageListener(AvatarMessage.class, this::onAvatarMessage); } private void onAvatarMessage(AvatarMessage avatarMessage) { avatarsFuture.complete(avatarMessage.getAvatarList()); } private void onNotice(NoticeMessage noticeMessage) { if (noticeMessage.getText() == null) { return; } notificationService.addNotification(new ImmediateNotification(i18n.get("messageFromServer"), noticeMessage.getText(), noticeMessage.getSeverity())); } @Override @SuppressWarnings("unchecked") public <T extends ServerMessage> void addOnMessageListener(Class<T> type, Consumer<T> listener) { if (!messageListeners.containsKey(type)) { messageListeners.put(type, new LinkedList<>()); } messageListeners.get(type).add((Consumer<ServerMessage>) listener); } @Override @SuppressWarnings("unchecked") public <T extends ServerMessage> void removeOnMessageListener(Class<T> type, Consumer<T> listener) { messageListeners.get(type).remove(listener); } @Override public ReadOnlyObjectProperty<ConnectionState> connectionStateProperty() { return connectionState; } @Override public CompletionStage<LoginMessage> connectAndLogIn(String username, String password) { sessionFuture = new CompletableFuture<>(); loginFuture = new CompletableFuture<>(); this.username = username; this.password = password; // TODO extract class? fafConnectionTask = new Task<Void>() { @Override protected Void call() throws Exception { while (!isCancelled()) { logger.info("Trying to connect to FAF server at {}:{}", lobbyHost, lobbyPort); connectionState.set(ConnectionState.CONNECTING); try (Socket fafServerSocket = new Socket(lobbyHost, lobbyPort); OutputStream outputStream = fafServerSocket.getOutputStream()) { FafServerAccessorImpl.this.fafServerSocket = fafServerSocket; fafServerSocket.setKeepAlive(true); localIp = fafServerSocket.getLocalAddress().getHostAddress(); serverWriter = createServerWriter(outputStream); String version = clientUpdateService.getCurrentVersion().toString(); writeToServer(new InitSessionMessage(version)); logger.info("FAF server connection established"); connectionState.set(ConnectionState.CONNECTED); blockingReadServer(fafServerSocket); } catch (IOException e) { connectionState.set(ConnectionState.DISCONNECTED); if (isCancelled()) { logger.debug("Connection to FAF server has been closed"); } else { logger.warn("Lost connection to FAF server, trying to reconnect in " + RECONNECT_DELAY / 1000 + "s", e); Thread.sleep(RECONNECT_DELAY); } } } return null; } @Override protected void cancelled() { IOUtils.closeQuietly(serverWriter); IOUtils.closeQuietly(fafServerSocket); logger.debug("Closed connection to FAF lobby server"); } }; executeInBackground(fafConnectionTask); return loginFuture; } @Override public CompletionStage<GameLaunchMessage> requestHostGame(NewGameInfo newGameInfo, @Nullable InetSocketAddress relayAddress, int externalPort) { HostGameMessage hostGameMessage = new HostGameMessage( StringUtils.isEmpty(newGameInfo.getPassword()) ? GameAccess.PUBLIC : GameAccess.PASSWORD, newGameInfo.getMap(), newGameInfo.getTitle(), externalPort, new boolean[0], newGameInfo.getGameType(), newGameInfo.getPassword(), null, relayAddress ); gameLaunchFuture = new CompletableFuture<>(); writeToServer(hostGameMessage); return gameLaunchFuture; } @Override public CompletionStage<GameLaunchMessage> requestJoinGame(int gameId, String password, @Nullable InetSocketAddress relayAddress, int externalPort) { JoinGameMessage joinGameMessage = new JoinGameMessage( gameId, externalPort, password, relayAddress); gameLaunchFuture = new CompletableFuture<>(); writeToServer(joinGameMessage); return gameLaunchFuture; } @Override @PreDestroy public void disconnect() { if (fafConnectionTask != null) { fafConnectionTask.cancel(true); } } @Override public void reconnect() { IOUtils.closeQuietly(fafServerSocket); } @Override public void addFriend(int playerId) { writeToServer(new AddFriendMessage(playerId)); } @Override public void addFoe(int playerId) { writeToServer(new AddFoeMessage(playerId)); } @Override public CompletionStage<GameLaunchMessage> startSearchRanked1v1(Faction faction, int gamePort, @Nullable InetSocketAddress relayAddress) { gameLaunchFuture = new CompletableFuture<>(); writeToServer(new SearchRanked1V1ClientMessage(gamePort, faction, relayAddress)); return gameLaunchFuture; } @Override public void stopSearchingRanked() { writeToServer(new StopSearchRanked1V1ClientMessage()); gameLaunchFuture = null; } @Override public Long getSessionId() { return sessionId.get(); } @Override public void sendGpgMessage(GpgClientMessage message) { writeToServer(message); } @Override public void initConnectivityTest(int port) { writeToServer(new InitConnectivityTestMessage(port)); } @Override public void removeFriend(int playerId) { writeToServer(new RemoveFriendMessage(playerId)); } @Override public void removeFoe(int playerId) { writeToServer(new RemoveFoeMessage(playerId)); } @Override public void selectAvatar(URL url) { writeToServer(new SelectAvatarMessage(url)); } @Override @Cacheable(CacheNames.AVAILABLE_AVATARS) public List<Avatar> getAvailableAvatars() { avatarsFuture = new CompletableFuture<>(); writeToServer(new ListPersonalAvatarsMessage()); return NoCatch.noCatch(() -> avatarsFuture.get(10, TimeUnit.SECONDS)); } private ServerWriter createServerWriter(OutputStream outputStream) throws IOException { ServerWriter serverWriter = new ServerWriter(outputStream); serverWriter.registerMessageSerializer(new ClientMessageSerializer(login, sessionId), ClientMessage.class); serverWriter.registerMessageSerializer(new PongMessageSerializer(login, sessionId), PongMessage.class); serverWriter.registerMessageSerializer(new StringSerializer(), String.class); serverWriter.registerMessageSerializer(new GpgClientMessageSerializer(), GpgClientMessage.class); return serverWriter; } private void writeToServer(SerializableMessage message) { serverWriter.write(message); } public void onServerMessage(String message) throws IOException { ServerCommand serverCommand = ServerCommand.fromString(message); if (serverCommand != null) { dispatchServerMessage(serverCommand); } else { parseServerObject(message); } } private void dispatchServerMessage(ServerCommand serverCommand) throws IOException { switch (serverCommand) { case PING: logger.debug("Server PINGed"); onServerPing(); break; default: logger.warn("Unknown server response: {}", serverCommand); } } private void parseServerObject(String jsonString) { try { ServerMessage serverMessage = gson.fromJson(jsonString, ServerMessage.class); Class<?> messageClass = serverMessage.getClass(); while (messageClass != Object.class) { messageListeners.getOrDefault(messageClass, Collections.emptyList()) .forEach(consumer -> consumer.accept(serverMessage)); messageClass = messageClass.getSuperclass(); } for (Class<?> type : ClassUtils.getAllInterfacesForClassAsSet(messageClass)) { messageListeners.getOrDefault(messageClass, Collections.emptyList()) .forEach(consumer -> consumer.accept(serverMessage)); } } catch (JsonSyntaxException e) { logger.warn("Could not deserialize message: " + jsonString, e); } } private void onServerPing() { writeToServer(new PongMessage()); } private void dispatchAuthenticationFailed(AuthenticationFailedMessage message) { fafConnectionTask.cancel(); loginFuture.completeExceptionally(new LoginFailedException(message.getText())); loginFuture = null; } private void onFafLoginSucceeded(LoginMessage loginServerMessage) { logger.info("FAF login succeeded"); if (loginFuture != null) { loginFuture.complete(loginServerMessage); loginFuture = null; } } private void onSessionInitiated(SessionMessage sessionMessage) { logger.info("FAF session initiated, session ID: {}", sessionMessage.getSession()); this.sessionId.set(sessionMessage.getSession()); sessionFuture.complete(sessionMessage); logIn(username, password); } private void logIn(String username, String password) { String uniqueId = uidService.generate(String.valueOf(sessionId.get()), preferencesService.getFafDataDirectory().resolve("uid.log")); writeToServer(new LoginClientMessage(username, password, sessionId.get(), uniqueId, localIp)); } private void onGameLaunchInfo(GameLaunchMessage gameLaunchMessage) { gameLaunchFuture.complete(gameLaunchMessage); gameLaunchFuture = null; } }