Skip to content

Commit

Permalink
Update Mods before hosting
Browse files Browse the repository at this point in the history
Fixes #1680
  • Loading branch information
1-alex98 committed May 30, 2021
1 parent 598f958 commit 0ee9987
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 46 deletions.
43 changes: 29 additions & 14 deletions src/main/java/com/faforever/client/game/CreateGameController.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -407,26 +409,39 @@ private void onGenerateMap() {
}

public void onCreateButtonClicked() {
MapBean selectedMap = mapListView.getSelectionModel().getSelectedItem();
onCloseButtonClicked();
if (mapService.isOfficialMap(selectedMap)) {
hostGame(selectedMap);
} else {
mapService.updateLatestVersionIfNecessary(selectedMap)
.exceptionally(throwable -> {
log.error("error when updating the map", throwable);
hostGame(selectedMap);
return null;
})
.thenAccept(this::hostGame);
}
hostGameAfterUpdates();
}

private void hostGameAfterUpdates() {
MapBean selectedMap = mapListView.getSelectionModel().getSelectedItem();
List<ModVersion> selectedModVersions = modManagerController.getSelectedModVersions();

mapService.updateLatestVersionIfNecessary(selectedMap)
.exceptionally(throwable -> {
log.error("Error when updating the map", throwable);
return selectedMap;
})
.thenCombine(modService.updateAndActivateModVersions(selectedModVersions), (mapBean, mods) -> {
hostGame(mapBean, getUUIDsFromModVersions(mods));
return null;
})
.exceptionally(throwable -> {
log.error("Error when updating selected mods and peparing game", throwable);
notificationService.addImmediateErrorNotification(throwable, "game.create.errorUpdatingMods");
return null;
});

}

