diff --git a/src/main/java/org/terasology/launcher/config/Config.java b/src/main/java/org/terasology/launcher/config/Config.java index 0df18e334..b036906f2 100644 --- a/src/main/java/org/terasology/launcher/config/Config.java +++ b/src/main/java/org/terasology/launcher/config/Config.java @@ -35,6 +35,8 @@ public final class Config { private final boolean closeAfterGameStarts; private final boolean cacheGamePackages; private final Package selectedPackage; + private final Package lastPlayedGamePackage; + private final Package lastInstalledGamePackage; private Config(final Builder builder) { this.gameConfig = builder.gameConfig; @@ -44,6 +46,8 @@ private Config(final Builder builder) { this.closeAfterGameStarts = builder.closeAfterGameStarts; this.cacheGamePackages = builder.cacheGamePackages; this.selectedPackage = builder.selectedPackage; + this.lastPlayedGamePackage = builder.lastPlayedGamePackage; + this.lastInstalledGamePackage = builder.lastInstalledGamePackage; } public GameConfig getGameConfig() { @@ -74,6 +78,14 @@ public Package getSelectedPackage() { return selectedPackage; } + public Package getLastPlayedGamePackage() { + return lastPlayedGamePackage; + } + + public Package getLastInstalledGamePackage() { + return lastInstalledGamePackage; + } + /** * Provides a pre-filled {@link Builder} instance * with all configurations copied from this. Use it @@ -108,8 +120,11 @@ public static final class Builder { private boolean closeAfterGameStarts; private boolean cacheGamePackages; private Package selectedPackage; + private Package lastPlayedGamePackage; + private Package lastInstalledGamePackage; - private Builder() { } + private Builder() { + } private Builder(final Config last) { gameConfig = last.gameConfig; @@ -119,6 +134,8 @@ private Builder(final Config last) { closeAfterGameStarts = last.closeAfterGameStarts; cacheGamePackages = last.cacheGamePackages; selectedPackage = last.selectedPackage; + lastPlayedGamePackage = last.lastPlayedGamePackage; + lastInstalledGamePackage = last.lastInstalledGamePackage; } public Builder gameConfig(final GameConfig newGameConfig) { @@ -156,6 +173,16 @@ public Builder selectedPackage(final Package newSelectedPackage) { return this; } + public Builder lastPlayedGamePackage(final Package newLastPlayedGamePackage) { + lastPlayedGamePackage = newLastPlayedGamePackage; + return this; + } + + public Builder lastInstalledGamePackage(final Package newLastInstalledGamePackage) { + lastInstalledGamePackage = newLastInstalledGamePackage; + return this; + } + public Config build() { Objects.requireNonNull(gameConfig, "gameConfig must not be null"); Objects.requireNonNull(locale, "locale must not be null"); diff --git a/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java b/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java index de3543168..b4197d2d0 100644 --- a/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java +++ b/src/main/java/org/terasology/launcher/gui/javafx/ApplicationController.java @@ -74,6 +74,7 @@ import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Predicate; import java.util.stream.Collectors; public class ApplicationController { @@ -206,12 +207,17 @@ private void startGameAction() { launcherSettings.getUserGameParameterList(), launcherSettings.getLogLevel()); if (!gameStarted) { GuiUtils.showErrorMessageDialog(stage, BundleUtils.getLabel("message_error_gameStart")); - } else if (launcherSettings.isCloseLauncherAfterGameStart()) { - if (gameDownloadWorker == null) { - logger.info("Close launcher after game start."); - close(); - } else { - logger.info("The launcher can not be closed after game start, because a download is running."); + } else { + launcherSettings.setLastPlayedGameJob(selectedPackage.getId()); + launcherSettings.setLastPlayedGameVersion(selectedPackage.getVersion()); + + if (launcherSettings.isCloseLauncherAfterGameStart()) { + if (gameDownloadWorker == null) { + logger.info("Close launcher after game start."); + close(); + } else { + logger.info("The launcher can not be closed after game start, because a download is running."); + } } } } @@ -234,10 +240,13 @@ private void downloadAction() { progressBar.setVisible(false); startAndDownloadButton.setVisible(true); cancelDownloadButton.setVisible(false); + packageManager.syncDatabase(); if (selectedPackage.isInstalled()) { startAndDownloadButton.setGraphic(playImage); deleteButton.setDisable(false); + launcherSettings.setLastInstalledGameJob(selectedPackage.getId()); + launcherSettings.setLastInstalledGameVersion(selectedPackage.getVersion()); } downloadTask = null; }); @@ -275,10 +284,15 @@ protected void deleteAction() { .filter(response -> response == ButtonType.OK) .ifPresent(response -> { logger.info("Removing game: {}-{}", selectedPackage.getId(), selectedPackage.getVersion()); + // triggering a game deletion implies the player doesn't want to play this game anymore + // hence, we unset `lastPlayedGameJob` and `lastPlayedGameVersion` settings independent of deletion success + launcherSettings.setLastPlayedGameJob(""); + launcherSettings.setLastPlayedGameVersion(""); deleteButton.setDisable(true); final DeleteTask deleteTask = new DeleteTask(packageManager, selectedVersion); deleteTask.onDone(() -> { + packageManager.syncDatabase(); if (!selectedPackage.isInstalled()) { startAndDownloadButton.setGraphic(downloadImage); } else { @@ -324,6 +338,8 @@ public void update(final Path newLauncherDirectory, final Path newDownloadDirect } footerController.setHostServices(hostServices); + + initializeComboBoxSelection(); } // To be called after database sync is done @@ -356,8 +372,20 @@ private void resetScrollBar(final ComboBox cb) { private void initComboBoxes() { jobBox.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { buildVersionBox.setItems(newVal.versionItems); - //TODO remember selection / select latest installed version - buildVersionBox.getSelectionModel().select(0); + + String lastPlayedGameJob = launcherSettings.getLastPlayedGameJob(); + String selectedJobId = newVal.versionItems.get(0).linkedPackageProperty.get().getId(); + if (lastPlayedGameJob.isEmpty() || !lastPlayedGameJob.equals(selectedJobId)) { + // select last installed package for the selected job or the latest one if none installed + String lastInstalledVersion = packageManager.getLatestInstalledPackageForId(selectedJobId) + .map(pkg -> pkg.getVersion()).orElseGet(() -> ""); + selectItem(buildVersionBox, item -> + item.versionProperty.get().equals(lastInstalledVersion)); + } else { + // select the package last played + selectItem(buildVersionBox, item -> + item.versionProperty.get().equals(launcherSettings.getLastPlayedGameVersion())); + } }); buildVersionBox.setOnShowing(e -> resetScrollBar(buildVersionBox)); @@ -381,6 +409,70 @@ private void initComboBoxes() { }); } + /** + * 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); + } + + /** + * Select the package item with given {@code jobId} or the first item of {@code jobBox}. + * + * @param jobId the job id of the package to be selected + */ + private void selectItemForJob(final String jobId) { + selectItem(jobBox, jobItem -> + jobItem.versionItems.stream() + .anyMatch(vItem -> vItem.linkedPackageProperty.get().getId().equals(jobId))); + } + + /** + * Initialize selected game job and version based on last played and last installed games. + * + * The selection is derived from the following precedence rules: + *
    + *
  1. Select the last played game
  2. + *
  3. Select the last installed game
  4. + *
  5. Select latest version of default job otherwise
  6. + *
+ */ + //TODO: Reduce boilerplate code after switching to >= Java 9 + // Use 'Optional::or' to chain logic together + private void initializeComboBoxSelection() { + String lastPlayedGameJob = launcherSettings.getLastPlayedGameJob(); + if (!lastPlayedGameJob.isEmpty()) { + // select the package last played + selectItemForJob(launcherSettings.getLastPlayedGameJob()); + selectItem(buildVersionBox, item -> + item.versionProperty.get().equals(launcherSettings.getLastPlayedGameVersion())); + } else { + String lastInstalledGameJob = launcherSettings.getLastInstalledGameJob(); + if (!lastInstalledGameJob.isEmpty()) { + // select last installed package job and version + selectItemForJob(lastInstalledGameJob); + selectItem(buildVersionBox, item -> + item.versionProperty.get().equals(launcherSettings.getLastInstalledGameVersion())); + } else { + // select last installed package for the default job or the latest one if none installed + String defaultGameJob = launcherSettings.getDefaultGameJob(); + selectItemForJob(defaultGameJob); + String lastInstalledVersion = packageManager.getLatestInstalledPackageForId(defaultGameJob) + .map(pkg -> pkg.getVersion()).orElseGet(() -> ""); + selectItem(buildVersionBox, item -> + item.versionProperty.get().equals(lastInstalledVersion)); + } + } + } + private void initButtons() { cancelDownloadButton.setTooltip(new Tooltip(BundleUtils.getLabel("launcher_cancelDownload"))); cancelDownloadButton.managedProperty().bind(cancelDownloadButton.visibleProperty()); diff --git a/src/main/java/org/terasology/launcher/packages/PackageDatabase.java b/src/main/java/org/terasology/launcher/packages/PackageDatabase.java index cb4200826..79a3fb42f 100644 --- a/src/main/java/org/terasology/launcher/packages/PackageDatabase.java +++ b/src/main/java/org/terasology/launcher/packages/PackageDatabase.java @@ -30,9 +30,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.Optional; /** * Provides package details from all online and local repositories. @@ -131,6 +133,13 @@ private void saveDatabase() { List getPackages() { return Collections.unmodifiableList(database); + } + + Optional getLatestInstalledPackageForId(String packageId) { + return database.stream() + .filter(pkg -> pkg.getId().equals(packageId) && pkg.isInstalled()) + .sorted(Comparator.comparing(Package::getVersion).reversed()) + .findFirst(); } static class PackageMetadata implements Serializable { diff --git a/src/main/java/org/terasology/launcher/packages/PackageManager.java b/src/main/java/org/terasology/launcher/packages/PackageManager.java index 7c0d394ef..41ea9c215 100644 --- a/src/main/java/org/terasology/launcher/packages/PackageManager.java +++ b/src/main/java/org/terasology/launcher/packages/PackageManager.java @@ -32,6 +32,7 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Optional; /** * Handles installation, removal and update of game packages. @@ -198,6 +199,10 @@ public List getPackageVersions(PackageBuild pkgBuild) { .getPackageVersions(pkgBuild); } + public Optional getLatestInstalledPackageForId(String packageId) { + return database.getLatestInstalledPackageForId(packageId); + } + public Path resolveInstallDir(Package target) { return installDir.resolve(target.getId()).resolve(target.getVersion()); } diff --git a/src/main/java/org/terasology/launcher/settings/AbstractLauncherSettings.java b/src/main/java/org/terasology/launcher/settings/AbstractLauncherSettings.java index 8f45138a6..0ec991539 100644 --- a/src/main/java/org/terasology/launcher/settings/AbstractLauncherSettings.java +++ b/src/main/java/org/terasology/launcher/settings/AbstractLauncherSettings.java @@ -53,6 +53,11 @@ public synchronized void init() { initUserJavaParameters(); initUserGameParameters(); initLogLevel(); + initDefaultGameJob(); + initLastPlayedGameJob(); + initLastPlayedGameVersion(); + initLastInstalledGameJob(); + initLastInstalledGameVersion(); } // --------------------------------------------------------------------- // @@ -81,6 +86,16 @@ public synchronized void init() { protected abstract void initLocale(); + protected abstract void initDefaultGameJob(); + + protected abstract void initLastPlayedGameJob(); + + protected abstract void initLastPlayedGameVersion(); + + protected abstract void initLastInstalledGameJob(); + + protected abstract void initLastInstalledGameVersion(); + // --------------------------------------------------------------------- // // GETTERS // --------------------------------------------------------------------- // @@ -115,6 +130,16 @@ public synchronized List getUserGameParameterList() { public abstract boolean isKeepDownloadedFiles(); + public abstract String getDefaultGameJob(); + + public abstract String getLastPlayedGameJob(); + + public abstract String getLastPlayedGameVersion(); + + public abstract String getLastInstalledGameJob(); + + public abstract String getLastInstalledGameVersion(); + // --------------------------------------------------------------------- // // SETTERS // --------------------------------------------------------------------- // @@ -140,4 +165,14 @@ public synchronized List getUserGameParameterList() { public abstract void setGameDirectory(Path gameDirectory); public abstract void setGameDataDirectory(Path gameDataDirectory); + + public abstract void setDefaultGameJob(String lastPlayedGameJob); + + public abstract void setLastPlayedGameJob(String lastPlayedGameJob); + + public abstract void setLastPlayedGameVersion(String lastPlayedGameVersion); + + public abstract void setLastInstalledGameJob(String lastInstalledGameJob); + + public abstract void setLastInstalledGameVersion(String lastInstalledGameVersion); } diff --git a/src/main/java/org/terasology/launcher/settings/BaseLauncherSettings.java b/src/main/java/org/terasology/launcher/settings/BaseLauncherSettings.java index d6f973bc1..ef606b7a4 100644 --- a/src/main/java/org/terasology/launcher/settings/BaseLauncherSettings.java +++ b/src/main/java/org/terasology/launcher/settings/BaseLauncherSettings.java @@ -18,7 +18,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.terasology.launcher.game.GameJob; import org.terasology.launcher.util.JavaHeapSize; import org.terasology.launcher.util.Languages; import org.terasology.launcher.util.LogLevel; @@ -56,14 +55,22 @@ public final class BaseLauncherSettings extends AbstractLauncherSettings { public static final String PROPERTY_USER_JAVA_PARAMETERS = "userJavaParameters"; public static final String PROPERTY_USER_GAME_PARAMETERS = "userGameParameters"; public static final String PROPERTY_LOG_LEVEL = "logLevel"; + public static final String PROPERTY_DEFAULT_GAME_JOB = "defaultGameJob"; + public static final String PROPERTY_LAST_PLAYED_GAME_JOB = "lastPlayedGameJob"; + public static final String PROPERTY_LAST_PLAYED_GAME_VERSION = "lastPlayedGameVersion"; + public static final String PROPERTY_LAST_INSTALLED_GAME_JOB = "lastInstalledGameJob"; + public static final String PROPERTY_LAST_INSTALLED_GAME_VERSION = "lastInstalledGameVersion"; - public static final GameJob JOB_DEFAULT = GameJob.TerasologyStable; public static final JavaHeapSize MAX_HEAP_SIZE_DEFAULT = JavaHeapSize.NOT_USED; public static final JavaHeapSize INITIAL_HEAP_SIZE_DEFAULT = JavaHeapSize.NOT_USED; - public static final String LAST_BUILD_NUMBER_DEFAULT = ""; public static final boolean SEARCH_FOR_LAUNCHER_UPDATES_DEFAULT = true; public static final boolean CLOSE_LAUNCHER_AFTER_GAME_START_DEFAULT = true; public static final boolean SAVE_DOWNLOADED_FILES_DEFAULT = false; + public static final String DEFAULT_GAME_JOB_DEFAULT = "DistroOmegaRelease"; + public static final String LAST_PLAYED_GAME_JOB_DEFAULT = ""; + public static final String LAST_PLAYED_GAME_VERSION_DEFAULT = ""; + public static final String LAST_INSTALLED_GAME_JOB_DEFAULT = ""; + public static final String LAST_INSTALLED_GAME_VERSION_DEFAULT = ""; public static final String LAUNCHER_SETTINGS_FILE_NAME = "TerasologyLauncherSettings.properties"; @@ -244,6 +251,41 @@ protected void initGameDataDirectory() { } } + protected void initDefaultGameJob() { + final String defaultGameJobStr = properties.getProperty(PROPERTY_DEFAULT_GAME_JOB); + if (defaultGameJobStr == null || defaultGameJobStr.isEmpty()) { + properties.setProperty(PROPERTY_DEFAULT_GAME_JOB, DEFAULT_GAME_JOB_DEFAULT); + } + } + + protected void initLastPlayedGameJob() { + final String lastPlayedGameJobStr = properties.getProperty(PROPERTY_LAST_PLAYED_GAME_JOB); + if (lastPlayedGameJobStr == null || lastPlayedGameJobStr.isEmpty()) { + properties.setProperty(PROPERTY_LAST_PLAYED_GAME_JOB, LAST_PLAYED_GAME_JOB_DEFAULT); + } + } + + protected void initLastPlayedGameVersion() { + final String lastPlayedGameVersionStr = properties.getProperty(PROPERTY_LAST_PLAYED_GAME_VERSION); + if (lastPlayedGameVersionStr == null || lastPlayedGameVersionStr.isEmpty()) { + properties.setProperty(PROPERTY_LAST_PLAYED_GAME_VERSION, LAST_PLAYED_GAME_VERSION_DEFAULT); + } + } + + protected void initLastInstalledGameJob() { + final String lastInstalledGameJobStr = properties.getProperty(PROPERTY_LAST_INSTALLED_GAME_JOB); + if (lastInstalledGameJobStr == null || lastInstalledGameJobStr.isEmpty()) { + properties.setProperty(PROPERTY_LAST_INSTALLED_GAME_JOB, LAST_INSTALLED_GAME_JOB_DEFAULT); + } + } + + protected void initLastInstalledGameVersion() { + final String lastInstalledGameVersionStr = properties.getProperty(PROPERTY_LAST_INSTALLED_GAME_VERSION); + if (lastInstalledGameVersionStr == null || lastInstalledGameVersionStr.isEmpty()) { + properties.setProperty(PROPERTY_LAST_INSTALLED_GAME_VERSION, LAST_INSTALLED_GAME_VERSION_DEFAULT); + } + } + // --------------------------------------------------------------------- // // GETTERS // --------------------------------------------------------------------- // @@ -319,6 +361,31 @@ public synchronized boolean isKeepDownloadedFiles() { return Boolean.valueOf(properties.getProperty(PROPERTY_SAVE_DOWNLOADED_FILES)); } + @Override + public synchronized String getDefaultGameJob() { + return properties.getProperty(PROPERTY_DEFAULT_GAME_JOB); + } + + @Override + public synchronized String getLastPlayedGameJob() { + return properties.getProperty(PROPERTY_LAST_PLAYED_GAME_JOB); + } + + @Override + public synchronized String getLastPlayedGameVersion() { + return properties.getProperty(PROPERTY_LAST_PLAYED_GAME_VERSION); + } + + @Override + public synchronized String getLastInstalledGameJob() { + return properties.getProperty(PROPERTY_LAST_INSTALLED_GAME_JOB); + } + + @Override + public synchronized String getLastInstalledGameVersion() { + return properties.getProperty(PROPERTY_LAST_INSTALLED_GAME_VERSION); + } + // --------------------------------------------------------------------- // // SETTERS // --------------------------------------------------------------------- // @@ -378,6 +445,31 @@ public synchronized void setGameDataDirectory(Path gameDataDirectory) { properties.setProperty(PROPERTY_GAME_DATA_DIRECTORY, gameDataDirectory.toUri().toString()); } + @Override + public synchronized void setDefaultGameJob(String defaultGameJob) { + properties.setProperty(PROPERTY_DEFAULT_GAME_JOB, defaultGameJob); + } + + @Override + public synchronized void setLastPlayedGameJob(String lastPlayedGameJob) { + properties.setProperty(PROPERTY_LAST_PLAYED_GAME_JOB, lastPlayedGameJob); + } + + @Override + public synchronized void setLastPlayedGameVersion(String lastPlayedGameVersion) { + properties.setProperty(PROPERTY_LAST_PLAYED_GAME_VERSION, lastPlayedGameVersion); + } + + @Override + public synchronized void setLastInstalledGameJob(String lastInstalledGameJob) { + properties.setProperty(PROPERTY_LAST_INSTALLED_GAME_JOB, lastInstalledGameJob); + } + + @Override + public synchronized void setLastInstalledGameVersion(String lastInstalledGameVersion) { + properties.setProperty(PROPERTY_LAST_INSTALLED_GAME_VERSION, lastInstalledGameVersion); + } + @Override public String toString() { return this.getClass().getName() + "[" + properties.toString() + "]";