package com.faforever.client.game; import com.faforever.client.connectivity.ConnectivityService; import com.faforever.client.connectivity.ConnectivityState; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.fx.StringListCell; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapBean; import com.faforever.client.map.MapService; import com.faforever.client.map.MapSize; import com.faforever.client.mod.ModInfoBean; import com.faforever.client.mod.ModService; import com.faforever.client.notification.ImmediateNotification; import com.faforever.client.notification.NotificationService; import com.faforever.client.notification.ReportAction; import com.faforever.client.notification.Severity; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.reporting.ReportingService; import com.faforever.client.theme.ThemeService; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.SelectionMode; import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseEvent; import javafx.stage.Popup; import javafx.stage.PopupWindow; import javafx.stage.Window; import javafx.util.Callback; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.lang.invoke.MethodHandles; import java.util.Collections; import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; public class CreateGameController { public static final int MAX_RATING_LENGTH = 4; private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @FXML Label mapSizeLabel; @FXML Label mapPlayersLabel; @FXML Label mapDescriptionLabel; @FXML Label mapNameLabel; @FXML TextField mapSearchTextField; @FXML ImageView mapImageView; @FXML TextField titleTextField; @FXML ListView<ModInfoBean> modListView; @FXML TextField passwordTextField; @FXML TextField minRankingTextField; @FXML TextField maxRankingTextField; @FXML ListView<GameTypeBean> gameTypeListView; @FXML ListView<MapBean> mapListView; @FXML Node createGameRoot; @FXML Button createGameButton; @Resource Environment environment; @Resource MapService mapService; @Resource ModService modService; @Resource GameService gameService; @Resource PreferencesService preferencesService; @Resource I18n i18n; @Resource Locale locale; @VisibleForTesting FilteredList<MapBean> filteredMapBeans; @Resource ThemeService themeService; @Resource NotificationService notificationService; @Resource ReportingService reportingService; @Resource ConnectivityService connectivityService; private Popup createGamePopup; @FXML void initialize() { mapSearchTextField.textProperty().addListener((observable, oldValue, newValue) -> { if (newValue.isEmpty()) { filteredMapBeans.setPredicate(mapInfoBean -> true); } else { filteredMapBeans.setPredicate(mapInfoBean -> mapInfoBean.getDisplayName().toLowerCase().contains(newValue.toLowerCase()) || mapInfoBean.getFolderName().toLowerCase().contains(newValue.toLowerCase())); } if (!filteredMapBeans.isEmpty()) { mapListView.getSelectionModel().select(0); } }); mapSearchTextField.setOnKeyPressed(event -> { MultipleSelectionModel<MapBean> selectionModel = mapListView.getSelectionModel(); int currentMapIndex = selectionModel.getSelectedIndex(); int newMapIndex = currentMapIndex; if (KeyCode.DOWN == event.getCode()) { if (filteredMapBeans.size() > currentMapIndex + 1) { newMapIndex++; } event.consume(); } else if (KeyCode.UP == event.getCode()) { if (currentMapIndex > 0) { newMapIndex--; } event.consume(); } selectionModel.select(newMapIndex); mapListView.scrollTo(newMapIndex); }); gameTypeListView.setCellFactory(param -> new StringListCell<>(GameTypeBean::getFullName)); JavaFxUtil.makeNumericTextField(minRankingTextField, MAX_RATING_LENGTH); JavaFxUtil.makeNumericTextField(maxRankingTextField, MAX_RATING_LENGTH); } @PostConstruct void postConstruct() { if (preferencesService.getPreferences().getForgedAlliance().getPath() == null) { preferencesService.addUpdateListener(preferences -> { if (preferencesService.getPreferences().getForgedAlliance().getPath() != null) { init(); } }); } else { init(); } } private void init() { initModList(); initMapSelection(); initGameTypeComboBox(); initRatingBoundaries(); selectLastMap(); setLastGameTitle(); titleTextField.textProperty().addListener((observable, oldValue, newValue) -> { preferencesService.getPreferences().setLastGameTitle(newValue); preferencesService.storeInBackground(); }); createGameButton.textProperty().bind(Bindings.createStringBinding(() -> { if (Strings.isNullOrEmpty(titleTextField.getText())) { return i18n.get("game.create.titleMissing"); } switch (connectivityService.getConnectivityState()) { case BLOCKED: return i18n.get("game.create.portUnreachable"); case RUNNING: case UNKNOWN: return i18n.get("game.create.connectivityCheckPending"); default: return i18n.get("game.create.create"); } }, titleTextField.textProperty(), connectivityService.connectivityStateProperty())); createGameButton.disableProperty().bind( titleTextField.textProperty().isEmpty() .or(connectivityService.connectivityStateProperty().isEqualTo(ConnectivityState.BLOCKED)) .or(connectivityService.connectivityStateProperty().isEqualTo(ConnectivityState.UNKNOWN)) ); } private void initModList() { modListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); modListView.setCellFactory(modListCellFactory()); modListView.setItems(modService.getInstalledMods()); } private void initMapSelection() { filteredMapBeans = new FilteredList<>(mapService.getInstalledMaps()); mapListView.setItems(filteredMapBeans); mapListView.setCellFactory(param -> new StringListCell<>(MapBean::getDisplayName)); mapListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) { Platform.runLater(() -> mapNameLabel.setText("")); return; } preferencesService.getPreferences().setLastMap(newValue.getFolderName()); preferencesService.storeInBackground(); Image largePreview = mapService.loadLargePreview(newValue.getFolderName()); if (largePreview == null) { new Image(themeService.getThemeFile(ThemeService.UNKNOWN_MAP_IMAGE), true); } MapSize mapSize = newValue.getSize(); mapImageView.setImage(largePreview); mapNameLabel.setText(newValue.getDisplayName()); mapSizeLabel.setText(i18n.get("mapPreview.size", mapSize.getWidth(), mapSize.getHeight())); mapPlayersLabel.setText(i18n.get("mapPreview.maxPlayers", newValue.getPlayers())); mapDescriptionLabel.setText(newValue.getDescription()); }); } private void initGameTypeComboBox() { gameService.addOnGameTypesChangeListener(change -> { gameTypeListView.getItems().add(change.getValueAdded()); selectLastOrDefaultGameType(); }); gameTypeListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { preferencesService.getPreferences().setLastGameType(newValue.getName()); preferencesService.storeInBackground(); }); } private void initRatingBoundaries() { int lastGameMinRating = preferencesService.getPreferences().getLastGameMinRating(); int lastGameMaxRating = preferencesService.getPreferences().getLastGameMaxRating(); minRankingTextField.setText(String.format(locale, "%d", lastGameMinRating)); maxRankingTextField.setText(String.format(locale, "%d", lastGameMaxRating)); minRankingTextField.textProperty().addListener((observable, oldValue, newValue) -> { preferencesService.getPreferences().setLastGameMinRating(Integer.parseInt(newValue)); preferencesService.storeInBackground(); }); maxRankingTextField.textProperty().addListener((observable, oldValue, newValue) -> { preferencesService.getPreferences().setLastGameMaxRating(Integer.parseInt(newValue)); preferencesService.storeInBackground(); }); } private void selectLastMap() { String lastMap = preferencesService.getPreferences().getLastMap(); for (MapBean mapBean : mapListView.getItems()) { if (mapBean.getFolderName().equalsIgnoreCase(lastMap)) { mapListView.getSelectionModel().select(mapBean); return; } } if (mapListView.getSelectionModel().isEmpty()) { mapListView.getSelectionModel().selectFirst(); } } private void setLastGameTitle() { titleTextField.setText(Strings.nullToEmpty(preferencesService.getPreferences().getLastGameTitle())); } @NotNull private Callback<ListView<ModInfoBean>, ListCell<ModInfoBean>> modListCellFactory() { return param -> { ListCell<ModInfoBean> cell = new StringListCell<>(ModInfoBean::getName); cell.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { modListView.requestFocus(); MultipleSelectionModel<ModInfoBean> selectionModel = modListView.getSelectionModel(); if (!cell.isEmpty()) { int index = cell.getIndex(); if (selectionModel.getSelectedIndices().contains(index)) { selectionModel.clearSelection(index); } else { selectionModel.select(index); } event.consume(); } }); return cell; }; } private void selectLastOrDefaultGameType() { String lastGameMod = preferencesService.getPreferences().getLastGameType(); if (lastGameMod == null) { lastGameMod = GameType.DEFAULT.getString(); } for (GameTypeBean mod : gameTypeListView.getItems()) { if (Objects.equals(mod.getName(), lastGameMod)) { gameTypeListView.getSelectionModel().select(mod); break; } } } @FXML void onRandomMapButtonClicked() { int mapIndex = (int) (Math.random() * filteredMapBeans.size()); mapListView.getSelectionModel().select(mapIndex); mapListView.scrollTo(mapIndex); } @FXML void onCreateButtonClicked() { ObservableList<ModInfoBean> selectedMods = modListView.getSelectionModel().getSelectedItems(); Set<String> simMods = selectedMods.stream() .map(ModInfoBean::getId) .collect(Collectors.toSet()); NewGameInfo newGameInfo = new NewGameInfo( titleTextField.getText(), Strings.emptyToNull(passwordTextField.getText()), gameTypeListView.getSelectionModel().getSelectedItem().getName(), mapListView.getSelectionModel().getSelectedItem().getFolderName(), simMods); gameService.hostGame(newGameInfo).exceptionally(throwable -> { logger.warn("Game could not be hosted", throwable); notificationService.addNotification( new ImmediateNotification( i18n.get("errorTitle"), i18n.get("game.create.failed"), Severity.WARN, throwable, Collections.singletonList(new ReportAction(i18n, reportingService, throwable)))); return null; }); } public Node getRoot() { return createGameRoot; } @FXML void onSelectDefaultGameTypeButtonClicked(ActionEvent event) { for (GameTypeBean gameTypeBean : gameTypeListView.getItems()) { if (GameType.FAF.getString().equalsIgnoreCase(gameTypeBean.getName())) { gameTypeListView.getSelectionModel().select(gameTypeBean); return; } } } @FXML void onDeselectModsButtonClicked(ActionEvent event) { modListView.getSelectionModel().clearSelection(); } @FXML void onReloadModsButtonClicked(ActionEvent event) { modService.loadInstalledMods(); } public void show(Window window, double minX, double maxY) { createGamePopup = new Popup(); createGamePopup.setAutoFix(false); createGamePopup.setAutoHide(true); createGamePopup.setAnchorLocation(PopupWindow.AnchorLocation.CONTENT_TOP_LEFT); createGamePopup.getContent().setAll(createGameRoot); createGamePopup.show(window, minX, maxY); } }