1
0
mirror of https://github.com/SKCraft/Launcher.git synced 2024-11-24 12:16:28 +01:00

Added support for user files and features.

This commit is contained in:
sk89q 2014-01-09 17:18:05 -08:00
parent eb34fa7560
commit fc964d0d66
27 changed files with 1150 additions and 104 deletions

View File

@ -0,0 +1,37 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<FeaturePattern> 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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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);
}
}

View File

@ -0,0 +1,17 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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;
}

View File

@ -0,0 +1,75 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<FnMatch.Flag> 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<FeaturePattern> patterns = new ArrayList<FeaturePattern>();
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<String> patterns = new ArrayList<String>();
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);
}
}
}
}

View File

@ -0,0 +1,246 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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 /<tail> 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<Flag> 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<Flag> 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<Flag> 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<Flag> 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;
}
}

View File

@ -0,0 +1,43 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<FnMatch.Flag> DEFAULT_FLAGS = EnumSet.of(
FnMatch.Flag.CASEFOLD, FnMatch.Flag.PERIOD);
private List<String> include;
private List<String> exclude;
@Getter @Setter @JsonIgnore
private EnumSet<FnMatch.Flag> 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<String> patterns) {
for (String pattern : patterns) {
if (FnMatch.fnmatch(pattern, path, flags)) {
return true;
}
}
return false;
}
}

View File

@ -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> V read(File path, Class<V> 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.");
}

View File

@ -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;

View File

@ -0,0 +1,74 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Feature> used = new HashSet<Feature>();
private final List<FeaturePattern> features = new ArrayList<FeaturePattern>();
@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<Feature> found = new ArrayList<Feature>();
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<Feature> getFeaturesInUse() {
return new ArrayList<Feature>(used);
}
}

View File

@ -0,0 +1,99 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Feature> 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<Feature> 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"));
}
}
}

View File

@ -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<ProgressDialog> 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<ProgressDialog>(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<ProgressDialog> 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;

View File

@ -0,0 +1,19 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<String, Boolean> selected = new HashMap<String, Boolean>();
}

View File

@ -0,0 +1,21 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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();
}

View File

@ -0,0 +1,69 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Feature> {
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()));
}
}

View File

@ -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;

View File

@ -30,6 +30,7 @@ public class Manifest extends BaseManifest {
private String librariesLocation;
private String objectsLocation;
private String gameVersion;
private List<Feature> features = new ArrayList<Feature>();
@JsonManagedReference("manifest")
private List<ManifestEntry> tasks = new ArrayList<ManifestEntry>();
@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);
}
}
}

View File

@ -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;

View File

@ -0,0 +1,46 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Feature> features = new ArrayList<Feature>();
public RequireAll() {
}
public RequireAll(List<Feature> 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;
}
}

View File

@ -0,0 +1,46 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Feature> features = new ArrayList<Feature>();
public RequireAny() {
}
public RequireAny(List<Feature> 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;
}
}

View File

@ -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);

View File

@ -0,0 +1,107 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> 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<Feature> features;
public FeatureTableModel(List<Feature> 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 "<html>" + SwingHelper.htmlEscape(feature.getName()) + getAddendum(feature) + "</html>";
default:
return null;
}
}
private String getAddendum(Feature feature) {
if (feature.getRecommendation() == null) {
return "";
}
switch (feature.getRecommendation()) {
case STARRED:
return " <span style=\"color: #3758DB\">" + _("features.starred") + "</span>";
case AVOID:
return " <span style=\"color: red\">" + _("features.avoid") + "</span>";
default:
return "";
}
}
}

View File

@ -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);

View File

@ -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<Feature> 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);
}
}
}

View File

@ -82,6 +82,9 @@ public class Updater extends BaseUpdater implements Callable<Instance>, 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
}

View File

@ -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.
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)