Skip to content

Commit

Permalink
Merge 2f10fa7 into 2389ec9
Browse files Browse the repository at this point in the history
  • Loading branch information
1-alex98 committed Nov 10, 2020
2 parents 2389ec9 + 2f10fa7 commit 5155ed7
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 148 deletions.
70 changes: 60 additions & 10 deletions src/main/java/com/faforever/client/game/GameService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.faforever.client.patch.GameUpdater;
import com.faforever.client.player.Player;
import com.faforever.client.player.PlayerService;
import com.faforever.client.preferences.ForgedAlliancePrefs;
import com.faforever.client.preferences.NotificationsPrefs;
import com.faforever.client.preferences.PreferencesService;
import com.faforever.client.rankedmatch.MatchmakerInfoMessage;
Expand Down Expand Up @@ -61,12 +62,15 @@
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.inject.Inject;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -81,20 +85,22 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.faforever.client.fa.RatingMode.NONE;
import static com.faforever.client.game.KnownFeaturedMod.LADDER_1V1;
import static com.faforever.client.game.KnownFeaturedMod.TUTORIALS;
import static com.github.nocatch.NoCatch.noCatch;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.CompletableFuture.failedFuture;

/**
* Downloads necessary maps, mods and updates before starting
Expand All @@ -105,6 +111,11 @@
@RequiredArgsConstructor
public class GameService implements InitializingBean {

private static final Pattern GAME_PREFS_ALLOW_MULTI_LAUNCH_PATTERN = Pattern.compile("debug\\s*=(\\s)*[{][^}]*enable_debug_facilities\\s*=\\s*true");
private static final String GAME_PREFS_ALLOW_MULTI_LAUNCH_STRING = "\ndebug = {\n" +
" enable_debug_facilities = true\n" +
"}";

@VisibleForTesting
final BooleanProperty gameRunning;

Expand Down Expand Up @@ -145,6 +156,7 @@ public class GameService implements InitializingBean {
private Process process;
private boolean rehostRequested;
private int localReplayPort;
private ForgedAlliancePrefs forgedAlliancePrefs;

@Inject
public GameService(ClientProperties clientProperties,
Expand Down Expand Up @@ -191,6 +203,7 @@ public GameService(ClientProperties clientProperties,
games = FXCollections.observableList(new ArrayList<>(),
item -> new Observable[]{item.statusProperty(), item.getTeams()}
);
forgedAlliancePrefs = preferencesService.getPreferences().getForgedAlliance();
}

@Override
Expand Down Expand Up @@ -360,7 +373,7 @@ private CompletableFuture<Void> downloadMapIfNecessary(String mapFolderName) {
* @param path a replay file that is readable by the preferences without any further conversion
*/
public void runWithReplay(Path path, @Nullable Integer replayId, String featuredMod, Integer version, Map<String, Integer> modVersions, Set<String> simMods, String mapName) {
if (isRunning()) {
if (isRunning() && !forgedAlliancePrefs.isAllowReplaysWhileInGame()) {
log.warn("Forged Alliance is already running, not starting replay");
return;
}
Expand All @@ -376,10 +389,14 @@ public void runWithReplay(Path path, @Nullable Integer replayId, String featured
.thenCompose(aVoid -> downloadMapIfNecessary(mapName).handleAsync((ignoredResult, throwable) -> askWhetherToStartWithOutMap(throwable)))
.thenRun(() -> {
try {
process = forgedAllianceService.startReplay(path, replayId);
Process processForReplay = forgedAllianceService.startReplay(path, replayId);
if (forgedAlliancePrefs.isAllowReplaysWhileInGame()) {
return;
}
this.process = processForReplay;
setGameRunning(true);
this.ratingMode = NONE;
spawnTerminationListener(process);
spawnTerminationListener(this.process);
} catch (IOException e) {
notifyCantPlayReplay(replayId, e);
}
Expand Down Expand Up @@ -421,12 +438,16 @@ private Void askWhetherToStartWithOutMap(Throwable throwable) {
}

private void notifyCantPlayReplay(@Nullable Integer replayId, Throwable throwable) {
log.error("Could not play replay '" + replayId + "'", throwable);
notificationService.addImmediateErrorNotification(throwable, "replayCouldNotBeStarted");
if (throwable.getCause() instanceof UnsupportedOperationException) {
notificationService.addImmediateErrorNotification(throwable, "gameUpdate.error.gameNotWritableAllowMultiOn");
} else {
log.error("Could not play replay '" + replayId + "'", throwable);
notificationService.addImmediateErrorNotification(throwable, "replayCouldNotBeStarted");
}
}

public CompletableFuture<Void> runWithLiveReplay(URI replayUrl, Integer gameId, String gameType, String mapName) {
if (isRunning()) {
if (isRunning() && !forgedAlliancePrefs.isAllowReplaysWhileInGame()) {
log.warn("Forged Alliance is already running, not starting live replay");
return completedFuture(null);
}
Expand All @@ -445,11 +466,19 @@ public CompletableFuture<Void> runWithLiveReplay(URI replayUrl, Integer gameId,
.thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, null, modVersions, simModUids))
.thenCompose(aVoid -> downloadMapIfNecessary(mapName))
.thenRun(() -> noCatch(() -> {
process = forgedAllianceService.startReplay(replayUrl, gameId, getCurrentPlayer());
Process processCreated = forgedAllianceService.startReplay(replayUrl, gameId, getCurrentPlayer());
if (forgedAlliancePrefs.isAllowReplaysWhileInGame()) {
return;
}
this.process = processCreated;
setGameRunning(true);
this.ratingMode = NONE;
spawnTerminationListener(process);
}));
spawnTerminationListener(this.process);
}))
.exceptionally(throwable -> {
notifyCantPlayReplay(gameId, throwable);
return null;
});
}

private Player getCurrentPlayer() {
Expand Down Expand Up @@ -861,4 +890,25 @@ public void launchTutorial(MapBean mapVersion, String technicalMapName) {
});

}

@Async
public CompletableFuture<Void> patchGamePrefsForMultiInstances() throws IOException, ExecutionException, InterruptedException {
if (isGamePrefsPatchedToAllowMultiInstances().get()) {
return failedFuture(new IllegalStateException("Can not patch game.prefs file cause it already is patched"));
}
Path preferencesFile = preferencesService.getPreferences().getForgedAlliance().getPreferencesFile();
Files.writeString(preferencesFile, GAME_PREFS_ALLOW_MULTI_LAUNCH_STRING, US_ASCII, StandardOpenOption.APPEND);
return completedFuture(null);
}

private String getGamePrefsContent() throws IOException {
Path preferencesFile = preferencesService.getPreferences().getForgedAlliance().getPreferencesFile();
return Files.readString(preferencesFile, US_ASCII);
}

@Async
public CompletableFuture<Boolean> isGamePrefsPatchedToAllowMultiInstances() throws IOException {
String gamePrefsContent = getGamePrefsContent();
return completedFuture(GAME_PREFS_ALLOW_MULTI_LAUNCH_PATTERN.matcher(gamePrefsContent).find());
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/faforever/client/mod/ModService.java
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ private Map<String, Boolean> readModStates() throws IOException {
Path preferencesFile = preferencesService.getPreferences().getForgedAlliance().getPreferencesFile();
Map<String, Boolean> mods = new HashMap<>();

String preferencesContent = new String(Files.readAllBytes(preferencesFile), US_ASCII);
String preferencesContent = Files.readString(preferencesFile, US_ASCII);
Matcher matcher = ACTIVE_MODS_PATTERN.matcher(preferencesContent);
if (matcher.find()) {
Matcher activeModMatcher = ACTIVE_MOD_PATTERN.matcher(matcher.group(0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,11 @@ void copyGameFilesToFafBinDirectory() throws IOException {

logger.debug("Copying file '{}' to '{}'", source, destination);
noCatch(() -> createDirectories(destination.getParent()));
noCatch(() -> copy(source, destination, REPLACE_EXISTING));
noCatch(() -> {
if (!Files.exists(destination)) {
copy(source, destination, REPLACE_EXISTING);
}
});

if (org.bridj.Platform.isWindows()) {
noCatch(() -> setAttribute(destination, "dos:readonly", false));
Expand Down
19 changes: 14 additions & 5 deletions src/main/java/com/faforever/client/patch/GameUpdaterImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.context.ApplicationContext;

import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
Expand Down Expand Up @@ -81,11 +82,6 @@ public CompletableFuture<Void> update(FeaturedMod featuredMod, Integer version,

return future
.thenCompose(s -> updateGameBinaries(patchResults.get(patchResults.size() - 1).getVersion()))
.exceptionally(throwable -> {
log.warn("Game not terminated correctly", throwable);
notificationService.addImmediateErrorNotification(throwable, "error.game.notTerminatedCorrectly");
return null;
})
.thenRun(() -> {
if (patchResults.stream().noneMatch(patchResult -> patchResult.getLegacyInitFile() != null)) {
generateInitFile(patchResults);
Expand All @@ -99,6 +95,19 @@ public CompletableFuture<Void> update(FeaturedMod featuredMod, Integer version,
createFaPathLuaFile(initFile.getParent().getParent());
copyLegacyInitFile(initFile);
}
})
.exceptionally(throwable -> {
boolean allowReplaysWhileInGame = preferencesService.getPreferences().getForgedAlliance().isAllowReplaysWhileInGame();
if (throwable.getCause().getCause() instanceof AccessDeniedException && allowReplaysWhileInGame) {
log.info("Unable to update files and experimental replay feature is turned on " +
"that allows multiple game instances to run in parallel this is most likely the cause.");
throw new UnsupportedOperationException("Unable to patch Forged Alliance to the required version " +
"due to conflicting version running", throwable);
} else if (!(allowReplaysWhileInGame && version != null)) {
log.warn("Game files not accessible", throwable);
notificationService.addImmediateErrorNotification(throwable, "error.game.filesNotAccessible");
}
return null;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class ForgedAlliancePrefs {
private final ObjectProperty<Path> modsDirectory;
private final BooleanProperty forceRelay;
private final BooleanProperty autoDownloadMaps;
private final BooleanProperty allowReplaysWhileInGame;
/**
* Saves if the client checked for special cases in which it needs to set the fallback vault location. See {@link
*/
Expand Down Expand Up @@ -85,6 +86,7 @@ public ForgedAlliancePrefs() {
executionDirectory = new SimpleObjectProperty<>();
vaultCheckDone = new SimpleBooleanProperty(false);
bindVaultPath();
allowReplaysWhileInGame = new SimpleBooleanProperty(false);
}

/**
Expand Down Expand Up @@ -237,4 +239,16 @@ public void setVaultCheckDone(boolean vaultCheckDone) {
public BooleanProperty vaultCheckDoneProperty() {
return vaultCheckDone;
}

public boolean isAllowReplaysWhileInGame() {
return allowReplaysWhileInGame.get();
}

public void setAllowReplaysWhileInGame(boolean allowReplaysWhileInGame) {
this.allowReplaysWhileInGame.set(allowReplaysWhileInGame);
}

public BooleanProperty allowReplaysWhileInGameProperty() {
return allowReplaysWhileInGame;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.faforever.client.fx.JavaFxUtil;
import com.faforever.client.fx.PlatformService;
import com.faforever.client.fx.StringListCell;
import com.faforever.client.game.GameService;
import com.faforever.client.i18n.I18n;
import com.faforever.client.main.event.NavigationItem;
import com.faforever.client.notification.Action;
Expand Down Expand Up @@ -65,6 +66,7 @@
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.text.NumberFormat;
import java.util.Collections;
Expand All @@ -89,6 +91,7 @@ public class SettingsController implements Controller<Node> {
private final PlatformService platformService;
private final ClientProperties clientProperties;
private final ClientUpdateService clientUpdateService;
private final GameService gameService;

public TextField executableDecoratorField;
public TextField executionDirectoryField;
Expand Down Expand Up @@ -150,6 +153,8 @@ public class SettingsController implements Controller<Node> {
public Button clearCacheButton;
public CheckBox gameDataCacheCheckBox;
public Spinner<Integer> gameDataCacheTimeSpinner;
public CheckBox allowReplayWhileInGameCheckBox;
public Button allowReplayWhileInGameButton;

private final InvalidationListener availableLanguagesListener;

Expand All @@ -158,7 +163,8 @@ public class SettingsController implements Controller<Node> {

public SettingsController(UserService userService, PreferencesService preferencesService, UiService uiService,
I18n i18n, EventBus eventBus, NotificationService notificationService,
PlatformService platformService, ClientProperties clientProperties, ClientUpdateService clientUpdateService) {
PlatformService platformService, ClientProperties clientProperties,
ClientUpdateService clientUpdateService, GameService gameService) {
this.userService = userService;
this.preferencesService = preferencesService;
this.uiService = uiService;
Expand All @@ -168,6 +174,7 @@ public SettingsController(UserService userService, PreferencesService preference
this.platformService = platformService;
this.clientProperties = clientProperties;
this.clientUpdateService = clientUpdateService;
this.gameService = gameService;

availableLanguagesListener = observable -> {
LocalizationPrefs localization = preferencesService.getPreferences().getLocalization();
Expand Down Expand Up @@ -337,6 +344,8 @@ public void initialize() {

initUnitDatabaseSelection(preferences);

initAllowReplaysWhileInGame(preferences);

initNotifyMeOnAtMention();

initGameDataCache();
Expand All @@ -356,6 +365,17 @@ private void initNotifyMeOnAtMention() {
notifyAtMentionDescription.setText(i18n.get("settings.chat.notifyOnAtMentionOnly.description", "@" + username));
}

private void initAllowReplaysWhileInGame(Preferences preferences) {
JavaFxUtil.bindBidirectional(allowReplayWhileInGameCheckBox.selectedProperty(), preferences.getForgedAlliance().allowReplaysWhileInGameProperty());
try {
gameService.isGamePrefsPatchedToAllowMultiInstances()
.thenAccept(isPatched -> allowReplayWhileInGameButton.setDisable(isPatched));
} catch (IOException e) {
log.warn("Failed evaluating if game.prefs file is patched for multiple instances.");
allowReplayWhileInGameButton.setDisable(true);
}
}

private void configureStartTab(Preferences preferences) {
WindowPrefs mainWindow = preferences.getMainWindow();
startTabChoiceBox.setItems(FXCollections.observableArrayList(NavigationItem.values()));
Expand Down Expand Up @@ -595,5 +615,19 @@ public void onAddAutoChannel() {
preferencesService.storeInBackground();
channelTextField.clear();
}

public void onPatchGamePrefsForMultipleInstances() {
try {
gameService.patchGamePrefsForMultiInstances()
.thenRun(() -> Platform.runLater(() -> allowReplayWhileInGameButton.setDisable(true))).exceptionally(throwable -> {
log.error("Game.prefs patch failed", throwable);
notificationService.addImmediateErrorNotification(throwable, "settings.fa.patchGamePrefsFailed");
return null;
});
} catch (Exception e) {
log.error("Game.prefs patch failed", e);
notificationService.addImmediateErrorNotification(e, "settings.fa.patchGamePrefsFailed");
}
}
}

8 changes: 7 additions & 1 deletion src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,11 @@ settings.fa.executionDirectory.description=In which directory the game executabl
settings.fa.advanced=Advanced
settings.fa.useDefault=Use default directory
settings.fa.chooseDirectory=Choose…
settings.fa.allowReplayWhileInGame=Allow watching replays while in game
settings.fa.patchGamePrefs=Patch Game.prefs
settings.fa.allowReplayWhileInGame.description=If you activate this experimental functionality you can watch replays while being in game. Besides checking the box you will also need to activate this in your game.prefs file, if you click the button below we can do that for you. If it is disabled your game.prefs are already patched. Also be aware that launching conflicting game versions will cause errors.
settings.fa.patchGamePrefsFailed=Failed to patch the game.prefs file.
settings.fa.allowReplayWhileInGame.experimental=experimental
settings.general.theme=Theme
settings.general.theme.useNoImage=No image
settings.notifications.friendOnline=Friend online
Expand Down Expand Up @@ -793,7 +798,7 @@ game.couldNotJoin=Could not join game with id id ''{0}''.
settings.general.disallowDiscordJoins=Disallow Joining via Discord
settings.general.disallowDiscordJoins.description=Do not allow other players to join your game via Discord Rich Presence.
replay.couldNotOpen=Replay with ID ''{0}'' could not be opened.
error.game.notTerminatedCorrectly=The game did not terminate correctly and has still certain files open. Some files from the installed game could not be copied over to FAF but unless you recently modified game files in the installed version you probably do not care. We are still starting the game for you.
error.game.filesNotAccessible=Some files from the installed game could not be copied over to FAF but unless you recently modified game files in the installed version you probably do not care. We are still starting the game for you.
map.id=#{0}
mapGenerator.generationFailed=There was an error during map generation
mapGenerator.invalidName=Map name is not a generated map name
Expand Down Expand Up @@ -847,3 +852,4 @@ theme.needsRestart.quit=Quit
discord.join=Join FAF on Discord
chat.filter.countryPrompt=Country code
games.create.enforceRating=Enforce rating
gameUpdate.error.gameNotWritableAllowMultiOn=The game could not be patched to the required version for this game launch. You are trying to watch a replay or open a game of a conflicting version to the one you have already opened.
Loading

0 comments on commit 5155ed7

Please sign in to comment.