diff --git a/src/main/java/com/skcraft/launcher/builder/BuilderConfig.java b/src/main/java/com/skcraft/launcher/builder/BuilderConfig.java new file mode 100644 index 0000000..d629fcb --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/BuilderConfig.java @@ -0,0 +1,37 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import lombok.Data; + +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; + +@Data +public class BuilderConfig { + + private String name; + private String title; + private String gameVersion; + private List features; + private FnPatternList userFiles; + + public void registerProperties(PropertiesApplicator applicator) { + if (features != null) { + for (FeaturePattern feature : features) { + checkNotNull(emptyToNull(feature.getFeature().getName()), + "Empty feature name found"); + applicator.register(feature); + } + } + + applicator.setUserFiles(userFiles); + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java b/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java index 617c2c3..db1b220 100644 --- a/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java +++ b/src/main/java/com/skcraft/launcher/builder/ClientFileCollector.java @@ -13,6 +13,7 @@ import com.skcraft.launcher.model.modpack.FileInstall; import com.skcraft.launcher.model.modpack.Manifest; import lombok.NonNull; import lombok.extern.java.Log; +import org.apache.commons.io.FilenameUtils; import java.io.File; import java.io.IOException; @@ -25,6 +26,7 @@ import java.io.IOException; public class ClientFileCollector extends DirectoryWalker { private final Manifest manifest; + private final PropertiesApplicator applicator; private final File destDir; private HashFunction hf = Hashing.sha1(); @@ -32,17 +34,45 @@ public class ClientFileCollector extends DirectoryWalker { * Create a new collector. * * @param manifest the manifest - * @param destDir the destination directory to copy the hashed objects + * @param applicator applies properties to manifest entries + * @param destDir the destination directory to copy the hashed objects */ - public ClientFileCollector(@NonNull Manifest manifest, @NonNull File destDir) { + public ClientFileCollector(@NonNull Manifest manifest, @NonNull PropertiesApplicator applicator, + @NonNull File destDir) { this.manifest = manifest; + this.applicator = applicator; this.destDir = destDir; } @Override - public DirectoryBehavior getBehavior(@NonNull String name) { + protected DirectoryBehavior getBehavior(@NonNull String name) { + return getDirectoryBehavior(name); + } + + @Override + protected void onFile(File file, String relPath) throws IOException { + if (file.getName().endsWith(FileInfoScanner.FILE_SUFFIX)) { + return; + } + + FileInstall entry = new FileInstall(); + String hash = Files.hash(file, hf).toString(); + String hashedPath = hash.substring(0, 2) + "/" + hash.substring(2, 4) + "/" + hash; + File destPath = new File(destDir, hashedPath); + entry.setHash(hash); + entry.setLocation(hashedPath); + entry.setTo(FilenameUtils.separatorsToUnix(FilenameUtils.normalize(relPath))); + entry.setSize(file.length()); + applicator.apply(entry); + destPath.getParentFile().mkdirs(); + ClientFileCollector.log.info(String.format("Adding %s from %s...", relPath, file.getAbsolutePath())); + Files.copy(file, destPath); + manifest.getTasks().add(entry); + } + + public static DirectoryBehavior getDirectoryBehavior(@NonNull String name) { if (name.equals("_OPTIONAL")) { - return DirectoryBehavior.SKIP; + return DirectoryBehavior.IGNORE; } else if (name.equals("_SERVER")) { return DirectoryBehavior.SKIP; } else if (name.equals("_CLIENT")) { @@ -52,20 +82,4 @@ public class ClientFileCollector extends DirectoryWalker { } } - @Override - protected void onFile(File file, String relPath) throws IOException { - FileInstall task = new FileInstall(); - String hash = Files.hash(file, hf).toString(); - String hashedPath = hash.substring(0, 2) + "/" + hash.substring(2, 4) + "/" + hash; - File destPath = new File(destDir, hashedPath); - task.setHash(hash); - task.setLocation(hashedPath); - task.setTo(relPath); - task.setSize(file.length()); - destPath.getParentFile().mkdirs(); - ClientFileCollector.log.info(String.format("Adding %s from %s...", relPath, file.getAbsolutePath())); - Files.copy(file, destPath); - manifest.getTasks().add(task); - } - } diff --git a/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java b/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java index cf8ea95..f92726c 100644 --- a/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java +++ b/src/main/java/com/skcraft/launcher/builder/DirectoryWalker.java @@ -82,7 +82,7 @@ public abstract class DirectoryWalker { * @param name the directory name * @return the behavor */ - public DirectoryBehavior getBehavior(String name) { + protected DirectoryBehavior getBehavior(String name) { return DirectoryBehavior.CONTINUE; } diff --git a/src/main/java/com/skcraft/launcher/builder/FeaturePattern.java b/src/main/java/com/skcraft/launcher/builder/FeaturePattern.java new file mode 100644 index 0000000..01cbf93 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/FeaturePattern.java @@ -0,0 +1,24 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.skcraft.launcher.model.modpack.Feature; +import lombok.Data; + +@Data +public class FeaturePattern { + + @JsonProperty("properties") + private Feature feature; + @JsonProperty("files") + private FnPatternList filePatterns; + + public boolean matches(String path) { + return filePatterns != null && filePatterns.matches(path); + } +} diff --git a/src/main/java/com/skcraft/launcher/builder/FileInfo.java b/src/main/java/com/skcraft/launcher/builder/FileInfo.java new file mode 100644 index 0000000..bea808a --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/FileInfo.java @@ -0,0 +1,17 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.skcraft.launcher.model.modpack.Feature; +import lombok.Data; + +@Data +public class FileInfo { + + private Feature feature; + +} diff --git a/src/main/java/com/skcraft/launcher/builder/FileInfoScanner.java b/src/main/java/com/skcraft/launcher/builder/FileInfoScanner.java new file mode 100644 index 0000000..bd1c1b7 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/FileInfoScanner.java @@ -0,0 +1,75 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.skcraft.launcher.model.modpack.Feature; +import lombok.Getter; +import lombok.extern.java.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; +import static com.skcraft.launcher.builder.ClientFileCollector.getDirectoryBehavior; +import static org.apache.commons.io.FilenameUtils.*; + +@Log +public class FileInfoScanner extends DirectoryWalker { + + private static final EnumSet MATCH_FLAGS = EnumSet.of( + FnMatch.Flag.CASEFOLD, FnMatch.Flag.PERIOD, FnMatch.Flag.PATHNAME); + public static final String FILE_SUFFIX = ".info.json"; + + private final ObjectMapper mapper; + @Getter + private final List patterns = new ArrayList(); + + public FileInfoScanner(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + protected DirectoryBehavior getBehavior(String name) { + return getDirectoryBehavior(name); + } + + @Override + protected void onFile(File file, String relPath) throws IOException { + if (file.getName().endsWith(FILE_SUFFIX)) { + String fnPattern = + separatorsToUnix(getPath(relPath)) + + getBaseName(getBaseName(file.getName())) + "*"; + + FileInfo info = mapper.readValue(file, FileInfo.class); + Feature feature = info.getFeature(); + + if (feature != null) { + checkNotNull(emptyToNull(feature.getName()), + "Empty component name found in " + file.getAbsolutePath()); + + List patterns = new ArrayList(); + patterns.add(fnPattern); + FnPatternList patternList = new FnPatternList(); + patternList.setInclude(patterns); + patternList.setFlags(MATCH_FLAGS); + FeaturePattern fp = new FeaturePattern(); + fp.setFeature(feature); + fp.setFilePatterns(patternList); + getPatterns().add(fp); + + FileInfoScanner.log.info("Found .info.json file at " + file.getAbsolutePath() + + ", with pattern " + fnPattern + ", and component " + feature); + } + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/FnMatch.java b/src/main/java/com/skcraft/launcher/builder/FnMatch.java new file mode 100644 index 0000000..9a65859 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/FnMatch.java @@ -0,0 +1,246 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ +/* $OpenBSD: fnmatch.c,v 1.13 2006/03/31 05:34:14 deraadt Exp $ */ + +package com.skcraft.launcher.builder; + +import java.util.EnumSet; + +/* + * Function fnmatch() as specified in POSIX 1003.2-1992, section B.6. + * Compares a filename or pathname to a pattern. + */ +public class FnMatch { + + public static enum Flag { + + /** Disable backslash escaping. */ + NOESCAPE, + /** Slash must be matched by slash. */ + PATHNAME, + /** Period must be matched by period. */ + PERIOD, + /** Ignore / after Imatch. */ + LEADING_DIR, + /** Case insensitive search. */ + CASEFOLD + } + private static final int RANGE_ERROR = -1; + private static final int RANGE_NOMATCH = 0; + + public static boolean fnmatch(String pattern, String string, EnumSet flags) { + return match(pattern, 0, string, 0, flags); + } + + public static boolean fnmatch(String pattern, String string, int stringPos, Flag flag) { + return match(pattern, 0, string, stringPos, EnumSet.of(flag)); + } + + public static boolean fnmatch(String pattern, String string, int stringPos) { + return match(pattern, 0, string, stringPos, EnumSet.noneOf(Flag.class)); + } + + public static boolean fnmatch(String pattern, String string) { + return fnmatch(pattern, string, 0); + } + + private static boolean match(String pattern, int patternPos, + String string, int stringPos, EnumSet flags) { + char c; + + while (true) { + if (patternPos >= pattern.length()) { + if (flags.contains(Flag.LEADING_DIR) && string.charAt(stringPos) == '/') { + return true; + } + return stringPos == string.length(); + } + c = pattern.charAt(patternPos++); + switch (c) { + case '?': + if (stringPos >= string.length()) { + return false; + } + if (string.charAt(stringPos) == '/' && flags.contains(Flag.PATHNAME)) { + return false; + } + if (hasLeadingPeriod(string, stringPos, flags)) { + return false; + } + ++stringPos; + continue; + case '*': + /* Collapse multiple stars. */ + while (patternPos < pattern.length() && + (c = pattern.charAt(patternPos)) == '*') { + patternPos++; + } + + if (hasLeadingPeriod(string, stringPos, flags)) { + return false; + } + + /* Optimize for pattern with * at end or before /. */ + if (patternPos == pattern.length()) { + if (flags.contains(Flag.PATHNAME)) { + return flags.contains(Flag.LEADING_DIR) || + string.indexOf('/', stringPos) == -1; + } + return true; + } else if (c == '/' && flags.contains(Flag.PATHNAME)) { + stringPos = string.indexOf('/', stringPos); + if (stringPos == -1) { + return false; + } + continue; + } + + /* General case, use recursion. */ + while (stringPos < string.length()) { + if (flags.contains(Flag.PERIOD)) { + flags = EnumSet.copyOf(flags); + flags.remove(Flag.PERIOD); + } + if (match(pattern, patternPos, string, stringPos, flags)) { + return true; + } + if (string.charAt(stringPos) == '/' && flags.contains(Flag.PATHNAME)) { + break; + } + ++stringPos; + } + return false; + + case '[': + if (stringPos >= string.length()) { + return false; + } + if (string.charAt(stringPos) == '/' && flags.contains(Flag.PATHNAME)) { + return false; + } + if (hasLeadingPeriod(string, stringPos, flags)) { + return false; + } + + int result = matchRange(pattern, patternPos, string.charAt(stringPos), flags); + if (result == RANGE_ERROR) /* not a good range, treat as normal text */ { + break; + } + + if (result == RANGE_NOMATCH) { + return false; + } + + patternPos = result; + ++stringPos; + continue; + + case '\\': + if (!flags.contains(Flag.NOESCAPE)) { + if (patternPos >= pattern.length()) { + c = '\\'; + } else { + c = pattern.charAt(patternPos++); + } + } + break; + } + + if (stringPos >= string.length()) { + return false; + } + if (c != string.charAt(stringPos) && + !(flags.contains(Flag.CASEFOLD) && + Character.toLowerCase(c) == Character.toLowerCase(string.charAt(stringPos)))) { + return false; + } + ++stringPos; + } + /* NOTREACHED */ + } + + private static boolean hasLeadingPeriod(String string, int stringPos, EnumSet flags) { + if (stringPos > string.length() - 1) + return false; + return (stringPos == 0 + || (flags.contains(Flag.PATHNAME) && string.charAt(stringPos - 1) == '/')) + && string.charAt(stringPos) == '.' && flags.contains(Flag.PERIOD); + } + + private static int matchRange(String pattern, int patternPos, char test, EnumSet flags) { + boolean negate, ok; + char c, c2; + + if (patternPos >= pattern.length()) { + return RANGE_ERROR; + } + + /* + * A bracket expression starting with an unquoted circumflex + * character produces unspecified results (IEEE 1003.2-1992, + * 3.13.2). This implementation treats it like '!', for + * consistency with the regular expression syntax. + * J.T. Conklin (conklin@ngai.kaleida.com) + */ + c = pattern.charAt(patternPos); + negate = c == '!' || c == '^'; + if (negate) { + ++patternPos; + } + + if (flags.contains(Flag.CASEFOLD)) { + test = Character.toLowerCase(test); + } + + /* + * A right bracket shall lose its special meaning and represent + * itself in a bracket expression if it occurs first in the list. + * -- POSIX.2 2.8.3.2 + */ + ok = false; + while (true) { + if (patternPos >= pattern.length()) { + return RANGE_ERROR; + } + + c = pattern.charAt(patternPos++); + if (c == ']') { + break; + } + + if (c == '\\' && !flags.contains(Flag.NOESCAPE)) { + c = pattern.charAt(patternPos++); + } + if (c == '/' && flags.contains(Flag.PATHNAME)) { + return RANGE_NOMATCH; + } + if (flags.contains(Flag.CASEFOLD)) { + c = Character.toLowerCase(c); + } + if (pattern.charAt(patternPos) == '-' && + patternPos + 1 < pattern.length() && + (c2 = pattern.charAt(patternPos + 1)) != ']') { + patternPos += 2; + if (c2 == '\\' && !flags.contains(Flag.NOESCAPE)) { + if (patternPos >= pattern.length()) { + return RANGE_ERROR; + } + c = pattern.charAt(patternPos++); + } + if (flags.contains(Flag.CASEFOLD)) { + c2 = Character.toLowerCase(c2); + } + if (c <= test && test <= c2) { + ok = true; + } + } else if (c == test) { + ok = true; + } + } + + return ok == negate ? RANGE_NOMATCH : patternPos; + } +} \ No newline at end of file diff --git a/src/main/java/com/skcraft/launcher/builder/FnPatternList.java b/src/main/java/com/skcraft/launcher/builder/FnPatternList.java new file mode 100644 index 0000000..dd9c3b0 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/FnPatternList.java @@ -0,0 +1,43 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; + +@Data +public class FnPatternList { + + private static final EnumSet DEFAULT_FLAGS = EnumSet.of( + FnMatch.Flag.CASEFOLD, FnMatch.Flag.PERIOD); + + private List include; + private List exclude; + @Getter @Setter @JsonIgnore + private EnumSet flags = DEFAULT_FLAGS; + + public boolean matches(String path) { + return include != null && matches(path, include) && (exclude == null || !matches(path, exclude)); + } + + public boolean matches(String path, Collection patterns) { + for (String pattern : patterns) { + if (FnMatch.fnmatch(pattern, path, flags)) { + return true; + } + } + + return false; + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java b/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java index 6d77754..ffe3ecd 100644 --- a/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java +++ b/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java @@ -20,6 +20,9 @@ import lombok.extern.java.Log; import java.io.File; import java.io.IOException; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.emptyToNull; + /** * Builds packages for the launcher. */ @@ -29,6 +32,7 @@ public class PackageBuilder { private final ObjectMapper mapper; private ObjectWriter writer; private final Manifest manifest; + private final PropertiesApplicator applicator; @Getter private boolean prettyPrint = false; @@ -41,14 +45,10 @@ public class PackageBuilder { public PackageBuilder(@NonNull ObjectMapper mapper, @NonNull Manifest manifest) { this.mapper = mapper; this.manifest = manifest; + this.applicator = new PropertiesApplicator(manifest); setPrettyPrint(false); // Set writer } - /** - * Set whether pretty printing should be used. - * - * @param prettyPrint true to pretty print - */ public void setPrettyPrint(boolean prettyPrint) { if (prettyPrint) { writer = mapper.writerWithDefaultPrettyPrinter(); @@ -58,41 +58,68 @@ public class PackageBuilder { this.prettyPrint = prettyPrint; } - /** - * Add the files in the given directory. - * - * @param dir the directory - * @param destDir the directory to copy the files to - * @throws IOException thrown on I/O error - */ - private void addFiles(File dir, File destDir) throws IOException { - ClientFileCollector collector = new ClientFileCollector(this.manifest, destDir); + public void scan(File dir) throws IOException { + FileInfoScanner scanner = new FileInfoScanner(mapper); + scanner.walk(dir); + for (FeaturePattern pattern : scanner.getPatterns()) { + applicator.register(pattern); + } + } + + public void addFiles(File dir, File destDir) throws IOException { + ClientFileCollector collector = new ClientFileCollector(this.manifest, applicator, destDir); collector.walk(dir); } - /** - * Write the manifest to a file. - * - * @param path the path - * @throws IOException thrown on I/O error - */ + public void validateManifest() { + checkNotNull(emptyToNull(manifest.getName()), "Package name is not defined"); + checkNotNull(emptyToNull(manifest.getGameVersion()), "Game version is not defined"); + } + + public void readConfig(File path) throws IOException { + if (path != null) { + BuilderConfig config = read(path, BuilderConfig.class); + manifest.updateName(config.getName()); + manifest.updateTitle(config.getTitle()); + manifest.updateGameVersion(config.getGameVersion()); + config.registerProperties(applicator); + } + } + + public void readVersionManifest(File path) throws IOException { + if (path != null) { + VersionManifest versionManifest = read(path, VersionManifest.class); + manifest.setVersionManifest(versionManifest); + } + } + public void writeManifest(@NonNull File path) throws IOException { + manifest.setFeatures(applicator.getFeaturesInUse()); + validateManifest(); path.getParentFile().mkdirs(); writer.writeValue(path, manifest); } - /** - * Parse arguments for the builder. - * - * @param args arguments - * @return options - */ private static PackageOptions parseArgs(String[] args) { PackageOptions options = new PackageOptions(); new JCommander(options, args); return options; } + private V read(File path, Class clazz) throws IOException { + try { + if (path == null) { + return clazz.newInstance(); + } else { + return mapper.readValue(path, clazz); + } + } catch (InstantiationException e) { + throw new IOException("Failed to create " + clazz.getCanonicalName(), e); + } catch (IllegalAccessException e) { + throw new IOException("Failed to create " + clazz.getCanonicalName(), e); + } + } + /** * Build a package given the arguments. * @@ -100,32 +127,33 @@ public class PackageBuilder { * @throws IOException thrown on I/O error */ public static void main(String[] args) throws IOException { - // May throw error here PackageOptions options = parseArgs(args); + // Initialize SimpleLogFormatter.configureGlobalLogger(); ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); Manifest manifest = new Manifest(); - manifest.setName(options.getName()); - manifest.setTitle(options.getTitle()); - manifest.setVersion(options.getVersion()); - manifest.setGameVersion(options.getGameVersion()); - manifest.setLibrariesLocation(options.getLibrariesLocation()); - manifest.setObjectsLocation(options.getObjectsLocation()); - - File path = options.getVersionManifestPath(); - if (path != null) { - manifest.setVersionManifest(mapper.readValue(path, VersionManifest.class)); - } - PackageBuilder builder = new PackageBuilder(mapper, manifest); builder.setPrettyPrint(options.isPrettyPrinting()); - log.info("Adding files..."); + // From config + builder.readConfig(options.getConfigPath()); + builder.readVersionManifest(options.getVersionManifestPath()); + + // From options + manifest.updateName(options.getName()); + manifest.updateTitle(options.getTitle()); + manifest.updateGameVersion(options.getGameVersion()); + manifest.setVersion(options.getVersion()); + manifest.setLibrariesLocation(options.getLibrariesLocation()); + manifest.setObjectsLocation(options.getObjectsLocation()); + + builder.scan(options.getFilesDir()); builder.addFiles(options.getFilesDir(), options.getObjectsDir()); builder.writeManifest(options.getManifestPath()); + log.info("Wrote manifest to " + options.getManifestPath().getAbsolutePath()); log.info("Done."); } diff --git a/src/main/java/com/skcraft/launcher/builder/PackageOptions.java b/src/main/java/com/skcraft/launcher/builder/PackageOptions.java index d1725ef..37f227c 100644 --- a/src/main/java/com/skcraft/launcher/builder/PackageOptions.java +++ b/src/main/java/com/skcraft/launcher/builder/PackageOptions.java @@ -14,36 +14,37 @@ import java.io.File; @Data public class PackageOptions { - @Parameter(names = "--name", required = true) - private String name; - - @Parameter(names = "--title", required = true) - private String title; - - @Parameter(names = "--version", required = true) - private String version; - - @Parameter(names = "--mc-version", required = true) - private String gameVersion; - - @Parameter(names = "--manifest-path", required = true) - private File manifestPath; - - @Parameter(names = "--objects-dest", required = true) - private File objectsDir; - - @Parameter(names = "--files", required = true) - private File filesDir; - + // Configuration + @Parameter(names = "--config") + private File configPath; @Parameter(names = "--version-file") private File versionManifestPath; - @Parameter(names = "--libs-url") private String librariesLocation; - @Parameter(names = "--objects-url") private String objectsLocation; + // Override config + @Parameter(names = "--name") + private String name; + @Parameter(names = "--title") + private String title; + @Parameter(names = "--mc-version") + private String gameVersion; + + // Required + @Parameter(names = "--version", required = true) + private String version; + + // Paths + @Parameter(names = "--files", required = true) + private File filesDir; + @Parameter(names = "--manifest-dest", required = true) + private File manifestPath; + @Parameter(names = "--objects-dest", required = true) + private File objectsDir; + + // Misc @Parameter(names = "--pretty-print") private boolean prettyPrinting; diff --git a/src/main/java/com/skcraft/launcher/builder/PropertiesApplicator.java b/src/main/java/com/skcraft/launcher/builder/PropertiesApplicator.java new file mode 100644 index 0000000..586a04b --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/PropertiesApplicator.java @@ -0,0 +1,74 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.builder; + +import com.skcraft.launcher.model.modpack.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class PropertiesApplicator { + + private final Manifest manifest; + private final Set used = new HashSet(); + private final List features = new ArrayList(); + @Getter @Setter + private FnPatternList userFiles; + + public PropertiesApplicator(Manifest manifest) { + this.manifest = manifest; + } + + public void apply(ManifestEntry entry) { + if (entry instanceof FileInstall) { + apply((FileInstall) entry); + } + } + + private void apply(FileInstall entry) { + String path = entry.getTargetPath(); + entry.setWhen(fromFeature(path)); + entry.setUserFile(isUserFile(path)); + } + + public boolean isUserFile(String path) { + if (userFiles != null) { + return userFiles.matches(path); + } else { + return false; + } + } + + public Condition fromFeature(String path) { + List found = new ArrayList(); + for (FeaturePattern pattern : features) { + if (pattern.matches(path)) { + used.add(pattern.getFeature()); + found.add(pattern.getFeature()); + } + } + + if (!found.isEmpty()) { + return new RequireAny(found); + } else { + return null; + } + } + + public void register(FeaturePattern component) { + features.add(component); + } + + public List getFeaturesInUse() { + return new ArrayList(used); + } + +} diff --git a/src/main/java/com/skcraft/launcher/dialog/FeatureSelectionDialog.java b/src/main/java/com/skcraft/launcher/dialog/FeatureSelectionDialog.java new file mode 100644 index 0000000..93ef873 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/dialog/FeatureSelectionDialog.java @@ -0,0 +1,99 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.dialog; + +import com.skcraft.launcher.model.modpack.Feature; +import com.skcraft.launcher.swing.*; +import lombok.NonNull; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.util.List; + +import static com.skcraft.launcher.util.SharedLocale._; +import static javax.swing.BorderFactory.createEmptyBorder; + +public class FeatureSelectionDialog extends JDialog { + + private final List features; + private final JPanel container = new JPanel(new BorderLayout()); + private final JTextArea descText = new JTextArea(_("features.selectForInfo")); + private final JScrollPane descScroll = new JScrollPane(descText); + private final CheckboxTable componentsTable = new CheckboxTable(); + private final JScrollPane componentsScroll = new JScrollPane(componentsTable); + private final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, componentsScroll, descScroll); + private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); + private final JButton installButton = new JButton(_("features.install")); + + public FeatureSelectionDialog(Window owner, @NonNull List features) { + super(owner, ModalityType.DOCUMENT_MODAL); + + this.features = features; + + setTitle(_("features.title")); + initComponents(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setSize(new Dimension(500, 400)); + setResizable(false); + setLocationRelativeTo(owner); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + } + + private void initComponents() { + componentsTable.setModel(new FeatureTableModel(features)); + + descScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + + descText.setFont(new JLabel().getFont()); + descText.setEditable(false); + descText.setWrapStyleWord(true); + descText.setLineWrap(true); + SwingHelper.removeOpaqueness(descText); + descText.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); + + splitPane.setDividerLocation(300); + splitPane.setDividerSize(6); + SwingHelper.flattenJSplitPane(splitPane); + + container.setBorder(createEmptyBorder(12, 12, 12, 12)); + container.add(splitPane, BorderLayout.CENTER); + + buttonsPanel.addGlue(); + buttonsPanel.addElement(installButton); + + JLabel descLabel = new JLabel(_("features.intro")); + descLabel.setBorder(createEmptyBorder(12, 12, 4, 12)); + + SwingHelper.equalWidth(installButton, new JButton(_("button.cancel"))); + + add(descLabel, BorderLayout.NORTH); + add(container, BorderLayout.CENTER); + add(buttonsPanel, BorderLayout.SOUTH); + + componentsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + updateDescription(); + } + }); + + installButton.addActionListener(ActionListeners.dispose(this)); + } + + private void updateDescription() { + Feature feature = features.get(componentsTable.getSelectedRow()); + + if (feature != null) { + descText.setText(feature.getDescription()); + } else { + descText.setText(_("features.selectForInfo")); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java b/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java index cbd61d9..6ddcd4a 100644 --- a/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java +++ b/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java @@ -21,6 +21,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.lang.ref.WeakReference; import java.util.Timer; import java.util.TimerTask; @@ -29,6 +30,8 @@ import static com.skcraft.launcher.util.SharedLocale._; @Log public class ProgressDialog extends JDialog { + private static WeakReference lastDialogRef; + private final String defaultTitle; private final String defaultMessage; private final JLabel label = new JLabel(); @@ -159,6 +162,8 @@ public class ProgressDialog extends JDialog { } }; + lastDialogRef = new WeakReference(dialog); + final Timer timer = new Timer(); timer.scheduleAtFixedRate(new UpdateProgress(dialog, future), 400, 400); @@ -179,6 +184,18 @@ public class ProgressDialog extends JDialog { dialog.setVisible(true); } + public static ProgressDialog getLastDialog() { + WeakReference ref = lastDialogRef; + if (ref != null) { + ProgressDialog dialog = ref.get(); + if (!dialog.isVisible()) { + return dialog; + } + } + + return null; + } + private static class UpdateProgress extends TimerTask { private final ProgressDialog dialog; private final ProgressObservable observable; diff --git a/src/main/java/com/skcraft/launcher/install/FeatureCache.java b/src/main/java/com/skcraft/launcher/install/FeatureCache.java new file mode 100644 index 0000000..052ed5d --- /dev/null +++ b/src/main/java/com/skcraft/launcher/install/FeatureCache.java @@ -0,0 +1,19 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.install; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +@Data +public class FeatureCache { + + private Map selected = new HashMap(); + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/Condition.java b/src/main/java/com/skcraft/launcher/model/modpack/Condition.java new file mode 100644 index 0000000..8f0332c --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/Condition.java @@ -0,0 +1,21 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="if") +@JsonSubTypes({ + @JsonSubTypes.Type(value = RequireAny.class, name = "requireAny"), + @JsonSubTypes.Type(value = RequireAll.class, name = "requireAll") +}) +public interface Condition { + + boolean matches(); + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/Feature.java b/src/main/java/com/skcraft/launcher/model/modpack/Feature.java new file mode 100644 index 0000000..820e701 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/Feature.java @@ -0,0 +1,69 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.google.common.base.Strings; +import lombok.Data; + +@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="name") +@Data +public class Feature implements Comparable { + + public enum Recommendation { + STARRED, + AVOID; + + @JsonCreator + public static Recommendation fromJson(String text) { + return valueOf(text.toUpperCase()); + } + + @JsonValue + public String toJson() { + return name().toLowerCase(); + }; + }; + + private String name; + private String description; + private Recommendation recommendation; + private boolean selected; + + public Feature() { + } + + public Feature(String name, String description, boolean selected) { + this.name = name; + this.description = description; + this.selected = selected; + } + + public Feature(Feature feature) { + setName(feature.getName()); + setDescription(feature.getDescription()); + setSelected(feature.isSelected()); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object other) { + return super.equals(other); + } + + @Override + public int compareTo(Feature o) { + return Strings.nullToEmpty(getName()).compareTo(Strings.nullToEmpty(o.getName())); + } +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java b/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java index e3dbf12..61e44e2 100644 --- a/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java +++ b/src/main/java/com/skcraft/launcher/model/modpack/FileInstall.java @@ -32,6 +32,7 @@ public class FileInstall extends ManifestEntry { private String location; private String to; private long size; + private boolean userFile; @JsonIgnore public String getImpliedVersion() { @@ -46,12 +47,17 @@ public class FileInstall extends ManifestEntry { @Override public void install(@NonNull Installer installer, @NonNull InstallLog log, @NonNull UpdateCache cache, @NonNull File contentDir) throws MalformedURLException { + if (getWhen() != null && !getWhen().matches()) { + return; + } + String targetPath = getTargetPath(); File targetFile = new File(contentDir, targetPath); String fileVersion = getImpliedVersion(); URL url = concat(getManifest().getObjectsUrl(), getLocation()); - if (cache.mark(FilenameUtils.normalize(targetPath), fileVersion)) { + if (!(isUserFile() && targetFile.exists()) && + (!targetFile.exists() || cache.mark(FilenameUtils.normalize(targetPath), fileVersion))) { long size = this.size; if (size <= 0) { size = 10 * 1024; diff --git a/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java b/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java index 8ab9b9d..3595e59 100644 --- a/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java +++ b/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java @@ -30,6 +30,7 @@ public class Manifest extends BaseManifest { private String librariesLocation; private String objectsLocation; private String gameVersion; + private List features = new ArrayList(); @JsonManagedReference("manifest") private List tasks = new ArrayList(); @Getter @Setter @JsonIgnore @@ -62,4 +63,21 @@ public class Manifest extends BaseManifest { } } + public void updateName(String name) { + if (name != null) { + setName(name); + } + } + + public void updateTitle(String title) { + if (title != null) { + setTitle(title); + } + } + + public void updateGameVersion(String gameVersion) { + if (gameVersion != null) { + setGameVersion(gameVersion); + } + } } diff --git a/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java b/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java index 25d2cf1..8d06fad 100644 --- a/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java +++ b/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java @@ -7,7 +7,6 @@ package com.skcraft.launcher.model.modpack; import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.skcraft.launcher.install.InstallLog; @@ -32,6 +31,7 @@ public abstract class ManifestEntry { @JsonBackReference("manifest") private Manifest manifest; + private Condition when; public abstract void install(Installer installer, InstallLog log, UpdateCache cache, File contentDir) throws Exception; diff --git a/src/main/java/com/skcraft/launcher/model/modpack/RequireAll.java b/src/main/java/com/skcraft/launcher/model/modpack/RequireAll.java new file mode 100644 index 0000000..ce84174 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/RequireAll.java @@ -0,0 +1,46 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Data +public class RequireAll implements Condition { + + private List features = new ArrayList(); + + public RequireAll() { + } + + public RequireAll(List features) { + this.features = features; + } + + public RequireAll(Feature... feature) { + features.addAll(Arrays.asList(feature)); + } + + @Override + public boolean matches() { + if (features == null) { + return true; + } + + for (Feature feature : features) { + if (!feature.isSelected()) { + return false; + } + } + + return true; + } + +} diff --git a/src/main/java/com/skcraft/launcher/model/modpack/RequireAny.java b/src/main/java/com/skcraft/launcher/model/modpack/RequireAny.java new file mode 100644 index 0000000..299595a --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/modpack/RequireAny.java @@ -0,0 +1,46 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.modpack; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Data +public class RequireAny implements Condition { + + private List features = new ArrayList(); + + public RequireAny() { + } + + public RequireAny(List features) { + this.features = features; + } + + public RequireAny(Feature... feature) { + features.addAll(Arrays.asList(feature)); + } + + @Override + public boolean matches() { + if (features == null) { + return true; + } + + for (Feature feature : features) { + if (feature.isSelected()) { + return true; + } + } + + return false; + } + +} diff --git a/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java b/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java index 2ab18e1..797ff05 100644 --- a/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java +++ b/src/main/java/com/skcraft/launcher/swing/CheckboxTable.java @@ -14,7 +14,7 @@ public class CheckboxTable extends JTable { public CheckboxTable() { setShowGrid(false); - setRowHeight(getRowHeight() + 4); + setRowHeight((int) (Math.max(getRowHeight(), new JCheckBox().getPreferredSize().getHeight() - 2))); setIntercellSpacing(new Dimension(0, 0)); setFillsViewportHeight(true); setSelectionMode(ListSelectionModel.SINGLE_SELECTION); diff --git a/src/main/java/com/skcraft/launcher/swing/FeatureTableModel.java b/src/main/java/com/skcraft/launcher/swing/FeatureTableModel.java new file mode 100644 index 0000000..8e4b873 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/swing/FeatureTableModel.java @@ -0,0 +1,107 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.swing; + +import com.skcraft.launcher.model.modpack.Feature; + +import javax.swing.table.AbstractTableModel; +import java.util.List; + +import static com.skcraft.launcher.util.SharedLocale._; + +public class FeatureTableModel extends AbstractTableModel { + + private final List features; + + public FeatureTableModel(List features) { + this.features = features; + } + + @Override + public String getColumnName(int columnIndex) { + switch (columnIndex) { + case 1: + return _("features.nameColumn"); + default: + return null; + } + } + + @Override + public Class getColumnClass(int columnIndex) { + switch (columnIndex) { + case 0: + return Boolean.class; + case 1: + return String.class; + default: + return null; + } + } + + @Override + public void setValueAt(Object value, int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + features.get(rowIndex).setSelected((boolean) (Boolean) value); + break; + case 1: + default: + break; + } + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return true; + case 1: + return false; + default: + return false; + } + } + + @Override + public int getRowCount() { + return features.size(); + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + return features.get(rowIndex).isSelected(); + case 1: + Feature feature = features.get(rowIndex); + return "" + SwingHelper.htmlEscape(feature.getName()) + getAddendum(feature) + ""; + default: + return null; + } + } + + private String getAddendum(Feature feature) { + if (feature.getRecommendation() == null) { + return ""; + } + switch (feature.getRecommendation()) { + case STARRED: + return " " + _("features.starred") + ""; + case AVOID: + return " " + _("features.avoid") + ""; + default: + return ""; + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/swing/InstanceTable.java b/src/main/java/com/skcraft/launcher/swing/InstanceTable.java index 7b85b0f..85fdce0 100644 --- a/src/main/java/com/skcraft/launcher/swing/InstanceTable.java +++ b/src/main/java/com/skcraft/launcher/swing/InstanceTable.java @@ -14,7 +14,7 @@ public class InstanceTable extends JTable { public InstanceTable() { setShowGrid(false); - setRowHeight(Math.max(getRowHeight() + 4, 20 )); + setRowHeight(Math.max(getRowHeight() + 4, 20)); setIntercellSpacing(new Dimension(0, 0)); setFillsViewportHeight(true); setSelectionMode(ListSelectionModel.SINGLE_SELECTION); diff --git a/src/main/java/com/skcraft/launcher/update/BaseUpdater.java b/src/main/java/com/skcraft/launcher/update/BaseUpdater.java index 014160a..fc6f874 100644 --- a/src/main/java/com/skcraft/launcher/update/BaseUpdater.java +++ b/src/main/java/com/skcraft/launcher/update/BaseUpdater.java @@ -6,17 +6,18 @@ package com.skcraft.launcher.update; +import com.google.common.base.Strings; import com.skcraft.launcher.AssetsRoot; import com.skcraft.launcher.Instance; import com.skcraft.launcher.Launcher; -import com.skcraft.launcher.install.FileMover; -import com.skcraft.launcher.install.InstallLog; -import com.skcraft.launcher.install.Installer; -import com.skcraft.launcher.install.UpdateCache; +import com.skcraft.launcher.dialog.FeatureSelectionDialog; +import com.skcraft.launcher.dialog.ProgressDialog; +import com.skcraft.launcher.install.*; import com.skcraft.launcher.model.minecraft.Asset; import com.skcraft.launcher.model.minecraft.AssetsIndex; import com.skcraft.launcher.model.minecraft.Library; import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.modpack.Feature; import com.skcraft.launcher.model.modpack.Manifest; import com.skcraft.launcher.model.modpack.ManifestEntry; import com.skcraft.launcher.persistence.Persistence; @@ -25,6 +26,7 @@ import com.skcraft.launcher.util.HttpRequest; import lombok.NonNull; import lombok.extern.java.Log; +import javax.swing.*; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; @@ -59,11 +61,13 @@ public abstract class BaseUpdater { final File contentDir = instance.getContentDir(); final File logPath = new File(instance.getDir(), "install_log.json"); final File cachePath = new File(instance.getDir(), "update_cache.json"); + final File featuresPath = new File(instance.getDir(), "features.json"); final InstallLog previousLog = Persistence.read(logPath, InstallLog.class); final InstallLog currentLog = new InstallLog(); currentLog.setBaseDir(contentDir); final UpdateCache updateCache = Persistence.read(cachePath, UpdateCache.class); + final FeatureCache featuresCache = Persistence.read(featuresPath, FeatureCache.class); Manifest manifest = HttpRequest .get(instance.getManifestURL()) @@ -77,6 +81,29 @@ public abstract class BaseUpdater { manifest.setBaseUrl(instance.getManifestURL()); } + final List features = manifest.getFeatures(); + if (!features.isEmpty()) { + for (Feature feature : features) { + Boolean last = featuresCache.getSelected().get(feature.getName()); + if (last != null) { + feature.setSelected(last); + } + } + + Collections.sort(features); + + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + new FeatureSelectionDialog(ProgressDialog.getLastDialog(), features).setVisible(true); + } + }); + + for (Feature feature : features) { + featuresCache.getSelected().put(Strings.nullToEmpty(feature.getName()), feature.isSelected()); + } + } + for (ManifestEntry entry : manifest.getTasks()) { entry.install(installer, currentLog, updateCache, contentDir); } @@ -92,17 +119,9 @@ public abstract class BaseUpdater { } } - try { - Persistence.write(logPath, currentLog); - } catch (IOException e) { - log.log(Level.WARNING, "Failed to write install log", e); - } - - try { - Persistence.write(cachePath, updateCache); - } catch (IOException e) { - log.log(Level.WARNING, "Failed to write update cache", e); - } + writeDataFile(logPath, currentLog); + writeDataFile(cachePath, updateCache); + writeDataFile(featuresPath, featuresCache); } }); @@ -196,4 +215,13 @@ public abstract class BaseUpdater { } } + private static void writeDataFile(File path, Object object) { + try { + Persistence.write(path, object); + } catch (IOException e) { + log.log(Level.WARNING, "Failed to write to " + path.getAbsolutePath() + + " for object " + object.getClass().getCanonicalName(), e); + } + } + } diff --git a/src/main/java/com/skcraft/launcher/update/Updater.java b/src/main/java/com/skcraft/launcher/update/Updater.java index 913ef44..5b78211 100644 --- a/src/main/java/com/skcraft/launcher/update/Updater.java +++ b/src/main/java/com/skcraft/launcher/update/Updater.java @@ -82,6 +82,9 @@ public class Updater extends BaseUpdater implements Callable, Progress String message = _("updater.updateRequiredButNoManifest"); throw new LauncherException("Update required but no manifest", message); } else { + instance.setUpdatePending(false); + Persistence.commitAndForget(instance); + log.info("Can't update " + instance.getTitle() + ", but update is not required"); return instance; // Can't update } diff --git a/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/src/main/resources/com/skcraft/launcher/lang/Launcher.properties index 8c8fcaa..8ee0559 100644 --- a/src/main/resources/com/skcraft/launcher/lang/Launcher.properties +++ b/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -171,4 +171,12 @@ runner.corruptAssetsIndex={0} needs to be relaunched and updated because its ass assets.expanding1=Expanding {0} asset... ({1} remaining) assets.expandingN=Expanding {0} assets... ({1} remaining) assets.missingIndex=You need to update this instance because its index file at ''{0}'' is missing. -assets.missingObject=You need to update this instance because the file at ''{0}'' is missing. \ No newline at end of file +assets.missingObject=You need to update this instance because the file at ''{0}'' is missing. + +features.nameColumn=Feature +features.title=Select Features +features.install=OK +features.selectForInfo=Select a feature to see more information. +features.intro=Please select the optional features to install. +features.starred=(recommended) +features.avoid=(not recommended) \ No newline at end of file