diff --git a/src/main/java/com/faforever/client/chat/ChatController.java b/src/main/java/com/faforever/client/chat/ChatController.java index 1a70898be..832fd38ad 100644 --- a/src/main/java/com/faforever/client/chat/ChatController.java +++ b/src/main/java/com/faforever/client/chat/ChatController.java @@ -48,7 +48,7 @@ public class ChatController extends AbstractViewController { private final EventBus eventBus; private final GameService gameService; public Node chatRoot; - public HBox chatContainer; + public VBox chatContainer; public TabPane tabPane; public Pane connectingProgressPane; public VBox noOpenTabsContainer; @@ -291,15 +291,13 @@ protected void onDisplay(NavigateEvent navigateEvent) { if (navigateEvent instanceof JoinChannelEvent) { String channelName = ((JoinChannelEvent) navigateEvent).getChannel(); chatService.joinChannel(channelName); - AbstractChatTabController controller = nameToChatTabController.get(channelName); if (controller != null) { this.tabPane.getSelectionModel().select(controller.getRoot()); } - - return; } - if (!tabPane.getTabs().isEmpty()) { + + else if (!tabPane.getTabs().isEmpty()) { Tab tab = tabPane.getSelectionModel().getSelectedItem(); nameToChatTabController.get(tab.getId()).onDisplay(); } diff --git a/src/main/java/com/faforever/client/chat/ChatUserItemController.java b/src/main/java/com/faforever/client/chat/ChatUserItemController.java index 37063ccfd..8f418832e 100644 --- a/src/main/java/com/faforever/client/chat/ChatUserItemController.java +++ b/src/main/java/com/faforever/client/chat/ChatUserItemController.java @@ -9,7 +9,7 @@ import com.faforever.client.fx.PlatformService; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.player.Player; import com.faforever.client.player.PlayerService; import com.faforever.client.preferences.ChatPrefs; @@ -492,7 +492,7 @@ private void updateGameStatus() { playerStatusIndicator.setImage(getPlayerStatusIcon(player.getStatus())); playerMapImage.setVisible(true); if (player.getGame() != null) { - playerMapImage.setImage(mapService.loadPreview(player.getGame().getFeaturedMod(), player.getGame().getMapFolderName(), PreviewSize.SMALL)); + playerMapImage.setImage(mapService.loadPreview(player.getGame().getFeaturedMod(), player.getGame().getMapName(), PreviewType.MINI, 10)); } } } diff --git a/src/main/java/com/faforever/client/coop/CoopController.java b/src/main/java/com/faforever/client/coop/CoopController.java index ea8223b5d..b0e6ea088 100644 --- a/src/main/java/com/faforever/client/coop/CoopController.java +++ b/src/main/java/com/faforever/client/coop/CoopController.java @@ -15,11 +15,10 @@ import com.faforever.client.game.NewGameInfo; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.mod.ModService; import com.faforever.client.notification.ImmediateErrorNotification; import com.faforever.client.notification.NotificationService; -import com.faforever.client.remote.domain.GameStatus; import com.faforever.client.replay.ReplayService; import com.faforever.client.reporting.ReportingService; import com.faforever.client.theme.UiService; @@ -252,7 +251,7 @@ private CoopMission getSelectedMission() { private void setSelectedMission(CoopMission mission) { Platform.runLater(() -> { descriptionWebView.getEngine().loadContent(mission.getDescription()); - mapImageView.setImage(mapService.loadPreview(COOP.getTechnicalName(), mission.getMapFolderName(), PreviewSize.SMALL)); + mapImageView.setImage(mapService.loadPreview(COOP.getTechnicalName(), mission.getMapFolderName(), PreviewType.MINI, 10)); }); loadLeaderboard(); diff --git a/src/main/java/com/faforever/client/fa/MapTool.java b/src/main/java/com/faforever/client/fa/MapTool.java new file mode 100644 index 000000000..bd794977b --- /dev/null +++ b/src/main/java/com/faforever/client/fa/MapTool.java @@ -0,0 +1,122 @@ +package com.faforever.client.fa; + +import com.faforever.client.map.MapBean; +import com.faforever.client.map.MapBean.Type; +import com.faforever.client.map.MapService.PreviewType; +import com.faforever.client.map.MapSize; +import com.faforever.client.update.ClientConfiguration.Downloadable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.invoke.MethodHandles; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class MapTool { + + public static final Integer MAP_DETAIL_COLUMN_NAME = 0; + public static final Integer MAP_DETAIL_COLUMN_ARCHIVE = 1; + public static final Integer MAP_DETAIL_COLUMN_CRC = 2; + public static final Integer MAP_DETAIL_COLUMN_DESCRIPTION = 3; + public static final Integer MAP_DETAIL_COLUMN_SIZE = 4; + public static final Integer MAP_DETAIL_COLUMN_NUM_PLAYERS = 5; + public static final Integer MAP_DETAIL_COLUMN_WIND = 6; + public static final Integer MAP_DETAIL_COLUMN_TIDAL = 7; + public static final Integer MAP_DETAIL_COLUMN_GRAVITY = 8; + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + static public List listMaps(Path gamePath, String mapNameSpec, Path mapCacheDirectory, boolean doCrc) { + String nativeDir = System.getProperty("nativeDir", "lib"); + Path exe = Paths.get(nativeDir).resolve("gpgnet4ta").resolve("maptool.exe"); + Path workingDirectory = exe.getParent(); + + String QUOTED = "\"%s\""; + List command = new ArrayList<>(); + command.add(String.format(QUOTED, exe.toAbsolutePath())); + command.add("--gamepath"); + command.add(String.format(QUOTED, gamePath)); + if (mapCacheDirectory != null) { + command.add("--featurescachedir"); + command.add(String.format(QUOTED, mapCacheDirectory)); + } + if (mapNameSpec != null) { + command.add("--mapname"); + command.add(String.format(QUOTED, mapNameSpec)); + } + if (doCrc) { + command.add("--hash"); + } + + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.directory(workingDirectory.toFile()); + processBuilder.command(command); + logger.info("Enumerating maps: {}", String.join(" ", processBuilder.command())); + + List mapList = new ArrayList<>(); + try { + final String UNIT_SEPARATOR = Character.toString((char)0x1f); + Process process = processBuilder.start(); + BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = input.readLine()) != null) { + mapList.add(line.split(UNIT_SEPARATOR)); + } + input.close(); + } + catch (IOException e) + { + logger.error("unable to enumerate maps: {}", e.getMessage()); + } + + return mapList; + } + + public static void generatePreview(Path gamePath, String mapName, Path mapCacheDirectory, PreviewType previewType, int maxPositions) { + String nativeDir = System.getProperty("nativeDir", "lib"); + Path exe = Paths.get(nativeDir).resolve("gpgnet4ta").resolve("maptool.exe"); + Path workingDirectory = exe.getParent(); + + String QUOTED = "\"%s\""; + List command = new ArrayList<>(); + command.add(String.format(QUOTED, exe.toAbsolutePath())); + command.add("--gamepath"); + command.add(String.format(QUOTED, gamePath)); + command.add("--mapname"); + command.add(String.format(QUOTED, mapName + "$")); + command.add("--thumb"); + command.add(String.format(QUOTED, mapCacheDirectory)); + command.add("--featurescachedir"); + command.add(String.format(QUOTED, mapCacheDirectory)); + command.add("--thumbtypes"); + command.add(previewType.toString().toLowerCase()); + command.add("--maxpositions"); + command.add(String.valueOf(maxPositions)); + + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.directory(workingDirectory.toFile()); + processBuilder.command(command); + logger.info("Generating map preview: {}", String.join(" ", processBuilder.command())); + try { + Process process = processBuilder.start(); + process.waitFor(); + } + catch (IOException e) + { + logger.error("unable to generate preview: {}", e.getMessage()); + } + catch (InterruptedException e) + { + logger.error("unable to generate preview: {}", e.getMessage()); + } + } + +} diff --git a/src/main/java/com/faforever/client/fa/OnGameFullNotifier.java b/src/main/java/com/faforever/client/fa/OnGameFullNotifier.java index 5c3d92be9..47bb0b145 100644 --- a/src/main/java/com/faforever/client/fa/OnGameFullNotifier.java +++ b/src/main/java/com/faforever/client/fa/OnGameFullNotifier.java @@ -7,7 +7,7 @@ import com.faforever.client.game.GameService; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.notification.NotificationService; import com.faforever.client.notification.TransientNotification; import com.faforever.client.util.ProgrammingError; @@ -82,7 +82,7 @@ public void onGameFull(GameFullEvent event) { } notificationService.addNotification(new TransientNotification(i18n.get("game.full"), i18n.get("game.full.action"), - mapService.loadPreview(currentGame.getFeaturedMod(), currentGame.getMapFolderName(), PreviewSize.SMALL), + mapService.loadPreview(currentGame.getFeaturedMod(), currentGame.getMapName(), PreviewType.MINI, 10), v -> platformService.focusWindow(faWindowTitle))); } } diff --git a/src/main/java/com/faforever/client/game/CreateGameController.java b/src/main/java/com/faforever/client/game/CreateGameController.java index db6342086..a6cafb91e 100644 --- a/src/main/java/com/faforever/client/game/CreateGameController.java +++ b/src/main/java/com/faforever/client/game/CreateGameController.java @@ -9,7 +9,7 @@ import com.faforever.client.map.MapBean; import com.faforever.client.map.MapBean.Type; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.map.MapSize; import com.faforever.client.mod.FeaturedMod; import com.faforever.client.mod.ModManagerController; @@ -23,7 +23,8 @@ import com.faforever.client.remote.FafService; import com.faforever.client.reporting.ReportingService; import com.faforever.client.theme.UiService; -import com.faforever.client.ui.dialog.Dialog; +import com.faforever.client.ui.preferences.event.GameDirectoryChooseEvent; +import com.google.common.eventbus.EventBus; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import javafx.application.Platform; @@ -31,8 +32,10 @@ import javafx.collections.FXCollections; import javafx.collections.transformation.FilteredList; import javafx.css.PseudoClass; +import javafx.event.ActionEvent; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.MultipleSelectionModel; @@ -44,6 +47,8 @@ import javafx.scene.layout.BackgroundSize; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.maven.artifact.versioning.ComparableVersion; @@ -57,6 +62,7 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import static com.faforever.client.net.ConnectionState.CONNECTED; import static javafx.scene.layout.BackgroundPosition.CENTER; @@ -79,11 +85,11 @@ public class CreateGameController implements Controller { private final NotificationService notificationService; private final ReportingService reportingService; private final FafService fafService; - private final UiService uiService; + private final UiService uiService; + private final EventBus eventBus; public Label mapSizeLabel; public Label mapPlayersLabel; public Label mapDescriptionLabel; - public Label mapNameLabel; public ModManagerController modManagerController; public TextField mapSearchTextField; public TextField titleTextField; @@ -95,10 +101,13 @@ public class CreateGameController implements Controller { public StackPane gamesRoot; public Pane createGameRoot; public Button createGameButton; + public Button setGamePathButton; + public VBox mapPreview; public Pane mapPreviewPane; public Label versionLabel; + public ComboBox mapPreviewTypeComboBox; + public ComboBox mapPreviewMaxPositionsComboBox; public CheckBox onlyForFriendsCheckBox; - public Button generateMapButton; @VisibleForTesting FilteredList filteredMapBeans; private Runnable onCloseButtonClickedListener; @@ -111,13 +120,45 @@ public class CreateGameController implements Controller { public void initialize() { versionLabel.managedProperty().bind(versionLabel.visibleProperty()); + mapPreviewTypeComboBox.getItems().setAll(PreviewType.values()); + mapPreviewMaxPositionsComboBox.getItems().setAll(IntStream.rangeClosed(2,10).boxed().collect(Collectors.toList())); + mapPreviewTypeComboBox.getSelectionModel().select(0); + mapPreviewMaxPositionsComboBox.getSelectionModel().select(0); + + mapPreviewTypeComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(PreviewType previewType) { + return previewType == null ? "null" : previewType.getDisplayName(); + } + @Override + public PreviewType fromString(String string) { + throw new UnsupportedOperationException("Not supported"); + } + }); + mapPreviewMaxPositionsComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(Integer maxPositions) { + return String.valueOf(maxPositions); + } + @Override + public Integer fromString(String string) { + throw new UnsupportedOperationException("Not supported"); + } + }); + + mapPreviewTypeComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + setSelectedMap(mapListView.getSelectionModel().getSelectedItem(), newValue, mapPreviewMaxPositionsComboBox.getSelectionModel().getSelectedItem()); + }); + mapPreviewMaxPositionsComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + setSelectedMap(mapListView.getSelectionModel().getSelectedItem(), mapPreviewTypeComboBox.getSelectionModel().getSelectedItem(), newValue); + }); + mapPreviewPane.prefHeightProperty().bind(mapPreviewPane.widthProperty()); mapSearchTextField.textProperty().addListener((observable, oldValue, newValue) -> { if (newValue.isEmpty()) { filteredMapBeans.setPredicate(null); } else { - filteredMapBeans.setPredicate(mapInfoBean -> mapInfoBean.getDisplayName().toLowerCase().contains(newValue.toLowerCase()) - || mapInfoBean.getFolderName().toLowerCase().contains(newValue.toLowerCase())); + filteredMapBeans.setPredicate(mapInfoBean -> mapInfoBean.getMapName().toLowerCase().contains(newValue.toLowerCase())); } if (!filteredMapBeans.isEmpty()) { mapListView.getSelectionModel().select(0); @@ -155,6 +196,10 @@ public void initialize() { modService.getFeaturedMods().thenAccept(featuredModBeans -> Platform.runLater(() -> { featuredModListView.setItems(FXCollections.observableList(featuredModBeans).filtered(FeaturedMod::isVisible)); + featuredModListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> Platform.runLater(() -> { + setAvailableMaps(newValue.getTechnicalName()); + selectLastMap(); + })); selectLastOrDefaultGameType(); })); @@ -186,7 +231,7 @@ public void onCloseButtonClicked() { private void init() { bindGameVisibility(); - initMapSelection(); + initMapSelection(KnownFeaturedMod.DEFAULT.getBaseGameName()); initFeaturedModList(); initRatingBoundaries(); selectLastMap(); @@ -243,25 +288,41 @@ private void bindGameVisibility() { onlyForFriendsCheckBox.selectedProperty().addListener(observable -> preferencesService.storeInBackground()); } - protected void initMapSelection() { + protected void initMapSelection(String modTechnical) { + setAvailableMaps(modTechnical); + mapListView.setCellFactory(param -> new StringListCell<>(MapBean::getMapName)); + mapListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> Platform.runLater(() -> { + PreviewType previewType = mapPreviewTypeComboBox.getSelectionModel().getSelectedItem(); + Integer maxPositions = mapPreviewMaxPositionsComboBox.getSelectionModel().getSelectedItem(); + setSelectedMap(newValue, previewType, maxPositions); + })); + } + + protected void setAvailableMaps(String modTechnical) { filteredMapBeans = new FilteredList<>( - mapService.getInstalledMaps().filtered(mapBean -> mapBean.getType() == Type.SKIRMISH).sorted((o1, o2) -> o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName())) + mapService.getInstalledMaps(modTechnical).filtered(mapBean -> mapBean.getType() == Type.SKIRMISH).sorted((o1, o2) -> o1.getMapName().compareToIgnoreCase(o2.getMapName())) ); mapListView.setItems(filteredMapBeans); - mapListView.setCellFactory(param -> new StringListCell<>(MapBean::getDisplayName)); - mapListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> Platform.runLater(() -> setSelectedMap(newValue))); + + if (filteredMapBeans.isEmpty()) { + this.setGamePathButton.setStyle(String.format("-fx-background-color: -fx-accent")); + } + else { + this.setGamePathButton.setStyle(String.format("-fx-background-color: -fx-background")); + } } - protected void setSelectedMap(MapBean newValue) { + protected void setSelectedMap(MapBean newValue, PreviewType previewType, int maxNumPlayers) { JavaFxUtil.assertApplicationThread(); if (newValue == null) { - mapNameLabel.setText(""); + mapPreview.setVisible(false); return; } + mapPreview.setVisible(true); - preferencesService.getPreferences().getLastGamePrefs().setLastMap(newValue.getFolderName()); + preferencesService.getPreferences().getLastGamePrefs().setLastMap(newValue.getMapName()); preferencesService.storeInBackground(); String activeMod = KnownFeaturedMod.DEFAULT.getTechnicalName(); @@ -270,13 +331,12 @@ protected void setSelectedMap(MapBean newValue) { activeMod = featuredModListView.getFocusModel().getFocusedItem().getTechnicalName(); } - Image largePreview = mapService.loadPreview(activeMod, newValue.getFolderName(), PreviewSize.LARGE); + Image largePreview = mapService.loadPreview(activeMod, newValue.getMapName(), previewType, maxNumPlayers); mapPreviewPane.setBackground(new Background(new BackgroundImage(largePreview, NO_REPEAT, NO_REPEAT, CENTER, new BackgroundSize(BackgroundSize.AUTO, BackgroundSize.AUTO, false, false, true, false)))); MapSize mapSize = newValue.getSize(); mapSizeLabel.setText(i18n.get("mapPreview.size", mapSize.getWidthInKm(), mapSize.getHeightInKm())); - mapNameLabel.setText(newValue.getDisplayName()); mapPlayersLabel.setText(i18n.number(newValue.getPlayers())); mapDescriptionLabel.setText(Optional.ofNullable(newValue.getDescription()) .map(Strings::emptyToNull) @@ -318,14 +378,19 @@ private void initRatingBoundaries() { private void selectLastMap() { String lastMap = preferencesService.getPreferences().getLastGamePrefs().getLastMap(); for (MapBean mapBean : mapListView.getItems()) { - if (mapBean.getFolderName().equalsIgnoreCase(lastMap)) { + if (mapBean.getMapName().equalsIgnoreCase(lastMap)) { mapListView.getSelectionModel().select(mapBean); mapListView.scrollTo(mapBean); return; } } - if (mapListView.getSelectionModel().isEmpty()) { + + if (mapListView.getItems().isEmpty()) { + setSelectedMap(null, null, 10); + } + else { mapListView.getSelectionModel().selectFirst(); + mapListView.scrollTo(0); } } @@ -354,14 +419,6 @@ public void onRandomMapButtonClicked() { mapListView.scrollTo(mapIndex); } - public void onGenerateMapButtonClicked() { - onGenerateMap(); - } - - private void onGenerateMap() { - - } - public void onCreateButtonClicked() { Set mods = modManagerController.apply().stream() .map(ModVersion::getUid) @@ -371,7 +428,7 @@ public void onCreateButtonClicked() { titleTextField.getText(), Strings.emptyToNull(passwordTextField.getText()), featuredModListView.getSelectionModel().getSelectedItem(), - mapListView.getSelectionModel().getSelectedItem().getFolderName(), + mapListView.getSelectionModel().getSelectedItem().getMapName(), mods, onlyForFriendsCheckBox.isSelected() ? GameVisibility.PRIVATE : GameVisibility.PUBLIC); @@ -401,8 +458,8 @@ public void setGamesRoot(StackPane root) { /** * @return returns true of the map was found and false if not */ - boolean selectMap(String mapFolderName) { - Optional mapBeanOptional = mapListView.getItems().stream().filter(mapBean -> mapBean.getFolderName().equalsIgnoreCase(mapFolderName)).findAny(); + boolean selectMap(String mapName) { + Optional mapBeanOptional = mapListView.getItems().stream().filter(mapBean -> mapBean.getMapName().equalsIgnoreCase(mapName)).findAny(); if (!mapBeanOptional.isPresent()) { return false; } @@ -414,4 +471,12 @@ boolean selectMap(String mapFolderName) { void setOnCloseButtonClickedListener(Runnable onCloseButtonClickedListener) { this.onCloseButtonClickedListener = onCloseButtonClickedListener; } + + public void onSetGamePathClicked(ActionEvent actionEvent) { + String modTechnical = KnownFeaturedMod.DEFAULT.getTechnicalName(); + if (featuredModListView.getSelectionModel().getSelectedItem() != null) { + modTechnical = featuredModListView.getSelectionModel().getSelectedItem().getTechnicalName(); + } + eventBus.post(new GameDirectoryChooseEvent(modTechnical)); + } } diff --git a/src/main/java/com/faforever/client/game/Game.java b/src/main/java/com/faforever/client/game/Game.java index 10928f677..4763f97e6 100644 --- a/src/main/java/com/faforever/client/game/Game.java +++ b/src/main/java/com/faforever/client/game/Game.java @@ -23,7 +23,9 @@ public class Game { private final StringProperty host; private final StringProperty title; - private final StringProperty mapFolderName; + private final StringProperty mapName; + private final StringProperty mapCrc; + private final StringProperty mapArchiveName; private final StringProperty featuredMod; private final IntegerProperty id; private final IntegerProperty numPlayers; @@ -51,7 +53,9 @@ public Game() { id = new SimpleIntegerProperty(); host = new SimpleStringProperty(); title = new SimpleStringProperty(); - mapFolderName = new SimpleStringProperty(); + mapName = new SimpleStringProperty(); + mapCrc = new SimpleStringProperty(); + mapArchiveName = new SimpleStringProperty(); featuredMod = new SimpleStringProperty(); numPlayers = new SimpleIntegerProperty(); maxPlayers = new SimpleIntegerProperty(); @@ -93,16 +97,40 @@ public StringProperty titleProperty() { return title; } - public String getMapFolderName() { - return mapFolderName.get(); + public String getMapName() { + return mapName.get(); } - public void setMapFolderName(String mapFolderName) { - this.mapFolderName.set(mapFolderName); + public void setMapName(String mapName) { + this.mapName.set(mapName); } - public StringProperty mapFolderNameProperty() { - return mapFolderName; + public StringProperty mapNameProperty() { + return mapName; + } + + public String getMapCrc() { + return mapCrc.get(); + } + + public void setMapCrc(String mapCrc) { + this.mapCrc.set(mapCrc); + } + + public StringProperty mapCrcProperty() { + return mapCrc; + } + + public String getMapArchiveName() { + return mapArchiveName.get(); + } + + public void setMapArchiveName(String mapArchiveName) { + this.mapArchiveName.set(mapArchiveName); + } + + public StringProperty mapArchiveNameProperty() { + return mapArchiveName; } public String getFeaturedMod() { diff --git a/src/main/java/com/faforever/client/game/GameDetailController.java b/src/main/java/com/faforever/client/game/GameDetailController.java index 8e2df44af..cf27d5045 100644 --- a/src/main/java/com/faforever/client/game/GameDetailController.java +++ b/src/main/java/com/faforever/client/game/GameDetailController.java @@ -1,11 +1,12 @@ package com.faforever.client.game; +import com.faforever.client.chat.ChatService; import com.faforever.client.fx.Controller; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.i18n.I18n; import com.faforever.client.main.event.JoinChannelEvent; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.mod.ModService; import com.faforever.client.player.Player; import com.faforever.client.player.PlayerService; @@ -53,6 +54,7 @@ public class GameDetailController implements Controller { private final GameService gameService; private final PlayerService playerService; private final UiService uiService; + private final ChatService chatService; private final JoinGameHelper joinGameHelper; private final EventBus eventBus; @@ -84,14 +86,15 @@ public class GameDetailController implements Controller { public GameDetailController(I18n i18n, MapService mapService, ModService modService, GameService gameService, PlayerService playerService, - UiService uiService, JoinGameHelper joinGameHelper, - EventBus eventBus) { + UiService uiService, ChatService chatService, + JoinGameHelper joinGameHelper, EventBus eventBus) { this.i18n = i18n; this.mapService = mapService; this.modService = modService; this.gameService = gameService; this.playerService = playerService; this.uiService = uiService; + this.chatService = chatService; this.joinGameHelper = joinGameHelper; this.eventBus = eventBus; @@ -183,7 +186,7 @@ public void setGame(Game game) { gameTitleLabel.textProperty().bind(game.titleProperty()); hostLabel.textProperty().bind(game.hostProperty()); - mapLabel.textProperty().bind(game.mapFolderNameProperty()); + mapLabel.textProperty().bind(game.mapNameProperty()); gameStatusLabel.textProperty().bind(game.statusProperty().asString()); numberOfPlayersLabel.textProperty().bind(createStringBinding( () -> i18n.get("game.detail.players.format", game.getNumPlayers(), game.getMaxPlayers()), @@ -191,8 +194,8 @@ public void setGame(Game game) { game.maxPlayersProperty() )); mapImageView.imageProperty().bind(createObjectBinding( - () -> mapService.loadPreview(game.getFeaturedMod(), game.getMapFolderName(), PreviewSize.LARGE), - game.mapFolderNameProperty() + () -> mapService.loadPreview(game.getFeaturedMod(), game.getMapName(), PreviewType.MINI, 10), + game.mapNameProperty() )); featuredModInvalidationListener = observable -> modService.getFeaturedMod(game.getFeaturedMod()) @@ -239,7 +242,7 @@ public void onJoinButtonClicked(ActionEvent event) { public void onChatButtonClicked(ActionEvent event) { if (game.get() != null) { - String gameChannel = gameService.getInGameIrcChannel(game.get().getHost()); + String gameChannel = gameService.getInGameIrcChannel(game.get()); eventBus.post(new JoinChannelEvent(gameChannel)); } } @@ -247,6 +250,8 @@ public void onChatButtonClicked(ActionEvent event) public void onLeaveButtonClicked(ActionEvent event) { log.info("[onLeaveButtonClicked] killGame()"); gameService.killGame(); + String gameChannel = gameService.getInGameIrcChannel(game.get()); + this.chatService.leaveChannel(gameChannel); } public void onStartButtonClicked(ActionEvent event) { diff --git a/src/main/java/com/faforever/client/game/GameService.java b/src/main/java/com/faforever/client/game/GameService.java index 66371e187..87cebd009 100644 --- a/src/main/java/com/faforever/client/game/GameService.java +++ b/src/main/java/com/faforever/client/game/GameService.java @@ -59,6 +59,7 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.text.WordUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; @@ -338,14 +339,14 @@ public CompletableFuture hostGame(NewGameInfo newGameInfo) { } stopSearchLadder1v1(); - String inGameIrcUrl = getInGameIrcUrl(getCurrentPlayer().getUsername()); + String inGameChannel = getInGameIrcChannel(newGameInfo); + String inGameIrcUrl = getInGameIrcUrl(inGameChannel); boolean autoLaunch = preferencesService.getPreferences().getAutoLaunchEnabled(); return updateGameIfNecessary(newGameInfo.getFeaturedMod(), null, emptyMap(), newGameInfo.getSimMods()) - .thenCompose(aVoid -> downloadMapIfNecessary(newGameInfo.getMap())) .thenCompose(aVoid -> fafService.requestHostGame(newGameInfo)) .thenAccept(gameLaunchMessage -> startGame(modTechnicalName, gameLaunchMessage, gameLaunchMessage.getFaction(), RatingMode.GLOBAL, inGameIrcUrl, autoLaunch)) - .thenRun(() -> eventBus.post(new JoinChannelEvent(getInGameIrcChannel(getCurrentPlayer().getUsername())))); + .thenRun(() -> eventBus.post(new JoinChannelEvent(inGameChannel))); } public CompletableFuture joinGame(Game game, String password) { @@ -363,14 +364,15 @@ public CompletableFuture joinGame(Game game, String password) { stopSearchLadder1v1(); - String inGameIrcUrl = getInGameIrcUrl(game.getHost()); + String inGameIrcChannel = getInGameIrcChannel(game); + String inGameIrcUrl = getInGameIrcUrl(inGameIrcChannel); Map featuredModVersions = game.getFeaturedModVersions(); Set simModUIds = game.getSimMods().keySet(); return modService.getFeaturedMod(game.getFeaturedMod()) .thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, null, featuredModVersions, simModUIds)) - .thenCompose(aVoid -> downloadMapIfNecessary(game.getMapFolderName())) + .thenCompose(aVoid -> downloadMapIfNecessary(game.getFeaturedMod(), game.getMapName(), game.getMapArchiveName())) .thenCompose(aVoid -> fafService.requestJoinGame(game.getId(), password)) .thenAccept(gameLaunchMessage -> { synchronized (currentGame) { @@ -381,7 +383,7 @@ public CompletableFuture joinGame(Game game, String password) { } boolean autoLaunch = preferencesService.getPreferences().getAutoLaunchEnabled() && game.getStatus() == GameStatus.BATTLEROOM; startGame(game.getFeaturedMod(), gameLaunchMessage, null, RatingMode.GLOBAL, inGameIrcUrl, autoLaunch); - Platform.runLater(() -> eventBus.post(new JoinChannelEvent(getInGameIrcChannel(game.getHost())))); + Platform.runLater(() -> eventBus.post(new JoinChannelEvent(inGameIrcChannel))); }) .exceptionally(throwable -> { log.warn("Game could not be joined", throwable); @@ -390,12 +392,12 @@ public CompletableFuture joinGame(Game game, String password) { }); } - private CompletableFuture downloadMapIfNecessary(String mapFolderName) { - return CompletableFuture.completedFuture(null); -// if (mapService.isInstalled(mapFolderName)) { -// return completedFuture(null); -// } -// return mapService.download(mapFolderName); + private CompletableFuture downloadMapIfNecessary(String modTechnical, String mapName, String hpiArchiveName) { + if (mapService.isInstalled(modTechnical, mapName)) { + return completedFuture(null); + } + return mapService.download(modTechnical, hpiArchiveName) + ;//.thenAccept(aVoid -> noCatch(() -> mapService.loadInstalledMaps(modTechnical))); // should not be necessary because MapService has a directory watcher looking for new maps } /** @@ -415,7 +417,6 @@ public void runWithReplay(Path path, @Nullable Integer replayId, String featured modService.getFeaturedMod(featuredMod) .thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, version, modVersions, simMods)) - .thenCompose(aVoid -> downloadMapIfNecessary(mapName).handleAsync((ignoredResult, throwable) -> askWhetherToStartWithOutMap(throwable))) .thenRun(() -> { try { process = totalAnnihilationService.startReplay(featuredMod, path, replayId); @@ -491,7 +492,7 @@ public CompletableFuture runWithLiveReplay(URI replayUrl, Integer gameId, return modService.getFeaturedMod(gameType) .thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, null, modVersions, simModUids)) - .thenCompose(aVoid -> downloadMapIfNecessary(mapName)) + .thenCompose(aVoid -> downloadMapIfNecessary(modTechnicalName, mapName, gameBean.getMapArchiveName())) .thenRun(() -> noCatch(() -> { process = totalAnnihilationService.startReplay(modTechnicalName, replayUrl, gameId, getCurrentPlayer()); setGameRunning(true); @@ -508,13 +509,30 @@ public String getInGameIrcUserName(String playerName) { return playerName.replace(" ", "") + "[ingame]"; } - public String getInGameIrcChannel(String hostPlayerName) { - return "#" + getInGameIrcUserName(hostPlayerName); + public String getInGameIrcChannel(String host, String title) { + title = WordUtils.capitalizeFully(title, ' ', ',', ':').replaceAll("[ ,:]", ""); + host = String.format("[%s]", host); + String channelName = "#"+title+host; + if (channelName.length() > 32 && host.length() <= 32) { + channelName = channelName.substring(0,32-host.length()) + host; + } + else if (channelName.length() > 32) { + channelName = "#"+host; + } + return channelName; } - public String getInGameIrcUrl(String hostPlayerName) { + public String getInGameIrcChannel(Game game) { + return getInGameIrcChannel(game.getHost(), game.getTitle()); + } + + public String getInGameIrcChannel(NewGameInfo gameInfo) { + return getInGameIrcChannel(getCurrentPlayer().getUsername(), gameInfo.getTitle()); + } + + public String getInGameIrcUrl(String channel) { if (preferencesService.getPreferences().getIrcIntegrationEnabled()) { - return getInGameIrcUserName(getCurrentPlayer().getUsername()) + "@" + this.ircHostAndPort + "/" + getInGameIrcChannel(hostPlayerName); + return getInGameIrcUserName(getCurrentPlayer().getUsername()) + "@" + this.ircHostAndPort + "/" + channel; } else { @@ -557,7 +575,7 @@ public CompletableFuture startSearchLadder1v1(Faction faction) { .thenAccept(featuredModBean -> updateGameIfNecessary(featuredModBean, null, emptyMap(), emptySet())) .thenCompose(aVoid -> fafService.startSearchLadder1v1(faction)) .thenAccept((gameLaunchMessage) -> - downloadMapIfNecessary(gameLaunchMessage.getMapname()) + downloadMapIfNecessary(LADDER_1V1.getBaseGameName(), gameLaunchMessage.getMapname(), "") .thenRun(() -> { gameLaunchMessage.setArgs(new ArrayList<>(gameLaunchMessage.getArgs())); @@ -750,7 +768,7 @@ private void rehost() { game.getTitle(), game.getPassword(), featuredModBean, - game.getMapFolderName(), + game.getMapName(), new HashSet<>(game.getSimMods().values()) ))); } @@ -883,10 +901,11 @@ private double calcAverageRating(GameInfoMessage gameInfoMessage) { } private void updateFromGameInfo(GameInfoMessage gameInfoMessage, Game game) { + game.setId(gameInfoMessage.getUid()); game.setHost(gameInfoMessage.getHost()); game.setTitle(StringEscapeUtils.unescapeHtml4(gameInfoMessage.getTitle())); - game.setMapFolderName(gameInfoMessage.getMapname()); + game.setMapName(gameInfoMessage.getMapName()); game.setFeaturedMod(gameInfoMessage.getFeaturedMod()); game.setNumPlayers(gameInfoMessage.getNumPlayers()); game.setMaxPlayers(gameInfoMessage.getMaxPlayers()); @@ -896,6 +915,14 @@ private void updateFromGameInfo(GameInfoMessage gameInfoMessage, Game game) { game.setStatus(gameInfoMessage.getState()); game.setPasswordProtected(gameInfoMessage.getPasswordProtected()); + //String UnitSeparator = Character.toString((char)0x1f); + //String mapDetails[] = gameInfoMessage.getMapDetails().split(UnitSeparator); // determined by host: name,archive,crc,desc,size,numplayers,minwind-maxwind,tide,gravity + String mapFilePath[] = gameInfoMessage.getMapFilePath().split("/"); // determined by faf db: archive/name/crc + if (mapFilePath.length >= 3) { + game.setMapArchiveName(mapFilePath[0]); + game.setMapCrc(mapFilePath[2]); + } + game.setAverageRating(calcAverageRating(gameInfoMessage)); synchronized (game.getSimMods()) { diff --git a/src/main/java/com/faforever/client/game/GameTileController.java b/src/main/java/com/faforever/client/game/GameTileController.java index d462a08f2..e954a9485 100644 --- a/src/main/java/com/faforever/client/game/GameTileController.java +++ b/src/main/java/com/faforever/client/game/GameTileController.java @@ -1,12 +1,13 @@ package com.faforever.client.game; +import com.faforever.client.chat.ChatService; import com.faforever.client.fx.Controller; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.i18n.I18n; import com.faforever.client.main.event.JoinChannelEvent; import com.faforever.client.map.MapBean; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.mod.ModService; import com.faforever.client.player.Player; import com.faforever.client.player.PlayerService; @@ -24,7 +25,6 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.image.ImageView; -import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -55,6 +55,7 @@ public class GameTileController implements Controller { private final ModService modService; private final GameService gameService; private final PlayerService playerService; + private final ChatService chatService; private final EventBus eventBus; public Node lockIconLabel; public Label gameTypeLabel; @@ -135,13 +136,7 @@ public void setGame(Game game) { gameTitleLabel.textProperty().bind(game.titleProperty()); hostLabel.setText(game.getHost()); - StringBinding mapNameBinding = createStringBinding( - () -> mapService.getMapLocallyFromName(game.getMapFolderName()) - .map(MapBean::getDisplayName) - .orElse(game.getMapFolderName()), - game.mapFolderNameProperty()); - - JavaFxUtil.bind(gameMapLabel.textProperty(), mapNameBinding); + JavaFxUtil.bind(gameMapLabel.textProperty(), game.mapNameProperty()); numberOfPlayersLabel.textProperty().bind(createStringBinding( () -> i18n.get("game.players.format", game.getNumPlayers(), game.getMaxPlayers()), @@ -161,8 +156,8 @@ public void setGame(Game game) { // TODO display "unknown map" image first since loading may take a while mapImageView.imageProperty().bind(createObjectBinding( - () -> mapService.loadPreview(game.getFeaturedMod(), game.getMapFolderName(), PreviewSize.SMALL), - game.mapFolderNameProperty() + () -> mapService.loadPreview(game.getFeaturedMod(), game.getMapName(), PreviewType.MINI, 10), + game.mapNameProperty() )); lockIconLabel.visibleProperty().bind(game.passwordProtectedProperty()); @@ -196,7 +191,7 @@ public void onJoinButtonClicked(ActionEvent event) { public void onChatButtonClicked(ActionEvent event) { if (game != null) { - String gameChannel = gameService.getInGameIrcChannel(game.getHost()); + String gameChannel = gameService.getInGameIrcChannel(game); eventBus.post(new JoinChannelEvent(gameChannel)); } } @@ -204,6 +199,8 @@ public void onChatButtonClicked(ActionEvent event) public void onLeaveButtonClicked(ActionEvent event) { log.info("[onLeaveButtonClicked] killGame()"); gameService.killGame(); + String gameChannel = gameService.getInGameIrcChannel(game); + this.chatService.leaveChannel(gameChannel); } public void onStartButtonClicked(ActionEvent event) { diff --git a/src/main/java/com/faforever/client/game/GamesTableController.java b/src/main/java/com/faforever/client/game/GamesTableController.java index cceafa14f..abeb5088d 100644 --- a/src/main/java/com/faforever/client/game/GamesTableController.java +++ b/src/main/java/com/faforever/client/game/GamesTableController.java @@ -7,7 +7,7 @@ import com.faforever.client.fx.StringCell; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.remote.domain.GameStatus; import com.faforever.client.remote.domain.RatingRange; @@ -117,8 +117,8 @@ public void initializeGameTable(ObservableList games, Function Bindings.createObjectBinding( () -> mapService - .loadPreview(param.getValue().getFeaturedMod(), param.getValue().getMapFolderName(), PreviewSize.SMALL), - param.getValue().mapFolderNameProperty() + .loadPreview(param.getValue().getFeaturedMod(), param.getValue().getMapName(), PreviewType.MINI, 10), + param.getValue().mapNameProperty() )); gameTitleColumn.setCellValueFactory(param -> param.getValue().titleProperty()); @@ -147,7 +147,7 @@ public void initializeGameTable(ObservableList games, Function new StringCell<>(name -> name)); - coopMissionName.setCellValueFactory(param -> new SimpleObjectProperty<>(coopMissionNameProvider.apply(param.getValue().getMapFolderName()))); + coopMissionName.setCellValueFactory(param -> new SimpleObjectProperty<>(coopMissionNameProvider.apply(param.getValue().getMapName()))); } gamesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) diff --git a/src/main/java/com/faforever/client/map/DownloadMapTask.java b/src/main/java/com/faforever/client/map/DownloadMapTask.java index 0cf266065..73dfe2dd7 100644 --- a/src/main/java/com/faforever/client/map/DownloadMapTask.java +++ b/src/main/java/com/faforever/client/map/DownloadMapTask.java @@ -2,10 +2,14 @@ import com.faforever.client.fx.PlatformService; import com.faforever.client.i18n.I18n; +import com.faforever.client.io.DownloadService; +import com.faforever.client.notification.DismissAction; +import com.faforever.client.notification.ImmediateNotification; +import com.faforever.client.notification.NotificationService; +import com.faforever.client.notification.Severity; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.task.CompletableTask; import com.faforever.commons.io.Unzipper; -import org.bridj.Platform; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -13,8 +17,18 @@ import org.springframework.stereotype.Component; import javax.inject.Inject; +import java.io.IOException; +import java.io.InputStream; import java.lang.invoke.MethodHandles; import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.Objects; +import java.util.zip.CRC32; +import java.util.zip.Checksum; @Component @@ -25,22 +39,91 @@ public class DownloadMapTask extends CompletableTask { private final PlatformService platformService; private final PreferencesService preferencesService; + private final NotificationService notificationService; + private final DownloadService downloadService; private final I18n i18n; private URL mapUrl; - private String folderName; + private String hpiArchiveName; + private Path installationPath; @Inject - public DownloadMapTask(PlatformService platformService, PreferencesService preferencesService, I18n i18n) { + public DownloadMapTask(PlatformService platformService, PreferencesService preferencesService, + NotificationService notificationService, DownloadService downloadService, I18n i18n) { super(Priority.HIGH); this.platformService = platformService; this.preferencesService = preferencesService; + this.notificationService = notificationService; + this.downloadService = downloadService; this.i18n = i18n; } + private Checksum getCrc(Path target) throws IOException { + byte[] data = Files.readAllBytes(target); + Checksum checksum = new CRC32(); + checksum.update(data); + return checksum; + } + @Override protected Void call() throws Exception { - platformService.showDocument(mapUrl.toString()); + + updateTitle(i18n.get("mapDownloadTask.title", hpiArchiveName)); + + Objects.requireNonNull(mapUrl, "mapUrl has not been set"); + Objects.requireNonNull(hpiArchiveName, "hpiArchiveName has not been set"); + Objects.requireNonNull(installationPath, "installationPath has not been set"); + + URLConnection urlConnection = mapUrl.openConnection(); + int bytesToRead = urlConnection.getContentLength(); + String content = urlConnection.getContentType(); + + Path cacheDirectory = preferencesService.getCacheDirectory().resolve("maps"); + long cacheFreeSpace = cacheDirectory.toFile().getFreeSpace(); + + Path target = installationPath.resolve(hpiArchiveName); + if (target.toFile().exists()) { + logger.info("{} already exists, skipping install", target); + return null; + } + + Path downloadDirectory = cacheDirectory; + if (cacheFreeSpace < 10e9) { + downloadDirectory = installationPath; + } + + target = downloadDirectory.resolve(hpiArchiveName); + if (content.equals("application/zip") || // @todo pass down expected file size or crc so we can cache zip files too + !target.toFile().exists() || + bytesToRead>0 && Files.size(target)!=bytesToRead || bytesToRead==0) { + logger.info("Downloading archive {} {} bytes from {} to {}", hpiArchiveName, bytesToRead, mapUrl, downloadDirectory); + try (InputStream inputStream = urlConnection.getInputStream()) { + if (content.equals("application/zip")) { + Unzipper.from(inputStream) + .zipBombByteCountThreshold(100_000_000) + .to(downloadDirectory) + .totalBytes(bytesToRead) + .listener(this::updateProgress) // @todo this only notifies progress on each file within the zip??c + .unzip(); + } + else { + downloadService.downloadFile(mapUrl, downloadDirectory.resolve(hpiArchiveName), this::updateProgress); + } + } + } + + if (!downloadDirectory.equals(installationPath)) { + logger.info("Installing archive {} from cache {} to {}", hpiArchiveName, downloadDirectory, installationPath); + Files.copy(downloadDirectory.resolve(hpiArchiveName), installationPath.resolve(hpiArchiveName), StandardCopyOption.REPLACE_EXISTING); + } + + if (!installationPath.resolve(hpiArchiveName).toFile().exists()) { + notificationService.addNotification(new ImmediateNotification( + i18n.get("mapDownloadTask.title", hpiArchiveName), "Download failed", + Severity.ERROR, Collections.singletonList(new DismissAction(i18n)))); + return null; + } + return null; } @@ -48,7 +131,12 @@ public void setMapUrl(URL mapUrl) { this.mapUrl = mapUrl; } - public void setFolderName(String folderName) { - this.folderName = folderName; + public void setHpiArchiveName(String hpiArchiveName) { + this.hpiArchiveName = hpiArchiveName; } + + public void setInstallationPath(Path installationPath) { + this.installationPath = installationPath; + } + } diff --git a/src/main/java/com/faforever/client/map/MapBean.java b/src/main/java/com/faforever/client/map/MapBean.java index 16c4c2f65..07ad63c23 100644 --- a/src/main/java/com/faforever/client/map/MapBean.java +++ b/src/main/java/com/faforever/client/map/MapBean.java @@ -28,8 +28,8 @@ public class MapBean implements Comparable { - private final StringProperty folderName; - private final StringProperty displayName; + private final StringProperty hpiArchiveName; + private final StringProperty mapName; private final IntegerProperty numberOfPlays; private final StringProperty description; private final IntegerProperty downloads; @@ -49,8 +49,8 @@ public class MapBean implements Comparable { public MapBean() { id = new SimpleStringProperty(); - displayName = new SimpleStringProperty(); - folderName = new SimpleStringProperty(); + mapName = new SimpleStringProperty(); + hpiArchiveName = new SimpleStringProperty(); description = new SimpleStringProperty(); numberOfPlays = new SimpleIntegerProperty(); downloads = new SimpleIntegerProperty(); @@ -75,8 +75,8 @@ public static MapBean fromMapDto(com.faforever.client.api.dto.Map map) { MapBean mapBean = new MapBean(); Optional.ofNullable(map.getAuthor()).ifPresent(author -> mapBean.setAuthor(author.getLogin())); mapBean.setDescription(mapVersion.getDescription()); - mapBean.setDisplayName(map.getDisplayName()); - mapBean.setFolderName(mapVersion.getFolderName()); + mapBean.setMapName(map.getDisplayName()); + mapBean.setHpiArchiveName(mapVersion.getFolderName()); mapBean.setSize(MapSize.valueOf(mapVersion.getWidth(), mapVersion.getHeight())); mapBean.setDownloads(map.getStatistics().getDownloads()); mapBean.setId(mapVersion.getId()); @@ -102,8 +102,8 @@ public static MapBean fromMapVersionDto(com.faforever.client.api.dto.MapVersion MapBean mapBean = new MapBean(); Optional.ofNullable(mapVersion.getMap().getAuthor()).ifPresent(author -> mapBean.setAuthor(author.getLogin())); mapBean.setDescription(mapVersion.getDescription()); - mapBean.setDisplayName(mapVersion.getMap().getDisplayName()); - mapBean.setFolderName(mapVersion.getFolderName()); + mapBean.setMapName(mapVersion.getMap().getDisplayName()); + mapBean.setHpiArchiveName(mapVersion.getFolderName()); mapBean.setSize(MapSize.valueOf(mapVersion.getWidth(), mapVersion.getHeight())); mapBean.setDownloads(mapVersion.getMap().getStatistics().getDownloads()); mapBean.setId(mapVersion.getId()); @@ -146,8 +146,8 @@ public ObjectProperty downloadUrlProperty() { return downloadUrl; } - public StringProperty displayNameProperty() { - return displayName; + public StringProperty mapNameProperty() { + return mapName; } public String getDescription() { @@ -225,15 +225,15 @@ public ObjectProperty versionProperty() { @Override public int compareTo(@NotNull MapBean o) { - return getDisplayName().compareTo(o.getDisplayName()); + return getMapName().compareTo(o.getMapName()); } - public String getDisplayName() { - return displayName.get(); + public String getMapName() { + return mapName.get(); } - public void setDisplayName(String displayName) { - this.displayName.set(displayName); + public void setMapName(String mapName) { + this.mapName.set(mapName); } public StringProperty idProperty() { @@ -248,16 +248,16 @@ public void setId(String id) { this.id.set(id); } - public String getFolderName() { - return folderName.get(); + public String getHpiArchiveName() { + return hpiArchiveName.get(); } - public void setFolderName(String folderName) { - this.folderName.set(folderName); + public void setHpiArchiveName(String hpiArchiveName) { + this.hpiArchiveName.set(hpiArchiveName); } - public StringProperty folderNameProperty() { - return folderName; + public StringProperty hpiArchiveNameProperty() { + return hpiArchiveName; } public URL getLargeThumbnailUrl() { diff --git a/src/main/java/com/faforever/client/map/MapCardController.java b/src/main/java/com/faforever/client/map/MapCardController.java index 86a87391b..bcbb18ae6 100644 --- a/src/main/java/com/faforever/client/map/MapCardController.java +++ b/src/main/java/com/faforever/client/map/MapCardController.java @@ -4,7 +4,7 @@ import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.game.KnownFeaturedMod; import com.faforever.client.i18n.I18n; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.notification.ImmediateErrorNotification; import com.faforever.client.notification.NotificationService; import com.faforever.client.reporting.ReportingService; @@ -66,13 +66,13 @@ public void initialize() { installStatusChangeListener = change -> { while (change.next()) { for (MapBean mapBean : change.getAddedSubList()) { - if (map.getFolderName().equalsIgnoreCase(mapBean.getFolderName())) { + if (map.getMapName().equalsIgnoreCase(mapBean.getMapName())) { setInstalled(true); return; } } for (MapBean mapBean : change.getRemoved()) { - if (map.getFolderName().equals(mapBean.getFolderName())) { + if (map.getMapName().equals(mapBean.getMapName())) { setInstalled(false); return; } @@ -82,15 +82,16 @@ public void initialize() { } public void setMap(MapBean map) { + String modTechnical = KnownFeaturedMod.DEFAULT.getTechnicalName(); this.map = map; Image image; if (map.getLargeThumbnailUrl() != null) { - image = mapService.loadPreview(map.getLargeThumbnailUrl(), PreviewSize.LARGE); + image = mapService.loadPreview(modTechnical, map, PreviewType.MINI, 10); } else { image = IdenticonUtil.createIdenticon(map.getId()); } thumbnailImageView.setImage(image); - nameLabel.setText(map.getDisplayName()); + nameLabel.setText(map.getMapName()); authorLabel.setText(Optional.ofNullable(map.getAuthor()).orElse(i18n.get("map.unknownAuthor"))); numberOfPlaysLabel.setText(i18n.number(map.getNumberOfPlays())); @@ -98,13 +99,13 @@ public void setMap(MapBean map) { sizeLabel.setText(i18n.get("mapPreview.size", size.getWidthInKm(), size.getHeightInKm())); maxPlayersLabel.setText(i18n.number(map.getPlayers())); - if (mapService.isOfficialMap(map.getFolderName())) { + if (mapService.isOfficialMap(map.getMapName())) { installButton.setVisible(false); uninstallButton.setVisible(false); } else { - ObservableList installedMaps = mapService.getInstalledMaps(); + ObservableList installedMaps = mapService.getInstalledMaps(modTechnical); JavaFxUtil.addListener(installedMaps, new WeakListChangeListener<>(installStatusChangeListener)); - setInstalled(mapService.isInstalled(map.getFolderName())); + setInstalled(mapService.isInstalled(modTechnical, map.getMapName())); } ObservableList reviews = map.getReviews(); @@ -126,7 +127,7 @@ public void onInstallButtonClicked() { .exceptionally(throwable -> { notificationService.addNotification(new ImmediateErrorNotification( i18n.get("errorTitle"), - i18n.get("mapVault.installationFailed", map.getDisplayName(), throwable.getLocalizedMessage()), + i18n.get("mapVault.installationFailed", map.getMapName(), throwable.getLocalizedMessage()), throwable, i18n, reportingService )); setInstalled(false); @@ -140,7 +141,7 @@ public void onUninstallButtonClicked() { .exceptionally(throwable -> { notificationService.addNotification(new ImmediateErrorNotification( i18n.get("errorTitle"), - i18n.get("mapVault.couldNotDeleteMap", map.getDisplayName(), throwable.getLocalizedMessage()), + i18n.get("mapVault.couldNotDeleteMap", map.getMapName(), throwable.getLocalizedMessage()), throwable, i18n, reportingService )); setInstalled(true); diff --git a/src/main/java/com/faforever/client/map/MapDetailController.java b/src/main/java/com/faforever/client/map/MapDetailController.java index de512a108..04a2529c5 100644 --- a/src/main/java/com/faforever/client/map/MapDetailController.java +++ b/src/main/java/com/faforever/client/map/MapDetailController.java @@ -6,7 +6,7 @@ import com.faforever.client.game.KnownFeaturedMod; import com.faforever.client.i18n.I18n; import com.faforever.client.main.event.HostGameEvent; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.notification.ImmediateErrorNotification; import com.faforever.client.notification.NotificationService; import com.faforever.client.player.Player; @@ -111,13 +111,13 @@ public void initialize() { installStatusChangeListener = change -> { while (change.next()) { for (MapBean mapBean : change.getAddedSubList()) { - if (map.getFolderName().equalsIgnoreCase(mapBean.getFolderName())) { + if (map.getMapName().equalsIgnoreCase(mapBean.getMapName())) { setInstalled(true); return; } } for (MapBean mapBean : change.getRemoved()) { - if (map.getFolderName().equals(mapBean.getFolderName())) { + if (map.getMapName().equals(mapBean.getMapName())) { setInstalled(false); return; } @@ -159,13 +159,14 @@ public Node getRoot() { public void setMap(MapBean map) { this.map = map; + String modTechnical = KnownFeaturedMod.DEFAULT.getTechnicalName(); if (map.getLargeThumbnailUrl() != null) { - thumbnailImageView.setImage(mapService.loadPreview(map, PreviewSize.LARGE)); + thumbnailImageView.setImage(mapService.loadPreview(modTechnical, map, PreviewType.MINI, 10)); } else { thumbnailImageView.setImage(IdenticonUtil.createIdenticon(map.getId())); } renewAuthorControls(); - nameLabel.setText(map.getDisplayName()); + nameLabel.setText(map.getMapName()); authorLabel.setText(Optional.ofNullable(map.getAuthor()).orElse(i18n.get("map.unknownAuthor"))); maxPlayersLabel.setText(i18n.number(map.getPlayers())); mapIdLabel.setText(i18n.get("map.id", map.getId())); @@ -176,7 +177,7 @@ public void setMap(MapBean map) { LocalDateTime createTime = map.getCreateTime(); dateLabel.setText(timeService.asDate(createTime)); - boolean mapInstalled = mapService.isInstalled(map.getFolderName()); + boolean mapInstalled = mapService.isInstalled(modTechnical, map.getMapName()); setInstalled(mapInstalled); Player player = playerService.getCurrentPlayer().orElseThrow(() -> new IllegalStateException("No user is logged in")); @@ -209,13 +210,13 @@ public void setMap(MapBean map) { .orElseGet(() -> i18n.get("map.noDescriptionAvailable"))); - if (mapService.isOfficialMap(map.getFolderName())) { + if (mapService.isOfficialMap(map.getMapName())) { installButton.setVisible(false); uninstallButton.setVisible(false); } else { - ObservableList installedMaps = mapService.getInstalledMaps(); + ObservableList installedMaps = mapService.getInstalledMaps(modTechnical); JavaFxUtil.addListener(installedMaps, new WeakListChangeListener<>(installStatusChangeListener)); - setInstalled(mapService.isInstalled(map.getFolderName())); + setInstalled(mapService.isInstalled(modTechnical, map.getMapName())); } } @@ -261,7 +262,7 @@ public CompletableFuture installMap(){ .exceptionally(throwable -> { notificationService.addNotification(new ImmediateErrorNotification( i18n.get("errorTitle"), - i18n.get("mapVault.installationFailed", map.getDisplayName(), throwable.getLocalizedMessage()), + i18n.get("mapVault.installationFailed", map.getMapName(), throwable.getLocalizedMessage()), throwable, i18n, reportingService )); setInstalled(false); @@ -278,7 +279,7 @@ public void onUninstallButtonClicked() { .exceptionally(throwable -> { notificationService.addNotification(new ImmediateErrorNotification( i18n.get("errorTitle"), - i18n.get("mapVault.couldNotDeleteMap", map.getDisplayName(), throwable.getLocalizedMessage()), + i18n.get("mapVault.couldNotDeleteMap", map.getMapName(), throwable.getLocalizedMessage()), throwable, i18n, reportingService )); setInstalled(true); @@ -295,10 +296,11 @@ public void onContentPaneClicked(MouseEvent event) { } public void onCreateGameButtonClicked() { - if (!mapService.isInstalled(map.getFolderName())) { - installMap().thenRun(() -> eventBus.post(new HostGameEvent(map.getFolderName()))); + String modTechnical = KnownFeaturedMod.DEFAULT.getTechnicalName(); + if (!mapService.isInstalled(modTechnical, map.getMapName())) { + installMap().thenRun(() -> eventBus.post(new HostGameEvent(map.getMapName()))); } else { - eventBus.post(new HostGameEvent(map.getFolderName())); + eventBus.post(new HostGameEvent(map.getMapName())); } } diff --git a/src/main/java/com/faforever/client/map/MapService.java b/src/main/java/com/faforever/client/map/MapService.java index 77868bc1a..b3e231412 100644 --- a/src/main/java/com/faforever/client/map/MapService.java +++ b/src/main/java/com/faforever/client/map/MapService.java @@ -3,25 +3,33 @@ import com.faforever.client.config.CacheNames; import com.faforever.client.config.ClientProperties; import com.faforever.client.config.ClientProperties.Vault; +import com.faforever.client.fa.MapTool; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.game.KnownFeaturedMod; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapBean.Type; +import com.faforever.client.notification.DismissAction; +import com.faforever.client.notification.ImmediateNotification; +import com.faforever.client.notification.NotificationService; +import com.faforever.client.notification.Severity; import com.faforever.client.player.Player; import com.faforever.client.player.PlayerService; import com.faforever.client.preferences.PreferencesService; +import com.faforever.client.preferences.TotalAnnihilationPrefs; import com.faforever.client.remote.AssetService; import com.faforever.client.remote.FafService; import com.faforever.client.task.CompletableTask; import com.faforever.client.task.CompletableTask.Priority; import com.faforever.client.task.TaskService; import com.faforever.client.theme.UiService; +import com.faforever.client.update.ClientConfiguration.Downloadable; import com.faforever.client.util.ProgrammingError; import com.faforever.client.util.Tuple; import com.faforever.client.vault.search.SearchController.SearchConfig; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; +import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; @@ -36,7 +44,6 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; @@ -45,12 +52,18 @@ import javax.inject.Inject; import java.io.IOException; import java.lang.invoke.MethodHandles; +import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.nio.file.Paths; +import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -59,10 +72,17 @@ import java.util.Set; import java.util.HashSet; import java.util.concurrent.CompletableFuture; + +import static com.faforever.client.fa.MapTool.MAP_DETAIL_COLUMN_ARCHIVE; +import static com.faforever.client.fa.MapTool.MAP_DETAIL_COLUMN_DESCRIPTION; +import static com.faforever.client.fa.MapTool.MAP_DETAIL_COLUMN_SIZE; import static com.github.nocatch.NoCatch.noCatch; import static com.google.common.net.UrlEscapers.urlFragmentEscaper; import static java.lang.String.format; +import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; +import static java.util.concurrent.CompletableFuture.completedFuture; @Lazy @@ -76,6 +96,7 @@ public class MapService implements InitializingBean, DisposableBean { private final ApplicationContext applicationContext; private final FafService fafService; private final AssetService assetService; + private final NotificationService notificationService; private final I18n i18n; private final UiService uiService; private final ClientProperties clientProperties; @@ -84,10 +105,57 @@ public class MapService implements InitializingBean, DisposableBean { private final String mapDownloadUrlFormat; private final String mapPreviewUrlFormat; - private final Map pathToMap = new HashMap<>(); - private final ObservableList installedMaps = FXCollections.observableArrayList(); - private final Map mapsByFolderName = new HashMap<>(); - private Thread directoryWatcherThread; + + private class Installation { + final String modTechnicalName; + private final Map mapsByName = new HashMap<>(); + private final ObservableList maps = FXCollections.observableArrayList(); + private Thread directoryWatcherThread; + private Integer enumerationsRequested = 0; + + public Installation(String modTechnical) { + this.modTechnicalName = modTechnical; + + maps.addListener((ListChangeListener) change -> { + while (change.next()) { + for (MapBean mapBean : change.getRemoved()) { + mapsByName.remove(mapBean.getMapName()); + } + for (MapBean mapBean : change.getAddedSubList()) { + mapsByName.put(mapBean.getMapName(), mapBean); + } + } + }); + } + + private void removeMap(String mapName) { + maps.remove(mapsByName.remove(mapName)); + } + + private void addMap(String mapName, String mapDetail[]) { + MapBean mapBean = readMap(mapName, mapDetail); + addMap(mapBean); + } + + private void addMap(MapBean mapBean) { + if (!mapsByName.containsKey(mapBean.getMapName())) { + mapsByName.put(mapBean.getMapName(), mapBean); + maps.add(mapBean); + } + } + } + + // keyed by ModTechnical + private Map installations = new HashMap<>(); + private List downloadables; + + public Installation getInstallation(String modTechnical) { + Installation installation = installations.get(modTechnical); + if (installation == null) { + installations.put(modTechnical, new Installation(modTechnical)); + } + return installations.get(modTechnical); + } @Inject public MapService(PreferencesService preferencesService, @@ -95,6 +163,7 @@ public MapService(PreferencesService preferencesService, ApplicationContext applicationContext, FafService fafService, AssetService assetService, + NotificationService notificationService, I18n i18n, UiService uiService, ClientProperties clientProperties, @@ -104,6 +173,7 @@ public MapService(PreferencesService preferencesService, this.applicationContext = applicationContext; this.fafService = fafService; this.assetService = assetService; + this.notificationService = notificationService; this.i18n = i18n; this.uiService = uiService; this.clientProperties = clientProperties; @@ -113,16 +183,31 @@ public MapService(PreferencesService preferencesService, this.mapDownloadUrlFormat = vault.getMapDownloadUrlFormat(); this.mapPreviewUrlFormat = vault.getMapPreviewUrlFormat(); - installedMaps.addListener((ListChangeListener) change -> { + try { + downloadables = preferencesService.getRemotePreferences().getDownloadables(); + } + catch (IOException e) { + logger.warn("Unable to retrieve list of downloadables from remote preferences"); + } + + preferencesService.getTotalAnnihilationAllMods().addListener((ListChangeListener) change -> { while (change.next()) { - for (MapBean mapBean : change.getRemoved()) { - mapsByFolderName.remove(mapBean.getFolderName().toLowerCase()); - } - for (MapBean mapBean : change.getAddedSubList()) { - mapsByFolderName.put(mapBean.getFolderName().toLowerCase(), mapBean); + for (TotalAnnihilationPrefs taPrefs : change.getAddedSubList()) { + taPrefs.getInstalledExePathProperty().addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> {tryLoadMaps(taPrefs.getBaseGameName());}); + }); } } }); + + for (TotalAnnihilationPrefs taPrefs: preferencesService.getTotalAnnihilationAllMods()) { + if (taPrefs == null) { + continue; + } + String modTechnical = taPrefs.getBaseGameName(); + Installation installation = new Installation(modTechnical); + installations.put(modTechnical, installation); + } } @VisibleForTesting @@ -141,7 +226,7 @@ public MapService(PreferencesService preferencesService, "Crystal Isles", "Crystal Maze", "Crystal Treasure Island", "Dire Straits", "East Indeez", "Eastside Westside", "Expanded Confluence", "Flooded Glaciers", "Gasbag Forests", "Gasplant Plain", "Higher Ground", "Ice Scream", "Icy Bergs", "John's Pass", "Lake Shore", "Lusch Puppy", "Luschaven", "Metal Isles", "Moon Quartet", "Ooooweeee", "Pillopeens", - "Plains and Passes", "Polar Range", "Poly Fields", "Red River North", "Red River", "Ror Shock", "Sail Away", "Sector 410b", + "Plains and Passes", "Polar Range", "Polyp Fields", "Red River North", "Red River", "Ror Shock", "Sail Away", "Sector 410b", "Show Down", "Slate Gordon", "Slated Fate", "Steel Jungle", "Surface Meltdown", "Temblorian Mist", "The Barrier Reef", "The Bayou", "Town & Country", "Trout Farm" ); @@ -153,56 +238,76 @@ public MapService(PreferencesService preferencesService, @VisibleForTesting Set cdMaps = ImmutableSet.of( - "A Plethora of Ponds", "Abysmal Lake", "Ancient Issaquah", "Cloudious Prime", "Comet Catcher", "Long Lakes", "LUSCHIE", - "Luschinfloggen", "Luschious", "Metal Isles", "Mounds of Mars", "PC Games' Evad River Delta", "Plains and Passes", + "A Plethora of Ponds", "Abysmal Lake", "Ancient Issaquah", "Cloudious Prime", "Long Lakes", "LUSCHIE", + "Luschinfloggen", "Luschious", "Mounds of Mars", "PC Games' Evad River Delta", "Starfish Isles", "Thundurlok Rok", "Tropical Paradise" ); - private static URL getDownloadUrl(String mapName, String baseUrl) { - return noCatch(() -> new URL(format(baseUrl, urlFragmentEscaper().escape(mapName).toLowerCase(Locale.US)))); + private URL getDownloadUrl(String hpiArchiveName) { + if (this.downloadables == null) { + return null; + } + for (Downloadable downloadable : this.downloadables) { + if (downloadable.getWhat().toLowerCase().equals(hpiArchiveName.toLowerCase())) { + noCatch(() -> new URL(downloadable.getUrl())); + } + } + return null; } - private static URL getPreviewUrl(String mapName, String baseUrl, PreviewSize previewSize) { - return noCatch(() -> new URL(format(baseUrl, previewSize.folderName, urlFragmentEscaper().escape(mapName).toLowerCase(Locale.US)))); + private static URL getPreviewUrl(String mapName, String baseUrl, PreviewType previewType) { + return noCatch(() -> new URL(format(baseUrl, previewType.folderName, urlFragmentEscaper().escape(mapName).toLowerCase(Locale.US)))); } @Override public void afterPropertiesSet() { eventBus.register(this); - JavaFxUtil.addListener(preferencesService.getTotalAnnihilation(KnownFeaturedMod.DEFAULT.getTechnicalName()).getInstalledExePathProperty(), observable -> tryLoadMaps(KnownFeaturedMod.DEFAULT.getTechnicalName())); - tryLoadMaps(KnownFeaturedMod.DEFAULT.getTechnicalName()); + + for (TotalAnnihilationPrefs taPrefs: preferencesService.getTotalAnnihilationAllMods()) { + if (taPrefs == null) { + continue; + } + String modTechnical = taPrefs.getBaseGameName(); + JavaFxUtil.addListener(taPrefs.getInstalledExePathProperty(), observable -> tryLoadMaps(modTechnical)); + tryLoadMaps(modTechnical); + } } - private void tryLoadMaps(String modelTechnical) { - Path mapsDirectory = preferencesService.getTotalAnnihilation(modelTechnical).getInstalledPath(); + private void tryLoadMaps(String modTechnical) { + Path mapsDirectory = preferencesService.getTotalAnnihilation(modTechnical).getInstalledPath(); if (mapsDirectory == null) { - logger.warn(String.format("Could not load maps: installation path is not set for mod: %s",modelTechnical)); + logger.warn(String.format("Could not load maps: installation path is not set for mod: %s",modTechnical)); return; } + Installation installation = getInstallation(modTechnical); + try { Files.createDirectories(mapsDirectory); - Optional.ofNullable(directoryWatcherThread).ifPresent(Thread::interrupt); - directoryWatcherThread = startDirectoryWatcher(mapsDirectory); + Optional.ofNullable(installation.directoryWatcherThread).ifPresent(Thread::interrupt); + installation.directoryWatcherThread = startDirectoryWatcher(installation, mapsDirectory); } catch (IOException e) { logger.warn("Could not start map directory watcher", e); // TODO notify user } - installedMaps.clear(); - loadInstalledMaps(modelTechnical); + loadInstalledMaps(installation); } - private Thread startDirectoryWatcher(Path mapsDirectory) { + private Thread startDirectoryWatcher(Installation installation, Path mapsDirectory) { Thread thread = new Thread(() -> noCatch(() -> { try (WatchService watcher = mapsDirectory.getFileSystem().newWatchService()) { // beware potential bug: this used to register with forgedAlliancePreferences.getCustomMapsDirectory() ... - mapsDirectory.register(watcher, ENTRY_DELETE); + mapsDirectory.register(watcher, new WatchEvent.Kind[]{ENTRY_DELETE, ENTRY_MODIFY, ENTRY_CREATE}); + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.{ufo,hpi,ccx}"); while (!Thread.interrupted()) { WatchKey key = watcher.take(); - key.pollEvents().stream() - .filter(event -> event.kind() == ENTRY_DELETE) - .forEach(event -> removeMap(mapsDirectory.resolve((Path) event.context()))); + List> events = key.pollEvents(); + if (events.stream() + .filter(event -> matcher.matches((Path)event.context()) ) + .findAny().isPresent()) { + Platform.runLater(() -> { loadInstalledMaps(installation); }); + } key.reset(); } } catch (InterruptedException e) { @@ -214,105 +319,115 @@ private Thread startDirectoryWatcher(Path mapsDirectory) { return thread; } + public void loadInstalledMaps(String modTechnical) { + Installation installation = getInstallation(modTechnical); + loadInstalledMaps(installation); + } + + private void loadInstalledMaps(Installation installation) { + synchronized(installation.enumerationsRequested) { + ++installation.enumerationsRequested; + if (installation.enumerationsRequested > 1) { + return; + } + } - private void loadInstalledMaps(String modTechnicalName) { taskService.submitTask(new CompletableTask(Priority.LOW) { protected Void call() { updateTitle(i18n.get("mapVault.loadingMaps")); - Path installationPath = preferencesService.getTotalAnnihilation(modTechnicalName).getInstalledPath(); - - boolean gotmaps = false; - if (installationPath.resolve("totala2.hpi").toFile().exists()) { - gotmaps = true; - for (String map : otaMaps) { - addInstalledMap(Paths.get(map)); + Path gamePath = preferencesService.getTotalAnnihilation(installation.modTechnicalName).getInstalledPath(); + if (gamePath == null) { + synchronized(installation.enumerationsRequested) { + installation.enumerationsRequested = 0; } + return null; } - updateProgress(1, 4); - if (installationPath.resolve("ccmaps.ccx").toFile().exists()) { - gotmaps = true; - for (String map : ccMaps) { - addInstalledMap(Paths.get(map)); - } + + List mapList = new ArrayList<>(); + for (String[] details: MapTool.listMaps(gamePath, null, preferencesService.getCacheDirectory().resolve("maps"), false)) { + MapBean mapBean = readMap(details[0], details); + mapList.add(mapBean); } - updateProgress(2, 4); - if (installationPath.resolve("btmaps.ccx").toFile().exists()) { - gotmaps = true; - for (String map : btMaps) { - addInstalledMap(Paths.get(map)); + installation.maps.clear(); + installation.maps.addAll(mapList); + if (installation.maps.isEmpty()) { + for (String map : otaMaps) { + installation.addMap(map, null); } } - updateProgress(3, 4); - if (installationPath.resolve("cdmaps.ccx").toFile().exists()) { - gotmaps = true; - for (String map : cdMaps) { - addInstalledMap(Paths.get(map)); + updateProgress(1, 1); + + boolean again = false; + synchronized(installation.enumerationsRequested) { + if (installation.enumerationsRequested > 1) { + installation.enumerationsRequested = 1; + again = true; } - } - if (installationPath.resolve("cdmaps.ufo").toFile().exists()) { - gotmaps = true; - for (String map : cdMaps) { - addInstalledMap(Paths.get(map)); + else { + installation.enumerationsRequested = 0; } } - if (!gotmaps) { - addInstalledMap(Paths.get("SHERWOOD")); + if (again) { + return call(); + } + else { + return null; } - - updateProgress(4, 4); - return null; } }); } - private void removeMap(Path path) { -// installedMaps.remove(pathToMap.remove(path)); - } - - private void addInstalledMap(Path path) throws MapLoadException { - try { - MapBean mapBean = readMap(path); - pathToMap.put(path, mapBean); - if (!mapsByFolderName.containsKey(mapBean.getFolderName())) { - installedMaps.add(mapBean); + @NotNull + public MapBean readMap(String mapName, String [] mapDetails) { + MapBean mapBean = new MapBean(); + String archiveName = mapDetails != null ? mapDetails[MAP_DETAIL_COLUMN_ARCHIVE] : ""; + String description = mapDetails != null ? mapDetails[MAP_DETAIL_COLUMN_DESCRIPTION] : mapName; + String mapSizeStr = mapDetails != null ? mapDetails[MAP_DETAIL_COLUMN_SIZE] : "16 x 16"; + + String mapSizeArray[] = mapSizeStr.replaceAll("[^0-9x]", "").split("x"); + + if (downloadables != null) { + Optional downloadURL = downloadables.stream().filter((downloadable) -> downloadable.getWhat() == archiveName).findFirst(); + if (downloadURL.isPresent()) { + try { + mapBean.setDownloadUrl(new URL(downloadURL.get().getUrl())); + } catch (MalformedURLException e) { + logger.warn("bad download url: {}", downloadURL.get()); + } } - } catch (MapLoadException e) { - logger.warn("Map could not be read: " + path.getFileName(), e); } - } + mapBean.setMapName(mapName); + mapBean.setDescription(description.replaceAll("[ ][ ]+", "\n")); // some maps insert spaces into description to move to new line when displayed in TA lobby + mapBean.setHpiArchiveName(archiveName); + mapBean.setPlayers(10); + mapBean.setType(Type.SKIRMISH); - @NotNull - public MapBean readMap(Path mapFolder) throws MapLoadException { - MapBean mapBean = new MapBean(); - mapBean.setFolderName(mapFolder.toString()); - mapBean.setDisplayName(mapFolder.toString()); - mapBean.setDescription(mapFolder.toString()); - mapBean.setType(Type.SKIRMISH); - mapBean.setSize(MapSize.valueOf(512, 512)); - mapBean.setPlayers(10); - - return mapBean; - } + mapBean.setSize(MapSize.valueOf(256, 256)); + try { + if (mapSizeArray.length == 2) { + Integer w = Integer.parseInt(mapSizeArray[0].trim()); + Integer h = Integer.parseInt(mapSizeArray[1].trim()); + mapBean.setSize(MapSize.valueOf(w * 16, h * 16)); + } + } + catch (NumberFormatException e) { + logger.error("Map '{}' has unparsable size '{}'", mapName, mapSizeStr); + } - @SneakyThrows() - @NotNull - @Cacheable(value = CacheNames.MAP_PREVIEW, unless = "#result == null") - public Image loadPreview(String modTechnicalName, String mapName, PreviewSize previewSize) { - return loadPreview(getPreviewUrl(mapName, mapPreviewUrlFormat, previewSize), previewSize); + return mapBean; } - - public ObservableList getInstalledMaps() { - return installedMaps; + public ObservableList getInstalledMaps(String modTechnical) { + Installation installation = getInstallation(modTechnical); + return installation.maps; } - public Optional getMapLocallyFromName(String mapFolderName) { - logger.debug("Trying to find map '{}' locally", mapFolderName); - String mapFolderKey = mapFolderName.toLowerCase(); - return Optional.ofNullable(mapsByFolderName.get(mapFolderKey)); + public Optional getMapLocallyFromName(String modTechnical, String mapName) { + logger.debug("Trying to find map '{}' locally", mapName); + return Optional.ofNullable(installations.getOrDefault(modTechnical, new Installation(modTechnical)).mapsByName.get(mapName)); } public boolean isOfficialMap(String mapName) { @@ -329,19 +444,54 @@ public boolean isOfficialMap(String mapName) { * Returns {@code true} if the given map is available locally, {@code false} otherwise. */ - public boolean isInstalled(String mapFolderName) { - return mapsByFolderName.containsKey(mapFolderName.toLowerCase()); + public boolean isInstalled(String modTechnical, String mapName) { + return getInstallation(modTechnical).mapsByName.containsKey(mapName); } - - public CompletableFuture download(String modTechnicalname, String technicalMapName) { - URL mapUrl = getDownloadUrl(technicalMapName, mapDownloadUrlFormat); - return downloadAndInstallMap(modTechnicalname, technicalMapName, mapUrl, null, null); + private void removeConflictingArchive(String hpiArchiveName, Path conflictingPath) { + try { + logger.info("Archive {} conflicts with {}: renaming to .taforever.bak", conflictingPath, hpiArchiveName); + Files.move(conflictingPath, Paths.get(conflictingPath + ".taforever.bak")); + } catch (IOException e1) { + try { + logger.warn("{}.taforever.bak already exists. will just delete", conflictingPath); + Files.deleteIfExists(conflictingPath); + } catch (IOException e2) { + logger.warn("Unable to delete conflicting archive {}", conflictingPath); + } + } } + public CompletableFuture download(String modTechnical, String hpiArchiveName) { + CompletableFuture future = completedFuture(null); + Optional downloadable = downloadables.stream().filter(d -> d.getWhat().equals(hpiArchiveName)).findFirst(); + if (downloadable.isPresent()) { + try { + URL url = new URL(downloadable.get().getUrl()); + future = downloadAndInstallArchive(modTechnical, hpiArchiveName, url, null, null); + for(String dependency: downloadable.get().getDepends()) { + future = future.thenCompose(aVoid -> download(modTechnical, dependency)); + } + for(String conflict: downloadable.get().getConflicts()) { + Path conflictingPath = preferencesService.getTotalAnnihilation(modTechnical).getInstalledPath().resolve(conflict); + if (conflictingPath.toFile().exists()) { + future = future.thenRun(() -> removeConflictingArchive(hpiArchiveName, conflictingPath)); + } + } + } catch (MalformedURLException e) { + logger.error("bad archive download url: {}", downloadable.get().getUrl()); + } + } + else { + notificationService.addNotification(new ImmediateNotification( + i18n.get("mapDownloadTask.title", hpiArchiveName), i18n.get("mapDownloadTask.notFound"), + Severity.WARN, Collections.singletonList(new DismissAction(i18n)))); + } + return future; + } public CompletableFuture downloadAndInstallMap(String modTechnicalName, MapBean map, @Nullable DoubleProperty progressProperty, @Nullable StringProperty titleProperty) { - return downloadAndInstallMap(modTechnicalName, map.getFolderName(), map.getDownloadUrl(), progressProperty, titleProperty); + return downloadAndInstallArchive(modTechnicalName, map.getHpiArchiveName(), map.getDownloadUrl(), progressProperty, titleProperty); } public CompletableFuture, Integer>> getRecommendedMapsWithPageCount(int count, int page) { @@ -367,66 +517,60 @@ public CompletableFuture, Integer>> getMostPlayedMapsWithPag return fafService.getMostPlayedMapsWithPageCount(count, page); } + public void generatePreview(String modTechnical, String mapName, Path cachedFile, PreviewType previewType, int maxPositions) { + if (Files.exists(cachedFile)) { + return; + } + + Path gamePath = preferencesService.getTotalAnnihilation(modTechnical).getInstalledPath(); + if (gamePath == null) { + gamePath = preferencesService.getTotalAnnihilation(KnownFeaturedMod.DEFAULT.getTechnicalName()).getInstalledPath(); + } + if (gamePath == null) { + return; + } + + MapTool.generatePreview(gamePath, mapName, cachedFile.getParent().getParent(), previewType, maxPositions); + } + /** * Loads the preview of a map or returns a "unknown map" image. */ - @Cacheable(CacheNames.MAP_PREVIEW) - public Image loadPreview(MapBean map, PreviewSize previewSize) { - URL url; - switch (previewSize) { - case SMALL: - url = map.getSmallThumbnailUrl(); - break; - case LARGE: - url = map.getLargeThumbnailUrl(); - break; - default: - throw new ProgrammingError("Uncovered preview size: " + previewSize); - } - return loadPreview(url, previewSize); - } - - @Cacheable(CacheNames.MAP_PREVIEW) - public Image loadPreview(URL url, PreviewSize previewSize) { - return assetService.loadAndCacheImage(url, Paths.get("maps").resolve(previewSize.folderName), - () -> uiService.getThemeImage(UiService.UNKNOWN_MAP_IMAGE)); + @SneakyThrows() + @NotNull + //@Cacheable(value = CacheNames.MAP_PREVIEW, unless = "#result == null") + public Image loadPreview(String modTechnicalName, String mapName, PreviewType previewType, int maxPositions) { + return loadPreview(modTechnicalName, mapName, getPreviewUrl(mapName, mapPreviewUrlFormat, previewType), previewType, maxPositions); } - - public CompletableFuture uninstallMap(MapBean map) { - if (isOfficialMap(map.getFolderName())) { - throw new IllegalArgumentException("Attempt to uninstall an official map"); - } - UninstallMapTask task = applicationContext.getBean(com.faforever.client.map.UninstallMapTask.class); - task.setMap(map); - return taskService.submitTask(task).getFuture(); + //@Cacheable(CacheNames.MAP_PREVIEW) + public Image loadPreview(String modTechnical, MapBean map, PreviewType previewType, int maxNumPlayers) { + URL url = map.getLargeThumbnailUrl(); + return loadPreview(modTechnical, map.getMapName(), url, previewType, maxNumPlayers); } + //@Cacheable(CacheNames.MAP_PREVIEW) + private Image loadPreview(String modTechnical, String mapName, URL url, PreviewType previewType, int maxPositions) { - public Path getPathForMap(Path localMapPath, MapBean map) { - return getPathForMapInsensitive(localMapPath, map.getFolderName()); - } + String urlString = url.toString(); + String cachedFilename = urlString.substring(urlString.lastIndexOf('/') + 1); + Path cacheSubFolder = Paths.get("maps").resolve(previewType.getFolderName(maxPositions)); + Path cachedFile = preferencesService.getCacheDirectory().resolve(cacheSubFolder).resolve(cachedFilename); - private Path getMapsDirectory(String modTechnicalName) { - return preferencesService.getTotalAnnihilation(modTechnicalName).getInstalledPath(); - } + generatePreview(modTechnical, mapName, cachedFile, previewType, maxPositions); - public Path getPathForMap(Path localMapsPath, String technicalName) { - Path path = localMapsPath.resolve(technicalName); - if (Files.notExists(path)) { - return null; - } - return path; + return assetService.loadAndCacheImage(url, Paths.get("maps").resolve(previewType.getFolderName(maxPositions)), + () -> uiService.getThemeImage(UiService.UNKNOWN_MAP_IMAGE)); } - public Path getPathForMapInsensitive(Path localMapPath, String approxName) { - for (Path entry : noCatch(() -> Files.newDirectoryStream(localMapPath))) { - if (entry.getFileName().toString().equalsIgnoreCase(approxName)) { - return entry; - } + public CompletableFuture uninstallMap(MapBean map) { + if (isOfficialMap(map.getMapName())) { + throw new IllegalArgumentException("Attempt to uninstall an official map"); } - return null; + UninstallMapTask task = applicationContext.getBean(com.faforever.client.map.UninstallMapTask.class); + task.setMap(map); + return taskService.submitTask(task).getFuture(); } public CompletableTask uploadMap(Path mapPath, boolean ranked) { @@ -437,7 +581,6 @@ public CompletableTask uploadMap(Path mapPath, boolean ranked) { return taskService.submitTask(mapUploadTask); } - @CacheEvict(CacheNames.MAPS) public void evictCache() { // Nothing to see here @@ -447,8 +590,8 @@ public void evictCache() { * Tries to find a map my its folder name, first locally then on the server. */ - public CompletableFuture> findByMapFolderName(String folderName) { - Optional installed = getMapLocallyFromName(folderName); + public CompletableFuture> findByMapFolderName(String modTechnical, String folderName) { + Optional installed = getMapLocallyFromName(modTechnical, folderName); if (installed.isPresent()) { return CompletableFuture.completedFuture(installed); } @@ -483,10 +626,18 @@ public CompletableFuture, Integer>> getLadderMapsWithPageCou return fafService.getLadder1v1MapsWithPageCount(loadMoreCount, page); } - private CompletableFuture downloadAndInstallMap(String modTechnicalName, String folderName, URL downloadUrl, @Nullable DoubleProperty progressProperty, @Nullable StringProperty titleProperty) { + private CompletableFuture downloadAndInstallArchive(String modTechnicalName, String hpiArchive, URL downloadUrl, @Nullable DoubleProperty progressProperty, @Nullable StringProperty titleProperty) { + + Path mapsDirectory = preferencesService.getTotalAnnihilation(modTechnicalName).getInstalledPath(); + if (mapsDirectory == null) { + logger.warn(String.format("Could not load maps: installation path is not set for mod: %s",modTechnicalName)); + return new CompletableFuture(); + } + DownloadMapTask task = applicationContext.getBean(DownloadMapTask.class); task.setMapUrl(downloadUrl); - task.setFolderName(folderName); + task.setInstallationPath(mapsDirectory); + task.setHpiArchiveName(hpiArchive); if (progressProperty != null) { progressProperty.bind(task.progressProperty()); @@ -495,9 +646,7 @@ private CompletableFuture downloadAndInstallMap(String modTechnicalName, S titleProperty.bind(task.titleProperty()); } - Path localMapPath = this.getMapsDirectory(modTechnicalName); - return taskService.submitTask(task).getFuture() - .thenAccept(aVoid -> noCatch(() -> addInstalledMap(getPathForMapInsensitive(localMapPath, folderName)))); + return taskService.submitTask(task).getFuture(); } public CompletableFuture, Integer>> getOwnedMapsWithPageCount(int loadMoreCount, int page) { @@ -519,17 +668,46 @@ public CompletableFuture unrankMapVersion(MapBean map) { @Override public void destroy() { - Optional.ofNullable(directoryWatcherThread).ifPresent(Thread::interrupt); + for (Installation installation: installations.values()) { + Optional.ofNullable(installation.directoryWatcherThread).ifPresent(Thread::interrupt); + } } - public enum PreviewSize { + public enum PreviewType { // These must match the preview URLs - SMALL("small"), LARGE("large"); + MINI("mini"), + POSITIONS("positions"), + MEXES("mexes"), + GEOS("geos"), + ROCKS("rocks"), + TREES("trees"); - String folderName; + private String folderName; - PreviewSize(String folderName) { + PreviewType(String folderName) { this.folderName = folderName; } + + public String getDisplayName() { + switch (this) { + case MINI: return "Minimap"; + case POSITIONS: return "Positions"; + case MEXES: return "Metal Patches"; + case GEOS: return "Geo Vents"; + case ROCKS: return "Reclaim (metal)"; + case TREES: return "Reclaim (energy)"; + default: return "null"; + } + } + + String getFolderName(int maxNumPlayers) { + switch (this) { + case MINI: + case POSITIONS: + return this.folderName; + default: + return String.format("%s_%s", this.folderName, maxNumPlayers); + } + } } } diff --git a/src/main/java/com/faforever/client/map/MapUploadController.java b/src/main/java/com/faforever/client/map/MapUploadController.java index 3380af0c5..91705f0a7 100644 --- a/src/main/java/com/faforever/client/map/MapUploadController.java +++ b/src/main/java/com/faforever/client/map/MapUploadController.java @@ -105,7 +105,7 @@ public void initialize() { public void setMapPath(Path mapPath) { this.mapPath = mapPath; enterParsingState(); - CompletableFuture.supplyAsync(() -> mapService.readMap(mapPath), executorService) + CompletableFuture.supplyAsync(() -> mapService.readMap(mapPath.toString(), null), executorService) .thenAccept(this::setMapInfo) .exceptionally(throwable -> { logger.warn("Map could not be read", throwable); @@ -129,7 +129,7 @@ private void setMapInfo(MapBean mapInfo) { this.mapInfo = mapInfo; enterMapInfoState(); - mapNameLabel.textProperty().bind(mapInfo.displayNameProperty()); + mapNameLabel.textProperty().bind(mapInfo.mapNameProperty()); descriptionLabel.textProperty().bind(mapInfo.descriptionProperty()); versionLabel.textProperty().bind(mapInfo.versionProperty().asString()); sizeLabel.textProperty().bind(Bindings.createStringBinding( diff --git a/src/main/java/com/faforever/client/remote/MockFafServerAccessor.java b/src/main/java/com/faforever/client/remote/MockFafServerAccessor.java index b55d5314a..2b4bf776b 100644 --- a/src/main/java/com/faforever/client/remote/MockFafServerAccessor.java +++ b/src/main/java/com/faforever/client/remote/MockFafServerAccessor.java @@ -324,7 +324,7 @@ private GameInfoMessage createGameInfo(int uid, String title, GameAccess access, gameInfoMessage.setUid(uid); gameInfoMessage.setTitle(title); gameInfoMessage.setFeaturedMod(featuredMod); - gameInfoMessage.setMapname(mapName); + gameInfoMessage.setMapName(mapName); gameInfoMessage.setNumPlayers(numPlayers); gameInfoMessage.setMaxPlayers(maxPlayers); gameInfoMessage.setHost(host); diff --git a/src/main/java/com/faforever/client/remote/domain/GameInfoMessage.java b/src/main/java/com/faforever/client/remote/domain/GameInfoMessage.java index 852f4d143..9410a4363 100644 --- a/src/main/java/com/faforever/client/remote/domain/GameInfoMessage.java +++ b/src/main/java/com/faforever/client/remote/domain/GameInfoMessage.java @@ -24,7 +24,8 @@ public class GameInfoMessage extends FafServerMessage { private Integer maxPlayers; private String title; private Map simMods; - private String mapname; + private String mapName; + private String mapFilePath; private Double launchedAt; /** * The server may either send a single game or a list of games in the same message... *cringe*. diff --git a/src/main/java/com/faforever/client/replay/LocalReplayInfo.java b/src/main/java/com/faforever/client/replay/LocalReplayInfo.java index ccda95fbf..0d3c3972e 100644 --- a/src/main/java/com/faforever/client/replay/LocalReplayInfo.java +++ b/src/main/java/com/faforever/client/replay/LocalReplayInfo.java @@ -47,7 +47,7 @@ public void updateFromGameInfoBean(Game game) { host = game.getHost(); uid = game.getId(); title = game.getTitle(); - mapname = game.getMapFolderName(); + mapname = game.getMapName(); state = game.getStatus(); gameType = game.getVictoryCondition(); featuredMod = game.getFeaturedMod(); diff --git a/src/main/java/com/faforever/client/replay/ReplayCardController.java b/src/main/java/com/faforever/client/replay/ReplayCardController.java index 891a2dcd7..a9d23d763 100644 --- a/src/main/java/com/faforever/client/replay/ReplayCardController.java +++ b/src/main/java/com/faforever/client/replay/ReplayCardController.java @@ -2,10 +2,11 @@ import com.faforever.client.fx.Controller; import com.faforever.client.fx.JavaFxUtil; +import com.faforever.client.game.KnownFeaturedMod; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapBean; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.rating.RatingService; import com.faforever.client.util.RatingUtil; import com.faforever.client.util.TimeService; @@ -75,9 +76,9 @@ public void setReplay(Replay replay) { Optional optionalMap = Optional.ofNullable(replay.getMap()); if (optionalMap.isPresent()) { MapBean map = optionalMap.get(); - Image image = mapService.loadPreview(map, PreviewSize.SMALL); + Image image = mapService.loadPreview(KnownFeaturedMod.DEFAULT.getTechnicalName(), map, PreviewType.MINI, 10); mapThumbnailImageView.setImage(image); - onMapLabel.setText(i18n.get("game.onMapFormat", map.getDisplayName())); + onMapLabel.setText(i18n.get("game.onMapFormat", map.getMapName())); } else { onMapLabel.setText(i18n.get("game.onUnknownMap")); } diff --git a/src/main/java/com/faforever/client/replay/ReplayDetailController.java b/src/main/java/com/faforever/client/replay/ReplayDetailController.java index 50637b730..404b1b95c 100644 --- a/src/main/java/com/faforever/client/replay/ReplayDetailController.java +++ b/src/main/java/com/faforever/client/replay/ReplayDetailController.java @@ -6,12 +6,13 @@ import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.fx.StringCell; import com.faforever.client.game.Faction; +import com.faforever.client.game.KnownFeaturedMod; import com.faforever.client.game.RatingType; import com.faforever.client.game.TeamCardController; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapBean; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.player.Player; import com.faforever.client.player.PlayerService; import com.faforever.client.rating.RatingService; @@ -158,9 +159,9 @@ public void setReplay(Replay replay) { Optional optionalMap = Optional.ofNullable(replay.getMap()); if (optionalMap.isPresent()) { MapBean map = optionalMap.get(); - Image image = mapService.loadPreview(map, PreviewSize.LARGE); + Image image = mapService.loadPreview(KnownFeaturedMod.DEFAULT.getTechnicalName(), map, PreviewType.MINI, 10); mapThumbnailImageView.setImage(image); - onMapLabel.setText(i18n.get("game.onMapFormat", map.getDisplayName())); + onMapLabel.setText(i18n.get("game.onMapFormat", map.getMapName())); } else { onMapLabel.setText(i18n.get("game.onUnknownMap")); } diff --git a/src/main/java/com/faforever/client/replay/ReplayService.java b/src/main/java/com/faforever/client/replay/ReplayService.java index 12869b6e7..a47de3a63 100644 --- a/src/main/java/com/faforever/client/replay/ReplayService.java +++ b/src/main/java/com/faforever/client/replay/ReplayService.java @@ -310,7 +310,7 @@ private CompletableFuture tryLoadingLocalReplay(Path replayFile) { LocalReplayInfo replayInfo = replayFileReader.parseMetaData(replayFile); CompletableFuture featuredModFuture = modService.getFeaturedMod(replayInfo.getFeaturedMod()); - CompletableFuture> mapBeanFuture = mapService.findByMapFolderName(replayInfo.getMapname()); + CompletableFuture> mapBeanFuture = mapService.findByMapFolderName(replayInfo.getFeaturedMod(), replayInfo.getMapname()); return CompletableFuture.allOf(featuredModFuture, mapBeanFuture).thenApply(ignoredVoid -> { Optional mapBean = mapBeanFuture.join(); @@ -370,7 +370,7 @@ public void runLiveReplay(int gameId) { .scheme(FAF_LIFE_PROTOCOL) .host(clientProperties.getReplay().getRemoteHost()) .path("/" + gameId + "/" + playerName + SUP_COM_REPLAY_FILE_ENDING) - .queryParam("map", UrlEscapers.urlFragmentEscaper().escape(game.getMapFolderName())) + .queryParam("map", UrlEscapers.urlFragmentEscaper().escape(game.getMapName())) .queryParam("mod", game.getFeaturedMod()) .build() .toUri(); diff --git a/src/main/java/com/faforever/client/tutorial/TutorialDetailController.java b/src/main/java/com/faforever/client/tutorial/TutorialDetailController.java index 77f0c4679..50b1d157b 100644 --- a/src/main/java/com/faforever/client/tutorial/TutorialDetailController.java +++ b/src/main/java/com/faforever/client/tutorial/TutorialDetailController.java @@ -2,9 +2,10 @@ import com.faforever.client.fx.AbstractViewController; import com.faforever.client.fx.WebViewConfigurer; +import com.faforever.client.game.KnownFeaturedMod; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import javafx.beans.binding.Bindings; import javafx.scene.Node; import javafx.scene.control.Button; @@ -67,10 +68,10 @@ public void setTutorial(Tutorial tutorial) { this.tutorial = tutorial; titleLabel.textProperty().bind(tutorial.titleProperty()); if (tutorial.getMapVersion() != null) { - mapNameLabel.textProperty().bind(Bindings.createStringBinding(() -> i18n.get("tutorial.mapName", tutorial.getMapVersion().getDisplayName()), + mapNameLabel.textProperty().bind(Bindings.createStringBinding(() -> i18n.get("tutorial.mapName", tutorial.getMapVersion().getMapName()), tutorial.mapVersionProperty(), - tutorial.getMapVersion().displayNameProperty())); - mapImage.setImage(mapService.loadPreview(tutorial.getMapVersion(), PreviewSize.LARGE)); + tutorial.getMapVersion().mapNameProperty())); + mapImage.setImage(mapService.loadPreview(KnownFeaturedMod.DEFAULT.getTechnicalName(), tutorial.getMapVersion(), PreviewType.MINI, 10)); mapContainer.setVisible(true); } else { mapContainer.setVisible(false); diff --git a/src/main/java/com/faforever/client/tutorial/TutorialListItemController.java b/src/main/java/com/faforever/client/tutorial/TutorialListItemController.java index aab2d5f79..87563d9ad 100644 --- a/src/main/java/com/faforever/client/tutorial/TutorialListItemController.java +++ b/src/main/java/com/faforever/client/tutorial/TutorialListItemController.java @@ -1,8 +1,9 @@ package com.faforever.client.tutorial; import com.faforever.client.fx.AbstractViewController; +import com.faforever.client.game.KnownFeaturedMod; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.google.common.base.Strings; import javafx.beans.binding.Bindings; import javafx.css.PseudoClass; @@ -54,6 +55,6 @@ private Image getImage(Tutorial tutorial) { if (!Strings.isNullOrEmpty(tutorial.getImageUrl())) { return new Image(tutorial.getImageUrl()); } - return tutorial.getMapVersion() != null ? mapService.loadPreview(tutorial.getMapVersion(), PreviewSize.SMALL) : null; + return tutorial.getMapVersion() != null ? mapService.loadPreview(KnownFeaturedMod.DEFAULT.getTechnicalName(), tutorial.getMapVersion(), PreviewType.MINI, 10) : null; } } diff --git a/src/main/java/com/faforever/client/update/ClientConfiguration.java b/src/main/java/com/faforever/client/update/ClientConfiguration.java index 7b971cdc8..11d7ca548 100644 --- a/src/main/java/com/faforever/client/update/ClientConfiguration.java +++ b/src/main/java/com/faforever/client/update/ClientConfiguration.java @@ -11,11 +11,21 @@ * A representation of a config file read from the faf server on start up. The file on the server allows to dynamically change settings in the client remotely. */ public class ClientConfiguration { + List downloadables; ReleaseInfo latestRelease; List recommendedMaps; List endpoints; GitHubRepo gitHubRepo; + @Data + public static class Downloadable { + String what; + String url; + String crc32; + List depends; + List conflicts; + } + @Data public static class GitHubRepo { /** diff --git a/src/main/java/com/faforever/client/vault/replay/LiveReplayController.java b/src/main/java/com/faforever/client/vault/replay/LiveReplayController.java index ba7fbba5e..fd95a5e4e 100644 --- a/src/main/java/com/faforever/client/vault/replay/LiveReplayController.java +++ b/src/main/java/com/faforever/client/vault/replay/LiveReplayController.java @@ -8,7 +8,7 @@ import com.faforever.client.game.MapPreviewTableCell; import com.faforever.client.i18n.I18n; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.remote.domain.GameStatus; import com.faforever.client.theme.UiService; import com.faforever.client.util.TimeService; @@ -84,8 +84,8 @@ private void initializeGameTable(ObservableList games) { mapPreviewColumn.setCellFactory(param -> new MapPreviewTableCell(uiService)); mapPreviewColumn.setCellValueFactory(param -> Bindings.createObjectBinding( - () -> mapService.loadPreview(param.getValue().getFeaturedMod(), param.getValue().getMapFolderName(), PreviewSize.SMALL), - param.getValue().mapFolderNameProperty() + () -> mapService.loadPreview(param.getValue().getFeaturedMod(), param.getValue().getMapName(), PreviewType.MINI, 10), + param.getValue().mapNameProperty() )); startTimeColumn.setCellValueFactory(param -> param.getValue().startTimeProperty()); diff --git a/src/main/java/com/faforever/client/vault/replay/ReplayVaultController.java b/src/main/java/com/faforever/client/vault/replay/ReplayVaultController.java index eb3cb9b7d..1041b469b 100644 --- a/src/main/java/com/faforever/client/vault/replay/ReplayVaultController.java +++ b/src/main/java/com/faforever/client/vault/replay/ReplayVaultController.java @@ -7,7 +7,7 @@ import com.faforever.client.main.event.NavigateEvent; import com.faforever.client.map.MapBean; import com.faforever.client.map.MapService; -import com.faforever.client.map.MapService.PreviewSize; +import com.faforever.client.map.MapService.PreviewType; import com.faforever.client.notification.NotificationService; import com.faforever.client.replay.Replay; import com.faforever.client.replay.ReplayService; @@ -209,9 +209,9 @@ protected void updateItem(MapBean map, boolean empty) { setGraphic(null); setText(i18n.get("map.unknown")); } else { - imageView.setImage(mapService.loadPreview(KnownFeaturedMod.DEFAULT.getTechnicalName(), map.getFolderName(), PreviewSize.SMALL)); + imageView.setImage(mapService.loadPreview(KnownFeaturedMod.DEFAULT.getTechnicalName(), map.getMapName(), PreviewType.MINI, 10)); setGraphic(imageView); - setText(map.getDisplayName()); + setText(map.getMapName()); } } }; diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index a43a36a79..d4a734b34 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -26,6 +26,10 @@ faf-client: forged-alliance: exe-url: https://content.faforever.com/faf/updaterNew/updates_faf_files/ForgedAlliance.exe + use-remote-preferences: true + client-config-url: http://192.168.1.109/dfc-config.json + + logging: level: org.pircbotx.PircBotX: off diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 949cfa7e0..0d1e5bfb0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -54,6 +54,8 @@ faf-client: url: https://discord.gg/KP7ndRagrM - title: TA Escalation url: https://discord.gg/W2ErD5H + - title: TA Twilight + url: https://discord.gg/V9fmjnbV6e spring: profiles: diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index cecd69dd6..f2fb7bf2d 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -424,6 +424,7 @@ updateCheckTask.title=Checking for update clientUpdateCheckTask.title=Checking for client update clientUpdateDownloadTask.title=Downloading client update mapDownloadTask.title=Downloading map {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself mapReplayTask.title=Downloading replay {0} downloadingModTask.downloading=Downloading mod {0} downloadingModTask.unzipping=Unzipping mod to {0} @@ -814,3 +815,4 @@ game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location diff --git a/src/main/resources/i18n/messages_cs.properties b/src/main/resources/i18n/messages_cs.properties index 015cffce8..9877aac3a 100644 --- a/src/main/resources/i18n/messages_cs.properties +++ b/src/main/resources/i18n/messages_cs.properties @@ -344,6 +344,7 @@ updateCheckTask.title = Hledání aktualizace clientUpdateCheckTask.title = Hledání aktualizace klientu clientUpdateDownloadTask.title = Stahování aktualizace klientu mapDownloadTask.title = Stahování mapy {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Stahování záznamu {0} downloadingModTask.downloading = Stahování módu {0} downloadingModTask.unzipping = Rozbalování módu do {0} @@ -736,4 +737,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index 10d5b049a..7fa91e2d2 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -814,4 +814,5 @@ game.leave.tooltip = Ihre Freunde lassen Sie hängen! Klicken Sie hier um neue F game.start.tooltip = Ihre Freunde wollen spielen! Klicken Sie hier um TA zu starten. game.leave = Verlassen game.start = Starten -chat.currentGame = Ihr beigetretenes/gehostetes Spiel \ No newline at end of file +chat.currentGame = Ihr beigetretenes/gehostetes Spiel +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_es.properties b/src/main/resources/i18n/messages_es.properties index d88c926ae..e487c24d8 100644 --- a/src/main/resources/i18n/messages_es.properties +++ b/src/main/resources/i18n/messages_es.properties @@ -353,6 +353,7 @@ updateCheckTask.title = Buscando actualizaciones clientUpdateCheckTask.title = Buscando actualizaciones del cliente clientUpdateDownloadTask.title = Descargando actualización del cliente mapDownloadTask.title = Descargando mapa {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Descargando repetición {0} downloadingModTask.downloading = Descargando mod {0} downloadingModTask.unzipping = Descomprimiendo mod a {0} @@ -688,4 +689,5 @@ game.leave.tooltip=Tus amigos son malos!\nHaga clic para buscar nuevos amigos. game.start.tooltip=Tus amigos quieren jugar!\nHaga clic para empezar TA. game.leave=Dejar game.start=Lanzar -chat.currentGame=Crear/Conectar a una partida \ No newline at end of file +chat.currentGame=Crear/Conectar a una partida +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_fr.properties b/src/main/resources/i18n/messages_fr.properties index 73aff0de5..3f95e7c5f 100644 --- a/src/main/resources/i18n/messages_fr.properties +++ b/src/main/resources/i18n/messages_fr.properties @@ -360,6 +360,7 @@ updateCheckTask.title = Vérification des mises à jour clientUpdateCheckTask.title = Vérification des m.à.j du client clientUpdateDownloadTask.title = Téléchargement de la m.à.j mapDownloadTask.title = Téléchargement de la carte {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Téléchargement de la rediff. {0} downloadingModTask.downloading = Téléchargement du mod {0} downloadingModTask.unzipping = Décompression du mod vers {0} @@ -812,4 +813,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_it.properties b/src/main/resources/i18n/messages_it.properties index a08c05594..16d2f6342 100644 --- a/src/main/resources/i18n/messages_it.properties +++ b/src/main/resources/i18n/messages_it.properties @@ -344,6 +344,7 @@ updateCheckTask.title = Controllo aggiornamenti clientUpdateCheckTask.title = Controllo aggiornamenti del client clientUpdateDownloadTask.title = Scaricamento aggiornamenti del client mapDownloadTask.title = Scaricamento mappa {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Scaricamento replay {0} downloadingModTask.downloading = Scaricamento mod {0} downloadingModTask.unzipping = Decompressione mod in {0} @@ -882,4 +883,5 @@ game.leave.tooltip=I tuoi amici sono dei coglioni!\nClicca per trovare nuovi ami game.start.tooltip=I tuoi amici vogliono giocare!\nClicca per lanciare TA game.leave=Abbandona game.start=Inizia -chat.currentGame=La tua partita \ No newline at end of file +chat.currentGame=La tua partita +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_iw.properties b/src/main/resources/i18n/messages_iw.properties index 18153c3ae..96a360093 100644 --- a/src/main/resources/i18n/messages_iw.properties +++ b/src/main/resources/i18n/messages_iw.properties @@ -344,6 +344,7 @@ updateCheckTask.title = בודק עדכונים clientUpdateCheckTask.title = מחפש עדכוני תוכנה clientUpdateDownloadTask.title = מוריד עדכוני תוכנה mapDownloadTask.title = מוריד מפה {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = מוריד הקלטה {0} downloadingModTask.downloading = מוריד מוד {0} downloadingModTask.unzipping = מחלץ מוד ל {0} @@ -712,4 +713,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_nl.properties b/src/main/resources/i18n/messages_nl.properties index 01c50b521..f48177f50 100644 --- a/src/main/resources/i18n/messages_nl.properties +++ b/src/main/resources/i18n/messages_nl.properties @@ -346,6 +346,7 @@ updateCheckTask.title = Controleer voor update clientUpdateCheckTask.title = Controleer client voor update clientUpdateDownloadTask.title = Downloaden client update mapDownloadTask.title = Downloaden map {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Laden herhaling {0} downloadingModTask.downloading = Downloaden mod {0} downloadingModTask.unzipping = Uitpakken mod naar {0} @@ -786,4 +787,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_pl.properties b/src/main/resources/i18n/messages_pl.properties index b4111db1e..226e6604d 100644 --- a/src/main/resources/i18n/messages_pl.properties +++ b/src/main/resources/i18n/messages_pl.properties @@ -367,6 +367,7 @@ updateCheckTask.title = Sprawdzanie aktualizacji clientUpdateCheckTask.title = Sprawdzanie aktualizacji klienta clientUpdateDownloadTask.title = Pobieranie aktualizacji klienta mapDownloadTask.title = Pobieranie mapy {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Pobieranie powtórki {0} downloadingModTask.downloading = Pobieranie moda {0} downloadingModTask.unzipping = Wyodrębnianie modu {0} @@ -683,4 +684,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_ru.properties b/src/main/resources/i18n/messages_ru.properties index df152397b..f4075374f 100644 --- a/src/main/resources/i18n/messages_ru.properties +++ b/src/main/resources/i18n/messages_ru.properties @@ -356,6 +356,7 @@ updateCheckTask.title = Поиск обновлений clientUpdateCheckTask.title = Поиск обновлений клиента clientUpdateDownloadTask.title = Скачивание обновлений mapDownloadTask.title = Загрузка карты {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Скачивание реплея {0} downloadingModTask.downloading = Загрузка мода {0} downloadingModTask.unzipping = Распаковка мода в {0} @@ -807,4 +808,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_tr.properties b/src/main/resources/i18n/messages_tr.properties index 29ccc76ca..37f6297b2 100644 --- a/src/main/resources/i18n/messages_tr.properties +++ b/src/main/resources/i18n/messages_tr.properties @@ -343,6 +343,7 @@ updateCheckTask.title = Güncellemeler kontrol ediliyor clientUpdateCheckTask.title = İstemci güncellemesi kontrol ediliyor clientUpdateDownloadTask.title = İstemci güncellemesi indiriliyor mapDownloadTask.title = Harita indiriliyor {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Tekrar indiriliyor {0} downloadingModTask.downloading = Mod indiriliyor {0} downloadingModTask.unzipping = Mod {0} a açılıyor @@ -779,4 +780,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_uk.properties b/src/main/resources/i18n/messages_uk.properties index 12977867b..372833cab 100644 --- a/src/main/resources/i18n/messages_uk.properties +++ b/src/main/resources/i18n/messages_uk.properties @@ -329,6 +329,7 @@ updateCheckTask.title = Перевірка оновлень clientUpdateCheckTask.title = Перевірка оновлень для клієнта clientUpdateDownloadTask.title = Завантаження оновлення mapDownloadTask.title = Завантаження мапи {0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = Завантажую повтор {0} downloadingModTask.downloading = Завантаження моду {0} downloadingModTask.unzipping = Розпакування моду до {0} @@ -676,4 +677,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/i18n/messages_zh.properties b/src/main/resources/i18n/messages_zh.properties index 46f5b2721..7f1595157 100644 --- a/src/main/resources/i18n/messages_zh.properties +++ b/src/main/resources/i18n/messages_zh.properties @@ -350,6 +350,7 @@ updateCheckTask.title = 检查更新 clientUpdateCheckTask.title = 检查客户端更新 clientUpdateDownloadTask.title = 正在下载客户端更新 mapDownloadTask.title = 正在下载地图{0} +mapDownloadTask.notFound=Map Archive not found. You may need to install this map yourself. mapReplayTask.title = 正在下载录像{0} downloadingModTask.downloading = 正在下载MOD {0} downloadingModTask.unzipping = 解压缩到{0} @@ -678,4 +679,5 @@ game.leave.tooltip=Your friends are dicks!\nClick to go find some new friends game.start.tooltip=Your friends want to play!\nClick to start TA game.leave=Leave game.start=Start -chat.currentGame=Your Hosted/Joined Game \ No newline at end of file +chat.currentGame=Your Hosted/Joined Game +game.create.install=Set Game Location \ No newline at end of file diff --git a/src/main/resources/theme/chat/chat.fxml b/src/main/resources/theme/chat/chat.fxml index bc7b840c9..bc0765811 100644 --- a/src/main/resources/theme/chat/chat.fxml +++ b/src/main/resources/theme/chat/chat.fxml @@ -11,53 +11,59 @@ - + - + - - - + + + + + + + + + + + + + - - - - - - - - - + + - - - + + + + - - - + + - + diff --git a/src/main/resources/theme/play/create_game.fxml b/src/main/resources/theme/play/create_game.fxml index 9baac3c33..54988777a 100644 --- a/src/main/resources/theme/play/create_game.fxml +++ b/src/main/resources/theme/play/create_game.fxml @@ -17,13 +17,12 @@ + - - - + @@ -40,13 +39,11 @@ -