Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow watching replays while in queue #2945

Merged
merged 22 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
BlackYps marked this conversation as resolved.
Show resolved Hide resolved
}

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);
}
Sheikah45 marked this conversation as resolved.
Show resolved Hide resolved

@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