/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.launcher.game; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.launcher.util.BundleUtils; import org.terasology.launcher.util.DirectoryUtils; import org.terasology.launcher.util.DownloadException; import org.terasology.launcher.util.DownloadUtils; import org.terasology.launcher.util.JobResult; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.URL; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; public final class TerasologyGameVersions { private static final Logger logger = LoggerFactory.getLogger(TerasologyGameVersions.class); private static final String FILE_TERASOLOGY_JAR = "Terasology.jar"; private static final String DIR_LIBS = "libs"; private static final String FILE_ENGINE_JAR = "engine.*jar"; private static final long OUTDATED_CACHE_MILLI_SECONDS = 1000L * 60L * 60L * 24L; // one day private static final String FILE_SUFFIX_CACHE = ".cache"; private final Map<GameJob, List<TerasologyGameVersion>> gameVersionLists; private final Map<GameJob, SortedMap<Integer, TerasologyGameVersion>> gameVersionMaps; public TerasologyGameVersions() { gameVersionLists = new HashMap<>(); gameVersionMaps = new HashMap<>(); } public synchronized List<TerasologyGameVersion> getGameVersionList(GameJob job) { return gameVersionLists.get(job); } public synchronized TerasologyGameVersion getGameVersionForBuildVersion(GameJob job, int buildVersion) { final List<TerasologyGameVersion> gameVersionList = getGameVersionList(job); for (TerasologyGameVersion gameVersion : gameVersionList) { if (buildVersion == gameVersion.getBuildVersion()) { return gameVersion; } } logger.warn("GameVersion not found for '{}' '{}'.", job, buildVersion); return null; } public synchronized void loadGameVersions(GameSettings gameSettings, File launcherDirectory, File gameDirectory) { final File cacheDirectory = getAndCheckCacheDirectory(launcherDirectory); gameVersionLists.clear(); gameVersionMaps.clear(); final Map<GameJob, SortedSet<Integer>> buildNumbersMap = new HashMap<>(); final Map<GameJob, Integer> lastBuildNumbers = new HashMap<>(); // Go over the job lines and figure out available build numbers for (GameJob job : GameJob.values()) { gameVersionMaps.put(job, new TreeMap<Integer, TerasologyGameVersion>()); final SortedSet<Integer> buildNumbers = new TreeSet<>(); buildNumbersMap.put(job, buildNumbers); // ?? loadSettingsBuildNumber(gameSettings, buildNumbers, job); // See if we have a known build number in the settings already (?) Integer lastBuildNumberFromSettings = getLastBuildNumberFromSettings(gameSettings, job); // Go check Jenkins for the last successful build (so failures are skipped), then add more going backwards Integer lastBuildNumberFromJenkins = loadLastSuccessfulBuildNumber(lastBuildNumberFromSettings, buildNumbers, job); // Finally add the mapping to our storage lastBuildNumbers.put(job, lastBuildNumberFromJenkins); } // With the build numbers in hand we can go check for any existing installs locally loadInstalledGames(gameDirectory, buildNumbersMap); // For each job line now fill in the extra version details needed for each build for (GameJob job : GameJob.values()) { final SortedMap<Integer, TerasologyGameVersion> gameVersionMap = gameVersionMaps.get(job); final SortedSet<Integer> buildNumbers = buildNumbersMap.get(job); final Integer lastBuildNumber = lastBuildNumbers.get(job); // Update the settings with the latest build number we know about (?) gameSettings.setLastBuildNumber(lastBuildNumber, job); if (job.isStable() && !job.isOnlyInstalled()) { fillBuildNumbers(buildNumbers, job.getMinBuildNumber(), lastBuildNumber); } // Add in more detailed info for any existing local game versions (?) SortedMap<Integer, TerasologyGameVersion> cachedGameVersions = null; if (cacheDirectory != null) { cachedGameVersions = readFromCache(job, buildNumbers, cacheDirectory); } // Add even more info including defaults if we didn't have a local copy (?) loadGameVersions(buildNumbers, job, gameVersionMap, cachedGameVersions); // Now go back over the list of good builds and match them to Omega builds if possible fillInOmegaBuilds(gameVersionMap, buildNumbers, job); // Finally update the local cache if appropriate (?) if (cacheDirectory != null) { writeToCache(job, cacheDirectory); } // Prepare the list in memory for display to the user (?) final List<TerasologyGameVersion> gameVersionList = createList(lastBuildNumber, job, gameVersionMap); gameVersionLists.put(job, gameVersionList); } if (cacheDirectory != null) { deleteOldCache(cacheDirectory); } } public synchronized void fixSettingsBuildVersion(GameSettings gameSettings) { for (GameJob job : GameJob.values()) { final SortedMap<Integer, TerasologyGameVersion> gameVersions = gameVersionMaps.get(job); fixSettingsBuildVersion(gameSettings, job, gameVersions); } } private File getAndCheckCacheDirectory(File launcherDirectory) { File cacheDirectory = null; try { cacheDirectory = new File(launcherDirectory, DirectoryUtils.CACHE_DIR_NAME); DirectoryUtils.checkDirectory(cacheDirectory); } catch (IOException e) { logger.error("Could not create or use cache directory '{}'!", cacheDirectory, e); cacheDirectory = null; } return cacheDirectory; } private void loadSettingsBuildNumber(GameSettings gameSettings, SortedSet<Integer> buildNumbers, GameJob job) { final int buildVersion = gameSettings.getBuildVersion(job); if ((TerasologyGameVersion.BUILD_VERSION_LATEST != buildVersion) && (buildVersion >= job.getMinBuildNumber())) { buildNumbers.add(buildVersion); } } private Integer getLastBuildNumberFromSettings(GameSettings gameSettings, GameJob job) { final Integer lastBuildNumber = gameSettings.getLastBuildNumber(job); final int buildVersion = gameSettings.getBuildVersion(job); final int lastBuildVersion; if (lastBuildNumber == null) { lastBuildVersion = buildVersion; } else { lastBuildVersion = Math.max(lastBuildNumber, buildVersion); } if ((TerasologyGameVersion.BUILD_VERSION_LATEST != lastBuildVersion) && (lastBuildVersion >= job.getMinBuildNumber())) { return lastBuildVersion; } return null; } private Integer loadLastSuccessfulBuildNumber(Integer lastBuildNumber, SortedSet<Integer> buildNumbers, GameJob job) { Integer lastSuccessfulBuildNumber = null; if (!job.isOnlyInstalled()) { try { // Use "successful" and not "stable" for TerasologyGame. lastSuccessfulBuildNumber = DownloadUtils.loadLastSuccessfulBuildNumberJenkins(job.name()); } catch (DownloadException e) { logger.info("Retrieving last successful build number failed. '{}'", job, e); lastSuccessfulBuildNumber = lastBuildNumber; } if ((lastSuccessfulBuildNumber != null) && (lastSuccessfulBuildNumber >= job.getMinBuildNumber())) { buildNumbers.add(lastSuccessfulBuildNumber); // add previous build numbers for (int buildNumber = lastSuccessfulBuildNumber - 1; (buildNumbers.size() <= job.getPrevBuildNumbers() && buildNumber > job.getMinBuildNumber()); buildNumber--) { try { // Skip unavailable builds DownloadUtils.loadJobResultJenkins(job.name(), buildNumber); buildNumbers.add(buildNumber); } catch (DownloadException e) { logger.info("Cannot find build number '{}' for job '{}'.", buildNumber, job); } } } } return lastSuccessfulBuildNumber; } /** * Take an existing set of engine build numbers loaded from Jenkins and look for mapped Omega distributions to add. * @param buildNumbers The engine build numbers we know exist * @param job the job line we're working on */ private void fillInOmegaBuilds(SortedMap<Integer, TerasologyGameVersion> gameVersionMap, SortedSet<Integer> buildNumbers, GameJob job) { logger.info("Will try to load Omega build numbers from " + job.getOmegaJobName()); // We more or less redo the original process in looking up the Omega job then later going back in history to map to the engine job Integer lastSuccessfulBuildNumber; try { lastSuccessfulBuildNumber = DownloadUtils.loadLastSuccessfulBuildNumberJenkins(job.getOmegaJobName()); } catch (DownloadException e) { logger.info("Retrieving last successful Omega build number failed, unable to load Omega distributions. '{}'", job, e); return; } int oldestEngine = buildNumbers.first(); logger.info("Latest successful Omega build number is {} and oldest engine we care about is {}", lastSuccessfulBuildNumber, oldestEngine); // Go through at the most twice as many Omega builds as we have engine builds to care about (not expecting many oddities) int omegaRebuild = -1; final Map<Integer, Integer> omegaMapping = new HashMap<>(); for (int i = 0; i < buildNumbers.size() * 2; i++) { int omegaBuildNumber = lastSuccessfulBuildNumber - i; if (omegaBuildNumber < 1) { logger.warn("Searched past the beginning of an Omega line, maybe range is too long? Finishing loop."); break; } try { // See if the job exists and is successful. If not we don't care so try the next one JobResult jobResult = DownloadUtils.loadJobResultJenkins(job.getOmegaJobName(), omegaBuildNumber); if (jobResult != JobResult.SUCCESS) { logger.info("Retrieved an Omega result of {} for build number {}, skipping", jobResult, omegaBuildNumber); continue; } } catch (DownloadException e) { logger.info("Cannot find build number '{}' for job '{}'.", omegaBuildNumber, job.getOmegaJobName()); continue; } // We have a successful Omega build. See if it was triggered directly by an engine build int matchingEngineBuildNumber = -1; try { // See if the job exists and is successful. If not we don't care so try the next one matchingEngineBuildNumber = DownloadUtils.loadEngineTriggerJenkins(job, omegaBuildNumber); if (matchingEngineBuildNumber == -1) { // In this case we know there is a successful Omega build that didn't trigger from an engine build // By storing the Omega number we can keep looking logger.info("Parking omega build {} for claiming by a later engine", omegaBuildNumber); omegaRebuild = omegaBuildNumber; } else { // We have a valid engine trigger. Pair the two together, considering if we had a parked rebuild if (omegaRebuild != -1) { logger.info("Mapping engine build {} with parked Omega build {}", matchingEngineBuildNumber, omegaRebuild); omegaMapping.put(matchingEngineBuildNumber, omegaRebuild); omegaRebuild = -1; } else { //logger.debug("Mapping engine build {} with exact Omega build {}", matchingEngineBuildNumber, omegaBuildNumber); omegaMapping.put(matchingEngineBuildNumber, omegaBuildNumber); } // See if we've searched far enough the Omega line to have matched the oldest engine we care about if (matchingEngineBuildNumber <= oldestEngine) { logger.info("We've reached or passed the number of engine releases we're pairing with Omega, done here"); break; } } } catch (DownloadException e) { logger.info("Failed to retrieve a cause for job {} - ignoring.", job.getOmegaJobName()); } } logger.info("Now checking build number mappings with game versions"); int processed = 0; // TODO: Is it safe to use the entries from buildNumbers as keys or can they go out of sync vs loaded game versions? for (Integer engineBuildNumber : buildNumbers) { Integer matchingOmega = omegaMapping.get(engineBuildNumber); if (matchingOmega != null) { //logger.info("Omega build {} matches engine build {}", matchingOmega, engineBuildNumber); TerasologyGameVersion gameVersion = gameVersionMap.get(engineBuildNumber); if (gameVersion == null) { logger.warn("Failed to find a game version entry for engine build {} !", engineBuildNumber); continue; } else { //logger.debug("Updating game version for engine {} with omega mapping {}", engineBuildNumber, matchingOmega); gameVersion.setOmegaNumber(matchingOmega); } } else { logger.warn("*WARNING:* No Omega distribution found for engine build {}", engineBuildNumber); // TODO: Display some sort of warning for the user if this build gets selected } //logger.debug("Final game version object: {} ", gameVersionMap.get(engineBuildNumber)); processed++; } logger.info("Processed " + processed + " out of " + gameVersionMap.size()); } /** * Given a job line and a set of build numbers see what if any existing builds we have installed locally. * @param directory The game directory to search * @param buildNumbersMap The set of build numbers to look for */ private void loadInstalledGames(File directory, Map<GameJob, SortedSet<Integer>> buildNumbersMap) { final Set<File> candidateFiles = new HashSet<>(); try { Files.walkFileTree(directory.toPath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { if (path.toFile().getName().matches(FILE_ENGINE_JAR)) { logger.debug("Matched path to engine jar file: {}", path.toFile().getName()); candidateFiles.add(path.toFile()); } return FileVisitResult.CONTINUE; } }); } catch (IOException e) { logger.error("Hit an error scanning for existing file directories: {}", e); } logger.info("Found the following existing engine install dirs: {}", candidateFiles); for (File engineJar : candidateFiles) { final TerasologyGameVersion gameVersion = loadInstalledGameVersion(engineJar); if (gameVersion != null) { final SortedMap<Integer, TerasologyGameVersion> gameVersionMap = gameVersionMaps.get(gameVersion.getJob()); final SortedSet<Integer> buildNumbers = buildNumbersMap.get(gameVersion.getJob()); buildNumbers.add(gameVersion.getBuildNumber()); if (!gameVersionMap.containsKey(gameVersion.getBuildNumber())) { gameVersionMap.put(gameVersion.getBuildNumber(), gameVersion); } else { logger.info("Installed game already loaded. '{}'", engineJar); } } } } private TerasologyGameVersion loadInstalledGameVersion(File engineJar) { TerasologyGameVersion gameVersion = null; final TerasologyGameVersionInfo gameVersionInfo = TerasologyGameVersionInfo.loadFromJar(engineJar); if ((gameVersionInfo != null) && (gameVersionInfo.getJobName() != null) && (gameVersionInfo.getJobName().length() > 0) && (gameVersionInfo.getBuildNumber() != null) && (gameVersionInfo.getBuildNumber().length() > 0)) { GameJob installedJob = null; try { installedJob = GameJob.valueOf(gameVersionInfo.getJobName()); } catch (IllegalArgumentException e) { logger.error("Unknown job '{}' found for game '{}'!", gameVersionInfo.getJobName(), engineJar); } Integer installedBuildNumber = null; try { installedBuildNumber = Integer.parseInt(gameVersionInfo.getBuildNumber()); } catch (NumberFormatException e) { logger.error("Could not parse build number '{}'!", gameVersionInfo.getBuildNumber()); } File terasologyJar = new File(engineJar.getParentFile(), FILE_TERASOLOGY_JAR); if (!terasologyJar.exists()) { logger.error("Expected game jar {} did not exist at {} ! ", FILE_TERASOLOGY_JAR, terasologyJar); } if ((installedJob != null) && (installedBuildNumber != null) && (gameVersionInfo.getGitBranch().endsWith(installedJob.getGitBranch())) && (installedJob.getMinBuildNumber() <= installedBuildNumber)) { gameVersion = new TerasologyGameVersion(); gameVersion.setJob(installedJob); gameVersion.setBuildNumber(installedBuildNumber); gameVersion.setInstallationPath(engineJar.getParentFile()); gameVersion.setGameJar(terasologyJar); gameVersion.setGameVersionInfo(gameVersionInfo); gameVersion.setChangeLog(null); gameVersion.setSuccessful(Boolean.TRUE); gameVersion.setLatest(false); } else { logger.warn("The game version info can not be used from the file '{}' !", engineJar); } } else { logger.warn("The game version info can not be loaded from the file '{}' !", engineJar); } return gameVersion; } private void fillBuildNumbers(SortedSet<Integer> buildNumbers, int minBuildNumber, Integer lastBuildNumber) { if ((buildNumbers != null) && !buildNumbers.isEmpty()) { int first = buildNumbers.first(); if (first < minBuildNumber) { first = minBuildNumber; } int last = buildNumbers.last(); if ((lastBuildNumber != null) && (last > lastBuildNumber)) { last = lastBuildNumber; } // Add all build numbers between first and last for (int buildNumber = first + 1; buildNumber < last; buildNumber++) { buildNumbers.add(buildNumber); } } } private SortedMap<Integer, TerasologyGameVersion> readFromCache(GameJob job, SortedSet<Integer> buildNumbers, File cacheDirectory) { final SortedMap<Integer, TerasologyGameVersion> cachedGameVersions = new TreeMap<>(); for (Integer buildNumber : buildNumbers) { final File cacheFile = createCacheFile(job, buildNumber, cacheDirectory); try { if (cacheFile.exists() && cacheFile.canRead() && cacheFile.isFile()) { try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(cacheFile))) { final TerasologyGameVersion gameVersion = (TerasologyGameVersion) ois.readObject(); cachedGameVersions.put(buildNumber, gameVersion); } } } catch (IOException | ClassNotFoundException e) { logger.warn("Could not load cached data! '{}'", cacheFile); } } return cachedGameVersions; } private File createCacheFile(GameJob job, Integer buildNumber, File cacheDirectory) { return new File(cacheDirectory, "TerasologyGameVersion_" + job.name() + "_" + buildNumber.toString() + FILE_SUFFIX_CACHE); } private void loadGameVersions(SortedSet<Integer> buildNumbers, GameJob job, SortedMap<Integer, TerasologyGameVersion> gameVersions, SortedMap<Integer, TerasologyGameVersion> cachedGameVersionMap) { for (Integer buildNumber : buildNumbers) { final TerasologyGameVersion gameVersion; if (gameVersions.containsKey(buildNumber)) { gameVersion = gameVersions.get(buildNumber); } else { gameVersion = new TerasologyGameVersion(); gameVersion.setBuildNumber(buildNumber); gameVersion.setJob(job); gameVersions.put(buildNumber, gameVersion); } TerasologyGameVersion cachedGameVersion = null; if ((cachedGameVersionMap != null) && cachedGameVersionMap.containsKey(buildNumber)) { cachedGameVersion = cachedGameVersionMap.get(buildNumber); if (!buildNumber.equals(cachedGameVersion.getBuildNumber()) || !job.equals(cachedGameVersion.getJob())) { logger.warn("The cached game version can not be used! '{}'", cachedGameVersion); cachedGameVersion = null; } } loadAndSetSuccessful(gameVersion, cachedGameVersion, job, buildNumber); loadAndSetChangeLog(gameVersion, cachedGameVersion, job, buildNumber); loadAndSetGameVersionInfo(gameVersion, cachedGameVersion, job, buildNumber); } } private void loadAndSetSuccessful(TerasologyGameVersion gameVersion, TerasologyGameVersion cachedGameVersion, GameJob job, Integer buildNumber) { if (gameVersion.getSuccessful() == null) { if ((cachedGameVersion != null) && (cachedGameVersion.getSuccessful() != null)) { gameVersion.setSuccessful(cachedGameVersion.getSuccessful()); } else if (!job.isOnlyInstalled()) { Boolean successful = null; try { JobResult jobResult = DownloadUtils.loadJobResultJenkins(job.name(), buildNumber); successful = jobResult != null && ((jobResult == JobResult.SUCCESS) || (jobResult == JobResult.UNSTABLE)); } catch (DownloadException e) { logger.warn("Failed to load job result (probably OK): '{}' '{}'", job, buildNumber); } gameVersion.setSuccessful(successful); } } } private void loadAndSetChangeLog(TerasologyGameVersion gameVersion, TerasologyGameVersion cachedGameVersion, GameJob job, Integer buildNumber) { if (gameVersion.getChangeLog() == null) { if ((cachedGameVersion != null) && (cachedGameVersion.getChangeLog() != null)) { gameVersion.setChangeLog(cachedGameVersion.getChangeLog()); } else if (!job.isOnlyInstalled()) { try { final List<String> changeLog = DownloadUtils.loadChangeLogJenkins(job.name(), buildNumber); if (changeLog != null) { if (changeLog.isEmpty()) { changeLog.add(BundleUtils.getLabel("message_noChangeLog")); } gameVersion.setChangeLog(Collections.unmodifiableList(changeLog)); } } catch (DownloadException e) { logger.warn("Loading change log failed (probably OK). '{}' '{}'", job, buildNumber); } } } } private void loadAndSetGameVersionInfo(TerasologyGameVersion gameVersion, TerasologyGameVersion cachedGameVersion, GameJob job, Integer buildNumber) { if (gameVersion.getGameVersionInfo() == null) { if ((cachedGameVersion != null) && (cachedGameVersion.getGameVersionInfo() != null)) { gameVersion.setGameVersionInfo(cachedGameVersion.getGameVersionInfo()); } else if (!job.isOnlyInstalled() && ((cachedGameVersion == null) || (gameVersion.getSuccessful() == null) || gameVersion.getSuccessful())) { TerasologyGameVersionInfo gameVersionInfo = null; URL urlVersionInfo = null; try { urlVersionInfo = DownloadUtils.createFileDownloadUrlJenkins(job.name(), buildNumber, DownloadUtils.FILE_TERASOLOGY_GAME_VERSION_INFO); gameVersionInfo = TerasologyGameVersionInfo.loadFromInputStream(urlVersionInfo.openStream()); } catch (IOException e) { if (e instanceof FileNotFoundException) { logger.debug("Load game version info failed. '{}' '{}' '{}'", job, buildNumber, urlVersionInfo); gameVersionInfo = TerasologyGameVersionInfo.getEmptyGameVersionInfo(); } else { logger.info("Load game version info failed. '{}' '{}' '{}'", job, buildNumber, urlVersionInfo, e); } } gameVersion.setGameVersionInfo(gameVersionInfo); } } } private void writeToCache(GameJob job, File cacheDirectory) { try { final SortedMap<Integer, TerasologyGameVersion> gameVersions = gameVersionMaps.get(job); for (TerasologyGameVersion gameVersion : gameVersions.values()) { final File cacheFile = createCacheFile(job, gameVersion.getBuildNumber(), cacheDirectory); try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(cacheFile))) { oos.writeObject(gameVersion); } } } catch (IOException e) { logger.error("Could not write cache data!", e); } } private void deleteOldCache(File cacheDirectory) { final File[] cacheFiles = cacheDirectory.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.exists() && pathname.isFile() && pathname.canRead() && pathname.canWrite() && pathname.getName().endsWith(FILE_SUFFIX_CACHE) && pathname.lastModified() < (System.currentTimeMillis() - OUTDATED_CACHE_MILLI_SECONDS); } }); if ((cacheFiles != null) && (cacheFiles.length > 0)) { logger.debug("Delete {} outdated cache files on exit.", cacheFiles.length); for (File cacheFile : cacheFiles) { cacheFile.deleteOnExit(); } } } private List<TerasologyGameVersion> createList(Integer lastBuildNumber, GameJob job, SortedMap<Integer, TerasologyGameVersion> gameVersionMap) { final List<TerasologyGameVersion> gameVersionList = new ArrayList<>(); // add only available builds for (TerasologyGameVersion version : gameVersionMap.values()) { if (version.getSuccessful() != null) { gameVersionList.add(version); } } final TerasologyGameVersion latestGameVersion = new TerasologyGameVersion(); latestGameVersion.setLatest(true); latestGameVersion.setJob(job); latestGameVersion.setBuildNumber(lastBuildNumber); if ((lastBuildNumber != null) && gameVersionMap.containsKey(lastBuildNumber)) { gameVersionMap.get(lastBuildNumber).copyTo(latestGameVersion); } else if ((lastBuildNumber == null) && !gameVersionMap.isEmpty()) { gameVersionMap.get(gameVersionMap.lastKey()).copyTo(latestGameVersion); } gameVersionList.add(latestGameVersion); Collections.reverse(gameVersionList); return Collections.unmodifiableList(gameVersionList); } private void fixSettingsBuildVersion(GameSettings gameSettings, GameJob job, SortedMap<Integer, TerasologyGameVersion> gameVersionMap) { final int buildVersion = gameSettings.getBuildVersion(job); if ((buildVersion != TerasologyGameVersion.BUILD_VERSION_LATEST) && !gameVersionMap.containsKey(buildVersion)) { Integer newBuildVersion = TerasologyGameVersion.BUILD_VERSION_LATEST; for (TerasologyGameVersion gameVersion : gameVersionMap.values()) { if (gameVersion.isInstalled()) { newBuildVersion = gameVersion.getBuildNumber(); // no break => find highest installed version } } gameSettings.setBuildVersion(newBuildVersion, job); // don't store settings } } /** * Re-checks version info after a game has been installed. * @param terasologyDirectory The direction the game was installed to. * @return boolean indicating success or failure. */ public synchronized boolean updateGameVersionsAfterInstallation(File terasologyDirectory) { File engineJar = null; File libsDir = new File(terasologyDirectory, DIR_LIBS); if (!libsDir.exists()) { logger.error("Failed to find the libs dir in {} - cannot update game versions", terasologyDirectory); return false; } File[] files = libsDir.listFiles(); if (files == null) { logger.error("No files returned trying to scan directory {} for game versioning", terasologyDirectory); return false; } for (File f : files) { if (f.getName().matches(FILE_ENGINE_JAR)) { engineJar = f; } } if (engineJar == null) { logger.error("Failed to find the engine jar in game install dir {} - cannot update game versions", terasologyDirectory); return false; } final TerasologyGameVersion gameVersion = loadInstalledGameVersion(engineJar); if (gameVersion != null) { final List<TerasologyGameVersion> gameVersionList = getGameVersionList(gameVersion.getJob()); for (TerasologyGameVersion currentGameVersion : gameVersionList) { if (gameVersion.getBuildNumber().equals(currentGameVersion.getBuildNumber())) { if (gameVersion.getGameVersionInfo() != null) { currentGameVersion.setGameVersionInfo(gameVersion.getGameVersionInfo()); } currentGameVersion.setInstallationPath(gameVersion.getInstallationPath()); currentGameVersion.setGameJar(gameVersion.getGameJar()); logger.debug("Update game version with new installation: {}", currentGameVersion); } } return true; } else { logger.error("The game version can not be loaded from directory '{}'!", terasologyDirectory); } return false; } public synchronized void removeInstallationInfo(TerasologyGameVersion gameVersion) { if (gameVersion.isInstalled()) { if (gameVersion.isLatest()) { final TerasologyGameVersion related = getGameVersionForBuildVersion(gameVersion.getJob(), gameVersion.getBuildNumber()); if ((related != null) && related.isInstalled() && (related.getInstallationPath().equals(gameVersion.getInstallationPath()))) { logger.debug("Remove installation info from related game version. '{}'", related); related.setInstallationPath(null); related.setGameJar(null); } } else { final TerasologyGameVersion latest = getGameVersionForBuildVersion(gameVersion.getJob(), TerasologyGameVersion.BUILD_VERSION_LATEST); if ((latest != null) && latest.isInstalled() && (latest.getInstallationPath().equals(gameVersion.getInstallationPath()))) { logger.debug("Remove installation info from latest game version. '{}'", latest); latest.setInstallationPath(null); latest.setGameJar(null); } } logger.debug("Remove installation info from game version. '{}'", gameVersion); gameVersion.setInstallationPath(null); gameVersion.setGameJar(null); } } public synchronized List<String> getAggregatedChangeLog(TerasologyGameVersion gameVersion, int builds) { List<String> aggregatedChangeLog = new ArrayList<>(); List<TerasologyGameVersion> gameVersions = gameVersionLists.get(gameVersion.getJob()); int idx = gameVersions.indexOf(gameVersion) + 1; int upper = Math.min(idx + builds, gameVersions.size()); for (int i = idx; i < upper; i++) { final List<String> log = gameVersions.get(i).getChangeLog(); /* Don't include empty change logs (nothing changed) in the aggregate. */ if (log.size() == 1) { final String msg = log.get(0); if (BundleUtils.getLabel("message_noChangeLog").equals(msg)) { continue; } } aggregatedChangeLog.addAll(gameVersions.get(i).getChangeLog()); } return aggregatedChangeLog; } @Override public String toString() { return this.getClass().getName() + "[" + gameVersionLists + "]"; } }