Skip to content

Commit

Permalink
Always ask for game path (#1595)
Browse files Browse the repository at this point in the history
Fixes #1594

Fixes #1355

Fixes #1100

Fixes #1614  

* fixed hash function for supcom.exe (#1614)

Co-authored-by: Slothologist <rfeldh@gmail.com>
  • Loading branch information
1-alex98 and Slothologist committed Mar 11, 2020
1 parent 7dd07f6 commit ab976de
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Data
Expand Down Expand Up @@ -32,6 +34,7 @@ public class ClientProperties {
private boolean showIceAdapterDebugWindow;
private String statusPageUrl;
private Map<String, String> links = new HashMap<>();
private List<String> vanillaGameHashes = new ArrayList<>();

@Data
public static class News {
Expand Down
34 changes: 27 additions & 7 deletions src/main/java/com/faforever/client/game/GamePathHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;

@Component
public class GamePathHandler implements InitializingBean {
Expand Down Expand Up @@ -55,30 +58,47 @@ public void afterPropertiesSet() {
@Subscribe
public void onGameDirectoryChosenEvent(GameDirectoryChosenEvent event) {
Path path = event.getPath();
Optional<CompletableFuture<Path>> future = event.getFuture();

if (path == null || !Files.isDirectory(path)) {
notificationService.addNotification(new ImmediateNotification(i18n.get("gameChosen.invalidPath"), i18n.get("gameChosen.couldNotLocatedGame"), Severity.WARN));
if (path == null) {
notificationService.addNotification(new ImmediateNotification(i18n.get("gameSelect.select.invalidPath"), i18n.get("gamePath.select.noneChosen"), Severity.WARN));
future.ifPresent(pathCompletableFuture -> pathCompletableFuture.completeExceptionally(new CancellationException("User cancelled")));
return;
}

if (!Files.isRegularFile(path.resolve(PreferencesService.FORGED_ALLIANCE_EXE)) && !Files.isRegularFile(path.resolve(PreferencesService.SUPREME_COMMANDER_EXE))) {
onGameDirectoryChosenEvent(new GameDirectoryChosenEvent(path.resolve("bin")));
return;
Path pathWithBin = path.resolve("bin");
if (Files.isDirectory(pathWithBin)) {
path = pathWithBin;
}

// At this point, path points to the "bin" directory
Path gamePath = path.getParent();

String gamePathValidWithError;
try {
gamePathValidWithError = preferencesService.isGamePathValidWithError(gamePath);
} catch (Exception e) {
notificationService.addImmediateErrorNotification(e, "gamePath.select.error");
future.ifPresent(pathCompletableFuture -> pathCompletableFuture.completeExceptionally(e));
return;
}
if (gamePathValidWithError != null) {
notificationService.addNotification(new ImmediateNotification(i18n.get("gameSelect.select.invalidPath"), i18n.get(gamePathValidWithError), Severity.WARN));
future.ifPresent(pathCompletableFuture -> pathCompletableFuture.completeExceptionally(new IllegalArgumentException("Invalid path")));
return;
}


logger.info("Found game path at {}", gamePath);
preferencesService.getPreferences().getForgedAlliance().setInstallationPath(gamePath);
preferencesService.storeInBackground();
future.ifPresent(pathCompletableFuture -> pathCompletableFuture.complete(gamePath));
}


private void detectGamePath() {
for (Path path : USUAL_GAME_PATHS) {
if (preferencesService.isGamePathValid(path.resolve("bin"))) {
onGameDirectoryChosenEvent(new GameDirectoryChosenEvent(path));
onGameDirectoryChosenEvent(new GameDirectoryChosenEvent(path, Optional.empty()));
return;
}
}
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/com/faforever/client/game/GameService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.faforever.client.remote.domain.LoginMessage;
import com.faforever.client.replay.ReplayServer;
import com.faforever.client.reporting.ReportingService;
import com.faforever.client.ui.preferences.event.GameDirectoryChooseEvent;
import com.faforever.client.util.RatingUtil;
import com.faforever.client.util.TimeUtil;
import com.google.common.annotations.VisibleForTesting;
Expand Down Expand Up @@ -298,6 +299,11 @@ public CompletableFuture<Void> hostGame(NewGameInfo newGameInfo) {
return completedFuture(null);
}

if (!preferencesService.isGamePathValid()) {
CompletableFuture<Path> gameDirectoryFuture = postGameDirectoryChooseEvent();
return gameDirectoryFuture.thenCompose(path -> hostGame(newGameInfo));
}

stopSearchLadder1v1();

return updateGameIfNecessary(newGameInfo.getFeaturedMod(), null, emptyMap(), newGameInfo.getSimMods())
Expand All @@ -312,6 +318,11 @@ public CompletableFuture<Void> joinGame(Game game, String password) {
return completedFuture(null);
}

if (!preferencesService.isGamePathValid()) {
CompletableFuture<Path> gameDirectoryFuture = postGameDirectoryChooseEvent();
return gameDirectoryFuture.thenCompose(path -> joinGame(game, password));
}

log.info("Joining game: '{}' ({})", game.getTitle(), game.getId());

stopSearchLadder1v1();
Expand Down Expand Up @@ -360,6 +371,13 @@ public void runWithReplay(Path path, @Nullable Integer replayId, String featured
log.warn("Forged Alliance is already running, not starting replay");
return;
}

if (!preferencesService.isGamePathValid()) {
CompletableFuture<Path> gameDirectoryFuture = postGameDirectoryChooseEvent();
gameDirectoryFuture.thenAccept(pathSet -> runWithReplay(path, replayId, featuredMod, version, modVersions, simMods, mapName));
return;
}

modService.getFeaturedMod(featuredMod)
.thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, version, modVersions, simMods))
.thenCompose(aVoid -> downloadMapIfNecessary(mapName).handleAsync((ignoredResult, throwable) -> askWhetherToStartWithOutMap(throwable)))
Expand All @@ -379,6 +397,13 @@ public void runWithReplay(Path path, @Nullable Integer replayId, String featured
});
}

@NotNull
private CompletableFuture<Path> postGameDirectoryChooseEvent() {
CompletableFuture<Path> gameDirectoryFuture = new CompletableFuture<>();
eventBus.post(new GameDirectoryChooseEvent(gameDirectoryFuture));
return gameDirectoryFuture;
}

@SneakyThrows
private Void askWhetherToStartWithOutMap(Throwable throwable) {
if (throwable == null) {
Expand Down Expand Up @@ -418,6 +443,11 @@ public CompletableFuture<Void> runWithLiveReplay(URI replayUrl, Integer gameId,
return completedFuture(null);
}

if (!preferencesService.isGamePathValid()) {
CompletableFuture<Path> gameDirectoryFuture = postGameDirectoryChooseEvent();
return gameDirectoryFuture.thenCompose(path -> runWithLiveReplay(replayUrl, gameId, gameType, mapName));
}

Game gameBean = getByUid(gameId);

Map<String, Integer> modVersions = gameBean.getFeaturedModVersions();
Expand Down Expand Up @@ -460,6 +490,11 @@ public CompletableFuture<Void> startSearchLadder1v1(Faction faction) {
return completedFuture(null);
}

if (!preferencesService.isGamePathValid()) {
CompletableFuture<Path> gameDirectoryFuture = postGameDirectoryChooseEvent();
return gameDirectoryFuture.thenCompose(path -> startSearchLadder1v1(faction));
}

searching1v1.set(true);

return modService.getFeaturedMod(LADDER_1V1.getTechnicalName())
Expand Down Expand Up @@ -849,6 +884,13 @@ public void onGameCloseRequested(CloseGameEvent event) {
}

public void launchTutorial(MapBean mapVersion, String technicalMapName) {

if (!preferencesService.isGamePathValid()) {
CompletableFuture<Path> gameDirectoryFuture = postGameDirectoryChooseEvent();
gameDirectoryFuture.thenAccept(path -> launchTutorial(mapVersion, technicalMapName));
return;
}

modService.getFeaturedMod(TUTORIALS.getTechnicalName())
.thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, null, emptyMap(), emptySet()))
.thenCompose(aVoid -> downloadMapIfNecessary(mapVersion.getFolderName()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
Expand Down Expand Up @@ -68,7 +67,6 @@ public class GameBinariesUpdateTaskImpl extends CompletableTask<Void> implements

private Integer version;

@Inject
public GameBinariesUpdateTaskImpl(I18n i18n, PreferencesService preferencesService, PlatformService platformService, ClientProperties clientProperties) {
super(Priority.HIGH);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.paint.Color;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
Expand All @@ -40,6 +43,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.DigestInputStream;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -334,8 +340,52 @@ public Path getThemesDirectory() {
return getFafDataDirectory().resolve("themes");
}

@SneakyThrows
public boolean isGamePathValid() {
return preferences.getForgedAlliance().getInstallationPath() != null && isGamePathValid(preferences.getForgedAlliance().getInstallationPath().resolve("bin"));
return isGamePathValidWithError(preferences.getForgedAlliance().getInstallationPath()) == null;
}

public String isGamePathValidWithError(Path installationPath) throws IOException, NoSuchAlgorithmException {
boolean valid = installationPath != null && isGamePathValid(installationPath.resolve("bin"));
if (!valid) {
return "gamePath.select.noValidExe";
}
Path binPath = installationPath.resolve("bin");
String exeHash;
if (Files.exists(binPath.resolve(FORGED_ALLIANCE_EXE))) {
exeHash = sha256OfFile(binPath.resolve(FORGED_ALLIANCE_EXE));
} else {
exeHash = sha256OfFile(binPath.resolve(SUPREME_COMMANDER_EXE));
}
for (String hash : clientProperties.getVanillaGameHashes()) {
logger.debug("Hash of Supreme Commander.exe in selected User directory: " + exeHash);
if (hash.equals(exeHash)) {
return "gamePath.select.vanillaGameSelected";
}
}

if (binPath.equals(getFafBinDirectory())) {
return "gamePath.select.fafDataSelected";
}

return null;
}

private String sha256OfFile(Path path) throws IOException, NoSuchAlgorithmException {
byte[] buffer = new byte[4096];
MessageDigest digest = MessageDigest.getInstance("SHA-256");
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path.toFile()));
DigestInputStream digestInputStream = new DigestInputStream(bis, digest);
while (digestInputStream.read(buffer) > -1) {
}
digest = digestInputStream.getMessageDigest();
digestInputStream.close();
byte[] sha256 = digest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : sha256) {
sb.append(String.format("%02X", b));
}
return sb.toString().toUpperCase();
}

public boolean isGamePathValid(Path binPath) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

import java.io.File;
import java.lang.invoke.MethodHandles;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import static javafx.application.Platform.runLater;

Expand All @@ -28,6 +30,7 @@ public class GameDirectoryRequiredHandler implements InitializingBean {

private final EventBus eventBus;
private final I18n i18n;
private CompletableFuture<Path> future;

@Override
public void afterPropertiesSet() {
Expand All @@ -43,7 +46,10 @@ public void onChooseGameDirectory(GameDirectoryChooseEvent event) {

logger.info("User selected game directory: {}", result);

eventBus.post(new GameDirectoryChosenEvent(Optional.ofNullable(result).map(File::toPath).orElse(null)));
Path path = Optional.ofNullable(result).map(File::toPath).orElse(null);
eventBus.post(new GameDirectoryChosenEvent(path, event.getFuture()));

});
}

}
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package com.faforever.client.ui.preferences.event;

import lombok.Value;
import org.jetbrains.annotations.Nullable;

import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

@Value
public class GameDirectoryChosenEvent {
@Nullable
private Path path;

public GameDirectoryChosenEvent(@Nullable Path path) {
this.path = path;
}

@Nullable
public Path getPath() {
return path;
}
private Optional<CompletableFuture<Path>> future;
}
3 changes: 3 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ faf-client:
spookiesUrl: https://spooky.github.io/unitdb/#/
rackOversUrl: https://unitdb.faforever.com?settings64=eyJwcmV2aWV3Q29ybmVyIjoiTm9uZSJ9

vanillaGameHashes:
- 1AFBCD0CA85546470660FA8AC09B230E870EC65C8D1B33FEB6731BB5D4C366C5

map-generator:
repoAndOwnerName: FAForever/Neroxis-Map-Generator
queryLatestVersionUrl: https://api.github.com/repos/${faf-client.map-generator.repoAndOwnerName}/releases/latest
Expand Down
9 changes: 6 additions & 3 deletions src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -668,15 +668,18 @@ map.created=Created:
modType.ui=Ui mod
modType.sim=Sim mod
menu.connect=Connecting
gameChosen.invalidPath=Invalid path
gameChosen.couldNotLocatedGame=The specified location does not contain a valid Supreme Commander: Forged Alliance installation.
gameSelect.select.invalidPath=Invalid path
gamePath.select.noneChosen=You did not select a game location!
gamePath.select.noValidExe=The specified location does not contain a valid Supreme Commander.exe and is therefore is no valid Forged Alliance installation.
gamePath.select.vanillaGameSelected=The specified location is a vanilla installation, you need to select the extension Supreme Commander Forged Alliance.
gamePath.select.fafDataSelected=The specified location is the Forged Alliance Forever game data directory, please select the Forged Alliance installation instead.
gamePath.select.error=Something went wrong selecting game path
userInfo.statistics.errorLoading=Could not load statistics
settings.general.unitDatabase=Unit database
unitDatabase.rackover=Rackover
unitDatabase.spooky=Spooky
game.player.globalRating=One of the player's global ratings
game.player.ladderRating=One of the player's ladder ratings

mapVault.ladder=Ladder maps
news.showLadderMaps=Show ladder maps
ranked1v1.showMaps=Show ladder maps
Expand Down
14 changes: 11 additions & 3 deletions src/test/java/com/faforever/client/game/GamePathHandlerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.nio.file.Paths;
import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;

Expand All @@ -35,13 +39,17 @@ public void setUp() throws Exception {

@Test
public void testNotificationOnEmptyString() throws Exception {
instance.onGameDirectoryChosenEvent(new GameDirectoryChosenEvent(Paths.get("")));
CompletableFuture<Path> completableFuture = new CompletableFuture<>();
instance.onGameDirectoryChosenEvent(new GameDirectoryChosenEvent(null, Optional.of(completableFuture)));
verify(notificationService).addNotification(any(ImmediateNotification.class));
assertThat(completableFuture.isCompletedExceptionally(), is(true));
}

@Test
public void testNotificationOnNull() throws Exception {
instance.onGameDirectoryChosenEvent(new GameDirectoryChosenEvent(null));
CompletableFuture<Path> completableFuture = new CompletableFuture<>();
instance.onGameDirectoryChosenEvent(new GameDirectoryChosenEvent(null, Optional.of(completableFuture)));
verify(notificationService).addNotification(any(ImmediateNotification.class));
assertThat(completableFuture.isCompletedExceptionally(), is(true));
}
}

0 comments on commit ab976de

Please sign in to comment.