private void hostGame(MapBean map) {
Set<String> mods = modManagerController.apply().stream()
@NotNull
private Set<String> getUUIDsFromModVersions(List<ModVersion> modVersions) {
return modVersions.stream()
.map(ModVersion::getUid)
.collect(Collectors.toSet());
}

private void hostGame(MapBean map, Set<String> mods) {
Integer minRating = null;
Integer maxRating = null;
boolean enforceRating;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/faforever/client/map/MapService.java
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,9 @@ private boolean containsVersionControl(String mapFolderName) {
}

public CompletableFuture<MapBean> updateLatestVersionIfNecessary(MapBean map) {
if (isOfficialMap(map) || !preferencesService.getPreferences().getMapAndModAutoUpdate()) {
return CompletableFuture.completedFuture(map);
}
return getMapLatestVersion(map).thenCompose(latestMap -> {
CompletableFuture<Void> downloadFuture;
if (!isInstalled(latestMap.getFolderName())) {
Expand Down
13 changes: 9 additions & 4 deletions src/main/java/com/faforever/client/mod/ModManagerController.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,20 @@ private Callback<ListView<ModVersion>, ListCell<ModVersion>> modListCellFactory(
}

public List<ModVersion> apply() {
List<ModVersion> mods = modToSelectedMap.entrySet().stream()
.filter(Entry::getValue)
.map(Entry::getKey)
.collect(Collectors.toList());
List<ModVersion> mods = getSelectedModVersions();
try {
modService.overrideActivatedMods(mods);
} catch (IOException e) {
log.warn("Activated mods could not be updated", e);
}
return mods;
}

@NotNull
public List<ModVersion> getSelectedModVersions() {
return modToSelectedMap.entrySet().stream()
.filter(Entry::getValue)
.map(Entry::getKey)
.collect(Collectors.toList());
}
}
35 changes: 33 additions & 2 deletions src/main/java/com/faforever/client/mod/ModService.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import java.nio.file.Paths;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -373,7 +374,7 @@ private Map<String, Boolean> readModStates() throws IOException {

private void writeModStates(Map<String, Boolean> modStates) throws IOException {
Path preferencesFile = preferencesService.getPreferences().getForgedAlliance().getPreferencesFile();
String preferencesContent = new String(Files.readAllBytes(preferencesFile), US_ASCII);
String preferencesContent = Files.readString(preferencesFile, US_ASCII);

String currentActiveModsContent = null;
Matcher matcher = ACTIVE_MODS_PATTERN.matcher(preferencesContent);
Expand Down Expand Up @@ -405,7 +406,7 @@ private void writeModStates(Map<String, Boolean> modStates) throws IOException {
preferencesContent += newActiveModsContentBuilder.toString();
}

Files.write(preferencesFile, preferencesContent.getBytes(US_ASCII));
Files.writeString(preferencesFile, preferencesContent, US_ASCII);
}

private void removeMod(Path path) {
Expand Down Expand Up @@ -441,4 +442,34 @@ private void addMod(Path path) {
public void destroy() {
Optional.ofNullable(directoryWatcherThread).ifPresent(Thread::interrupt);
}

@Async
@SneakyThrows
public CompletableFuture<List<ModVersion>> updateAndActivateModVersions(final List<ModVersion> selectedModVersions) {
if (!preferencesService.getPreferences().getMapAndModAutoUpdate()) {
return CompletableFuture.completedFuture(selectedModVersions);
}

final List<ModVersion> newlySelectedMods = new ArrayList<>(selectedModVersions);
selectedModVersions.forEach(installedModVersion -> {
try {
ModVersion modVersionFromApi = getModVersionById(installedModVersion.getUid()).get();
ModVersion latestVersion = modVersionFromApi.getMod().getLatestVersion();
boolean isLatest = latestVersion.equals(installedModVersion);
if (!isLatest) {
downloadAndInstallMod(latestVersion.getDownloadUrl()).get();
newlySelectedMods.remove(installedModVersion);
newlySelectedMods.add(latestVersion);
}
} catch (Exception e) {
log.debug("Failed fetching info about mod from the api.", e);
}
});
overrideActivatedMods(newlySelectedMods);
return completedFuture(newlySelectedMods);
}

private CompletableFuture<ModVersion> getModVersionById(String id) {
return fafService.getModVersion(id);
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/faforever/client/preferences/Preferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class Preferences {
private final VaultPrefs vault;
private final StringProperty themeName;
private final BooleanProperty preReleaseCheckEnabled;
private final BooleanProperty mapAndModAutoUpdate;
private final BooleanProperty showPasswordProtectedGames;
private final BooleanProperty showModdedGames;
private final ListProperty<String> ignoredNotifications;
Expand Down Expand Up @@ -89,6 +90,7 @@ public Preferences() {
cacheLifeTimeInDays = new SimpleIntegerProperty(30);
gameDataCacheActivated = new SimpleBooleanProperty(false);
debugLogEnabled = new SimpleBooleanProperty(false);
mapAndModAutoUpdate = new SimpleBooleanProperty(true);
}

public VaultPrefs getVault() {
Expand Down Expand Up @@ -326,4 +328,16 @@ public void setDebugLogEnabled(boolean debugLogEnabled) {
public BooleanProperty debugLogEnabledProperty() {
return debugLogEnabled;
}

public boolean getMapAndModAutoUpdate() {
return mapAndModAutoUpdate.get();
}

public void setMapAndModAutoUpdate(boolean mapAndModAutoUpdate) {
this.mapAndModAutoUpdate.set(mapAndModAutoUpdate);
}

public BooleanProperty mapAndModAutoUpdateProperty() {
return mapAndModAutoUpdate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public class SettingsController implements Controller<Node> {
public CheckBox allowReplayWhileInGameCheckBox;
public Button allowReplayWhileInGameButton;
public CheckBox debugLogToggle;
public CheckBox mapAndModAutoUpdateCheckBox;

private final InvalidationListener availableLanguagesListener;

Expand Down Expand Up @@ -338,6 +339,13 @@ public void initialize() {
initNotifyMeOnAtMention();

initGameDataCache();

initMapAndModAutoUpdate();
}

private void initMapAndModAutoUpdate() {
mapAndModAutoUpdateCheckBox.selectedProperty()
.bindBidirectional(preferencesService.getPreferences().mapAndModAutoUpdateProperty());
}

private void initGameDataCache() {
Expand Down
47 changes: 25 additions & 22 deletions src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,9 @@ game.create.gameType = Game type
game.create.mods = Mods
game.create.titleMissing = Title is missing
game.create.featuredModMissing = Game type is missing
game.create.failed = Game could not be created.
game.rehost.failed = Rehosting game failed.
game.create.failed=Game could not be created.
game.create.errorUpdatingMods=Could not update selected mods. This might be due to file permission errors. Also not selecting the mod on game creation will make this error disappear.
game.rehost.failed=Rehosting game failed.
game.title = Title
game.players = Players
game.map = Map
Expand Down Expand Up @@ -864,26 +865,28 @@ game.reasonNotValid.unknown = Unknown Result
game.reasonNotValid.teamsUnlocked = Teams Unlocked
game.reasonNotValid.multipleTeams = Multiple Teams
game.reasonNotValid.ai = Had AI
game.reasonNotValid.civiliansRevealed = Civilians Revealed
game.reasonNotValid.difficulty = Wrong Difficulty
game.reasonNotValid.expansion = Expansion Disabled
game.reasonNotValid.spawn = Spawn Not Fixed
game.reasonNotValid.other = Other
game.valid = Game Rated
exitWarning.title = Warning
exitWarning.message = Forged Alliance is still running, are you sure you want to exit the client?
chat.userContext.broadcast = Broadcast message...
corruptedModsError.notification = An unexpected error kept the client from loading the following mod\: {0}.
settings.data = Data and caches
settings.data.gameDataCache.time = Game data cache validity in days
settings.data.gameDataCache.time.description = Defines how long game data files are cached. Set to big amount of time if you want to avoid downloads. Set to small amount of time to avoid high disk usage.
settings.data.gameDataCache = Game data cache
settings.data.gameDataCache.description = Cache game data by saving unused versions. Significantly decreases the amount of files downloaded especially when watching old replays.
settings.data.clearCache = Clear cache
settings.data.clearCache.description = Not yet implemented. Help us build this button.
settings.data.clearCache.button = Clear caches
settings.general.cacheLifeTime = Files cached for (in days)\:
games.create.enforceRating = Enforce rating
game.reasonNotValid.civiliansRevealed=Civilians Revealed
game.reasonNotValid.difficulty=Wrong Difficulty
game.reasonNotValid.expansion=Expansion Disabled
game.reasonNotValid.spawn=Spawn Not Fixed
game.reasonNotValid.other=Other
game.valid=Game Rated
exitWarning.title=Warning
exitWarning.message=Forged Alliance is still running, are you sure you want to exit the client?
chat.userContext.broadcast=Broadcast message...
corruptedModsError.notification=An unexpected error kept the client from loading the following mod\: {0}.
settings.data.mapAndModAutoUpdate=Map and mod auto update
settings.data.mapAndModAutoUpdate.description=Automatically downloads the newest version of a map or mod on hosting the map/mod. It is highly advised to keep the option on to get bug fixes and the newest working versions.
settings.data=Data and caches
settings.data.gameDataCache.time=Game data cache validity in days
settings.data.gameDataCache.time.description=Defines how long game data files are cached. Set to big amount of time if you want to avoid downloads. Set to small amount of time to avoid high disk usage.
settings.data.gameDataCache=Game data cache
settings.data.gameDataCache.description=Cache game data by saving unused versions. Significantly decreases the amount of files downloaded especially when watching old replays.
settings.data.clearCache=Clear cache
settings.data.clearCache.description=Not yet implemented. Help us build this button.
settings.data.clearCache.button=Clear caches
settings.general.cacheLifeTime=Files cached for (in days)\:
games.create.enforceRating=Enforce rating
chat.filter.countryPrompt = Country code
game.generateMap.generationType = Generation Type
game.generateMap.casual = Casual
Expand Down
20 changes: 20 additions & 0 deletions src/main/resources/theme/settings/settings.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,26 @@
</Spinner>
</children>
</GridPane>
<GridPane styleClass="setting-container">
<columnConstraints>
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0"/>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" valignment="TOP" vgrow="SOMETIMES"/>
<RowConstraints vgrow="SOMETIMES"/>
</rowConstraints>
<children>
<Label styleClass="setting-title"
text="%settings.data.mapAndModAutoUpdate"/>
<Label styleClass="setting-description"
text="%settings.data.mapAndModAutoUpdate.description"
GridPane.columnSpan="2147483647" GridPane.hgrow="ALWAYS"
GridPane.rowIndex="1"/>
<CheckBox fx:id="mapAndModAutoUpdateCheckBox" contentDisplay="RIGHT"
mnemonicParsing="false" GridPane.columnIndex="1"/>
</children>
</GridPane>
</children>
</VBox>
</content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import org.testfx.util.WaitForAsyncUtils;

import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;

Expand All @@ -53,6 +52,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -122,6 +122,8 @@ public void setUp() throws Exception {
when(i18n.get(any(), any())).then(invocation -> invocation.getArgument(0));
when(i18n.number(anyInt())).then(invocation -> invocation.getArgument(0).toString());
when(fafService.connectionStateProperty()).thenReturn(new SimpleObjectProperty<>(ConnectionState.CONNECTED));
when(modService.updateAndActivateModVersions(any()))
.thenAnswer(invocation -> completedFuture(invocation.getArgument(0)));

loadFxml("theme/play/create_game.fxml", clazz -> {
if (clazz.equals(ModManagerController.class)) {
Expand Down Expand Up @@ -292,7 +294,7 @@ public void testCreateGameWithSelectedModAndMap() {
String uidMod = "junit-mod";
modVersion.setUid(uidMod);

when(modManagerController.apply()).thenReturn(Collections.singletonList(modVersion));
when(modManagerController.getSelectedModVersions()).thenReturn(List.of(modVersion));

MapBean map = MapBuilder.create().defaultValues().get();
when(mapService.isOfficialMap(map)).thenReturn(false);
Expand All @@ -304,11 +306,44 @@ public void testCreateGameWithSelectedModAndMap() {
instance.setOnCloseButtonClickedListener(mock(Runnable.class));
instance.onCreateButtonClicked();

verify(modManagerController).apply();
verify(modManagerController).getSelectedModVersions();
assertThat(newGameInfoArgumentCaptor.getValue().getSimMods(), contains(uidMod));
assertThat(newGameInfoArgumentCaptor.getValue().getMap(), is(map.getFolderName()));
}

@Test
public void testCreateGameWithOutdatedMod() {
ArgumentCaptor<NewGameInfo> newGameInfoArgumentCaptor = ArgumentCaptor.forClass(NewGameInfo.class);
ModVersion modVersion = new ModVersion();
String uidMod = "outdated-mod";
modVersion.setUid(uidMod);

ModVersion newModVersion = new ModVersion();
String newUidMod = "new-mod";
newModVersion.setUid(newUidMod);

List<ModVersion> selectedMods = singletonList(modVersion);
when(modManagerController.getSelectedModVersions()).thenReturn(selectedMods);

MapBean map = MapBuilder.create().defaultValues().get();
when(mapService.isOfficialMap(map)).thenReturn(true);

when(modService.updateAndActivateModVersions(eq(selectedMods)))
.thenAnswer(invocation -> completedFuture(List.of(newModVersion)));

when(gameService.hostGame(newGameInfoArgumentCaptor.capture())).thenReturn(completedFuture(null));

mapList.add(map);
instance.mapListView.getSelectionModel().select(0);
instance.setOnCloseButtonClickedListener(mock(Runnable.class));

instance.onCreateButtonClicked();

verify(modManagerController).getSelectedModVersions();
assertThat(newGameInfoArgumentCaptor.getValue().getSimMods(), contains(newUidMod));
assertThat(newGameInfoArgumentCaptor.getValue().getMap(), is(map.getFolderName()));
}

@Test
public void testCreateGameOnSelectedMapIfNoNewVersionMap() {
ArgumentCaptor<NewGameInfo> captor = ArgumentCaptor.forClass(NewGameInfo.class);
Expand Down

0 comments on commit 0ee9987

Please sign in to comment.