feat: update command line options for Terasology (#646)

* feat: update command line options for Terasology to use POSIX-style syntax for https://github.com/MovingBlocks/Terasology/pull/4157
* refactor(GameStarter): put platform-specific launch parameters here
* fix(VersionHistory): picocli begins with 5.2.0-SNAPSHOT
master
Kevin Turner 2021-08-08 19:11:01 -07:00 committed by GitHub
parent e1e530f3fa
commit 4327450c1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 60 deletions

View File

@ -1,4 +1,4 @@
// Copyright 2020 The Terasology Foundation
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.launcher.game;
@ -9,6 +9,7 @@ import javafx.concurrent.Service;
import javafx.concurrent.Worker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.launcher.model.GameRelease;
import org.terasology.launcher.settings.LauncherSettings;
import java.nio.file.Path;
@ -20,7 +21,7 @@ import static com.google.common.base.Verify.verifyNotNull;
/**
* This service starts and monitors the game process.
* <p>
* Its {@linkplain #GameService() constructor} requires no arguments. Use {@link #start(Path, LauncherSettings, List, List)} to
* Its {@linkplain #GameService() constructor} requires no arguments. Use {@link #start(GameRelease, Path, LauncherSettings)} to
* start the game process; the zero-argument form of {@code start()} will not have enough information.
* <p>
* The Boolean value of this service is true when it believes the game process has started <em>successfully.</em>
@ -48,8 +49,7 @@ public class GameService extends Service<Boolean> {
private Path gamePath;
private LauncherSettings settings;
private List<String> additionalJavaParameters;
private List<String> additionalGameParameters;
private GameRelease release;
public GameService() {
setExecutor(Executors.newSingleThreadExecutor(
@ -63,24 +63,22 @@ public class GameService extends Service<Boolean> {
/**
* Start a new game process with these settings.
* @param release the version of the game being run
* @param gamePath 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
* @param additionalJavaParameters
* @param additionalGameParameters
*/
@SuppressWarnings("checkstyle:HiddenField")
public void start(Path gamePath, LauncherSettings settings, List<String> additionalJavaParameters, List<String> additionalGameParameters) {
public void start(GameRelease release, Path gamePath, LauncherSettings settings) {
this.release = release;
this.gamePath = gamePath;
this.settings = settings;
this.additionalJavaParameters = additionalJavaParameters;
this.additionalGameParameters = additionalGameParameters;
start();
}
/**
* Use {@link #start(Path, LauncherSettings, List, List)} instead.
* Use {@link #start(GameRelease, Path, LauncherSettings)} instead.
* <p>
* It is an error to call this method before providing the configuration.
*/
@ -130,13 +128,11 @@ public class GameService extends Service<Boolean> {
final List<String> javaParameters = Lists.newArrayList();
javaParameters.addAll(settings.getJavaParameterList());
javaParameters.addAll(additionalJavaParameters);
final List<String> gameParameters = Lists.newArrayList();
gameParameters.addAll(settings.getUserGameParameterList());
gameParameters.addAll(additionalGameParameters);
var starter = new GameStarter(verifyNotNull(gamePath), settings.getGameDataDirectory(),
var starter = new GameStarter(release, verifyNotNull(gamePath), settings.getGameDataDirectory(),
settings.getMaxHeapSize(), settings.getInitialHeapSize(),
javaParameters, gameParameters,
settings.getLogLevel());

View File

@ -1,4 +1,4 @@
// Copyright 2020 The Terasology Foundation
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.launcher.game;
@ -6,7 +6,9 @@ package org.terasology.launcher.game;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.terasology.launcher.model.GameRelease;
import org.terasology.launcher.util.JavaHeapSize;
import org.terasology.launcher.util.Platform;
import java.io.IOException;
import java.nio.file.Path;
@ -24,8 +26,10 @@ class GameStarter implements Callable<Process> {
private static final Logger logger = LoggerFactory.getLogger(GameStarter.class);
final ProcessBuilder processBuilder;
final GameRelease release;
/**
* @param release the version of the game being run
* @param gamePath 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
@ -35,9 +39,13 @@ class GameStarter implements Callable<Process> {
* @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(Path gamePath, Path gameDataDirectory, JavaHeapSize heapMin, JavaHeapSize heapMax, List<String> javaParams, List<String> gameParams,
GameStarter(GameRelease release, Path gamePath, Path gameDataDirectory, JavaHeapSize heapMin, JavaHeapSize heapMax, List<String> javaParams, List<String> gameParams,
Level logLevel) {
this.release = release;
final boolean isMac = Platform.getPlatform().isMac();
final List<String> processParameters = new ArrayList<>();
processParameters.add(getRuntimePath().toString());
if (heapMin.isUsed()) {
@ -47,13 +55,28 @@ class GameStarter implements Callable<Process> {
processParameters.add("-Xmx" + heapMax.getSizeParameter());
}
processParameters.add("-DlogOverrideLevel=" + logLevel.name());
if (isMac && VersionHistory.LWJGL3.isProvidedBy(release.getId())) {
processParameters.add("-XstartOnFirstThread"); // lwjgl3 requires this on OS X
// awt didn't work either, but maybe fixed on newer versions?
// https://github.com/LWJGLX/lwjgl3-awt/issues/1
processParameters.add("-Djava.awt.headless=true");
}
processParameters.addAll(javaParams);
processParameters.add("-jar");
processParameters.add(gamePath.resolve(Path.of("libs", "Terasology.jar")).toString());
processParameters.add("-homedir=" + gameDataDirectory.toAbsolutePath().toString());
// Parameters after this are for the game facade, not the java runtime.
processParameters.add(homeDirParameter(gameDataDirectory));
processParameters.addAll(gameParams);
if (isMac) {
// splash screen uses awt, so no awt => no splash
processParameters.add(noSplashParameter());
}
processBuilder = new ProcessBuilder(processParameters)
.directory(gamePath.toFile())
.redirectErrorStream(true);
@ -77,4 +100,20 @@ class GameStarter implements Callable<Process> {
Path getRuntimePath() {
return Paths.get(System.getProperty("java.home"), "bin", "java");
}
String homeDirParameter(Path gameDataDirectory) {
if (terasologyUsesPosixOptions()) {
return "--homedir=" + gameDataDirectory.toAbsolutePath();
} else {
return "-homedir=" + gameDataDirectory.toAbsolutePath();
}
}
String noSplashParameter() {
return terasologyUsesPosixOptions() ? "--no-splash" : "-noSplash";
}
boolean terasologyUsesPosixOptions() {
return VersionHistory.PICOCLI.isProvidedBy(release.getId());
}
}

View File

@ -0,0 +1,35 @@
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.launcher.game;
import com.vdurmont.semver4j.Semver;
import org.terasology.launcher.model.GameIdentifier;
/**
* Terasology versions which introduce specific features.
* <p>
* These features change the way Launcher must interact with Terasology.
*/
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
*/
LWJGL3("4.1.0-rc.1"),
PICOCLI("5.2.0-SNAPSHOT");
public final Semver engineVersion;
VersionHistory(String s) {
engineVersion = new Semver(s, Semver.SemverType.IVY);
}
boolean isProvidedBy(Semver version) {
return version.isGreaterThanOrEqualTo(engineVersion);
}
boolean isProvidedBy(GameIdentifier version) {
return isProvidedBy(version.getEngineVersion());
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2020 The Terasology Foundation
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.launcher.model;
@ -45,8 +45,4 @@ public class GameRelease {
public Date getTimestamp() {
return releaseMetadata.getTimestamp();
}
public boolean isLwjgl3() {
return releaseMetadata.isLwjgl3();
}
}

View File

@ -8,18 +8,16 @@ import java.util.Date;
/**
* Data container for metadata associated with a game release.
*
* The metadata in this class is either relevant for displaying more information to the user (e.g., {@code changelog},
* {@code timestamp}) or for managing and starting the game itself (e.g., {@code isLwjgl3}).
* The metadata in this class is relevant for displaying more information to the user, e.g., {@code changelog},
* {@code timestamp}.
*/
public class ReleaseMetadata {
private final String changelog;
private final Date timestamp;
private final boolean isLwjgl3;
public ReleaseMetadata(String changelog, Date timestamp, boolean isLwjgl3) {
public ReleaseMetadata(String changelog, Date timestamp) {
this.changelog = changelog;
this.timestamp = timestamp;
this.isLwjgl3 = isLwjgl3;
}
/**
@ -35,11 +33,4 @@ public class ReleaseMetadata {
public Date getTimestamp() {
return timestamp;
}
/**
* Whether this release uses LWJGL v3 or not.
*/
public boolean isLwjgl3() {
return isLwjgl3;
}
}

View File

@ -29,12 +29,6 @@ public class GithubRepositoryAdapter implements ReleaseRepository {
private static final Logger logger = LoggerFactory.getLogger(GithubRepositoryAdapter.class);
/**
* 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
*/
private static final Semver FIRST_LWJGL3_RELEASE = new Semver("4.1.0-rc.1");
private GitHub github;
public GithubRepositoryAdapter() {
@ -64,8 +58,7 @@ public class GithubRepositoryAdapter implements ReleaseRepository {
final String changelog = ghRelease.getBody();
GameIdentifier id = new GameIdentifier(engineVersion.toString(), engineVersion, build, profile);
boolean isLwjgl3 = engineVersion.isGreaterThanOrEqualTo(FIRST_LWJGL3_RELEASE);
ReleaseMetadata metadata = new ReleaseMetadata(changelog, ghRelease.getPublished_at(), isLwjgl3);
ReleaseMetadata metadata = new ReleaseMetadata(changelog, ghRelease.getPublished_at());
return new GameRelease(id, url, metadata);
} catch (SemverException | IOException e) {
logger.info("Could not create game release from Github release {}: {}", ghRelease.getHtmlUrl(), e.getMessage());

View File

@ -103,7 +103,7 @@ class JenkinsRepositoryAdapter implements ReleaseRepository {
String changelog = computeChangelogFrom(jenkinsBuildInfo.changeSet);
final Date timestamp = new Date(jenkinsBuildInfo.timestamp);
// all builds from this Jenkins are using LWJGL v3
return new ReleaseMetadata(changelog, timestamp, true);
return new ReleaseMetadata(changelog, timestamp);
}
private String computeChangelogFrom(Jenkins.ChangeSet changeSet) {

View File

@ -1,9 +1,8 @@
// Copyright 2020 The Terasology Foundation
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.launcher.ui;
import com.google.common.collect.Lists;
import javafx.animation.Transition;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
@ -49,7 +48,6 @@ import org.terasology.launcher.tasks.DownloadTask;
import org.terasology.launcher.util.BundleUtils;
import org.terasology.launcher.util.HostServices;
import org.terasology.launcher.util.Languages;
import org.terasology.launcher.util.Platform;
import java.io.IOException;
import java.nio.file.Path;
@ -345,15 +343,7 @@ public class ApplicationController {
final GameRelease release = selectedRelease.getValue();
final GameIdentifier id = release.getId();
final Path gamePath = gameManager.getInstallDirectory(id);
List<String> additionalJavaParameters = Lists.newArrayList();
List<String> additionalGameParameters = Lists.newArrayList();
if (release.isLwjgl3() && Platform.getPlatform().isMac()) {
additionalJavaParameters.add("-XstartOnFirstThread");
additionalJavaParameters.add("-Djava.awt.headless=true");
additionalGameParameters.add("-noSplash");
}
gameService.start(gamePath, launcherSettings, additionalJavaParameters, additionalGameParameters);
gameService.start(release, gamePath, launcherSettings);
}
}

View File

@ -1,15 +1,23 @@
// Copyright 2020 The Terasology Foundation
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.launcher.game;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.event.Level;
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.model.ReleaseMetadata;
import org.terasology.launcher.util.JavaHeapSize;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Calendar;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
@ -50,7 +58,20 @@ public class TestGameStarter {
}
private GameStarter newStarter() {
return new GameStarter(gamePath, gameDataPath, HEAP_MIN, HEAP_MAX, javaParams, gameParams, LOG_LEVEL);
GameRelease release;
try {
release = new GameRelease(
new GameIdentifier("alpha-20", "5.1.0", Build.STABLE, Profile.OMEGA),
new URL("https://repository.example"),
new ReleaseMetadata(
"# CHANGES",
(new Calendar.Builder()).setDate(2021, 1, 1).build().getTime()
)
);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
return new GameStarter(release, gamePath, gameDataPath, HEAP_MIN, HEAP_MAX, javaParams, gameParams, LOG_LEVEL);
}
@Test

View File

@ -0,0 +1,28 @@
// Copyright 2021 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.launcher.game;
import com.vdurmont.semver4j.Semver;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** test edge cases in Semver comparison */
class VersionHistoryTest {
@ParameterizedTest
@ValueSource(strings = {"5.2.0-SNAPSHOT", "5.2.0", "5.2.1", "6.0.0"})
void hasPicocli(String version) {
assertTrue(VersionHistory.PICOCLI.isProvidedBy(new Semver(version, Semver.SemverType.IVY)));
}
@ParameterizedTest
@ValueSource(strings = {"5.1.0", "5.1.0-SNAPSHOT", "5.1.1"})
void lacksPicocli(String version) {
assertFalse(VersionHistory.PICOCLI.isProvidedBy(new Semver(version, Semver.SemverType.IVY)));
}
}

View File

@ -90,7 +90,7 @@ class JenkinsRepositoryAdapterTest {
// is the same of subsequent builds...
final String expectedVersion = displayVersion + "+" + validResult.builds[0].number;
final GameIdentifier id = new GameIdentifier(expectedVersion, engineVersion, Build.STABLE, Profile.OMEGA);
final ReleaseMetadata releaseMetadata = new ReleaseMetadata("", new Date(1604285977306L), true);
final ReleaseMetadata releaseMetadata = new ReleaseMetadata("", new Date(1604285977306L));
final GameRelease expected = new GameRelease(id, expectedArtifactUrl, releaseMetadata);
final JenkinsRepositoryAdapter adapter = new JenkinsRepositoryAdapter(Profile.OMEGA, Build.STABLE, stubClient);
@ -99,9 +99,7 @@ class JenkinsRepositoryAdapterTest {
assertAll(
() -> assertEquals(expected.getId(), adapter.fetchReleases().get(0).getId()),
() -> assertEquals(expected.getUrl(), adapter.fetchReleases().get(0).getUrl()),
() -> assertEquals(expected.getTimestamp(), adapter.fetchReleases().get(0).getTimestamp()),
() -> assertEquals(expected.isLwjgl3(), adapter.fetchReleases().get(0).isLwjgl3(),
"Jenkins adapter should assume only builds for LWJGL v3 releases")
() -> assertEquals(expected.getTimestamp(), adapter.fetchReleases().get(0).getTimestamp())
);
}