Skip to content

Commit

Permalink
Allow watching replays while in queue (#2945)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackYps authored Mar 28, 2023
1 parent e001edc commit b65435b
Show file tree
Hide file tree
Showing 19 changed files with 433 additions and 286 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ public class FeaturedModUpdaterConfig {
@Bean
GameUpdater gameUpdater() {
return new GameUpdaterImpl(modService, taskService, dataPrefs, forgedAlliancePrefs, gameBinariesUpdateTaskFactory)
.addFeaturedModUpdater(httpFeaturedModUpdater);
.setFeaturedModUpdater(httpFeaturedModUpdater);
}
}
21 changes: 18 additions & 3 deletions src/main/java/com/faforever/client/fa/ForgedAllianceService.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public Process startGameOnline(GameParameters gameParameters) throws IOException
public Process startReplay(Path path, @Nullable Integer replayId) throws IOException {
int checkedReplayId = Objects.requireNonNullElse(replayId, -1);

List<String> launchCommand = defaultLaunchCommand().replayFile(path)
List<String> launchCommand = replayLaunchCommand().replayFile(path)
.replayId(checkedReplayId)
.logFile(loggingService.getNewGameLogFile(checkedReplayId))
.build();
Expand All @@ -98,7 +98,7 @@ public Process startReplay(Path path, @Nullable Integer replayId) throws IOExcep


public Process startReplay(URI replayUri, Integer replayId) throws IOException {
List<String> launchCommand = defaultLaunchCommand().replayUri(replayUri)
List<String> launchCommand = replayLaunchCommand().replayUri(replayUri)
.replayId(replayId)
.logFile(loggingService.getNewGameLogFile(replayId))
.username(playerService.getCurrentPlayer().getUsername())
Expand All @@ -111,6 +111,10 @@ public Path getExecutablePath() {
return dataPrefs.getBinDirectory().resolve(FORGED_ALLIANCE_EXE);
}

public Path getReplayExecutablePath() {
return dataPrefs.getReplayBinDirectory().resolve(FORGED_ALLIANCE_EXE);
}

public Path getDebuggerExecutablePath() {
return dataPrefs.getBinDirectory().resolve(DEBUGGER_EXE);
}
Expand All @@ -120,10 +124,21 @@ private LaunchCommandBuilder defaultLaunchCommand() {
.executableDecorator(forgedAlliancePrefs.getExecutableDecorator())
.executable(getExecutablePath());

return addDebugger(baseCommandBuilder);
}

private LaunchCommandBuilder replayLaunchCommand() {
LaunchCommandBuilder baseCommandBuilder = LaunchCommandBuilder.create()
.executableDecorator(forgedAlliancePrefs.getExecutableDecorator())
.executable(getReplayExecutablePath());

return addDebugger(baseCommandBuilder);
}

private LaunchCommandBuilder addDebugger(LaunchCommandBuilder baseCommandBuilder) {
if (forgedAlliancePrefs.isRunFAWithDebugger() && Files.exists(getDebuggerExecutablePath())) {
baseCommandBuilder = baseCommandBuilder.debuggerExecutable(getDebuggerExecutablePath());
}

return baseCommandBuilder;
}

Expand Down
142 changes: 81 additions & 61 deletions src/main/java/com/faforever/client/game/GameService.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public class GameService implements InitializingBean {

@VisibleForTesting
final BooleanProperty gameRunning = new SimpleBooleanProperty();
final BooleanProperty replayRunning = new SimpleBooleanProperty();
;
/** TODO: Explain why access needs to be synchronized. */
@VisibleForTesting
Expand All @@ -156,8 +157,10 @@ public class GameService implements InitializingBean {
private final ObservableList<GameBean> games = JavaFxUtil.attachListToMap(FXCollections.synchronizedObservableList(FXCollections.observableArrayList(game -> new Observable[]{game.statusProperty(), game.teamsProperty(), game.titleProperty(), game.mapFolderNameProperty(), game.simModsProperty(), game.passwordProtectedProperty()})), gameIdToGame);

private Process process;
private Process replayProcess;
private CompletableFuture<Void> matchmakerFuture;
private boolean gameKilled;
private boolean replayKilled;
private boolean rehostRequested;
private int localReplayPort;
private boolean inOthersParty;
Expand Down Expand Up @@ -260,6 +263,12 @@ public void afterPropertiesSet() {
onLoggedIn();
}
});

try {
patchGamePrefsForMultiInstances();
} catch (Exception e) {
log.error("Game.prefs patch failed", e);
}
}

private Mono<GameBean> initializeGameBean(GameInfo gameInfo) {
Expand Down Expand Up @@ -441,7 +450,7 @@ public CompletableFuture<Void> runWithReplay(Path path, @Nullable Integer replay

return modService.getFeaturedMod(featuredMod)
.toFuture()
.thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, simMods, featuredModFileVersions, baseFafVersion))
.thenCompose(featuredModBean -> updateReplayFilesIfNecessary(featuredModBean, simMods, featuredModFileVersions, baseFafVersion))
.thenCompose(aVoid -> downloadMapIfNecessary(mapFolderName).handleAsync((ignoredResult, throwable) -> {
try {
return askWhetherToStartWithOutMap(throwable);
Expand All @@ -450,14 +459,11 @@ public CompletableFuture<Void> runWithReplay(Path path, @Nullable Integer replay
}
}))
.thenRun(() -> {
replayKilled = false;
try {
Process processForReplay = forgedAllianceService.startReplay(path, replayId);
if (forgedAlliancePrefs.isAllowReplaysWhileInGame() && isRunning()) {
return;
}
this.process = processForReplay;
setGameRunning(true);
spawnTerminationListener(this.process);
this.replayProcess = forgedAllianceService.startReplay(path, replayId);
setReplayRunning(true);
spawnReplayTerminationListener(this.replayProcess);
} catch (IOException e) {
notifyCantPlayReplay(replayId, e);
}
Expand All @@ -469,17 +475,9 @@ public CompletableFuture<Void> runWithReplay(Path path, @Nullable Integer replay
}

private boolean canStartReplay() {
if (isRunning() && !forgedAlliancePrefs.isAllowReplaysWhileInGame()) {
log.info("Forged Alliance is already running and experimental concurrent game feature not turned on, not starting replay");
notificationService.addImmediateWarnNotification("replay.gameRunning");
return false;
} else if (waitingForMatchMakerGame()) {
log.info("In matchmaker queue, not starting replay");
notificationService.addImmediateWarnNotification("replay.inQueue");
return false;
} else if (inOthersParty) {
log.info("In party, not starting replay");
notificationService.addImmediateWarnNotification("replay.inParty");
if (isReplayRunning()) {
log.info("Another replay is already running, not starting replay");
notificationService.addImmediateWarnNotification("replay.replayRunning");
return false;
}
return true;
Expand Down Expand Up @@ -537,21 +535,17 @@ public CompletableFuture<Void> runWithLiveReplay(URI replayUrl, Integer gameId,

return modService.getFeaturedMod(gameType)
.toFuture()
.thenCompose(featuredModBean -> updateGameIfNecessary(featuredModBean, simModUids))
.thenCompose(featuredModBean -> updateReplayFilesIfNecessary(featuredModBean, simModUids, null, null))
.thenCompose(aVoid -> downloadMapIfNecessary(mapName))
.thenRun(() -> {
Process processCreated;
replayKilled = false;
try {
processCreated = forgedAllianceService.startReplay(replayUrl, gameId);
this.replayProcess = forgedAllianceService.startReplay(replayUrl, gameId);
setReplayRunning(true);
spawnReplayTerminationListener(this.replayProcess);
} catch (IOException e) {
throw new GameLaunchException("Live replay could not be started", e, "replay.live.startError");
}
if (forgedAlliancePrefs.isAllowReplaysWhileInGame() && isRunning()) {
return;
}
this.process = processCreated;
setGameRunning(true);
spawnTerminationListener(this.process);
})
.exceptionally(throwable -> {
throwable = ConcurrentUtil.unwrapIfCompletionException(throwable);
Expand Down Expand Up @@ -592,7 +586,14 @@ public void startSearchMatchmaker() {
.toFuture()
.thenAccept(featuredModBean -> updateGameIfNecessary(featuredModBean, Set.of()))
.thenCompose(aVoid -> fafServerAccessor.startSearchMatchmaker())
.thenCompose(gameLaunchResponse -> downloadMapIfNecessary(gameLaunchResponse.getMapName()).thenCompose(aVoid -> leaderboardService.getActiveLeagueEntryForPlayer(playerService.getCurrentPlayer(), gameLaunchResponse.getLeaderboard()))
.thenCompose(gameLaunchResponse -> downloadMapIfNecessary(gameLaunchResponse.getMapName()).thenCompose(aVoid -> {
// We need to kill the replay to free the lock on the game.prefs
if (isReplayRunning()) {
replayKilled = true;
replayProcess.destroy();
}
return leaderboardService.getActiveLeagueEntryForPlayer(playerService.getCurrentPlayer(), gameLaunchResponse.getLeaderboard());
})
.thenApply(leagueEntryOptional -> {
GameParameters parameters = gameMapper.map(gameLaunchResponse);
parameters.setDivision(leagueEntryOptional.map(bean -> bean.getSubdivision().getDivision().getNameKey())
Expand Down Expand Up @@ -644,13 +645,13 @@ private boolean isRunning() {
}

public CompletableFuture<Void> updateGameIfNecessary(FeaturedModBean featuredModBean, Set<String> simModUids) {
return updateGameIfNecessary(featuredModBean, simModUids, null, null);
return gameUpdater.update(featuredModBean, simModUids, null, null, false);
}

private CompletableFuture<Void> updateGameIfNecessary(FeaturedModBean featuredModBean, Set<String> simModUids,
@Nullable Map<String, Integer> featuredModFileVersions,
@Nullable Integer version) {
return gameUpdater.update(featuredModBean, simModUids, featuredModFileVersions, version);
private CompletableFuture<Void> updateReplayFilesIfNecessary(FeaturedModBean featuredModBean, Set<String> simModUids,
@Nullable Map<String, Integer> featuredModFileVersions,
@Nullable Integer version) {
return gameUpdater.update(featuredModBean, simModUids, featuredModFileVersions, version, true);
}

public boolean isGameRunning() {
Expand All @@ -665,6 +666,18 @@ private void setGameRunning(boolean running) {
}
}

public boolean isReplayRunning() {
synchronized (replayRunning) {
return replayRunning.get();
}
}

private void setReplayRunning(boolean running) {
synchronized (replayRunning) {
this.replayRunning.set(running);
}
}

private boolean waitingForMatchMakerGame() {
return matchmakerFuture != null && !matchmakerFuture.isDone();
}
Expand Down Expand Up @@ -714,7 +727,7 @@ private CompletableFuture<Void> startGame(GameParameters gameParameters) {
setGameRunning(false);
return null;
})
.thenCompose(this::spawnTerminationListener);
.thenCompose(process -> spawnTerminationListener(process, true));
}

private void onRecentlyPlayedGameEnded(GameBean game) {
Expand All @@ -725,35 +738,11 @@ private void onRecentlyPlayedGameEnded(GameBean game) {
notificationService.addNotification(new PersistentNotification(i18n.get("game.ended", game.getTitle()), Severity.INFO, singletonList(new Action(i18n.get("game.rate"), actionEvent -> eventBus.post(new ShowReplayEvent(game.getId()))))));
}

@VisibleForTesting
CompletableFuture<Void> spawnTerminationListener(Process process) {
return spawnTerminationListener(process, true);
}

@VisibleForTesting
CompletableFuture<Void> spawnTerminationListener(Process process, Boolean forOnlineGame) {
rehostRequested = false;
return process.onExit().thenAccept(finishedProcess -> {
fafServerAccessor.setPingIntervalSeconds(25);
int exitCode = finishedProcess.exitValue();
log.info("Forged Alliance terminated with exit code {}", exitCode);
Optional<Path> logFile = loggingService.getMostRecentGameLogFile();
logFile.ifPresent(file -> {
try {
Files.writeString(file, logMasker.maskMessage(Files.readString(file)));
} catch (IOException e) {
log.warn("Could not open log file", e);
}
});

if (exitCode != 0 && !gameKilled) {
if (exitCode == -1073741515) {
notificationService.addImmediateWarnNotification("game.crash.notInitialized");
} else {
notificationService.addNotification(new ImmediateNotification(i18n.get("errorTitle"), i18n.get("game.crash", exitCode, logFile.map(Path::toString)
.orElse("")), WARN, List.of(new Action(i18n.get("game.open.log"), event -> platformService.reveal(logFile.orElse(operatingSystem.getLoggingDirectory()))), new DismissAction(i18n))));
}
}
handleTermination(finishedProcess, false);

synchronized (gameRunning) {
gameRunning.set(false);
Expand All @@ -774,6 +763,37 @@ CompletableFuture<Void> spawnTerminationListener(Process process, Boolean forOnl
});
}

@VisibleForTesting
void spawnReplayTerminationListener(Process process) {
process.onExit().thenAccept(finishedProcess -> {
handleTermination(finishedProcess, true);
setReplayRunning(false);
});
}

private void handleTermination(Process finishedProcess, boolean isReplay) {
fafServerAccessor.setPingIntervalSeconds(25);
int exitCode = finishedProcess.exitValue();
log.info("Forged Alliance terminated with exit code {}", exitCode);
Optional<Path> logFile = loggingService.getMostRecentGameLogFile();
logFile.ifPresent(file -> {
try {
Files.writeString(file, logMasker.maskMessage(Files.readString(file)));
} catch (IOException e) {
log.warn("Could not open log file", e);
}
});

if (exitCode != 0 && ((!isReplay && !gameKilled) || (isReplay && !replayKilled))) {
if (exitCode == -1073741515) {
notificationService.addImmediateWarnNotification("game.crash.notInitialized");
} else {
notificationService.addNotification(new ImmediateNotification(i18n.get("errorTitle"), i18n.get("game.crash", exitCode, logFile.map(Path::toString)
.orElse("")), WARN, List.of(new Action(i18n.get("game.open.log"), event -> platformService.reveal(logFile.orElse(operatingSystem.getLoggingDirectory()))), new DismissAction(i18n))));
}
}
}

private void rehost() {
synchronized (currentGame) {
GameBean game = currentGame.get();
Expand Down Expand Up @@ -812,7 +832,7 @@ private GameBean enhanceWithLastPasswordIfPasswordProtected(GameBean game) {
}

public void killGame() {
if (process != null && process.isAlive()) {
if (isRunning()) {
log.info("ForgedAlliance still running, destroying process");
process.destroy();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ public interface FeaturedModUpdater {
* Updates the specified featured mod to the specified version. If {@code version} is null, it will update to the
* latest version
*/
CompletableFuture<PatchResult> updateMod(FeaturedModBean featuredMod, @Nullable Integer version);
CompletableFuture<PatchResult> updateMod(FeaturedModBean featuredMod, @Nullable Integer version, boolean useReplayFolder);

/**
* Returns {@code true} if this updater is able to update the specified featured mod.
*/
boolean canUpdate(FeaturedModBean featuredModBean);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

public interface GameBinariesUpdateTask extends PrioritizedCompletableTask<Void> {
void setVersion(ComparableVersion version);
void setForReplays(boolean forReplays);
}
Loading

0 comments on commit b65435b

Please sign in to comment.