diff --git a/src/main/java/org/terasology/launcher/game/Installation.java b/src/main/java/org/terasology/launcher/game/GameInstallation.java similarity index 75% rename from src/main/java/org/terasology/launcher/game/Installation.java rename to src/main/java/org/terasology/launcher/game/GameInstallation.java index 8729854c..df05c0b0 100644 --- a/src/main/java/org/terasology/launcher/game/Installation.java +++ b/src/main/java/org/terasology/launcher/game/GameInstallation.java @@ -5,6 +5,12 @@ import com.google.common.base.MoreObjects; import org.semver4j.Semver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.launcher.io.Installation; +import org.terasology.launcher.model.Build; +import org.terasology.launcher.model.GameIdentifier; +import org.terasology.launcher.model.Profile; import java.io.FileNotFoundException; import java.io.IOException; @@ -24,21 +30,23 @@ /** * A local installation of a Terasology release. */ -public class Installation { - final Path path; +public class GameInstallation implements Installation { - Installation(Path installDirectory) { + private static final Logger logger = LoggerFactory.getLogger(GameInstallation.class); + private final Path path; + + GameInstallation(Path installDirectory) { path = checkNotNull(installDirectory); } /** * Return an Installation after confirming it is present. */ - static Installation getExisting(Path directory) throws FileNotFoundException { + static GameInstallation getExisting(Path directory) throws FileNotFoundException { if (!Files.exists(directory)) { throw new FileNotFoundException("No installation present in " + directory); } - return new Installation(directory); + return new GameInstallation(directory); } /** @@ -53,13 +61,13 @@ Semver getEngineVersion() throws IOException { /** * Locate the main game jar. - * + *

* As of August 2021, Terasology has custom build logic to put libraries into a {@code libs} (plural) subdirectory. * As we plan to switch to using default Gradle behavior we have to do a quick check how the game distribution was * build (i.e., custom {@code libs} or default {@code lib}). */ Path getGameJarPath() throws IOException { - return findJar(path, Installation::matchGameJar, "game"); + return findJar(path, GameInstallation::matchGameJar, "game"); } @Override @@ -67,11 +75,11 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof Installation)) { + if (!(o instanceof GameInstallation)) { return false; } - Installation that = (Installation) o; + GameInstallation that = (GameInstallation) o; return path.equals(that.path); } @@ -115,7 +123,7 @@ static Path findJar(Path searchPath, BiPredicate pred * @throws FileNotFoundException if the engine or the version info could not be found */ static Semver getEngineVersion(Path versionDirectory) throws IOException { - Path engineJar = findJar(versionDirectory, Installation::matchEngineJar, "engine"); + Path engineJar = findJar(versionDirectory, GameInstallation::matchEngineJar, "engine"); Properties versionInfo = getVersionPropertiesFromJar(engineJar); return new Semver(versionInfo.getProperty("engineVersion")); } @@ -189,4 +197,36 @@ static Properties getVersionPropertiesFromJar(Path jarLocation) throws IOExcepti return properties; } } + + @Override + public Path getPath() { + return path; + } + + @Override + public GameIdentifier getInfo() { + //TODO: compute this information on instance creation (and fail creation in case it is not a valid installation) + Profile profile; + Build build; + var parts = path.getNameCount(); + try { + profile = Profile.valueOf(path.getName(parts - 3).toString()); + build = Build.valueOf(path.getName(parts - 2).toString()); + return new GameIdentifier(path.getFileName().toString(), build, profile); + } catch (IllegalArgumentException e) { + logger.debug("Cannot derive game information from installation: " + + "Expected directory format '...///' but got {}", path); + return null; + } + //TODO: this is not working as I expected - probably don't fully understand this code I copied from somewhere else... +// try { +// Path jarPath = getGameJarPath(); +// Properties info = getVersionPropertiesFromJar(jarPath); +// return new GameIdentifier(info.getProperty("displayVersion"), build, profile); +// } catch (IOException e) { +// logger.debug("Cannot derive game information from installation: " +// + "Cannot read 'displayVersion' from version info file for installation in {}.", path, e); +// return null; +// } + } } diff --git a/src/main/java/org/terasology/launcher/game/GameManager.java b/src/main/java/org/terasology/launcher/game/GameManager.java index ffbf7911..451cd128 100644 --- a/src/main/java/org/terasology/launcher/game/GameManager.java +++ b/src/main/java/org/terasology/launcher/game/GameManager.java @@ -8,10 +8,8 @@ import javafx.collections.ObservableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.terasology.launcher.model.Build; import org.terasology.launcher.model.GameIdentifier; import org.terasology.launcher.model.GameRelease; -import org.terasology.launcher.model.Profile; import org.terasology.launcher.tasks.ProgressListener; import org.terasology.launcher.util.DownloadException; import org.terasology.launcher.util.DownloadUtils; @@ -142,8 +140,8 @@ public Path getInstallDirectory(GameIdentifier id) { return installDirectory.resolve(id.getProfile().name()).resolve(id.getBuild().name()).resolve(id.getDisplayVersion()); } - public Installation getInstallation(GameIdentifier id) throws FileNotFoundException { - return Installation.getExisting(getInstallDirectory(id)); + public GameInstallation getInstallation(GameIdentifier id) throws FileNotFoundException { + return GameInstallation.getExisting(getInstallDirectory(id)); } /** @@ -157,7 +155,8 @@ private void scanInstallationDir() { // Skip the intermediate directories. .filter(d -> installDirectory.relativize(d).getNameCount() == 3); localGames = gameDirectories - .map(GameManager::getInstalledVersion) + .map(GameInstallation::new) + .map(GameInstallation::getInfo) .filter(Objects::nonNull) .collect(Collectors.toUnmodifiableSet()); } catch (IOException e) { @@ -166,18 +165,4 @@ private void scanInstallationDir() { } Platform.runLater(() -> installedGames.addAll(localGames)); } - - private static GameIdentifier getInstalledVersion(Path versionDirectory) { - Profile profile; - Build build; - var parts = versionDirectory.getNameCount(); - try { - profile = Profile.valueOf(versionDirectory.getName(parts - 3).toString()); - build = Build.valueOf(versionDirectory.getName(parts - 2).toString()); - } catch (IllegalArgumentException e) { - logger.debug("Directory does not match expected profile/build names: {}", versionDirectory, e); - return null; - } - return new GameIdentifier(versionDirectory.getFileName().toString(), build, profile); - } } diff --git a/src/main/java/org/terasology/launcher/game/GameService.java b/src/main/java/org/terasology/launcher/game/GameService.java index 10b6fe3a..a8221bf2 100644 --- a/src/main/java/org/terasology/launcher/game/GameService.java +++ b/src/main/java/org/terasology/launcher/game/GameService.java @@ -18,7 +18,7 @@ /** * This service starts and monitors the game process. *

- * Its {@linkplain #GameService() constructor} requires no arguments. Use {@link #start(Installation, Settings)} to + * Its {@linkplain #GameService() constructor} requires no arguments. Use {@link #start(GameInstallation, Settings)} to * start the game process; the zero-argument form of {@code start()} will not have enough information. *

* The Boolean value of this service is true when it believes the game process has started successfully. @@ -44,7 +44,7 @@ public class GameService extends Service { private static final Logger logger = LoggerFactory.getLogger(GameService.class); - private Installation gamePath; + private GameInstallation gamePath; private Settings settings; public GameService() { @@ -59,20 +59,20 @@ public GameService() { /** * Start a new game process with these settings. - * @param installation the directory under which we will find libs/Terasology.jar, also used as the process's + * @param gameInstallation the directory under which we will find libs/Terasology.jar, also used as the process's * working directory * @param settings supplies other settings relevant to configuring a process */ @SuppressWarnings("checkstyle:HiddenField") - public void start(Installation installation, Settings settings) { - this.gamePath = installation; + public void start(GameInstallation gameInstallation, Settings settings) { + this.gamePath = gameInstallation; this.settings = settings; start(); } /** - * Use {@link #start(Installation, Settings)} instead. + * Use {@link #start(GameInstallation, Settings)} instead. *

* It is an error to call this method before providing the configuration. */ @@ -118,7 +118,7 @@ public void restart() { * @throws RuntimeException when required files in the game directory are missing or inaccessible */ @Override - protected RunGameTask createTask() throws GameVersionNotSupportedException{ + 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 859de455..75e87b67 100644 --- a/src/main/java/org/terasology/launcher/game/GameStarter.java +++ b/src/main/java/org/terasology/launcher/game/GameStarter.java @@ -28,7 +28,7 @@ final class GameStarter implements Callable { final ProcessBuilder processBuilder; /** - * @param installation the directory under which we will find {@code libs/Terasology.jar}, also used as the process's + * @param gameInstallation 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} @@ -37,10 +37,10 @@ final class GameStarter implements Callable { * @param gameParams additional arguments for the Terasology command line * @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, + GameStarter(GameInstallation gameInstallation, Path gameDataDirectory, JavaHeapSize heapMin, JavaHeapSize heapMax, List javaParams, List gameParams, Level logLevel) throws IOException, GameVersionNotSupportedException { - Semver engineVersion = installation.getEngineVersion(); - var gamePath = installation.path; + Semver engineVersion = gameInstallation.getEngineVersion(); + var gamePath = gameInstallation.getPath(); final boolean isMac = Platform.getPlatform().isMac(); final List processParameters = new ArrayList<>(); @@ -65,7 +65,7 @@ final class GameStarter implements Callable { processParameters.addAll(javaParams); processParameters.add("-jar"); - processParameters.add(installation.getGameJarPath().toString()); + processParameters.add(gameInstallation.getGameJarPath().toString()); // Parameters after this are for the game facade, not the java runtime. processParameters.add(homeDirParameter(gameDataDirectory, engineVersion)); diff --git a/src/main/java/org/terasology/launcher/io/Installation.java b/src/main/java/org/terasology/launcher/io/Installation.java new file mode 100644 index 00000000..b485f438 --- /dev/null +++ b/src/main/java/org/terasology/launcher/io/Installation.java @@ -0,0 +1,21 @@ +// Copyright 2023 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.launcher.io; + +import java.nio.file.Path; + +//TODO: define behavior in error cases, annotate non-null, etc. +public interface Installation { + + /** + * @return The full path to the location of the installation. + */ + Path getPath(); + + /** + * @return The information object describing what is installed. + */ + T getInfo(); + +} diff --git a/src/main/java/org/terasology/launcher/ui/ApplicationController.java b/src/main/java/org/terasology/launcher/ui/ApplicationController.java index 45d67fe0..72d90637 100644 --- a/src/main/java/org/terasology/launcher/ui/ApplicationController.java +++ b/src/main/java/org/terasology/launcher/ui/ApplicationController.java @@ -38,7 +38,7 @@ 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.game.GameInstallation; import org.terasology.launcher.model.Build; import org.terasology.launcher.model.GameIdentifier; import org.terasology.launcher.model.GameRelease; @@ -400,9 +400,9 @@ protected void startGameAction() { return; } final GameRelease release = selectedRelease.getValue(); - final Installation installation; + final GameInstallation gameInstallation; try { - installation = gameManager.getInstallation(release.getId()); + gameInstallation = gameManager.getInstallation(release.getId()); } catch (FileNotFoundException e) { // TODO: Refresh the list of installed games or something? This should not be reachable if // the properties are up to date. @@ -411,7 +411,7 @@ protected void startGameAction() { return; } try { - gameService.start(installation, launcherSettings); + gameService.start(gameInstallation, launcherSettings); } catch (GameVersionNotSupportedException e) { Dialogs.showError(stage, e.getMessage()); } diff --git a/src/test/java/org/terasology/launcher/game/StubInstallation.java b/src/test/java/org/terasology/launcher/game/StubGameInstallation.java similarity index 72% rename from src/test/java/org/terasology/launcher/game/StubInstallation.java rename to src/test/java/org/terasology/launcher/game/StubGameInstallation.java index 33f49174..f9bcd9b2 100644 --- a/src/test/java/org/terasology/launcher/game/StubInstallation.java +++ b/src/test/java/org/terasology/launcher/game/StubGameInstallation.java @@ -8,13 +8,13 @@ import java.nio.file.Path; /** An Installation that does not depend on filesystem interaction to determine engineVersion or game jar. */ -class StubInstallation extends Installation { +class StubGameInstallation extends GameInstallation { Path gameJar; - StubInstallation(Path installDirectory, Path relativeGameJarPath) { + StubGameInstallation(Path installDirectory, Path relativeGameJarPath) { super(installDirectory); - gameJar = this.path.resolve(relativeGameJarPath); + gameJar = this.getPath().resolve(relativeGameJarPath); } @Override diff --git a/src/test/java/org/terasology/launcher/game/TestGameStarter.java b/src/test/java/org/terasology/launcher/game/TestGameStarter.java index 18bafb4b..90b7d4a8 100644 --- a/src/test/java/org/terasology/launcher/game/TestGameStarter.java +++ b/src/test/java/org/terasology/launcher/game/TestGameStarter.java @@ -56,7 +56,7 @@ private GameStarter newStarter() throws IOException { } private GameStarter newStarter(Path relativeGameJarPath) throws IOException { - return new GameStarter(new StubInstallation(gamePath, relativeGameJarPath), + return new GameStarter(new StubGameInstallation(gamePath, relativeGameJarPath), gameDataPath, HEAP_MIN, HEAP_MAX, javaParams, gameParams, LOG_LEVEL); }