From 1d0c05b5b63e7a555ae55ce5494eacd8c5aa2ae0 Mon Sep 17 00:00:00 2001 From: Tobias Nett Date: Fri, 24 Nov 2023 12:16:10 +0100 Subject: [PATCH] feat: prepare runtime management (#705) * feat: handle (potential) releases requiring unsupported Java versions This is in preparation for managing the Java runtime for the games independently of the launcher. We need this feature to be able to support playing older releases as well as the latest and greatest. * remove unused method ApplicationController::selectItem * test: add a test for supported and unsupported version of Java --- .../terasology/launcher/game/GameService.java | 2 +- .../terasology/launcher/game/GameStarter.java | 29 +++++++------- .../GameVersionNotSupportedException.java | 19 ++++++++++ .../launcher/game/VersionHistory.java | 14 ++++++- .../launcher/ui/ApplicationController.java | 38 +++++++------------ .../launcher/game/TestGameStarter.java | 20 +++++++++- 6 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 src/main/java/org/terasology/launcher/game/GameVersionNotSupportedException.java diff --git a/src/main/java/org/terasology/launcher/game/GameService.java b/src/main/java/org/terasology/launcher/game/GameService.java index ad066401..10b6fe3a 100644 --- a/src/main/java/org/terasology/launcher/game/GameService.java +++ b/src/main/java/org/terasology/launcher/game/GameService.java @@ -118,7 +118,7 @@ public void restart() { * @throws RuntimeException when required files in the game directory are missing or inaccessible */ @Override - protected RunGameTask createTask() { + protected RunGameTask createTask() throws GameVersionNotSupportedException{ verifyNotNull(settings); GameStarter starter; diff --git a/src/main/java/org/terasology/launcher/game/GameStarter.java b/src/main/java/org/terasology/launcher/game/GameStarter.java index b300d306..00d2f5eb 100644 --- a/src/main/java/org/terasology/launcher/game/GameStarter.java +++ b/src/main/java/org/terasology/launcher/game/GameStarter.java @@ -26,10 +26,9 @@ final class GameStarter implements Callable { private static final Logger logger = LoggerFactory.getLogger(GameStarter.class); final ProcessBuilder processBuilder; - private final Semver engineVersion; /** - * @param installation the directory under which we will find {@code libs/Terasology.jar}, also used as the process's + * @param installation the directory under which we will find {@code libs/Terasology.jar}, also used as the process's * working directory * @param gameDataDirectory {@code -homedir}, the directory where Terasology's data files (saves & etc) are kept * @param heapMin java's {@code -Xms} @@ -39,14 +38,14 @@ final class GameStarter implements Callable { * @param logLevel the minimum level of log events Terasology will include on its output stream to us */ GameStarter(Installation installation, Path gameDataDirectory, JavaHeapSize heapMin, JavaHeapSize heapMax, - List javaParams, List gameParams, Level logLevel) throws IOException { - engineVersion = installation.getEngineVersion(); + List javaParams, List gameParams, Level logLevel) throws IOException, GameVersionNotSupportedException { + Semver engineVersion = installation.getEngineVersion(); var gamePath = installation.path; final boolean isMac = Platform.getPlatform().isMac(); final List processParameters = new ArrayList<>(); - processParameters.add(getRuntimePath().toString()); + processParameters.add(getRuntimePath(engineVersion).toString()); if (heapMin.isUsed()) { processParameters.add("-Xms" + heapMin.getSizeParameter()); @@ -69,12 +68,12 @@ final class GameStarter implements Callable { processParameters.add(installation.getGameJarPath().toString()); // Parameters after this are for the game facade, not the java runtime. - processParameters.add(homeDirParameter(gameDataDirectory)); + processParameters.add(homeDirParameter(gameDataDirectory, engineVersion)); processParameters.addAll(gameParams); if (isMac) { // splash screen uses awt, so no awt => no splash - processParameters.add(noSplashParameter()); + processParameters.add(noSplashParameter(engineVersion)); } processBuilder = new ProcessBuilder(processParameters) @@ -97,23 +96,27 @@ public Process call() throws IOException { /** * @return the executable {@code java} file to run the game with */ - Path getRuntimePath() { + Path getRuntimePath(Semver engineVersion) throws GameVersionNotSupportedException { + if (VersionHistory.JAVA17.isProvidedBy(engineVersion)) { + // throw exception as the version is not supported + throw new GameVersionNotSupportedException(engineVersion); + } return Paths.get(System.getProperty("java.home"), "bin", "java"); } - String homeDirParameter(Path gameDataDirectory) { - if (terasologyUsesPosixOptions()) { + String homeDirParameter(Path gameDataDirectory, Semver engineVersion) { + if (terasologyUsesPosixOptions(engineVersion)) { return "--homedir=" + gameDataDirectory.toAbsolutePath(); } else { return "-homedir=" + gameDataDirectory.toAbsolutePath(); } } - String noSplashParameter() { - return terasologyUsesPosixOptions() ? "--no-splash" : "-noSplash"; + String noSplashParameter(Semver engineVersion) { + return terasologyUsesPosixOptions(engineVersion) ? "--no-splash" : "-noSplash"; } - boolean terasologyUsesPosixOptions() { + boolean terasologyUsesPosixOptions(Semver engineVersion) { return VersionHistory.PICOCLI.isProvidedBy(engineVersion); } } diff --git a/src/main/java/org/terasology/launcher/game/GameVersionNotSupportedException.java b/src/main/java/org/terasology/launcher/game/GameVersionNotSupportedException.java new file mode 100644 index 00000000..fa08651b --- /dev/null +++ b/src/main/java/org/terasology/launcher/game/GameVersionNotSupportedException.java @@ -0,0 +1,19 @@ +// Copyright 2023 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.launcher.game; + +import com.vdurmont.semver4j.Semver; + +public class GameVersionNotSupportedException extends RuntimeException { + private final Semver engineVersion; + + public GameVersionNotSupportedException(Semver engineVersion) { + this.engineVersion = engineVersion; + } + + @Override + public String getMessage() { + return "Unsupported engine version: " + engineVersion.toString(); + } +} diff --git a/src/main/java/org/terasology/launcher/game/VersionHistory.java b/src/main/java/org/terasology/launcher/game/VersionHistory.java index fc4d3d56..16301ed4 100644 --- a/src/main/java/org/terasology/launcher/game/VersionHistory.java +++ b/src/main/java/org/terasology/launcher/game/VersionHistory.java @@ -13,10 +13,20 @@ public enum VersionHistory { /** * The preview release of v4.1.0-rc.1 is the first release with LWJGL v3. - * See https://github.com/MovingBlocks/Terasology/releases/tag/v4.1.0-rc.1 + * See v4.1.0-rc.1 */ LWJGL3("4.1.0-rc.1"), - PICOCLI("5.2.0-SNAPSHOT"); + + /** + * With the 5.2.0-rc.1 preview release Terasology switches to PICOCLI with POSIX-style command line options. + * See v5.2.0-rc.1 + */ + PICOCLI("5.2.0-SNAPSHOT"), + + /** Since 3aa68c04f192243575f7f78de5b6ce268bb2da1a Terasology requires at least Java 17. + * See 3aa68c04 + */ + JAVA17("6.0.0-SNAPSHOT"); public final Semver engineVersion; diff --git a/src/main/java/org/terasology/launcher/ui/ApplicationController.java b/src/main/java/org/terasology/launcher/ui/ApplicationController.java index 69eafc5e..848d015d 100644 --- a/src/main/java/org/terasology/launcher/ui/ApplicationController.java +++ b/src/main/java/org/terasology/launcher/ui/ApplicationController.java @@ -36,6 +36,7 @@ import org.terasology.launcher.LauncherConfiguration; import org.terasology.launcher.game.GameManager; import org.terasology.launcher.game.GameService; +import org.terasology.launcher.game.GameVersionNotSupportedException; import org.terasology.launcher.game.Installation; import org.terasology.launcher.model.Build; import org.terasology.launcher.model.GameIdentifier; @@ -59,7 +60,6 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -74,21 +74,21 @@ public class ApplicationController { private Settings launcherSettings; private GameManager gameManager; - private RepositoryManager repositoryManager; + private final GameService gameService; private final ExecutorService executor = Executors.newSingleThreadExecutor(); private DownloadTask downloadTask; private Stage stage; - private Property config; + private final Property config; - private Property selectedRelease; - private Property gameAction; - private BooleanProperty downloading; - private BooleanProperty showPreReleases; + private final Property selectedRelease; + private final Property gameAction; + private final BooleanProperty downloading; + private final BooleanProperty showPreReleases; - private ObservableSet installedGames; + private final ObservableSet installedGames; /** * Indicate whether the user's hard drive is running out of space for game downloads. @@ -331,7 +331,6 @@ public void update(final LauncherConfiguration configuration, final Stage stage, this.launcherSettings = configuration.getLauncherSettings(); this.showPreReleases.bind(launcherSettings.showPreReleases); - this.repositoryManager = configuration.getRepositoryManager(); this.gameManager = configuration.getGameManager(); this.stage = stage; @@ -411,7 +410,11 @@ protected void startGameAction() { Dialogs.showError(stage, I18N.getMessage("message_error_installationNotFound", release)); return; } - gameService.start(installation, launcherSettings); + try { + gameService.start(installation, launcherSettings); + } catch (GameVersionNotSupportedException e) { + Dialogs.showError(stage, e.getMessage()); + } } private void handleRunStarted(ObservableValue o, Boolean oldValue, Boolean newValue) { @@ -492,21 +495,6 @@ protected void deleteAction() { }); } - /** - * Select the first item matching given predicate, select the first item otherwise. - * - * @param comboBox the combo box to change the selection for - * @param predicate first item matching this predicate will be selected - */ - private void selectItem(final ComboBox comboBox, Predicate predicate) { - final T item = comboBox.getItems().stream() - .filter(predicate) - .findFirst() - .orElse(comboBox.getItems().get(0)); - - comboBox.getSelectionModel().select(item); - } - /** * Closes the launcher frame this Controller handles. The launcher frame Stage is determined by the enclosing anchor pane. */ diff --git a/src/test/java/org/terasology/launcher/game/TestGameStarter.java b/src/test/java/org/terasology/launcher/game/TestGameStarter.java index e964b633..b28ec9c0 100644 --- a/src/test/java/org/terasology/launcher/game/TestGameStarter.java +++ b/src/test/java/org/terasology/launcher/game/TestGameStarter.java @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 package org.terasology.launcher.game; +import com.vdurmont.semver4j.Semver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -19,9 +20,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.terasology.launcher.Matchers.hasItemsFrom; public class TestGameStarter { @@ -65,11 +68,12 @@ public void testConstruction() throws IOException { @Test public void testJre() throws IOException { + Semver engineVersion = new Semver("5.0.0"); GameStarter task = newStarter(); // This is the sort of test where the code under test and the expectation are just copies // of the same source. But since there's a plan to separate the launcher runtime from the // game runtime, the runtime location seemed like a good thing to specify in its own test. - assertTrue(task.getRuntimePath().startsWith(Path.of(System.getProperty("java.home")))); + assertTrue(task.getRuntimePath(engineVersion).startsWith(Path.of(System.getProperty("java.home")))); } static Stream provideJarPaths() { @@ -95,4 +99,18 @@ public void testBuildProcess(Path jarRelativePath) throws IOException { // could parameterize this test for the things that are optional? // heap min, heap max, log level, gameParams and javaParams are all optional. } + + @Test + public void testSupportedJava11() throws IOException { + Semver engineVersion = new Semver("5.3.0"); + GameStarter task = newStarter(); + assertDoesNotThrow(() -> task.getRuntimePath(engineVersion)); + } + + @Test + public void testUnsupportedJava17() throws IOException { + Semver engineVersion = new Semver("6.0.0"); + GameStarter task = newStarter(); + assertThrows(GameVersionNotSupportedException.class, () -> task.getRuntimePath(engineVersion)); + } }