Add the base files to the Launcher Git.

This commit is contained in:
Charles Descamps 2024-12-12 10:36:18 -07:00
parent d1b3d2168c
commit 7242015c76
376 changed files with 27499 additions and 2 deletions

View file

@ -0,0 +1,28 @@
plugins {
id 'application'
id 'java-library'
id "com.github.johnrengelman.shadow"
id 'io.freefair.lombok'
}
application {
mainClassName = "com.skcraft.launcher.builder.PackageBuilder"
}
dependencies {
api project(':launcher')
implementation 'org.apache.commons:commons-compress:1.21'
}
shadowJar {
dependsOn ':launcher:shadowJar'
archiveClassifier.set("")
}
// Work around gradle shadow bug
// see https://github.com/johnrengelman/shadow/issues/713
startScripts.dependsOn(':launcher:shadowJar')
distTar.dependsOn(':launcher:shadowJar')
distZip.dependsOn(':launcher:shadowJar')
build.dependsOn(shadowJar)

View file

@ -0,0 +1,2 @@
# This file is generated by the 'io.freefair.lombok' Gradle plugin
config.stopBubbling = true

View file

@ -0,0 +1,62 @@
/*
* 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.google.common.collect.Lists;
import com.skcraft.launcher.model.modpack.LaunchModifier;
import com.skcraft.launcher.model.modpack.Manifest;
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;
@JsonProperty("launch")
private LaunchModifier launchModifier = new LaunchModifier();
private List<FeaturePattern> features = Lists.newArrayList();
private FnPatternList userFiles = new FnPatternList();
public void setLaunchModifier(LaunchModifier launchModifier) {
this.launchModifier = launchModifier != null ? launchModifier : new LaunchModifier();
}
public void setFeatures(List<FeaturePattern> features) {
this.features = features != null ? features : Lists.<FeaturePattern>newArrayList();
}
public void setUserFiles(FnPatternList userFiles) {
this.userFiles = userFiles != null ? userFiles : new FnPatternList();
}
public void update(Manifest manifest) {
manifest.updateName(getName());
manifest.updateTitle(getTitle());
manifest.updateGameVersion(getGameVersion());
manifest.setLaunchModifier(getLaunchModifier());
}
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

@ -0,0 +1,114 @@
/*
* 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.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import lombok.Data;
import java.io.File;
@Data
public class BuilderOptions {
public static final String DEFAULT_CONFIG_FILENAME = "modpack.json";
public static final String DEFAULT_VERSION_FILENAME = "version.json";
public static final String DEFAULT_SRC_DIRNAME = "src";
public static final String DEFAULT_LOADERS_DIRNAME = "loaders";
// Configuration
// 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;
@Parameter(names = "--manifest-dest", required = true)
private File manifestPath;
// Overall paths
@Parameter(names = {"--input", "-i"})
private File inputPath;
@Parameter(names = {"--output", "-o"})
private File outputPath;
// Input paths
@Parameter(names = "--config")
private File configPath;
@Parameter(names = "--version-file")
private File versionManifestPath;
@Parameter(names = "--files")
private File filesDir;
@Parameter(names = "--loaders")
private File loadersDir;
// Output paths
@Parameter(names = "--objects-dest")
private File objectsDir;
@Parameter(names = "--libraries-dest")
private File librariesDir;
@Parameter(names = "--libs-url")
private String librariesLocation = "libraries";
@Parameter(names = "--objects-url")
private String objectsLocation = "objects";
// Misc
@Parameter(names = "--pretty-print")
private boolean prettyPrinting;
public void choosePaths() throws ParameterException {
if (configPath == null) {
requireInputPath("--config");
configPath = new File(inputPath, DEFAULT_CONFIG_FILENAME);
}
if (versionManifestPath == null) {
requireInputPath("--version");
versionManifestPath = new File(inputPath, DEFAULT_VERSION_FILENAME);
}
if (filesDir == null) {
requireInputPath("--files");
filesDir = new File(inputPath, DEFAULT_SRC_DIRNAME);
}
if (loadersDir == null) {
requireInputPath("--loaders");
loadersDir = new File(inputPath, DEFAULT_LOADERS_DIRNAME);
}
if (objectsDir == null) {
requireOutputPath("--objects-dest");
objectsDir = new File(outputPath, objectsLocation);
}
if (librariesDir == null) {
requireOutputPath("--libs-dest");
librariesDir = new File(outputPath, librariesLocation);
}
}
private void requireOutputPath(String name) throws ParameterException {
if (outputPath == null) {
throw new ParameterException("Because " + name + " was not specified, --output needs to be specified as the output directory and then " + name + " will be default to a pre-set path within the output directory");
}
}
private void requireInputPath(String name) throws ParameterException {
if (inputPath == null) {
throw new ParameterException("Because " + name + " was not specified, --input needs to be specified as the project directory and then " + name + " will be default to a pre-set path within the project directory");
}
}
}

View file

@ -0,0 +1,68 @@
/*
* 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.google.common.collect.Lists;
import com.google.common.io.CharStreams;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class BuilderUtils {
private static final DateFormat VERSION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
private BuilderUtils() {
}
public static String normalizePath(String path) {
return path.replaceAll("^[/\\\\]*", "").replaceAll("[/\\\\]+", "/");
}
public static ZipEntry getZipEntry(ZipFile jarFile, String path) {
Enumeration<? extends ZipEntry> entries = jarFile.entries();
String expected = normalizePath(path);
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String test = normalizePath(entry.getName());
if (expected.equals(test)) {
return entry;
}
}
return null;
}
public static String readStringFromStream(Readable r) throws IOException {
String data = CharStreams.toString(r);
data = data.replaceAll(",\\s*\\}", "}"); // Fix issues with trailing commas
return data;
}
public static List<Compressor> getCompressors(String repoUrl) {
if (repoUrl.matches("^https?://files.minecraftforge.net/maven/?")) {
return Lists.newArrayList(
new Compressor("xz", CompressorStreamFactory.XZ),
new Compressor("pack", CompressorStreamFactory.PACK200));
} else {
return Collections.emptyList();
}
}
public static String generateVersionFromDate() {
Date today = Calendar.getInstance().getTime();
return VERSION_DATE_FORMAT.format(today);
}
}

View file

@ -0,0 +1,105 @@
/*
* 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.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
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;
/**
* Walks a path and adds hashed path versions to the given
* {@link com.skcraft.launcher.model.modpack.Manifest}.
*/
@Log
public class ClientFileCollector extends DirectoryWalker {
private final Manifest manifest;
private final PropertiesApplicator applicator;
private final File destDir;
private HashFunction hf = Hashing.sha1();
/**
* Create a new collector.
*
* @param manifest the manifest
* @param applicator applies properties to manifest entries
* @param destDir the destination directory to copy the hashed objects
*/
public ClientFileCollector(@NonNull Manifest manifest, @NonNull PropertiesApplicator applicator,
@NonNull File destDir) {
this.manifest = manifest;
this.applicator = applicator;
this.destDir = destDir;
}
@Override
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)
|| file.getName().endsWith(FileUrlScanner.URL_FILE_SUFFIX)) {
return;
}
FileInstall entry = new FileInstall();
String hash = Files.hash(file, hf).toString();
String to = FilenameUtils.separatorsToUnix(FilenameUtils.normalize(relPath));
// url.txt override file
File urlFile = new File(file.getAbsoluteFile().getParentFile(),
file.getName() + FileUrlScanner.URL_FILE_SUFFIX);
String location;
boolean copy = true;
if (urlFile.exists() && FileUrlScanner.isEnabled()) {
FileUrlRedirect redirect = FileUrlRedirect.fromFile(urlFile);
location = redirect.getUrl().toString();
copy = false;
} else {
location = hash.substring(0, 2) + "/" + hash.substring(2, 4) + "/" + hash;
}
File destPath = new File(destDir, location);
entry.setHash(hash);
entry.setLocation(location);
entry.setTo(to);
entry.setSize(file.length());
applicator.apply(entry);
destPath.getParentFile().mkdirs();
ClientFileCollector.log.info(String.format("Adding %s from %s...", relPath, file.getAbsolutePath()));
if (copy) {
Files.copy(file, destPath);
}
manifest.getTasks().add(entry);
}
public static DirectoryBehavior getDirectoryBehavior(@NonNull String name) {
if (name.startsWith(".")) {
return DirectoryBehavior.SKIP;
} else if (name.equals("_OPTIONAL")) {
return DirectoryBehavior.IGNORE;
} else if (name.equals("_SERVER")) {
return DirectoryBehavior.SKIP;
} else if (name.equals("_CLIENT")) {
return DirectoryBehavior.IGNORE;
} else {
return DirectoryBehavior.CONTINUE;
}
}
}

View file

@ -0,0 +1,48 @@
/*
* 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 org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class Compressor {
private static final CompressorStreamFactory factory = new CompressorStreamFactory();
private final String extension;
private final String format;
public Compressor(String extension, String format) {
this.extension = extension;
this.format = format;
}
public String transformPathname(String filename) {
return filename + "." + extension;
}
public InputStream createInputStream(InputStream inputStream) throws IOException {
try {
return factory.createCompressorInputStream(format, inputStream);
} catch (CompressorException e) {
throw new IOException("Failed to create decompressor", e);
}
}
public OutputStream createOutputStream(OutputStream outputStream) throws IOException {
try {
return factory.createCompressorOutputStream(format, outputStream);
} catch (CompressorException e) {
throw new IOException("Failed to create compressor", e);
}
}
}

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.builder;
import lombok.NonNull;
import java.io.File;
import java.io.IOException;
/**
* Abstract class to recursively walk a directory, keep track of a relative
* path (which may be modified by dropping certain directory entries),
* and call {@link #onFile(java.io.File, String)} with each file.
*/
public abstract class DirectoryWalker {
public enum DirectoryBehavior {
/**
* Continue and add the given directory to the relative path.
*/
CONTINUE,
/**
* Continue but don't add the given directory to the relative path.
*/
IGNORE,
/**
* Don't walk this directory.
*/
SKIP
}
/**
* Walk the given directory.
*
* @param dir the directory
* @throws IOException thrown on I/O error
*/
public final void walk(@NonNull File dir) throws IOException {
walk(dir, "");
}
/**
* Recursively walk the given directory and keep track of the relative path.
*
* @param dir the directory
* @param basePath the base path
* @throws IOException
*/
private void walk(@NonNull File dir, @NonNull String basePath) throws IOException {
if (!dir.isDirectory()) {
throw new IllegalArgumentException(dir.getAbsolutePath() + " is not a directory");
}
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
String newPath = basePath;
switch (getBehavior(file.getName())) {
case CONTINUE:
newPath += file.getName() + "/";
case IGNORE:
walk(file, newPath);
break;
case SKIP: break;
}
} else {
onFile(file, basePath + file.getName());
}
}
}
}
/**
* Return the behavior for the given directory name.
*
* @param name the directory name
* @return the behavor
*/
protected DirectoryBehavior getBehavior(String name) {
return DirectoryBehavior.CONTINUE;
}
/**
* Callback on each file.
*
* @param file the file
* @param relPath the relative path
* @throws IOException thrown on I/O error
*/
protected abstract void onFile(File file, String relPath) throws IOException;
}

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 = new FnPatternList();
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,43 @@
package com.skcraft.launcher.builder;
import com.google.common.io.Files;
import lombok.Data;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.List;
import static com.skcraft.launcher.util.HttpRequest.url;
@Data
public class FileUrlRedirect {
private URL url;
private String hash;
public void readFromFile(File file) throws IOException {
List<String> lines = Files.readLines(file, Charset.defaultCharset());
this.url = url(lines.get(0));
if (lines.size() > 1) {
String hash = lines.get(1);
if (!hash.isEmpty()) {
this.hash = hash;
}
}
}
public void writeToFile(File file) throws IOException {
String entry = url.toString() + '\n' + hash;
Files.write(entry, file, Charset.defaultCharset());
}
public static FileUrlRedirect fromFile(File file) throws IOException {
FileUrlRedirect entry = new FileUrlRedirect();
entry.readFromFile(file);
return entry;
}
}

View file

@ -0,0 +1,58 @@
package com.skcraft.launcher.builder;
import com.google.common.io.Files;
import com.skcraft.launcher.util.FileUtils;
import com.skcraft.launcher.util.HttpRequest;
import lombok.extern.java.Log;
import java.io.File;
import java.io.IOException;
@Log
public class FileUrlScanner extends DirectoryWalker {
public static final String URL_FILE_SUFFIX = ".url.txt";
public static boolean isEnabled() {
return !System.getProperty("com.skcraft.builder.ignoreURLOverrides", "false")
.equalsIgnoreCase("true");
}
@Override
protected void onFile(File file, String relPath) throws IOException {
if (!file.getName().endsWith(URL_FILE_SUFFIX)) return;
log.info("Found URL file " + file.getName());
File targetFile = new File(file.getAbsoluteFile().getParentFile(),
file.getName().replace(URL_FILE_SUFFIX, ""));
FileUrlRedirect info = FileUrlRedirect.fromFile(file);
if (targetFile.exists()) {
String localHash = FileUtils.getShaHash(targetFile);
if (info.getHash() == null) {
// Disabled for now, let's not touch source files
// info.setHash(localHash);
// info.writeToFile(file);
return;
}
// If everything matches, skip this file
if (info.getHash().equals(localHash)) return;
}
File tempFile = File.createTempFile("launcherlib", null);
try {
log.info("Downloading file " + targetFile.getName() + " from " + info.getUrl() + "...");
HttpRequest.get(info.getUrl())
.execute()
.expectResponseCode(200)
.saveContent(tempFile);
} catch (InterruptedException e) {
throw new IOException(e);
}
Files.move(tempFile, targetFile);
log.info("Updated " + targetFile.getName() + " from " + file.getName());
}
}

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,52 @@
/*
* 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 com.google.common.collect.Lists;
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 = Lists.newArrayList();
private List<String> exclude = Lists.newArrayList();
@Getter @Setter @JsonIgnore
private EnumSet<FnMatch.Flag> flags = DEFAULT_FLAGS;
public void setInclude(List<String> include) {
this.include = include != null ? include : Lists.<String>newArrayList();
}
public void setExclude(List<String> exclude) {
this.exclude = exclude != null ? exclude : Lists.<String>newArrayList();
}
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

@ -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.builder;
import java.io.File;
import java.io.FileFilter;
public class JarFileFilter implements FileFilter {
@Override
public boolean accept(File pathname) {
return pathname.getName().toLowerCase().endsWith(".jar");
}
}

View file

@ -0,0 +1,451 @@
/*
* 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.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.common.io.Files;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.LauncherUtils;
import com.skcraft.launcher.builder.loaders.*;
import com.skcraft.launcher.model.loader.BasicInstallProfile;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.ReleaseList;
import com.skcraft.launcher.model.minecraft.Version;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.model.modpack.Manifest;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SimpleLogFormatter;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.java.Log;
import java.io.*;
import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.emptyToNull;
import static com.skcraft.launcher.util.HttpRequest.url;
/**
* Builds packages for the launcher.
*/
@Log
public class PackageBuilder {
private final Properties properties;
private final ObjectMapper mapper;
private ObjectWriter writer;
private final Manifest manifest;
private final PropertiesApplicator applicator;
@Getter
private boolean prettyPrint = false;
@Getter @Setter
private File baseDir;
private List<Library> loaderLibraries = Lists.newArrayList();
private List<Library> installerLibraries = Lists.newArrayList();
private List<String> mavenRepos;
private List<URL> jarMavens = Lists.newArrayList();
/**
* Create a new package builder.
*
* @param mapper the mapper
* @param manifest the manifest
*/
public PackageBuilder(@NonNull ObjectMapper mapper, @NonNull Manifest manifest) throws IOException {
this.properties = LauncherUtils.loadProperties(Launcher.class,
"launcher.properties", "com.skcraft.launcher.propertiesFile");
this.mapper = mapper;
this.manifest = manifest;
this.applicator = new PropertiesApplicator(manifest);
setPrettyPrint(false); // Set writer
Closer closer = Closer.create();
try {
mavenRepos = mapper.readValue(closer.register(Launcher.class.getResourceAsStream("maven_repos.json")), new TypeReference<List<String>>() {
});
} finally {
closer.close();
}
}
public void setPrettyPrint(boolean prettyPrint) {
if (prettyPrint) {
writer = mapper.writerWithDefaultPrettyPrinter();
} else {
writer = mapper.writer();
}
this.prettyPrint = prettyPrint;
}
public void scan(File dir) throws IOException {
logSection("Scanning for .info.json files...");
FileInfoScanner infoScanner = new FileInfoScanner(mapper);
infoScanner.walk(dir);
for (FeaturePattern pattern : infoScanner.getPatterns()) {
applicator.register(pattern);
}
logSection("Scanning for .url.txt files...");
FileUrlScanner urlScanner = new FileUrlScanner();
urlScanner.walk(dir);
}
public void addFiles(File dir, File destDir) throws IOException {
logSection("Adding files to modpack...");
ClientFileCollector collector = new ClientFileCollector(this.manifest, applicator, destDir);
collector.walk(dir);
}
public void addLoaders(File dir, File librariesDir) {
logSection("Checking for mod loaders to install...");
LinkedHashSet<Library> collected = new LinkedHashSet<Library>();
File[] files = dir.listFiles(new JarFileFilter());
if (files != null) {
for (File file : files) {
try {
processLoader(collected, file, librariesDir);
} catch (IOException e) {
log.log(Level.WARNING, "Failed to add the loader at " + file.getAbsolutePath(), e);
}
}
}
this.loaderLibraries.addAll(collected);
VersionManifest version = manifest.getVersionManifest();
collected.addAll(version.getLibraries());
version.setLibraries(collected);
}
private void processLoader(LinkedHashSet<Library> loaderLibraries, File file, File librariesDir) throws IOException {
log.info("Installing " + file.getName() + "...");
JarFile jarFile = new JarFile(file);
Closer closer = Closer.create();
ILoaderProcessor processor = null;
try {
ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json");
if (profileEntry != null) {
InputStream stream = jarFile.getInputStream(profileEntry);
InputStreamReader reader = closer.register(new InputStreamReader(stream));
BasicInstallProfile basicProfile = mapper.readValue(BuilderUtils.readStringFromStream(reader),
BasicInstallProfile.class);
if (basicProfile.isLegacy()) {
processor = new OldForgeLoaderProcessor();
} else {
processor = new ModernForgeLoaderProcessor();
}
} else if (BuilderUtils.getZipEntry(jarFile, "fabric-installer.json") != null) {
processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.FABRIC);
} else if (BuilderUtils.getZipEntry(jarFile, "quilt_installer.json") != null) {
processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.QUILT);
}
} finally {
closer.close();
jarFile.close();
}
if (processor != null) {
LoaderResult result = processor.process(file, manifest, mapper, baseDir);
if (result == null) {
log.warning("Loader " + file.getName() + " failed to process.");
return;
}
loaderLibraries.addAll(result.getLoaderLibraries());
installerLibraries.addAll(result.getProcessorLibraries());
jarMavens.addAll(result.getJarMavens());
} else {
log.warning("Loader " + file.getName() + " was skipped due to missing metadata. " +
"Is it really a loader JAR?");
}
}
public void downloadLibraries(File librariesDir) throws IOException, InterruptedException {
logSection("Downloading libraries...");
for (Library library : Iterables.concat(loaderLibraries, installerLibraries)) {
library.ensureDownloadsExist();
for (Library.Artifact artifact : library.getDownloads().getAllArtifacts()) {
File outputPath = new File(librariesDir, artifact.getPath());
if (!outputPath.exists()) {
Files.createParentDirs(outputPath);
boolean found = false;
boolean urlEmpty = artifact.getUrl().isEmpty();
// If URL doesn't end with a /, it might be the direct file
if (!urlEmpty && !artifact.getUrl().endsWith("/")) {
found = tryDownloadLibrary(library, artifact, artifact.getUrl(), outputPath);
}
// Look inside the loader JARs
if (!found) {
for (URL base : jarMavens) {
found = tryFetchLibrary(library, new URL(base, artifact.getPath()), outputPath);
if (found) break;
}
}
// Assume artifact URL is a maven repository URL and try that
if (!found && !urlEmpty) {
URL url = LauncherUtils.concat(url(artifact.getUrl()), artifact.getPath());
found = tryDownloadLibrary(library, artifact, url.toString(), outputPath);
}
// Try each repository if not found yet
if (!found) {
for (String baseUrl : mavenRepos) {
found = tryDownloadLibrary(library, artifact, baseUrl + artifact.getPath(),
outputPath);
if (found) break;
}
}
if (!found) {
log.warning("!! Failed to download the library " + library.getName() +
" -- this means your copy of the libraries will lack this file");
}
}
}
}
}
private boolean tryDownloadLibrary(Library library, Library.Artifact artifact, String baseUrl, File outputPath)
throws IOException, InterruptedException {
URL url = new URL(baseUrl);
if (url.getPath().isEmpty() || url.getPath().equals("/")) {
// empty path, this is probably the first "is this a full URL" try.
return false;
}
// Some repositories compress their files
List<Compressor> compressors = BuilderUtils.getCompressors(baseUrl);
for (Compressor compressor : Lists.reverse(compressors)) {
url = new URL(compressor.transformPathname(url.toString()));
}
File tempFile = File.createTempFile("launcherlib", null);
try {
log.info("Downloading library " + library.getName() + " from " + url + "...");
HttpRequest.get(url).execute().expectResponseCode(200)
.expectContentType("application/java-archive", "application/octet-stream", "application/zip")
.saveContent(tempFile);
} catch (IOException e) {
log.info("Could not get file from " + url + ": " + e.getMessage());
return false;
}
writeLibraryToFile(outputPath, tempFile, compressors);
return true;
}
private boolean tryFetchLibrary(Library library, URL url, File outputPath)
throws IOException {
File tempFile = File.createTempFile("launcherlib", null);
Closer closer = Closer.create();
try {
log.info("Reading library " + library.getName() + " from " + url.toString());
InputStream stream = closer.register(url.openStream());
stream = closer.register(new BufferedInputStream(stream));
ByteStreams.copy(stream, closer.register(new FileOutputStream(tempFile)));
} catch (IOException e) {
log.info("Could not get file from " + url + ": " + e.getMessage());
return false;
} finally {
closer.close();
}
writeLibraryToFile(outputPath, tempFile, Collections.<Compressor>emptyList());
return true;
}
private void writeLibraryToFile(File outputPath, File inputFile, List<Compressor> compressors) throws IOException {
// Decompress (if needed) and write to file
Closer closer = Closer.create();
InputStream inputStream = closer.register(new FileInputStream(inputFile));
inputStream = closer.register(new BufferedInputStream(inputStream));
for (Compressor compressor : compressors) {
inputStream = closer.register(compressor.createInputStream(inputStream));
}
ByteStreams.copy(inputStream, closer.register(new FileOutputStream(outputPath)));
inputFile.delete();
closer.close();
}
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);
config.update(manifest);
config.registerProperties(applicator);
}
}
public void readVersionManifest(File path) throws IOException, InterruptedException {
logSection("Reading version manifest...");
if (path.exists()) {
VersionManifest versionManifest = read(path, VersionManifest.class);
manifest.setVersionManifest(versionManifest);
log.info("Loaded version manifest from " + path.getAbsolutePath());
} else {
URL url = url(properties.getProperty("versionManifestUrl"));
log.info("Fetching version manifest from " + url + "...");
ReleaseList releases = HttpRequest.get(url)
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(ReleaseList.class);
Version version = releases.find(manifest.getGameVersion());
VersionManifest versionManifest = HttpRequest.get(url(version.getUrl()))
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(VersionManifest.class);
manifest.setVersionManifest(versionManifest);
}
}
public void writeManifest(@NonNull File path) throws IOException {
logSection("Writing manifest...");
manifest.setFeatures(applicator.getFeaturesInUse());
VersionManifest versionManifest = manifest.getVersionManifest();
if (versionManifest != null) {
versionManifest.setId(manifest.getGameVersion());
}
validateManifest();
path.getAbsoluteFile().getParentFile().mkdirs();
writer.writeValue(path, manifest);
log.info("Wrote manifest to " + path.getAbsolutePath());
}
private static BuilderOptions parseArgs(String[] args) {
BuilderOptions options = new BuilderOptions();
new JCommander(options).parse(args);
options.choosePaths();
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.
*
* @param args arguments
* @throws IOException thrown on I/O error
* @throws InterruptedException on interruption
*/
public static void main(String[] args) throws IOException, InterruptedException {
BuilderOptions options;
try {
options = parseArgs(args);
} catch (ParameterException e) {
new JCommander().usage();
System.err.println("error: " + e.getMessage());
System.exit(1);
return;
}
// Initialize
SimpleLogFormatter.configureGlobalLogger();
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
Manifest manifest = new Manifest();
manifest.setMinimumVersion(Manifest.MIN_PROTOCOL_VERSION);
PackageBuilder builder = new PackageBuilder(mapper, manifest);
builder.setPrettyPrint(options.isPrettyPrinting());
// From config
builder.readConfig(options.getConfigPath());
builder.readVersionManifest(options.getVersionManifestPath());
builder.setBaseDir(options.getOutputPath());
// 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.addLoaders(options.getLoadersDir(), options.getLibrariesDir());
builder.downloadLibraries(options.getLibrariesDir());
builder.writeManifest(options.getManifestPath());
logSection("Done");
log.info("Now upload the contents of " + options.getOutputPath() + " to your web server or CDN!");
}
private static void logSection(String name) {
log.info("");
log.info("--- " + name + " ---");
}
}

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,61 @@
/*
* 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.beust.jcommander.JCommander;
import com.google.common.io.Files;
import com.skcraft.launcher.util.SimpleLogFormatter;
import lombok.NonNull;
import lombok.extern.java.Log;
import java.io.File;
import java.io.IOException;
@Log
public class ServerCopyExport extends DirectoryWalker {
private final File destDir;
public ServerCopyExport(@NonNull File destDir) {
this.destDir = destDir;
}
@Override
protected DirectoryBehavior getBehavior(String name) {
if (name.startsWith(".")) {
return DirectoryBehavior.SKIP;
} else if (name.equals("_SERVER")) {
return DirectoryBehavior.IGNORE;
} else if (name.equals("_CLIENT")) {
return DirectoryBehavior.SKIP;
} else {
return DirectoryBehavior.CONTINUE;
}
}
@Override
protected void onFile(File file, String relPath) throws IOException {
File dest = new File(destDir, relPath);
log.info("Copying " + file.getAbsolutePath() + " to " + dest.getAbsolutePath());
dest.getParentFile().mkdirs();
Files.copy(file, dest);
}
public static void main(String[] args) throws IOException {
SimpleLogFormatter.configureGlobalLogger();
ServerExportOptions options = new ServerExportOptions();
new JCommander(options, args);
log.info("From: " + options.getSourceDir().getAbsolutePath());
log.info("To: " + options.getDestDir().getAbsolutePath());
ServerCopyExport task = new ServerCopyExport(options.getDestDir());
task.walk(options.getSourceDir());
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.beust.jcommander.Parameter;
import lombok.Data;
import java.io.File;
@Data
public class ServerExportOptions {
@Parameter(names = "--source", required = true)
private File sourceDir;
@Parameter(names = "--dest", required = true)
private File destDir;
}

View file

@ -0,0 +1,103 @@
package com.skcraft.launcher.builder.loaders;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.Closer;
import com.skcraft.launcher.builder.BuilderUtils;
import com.skcraft.launcher.model.loader.FabricMod;
import com.skcraft.launcher.model.loader.QuiltMod;
import com.skcraft.launcher.model.loader.Versionable;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.model.modpack.Manifest;
import com.skcraft.launcher.util.HttpRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.java.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
@Log
@RequiredArgsConstructor
public class FabricLoaderProcessor implements ILoaderProcessor {
private final Variant variant;
@Override
public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException {
JarFile jarFile = new JarFile(loaderJar);
LoaderResult result = new LoaderResult();
Closer closer = Closer.create();
try {
Versionable loaderMod;
ZipEntry modEntry = BuilderUtils.getZipEntry(jarFile, variant.modJsonName);
if (modEntry != null) {
InputStreamReader reader = new InputStreamReader(jarFile.getInputStream(modEntry));
loaderMod = mapper.readValue(
BuilderUtils.readStringFromStream(closer.register(reader)), variant.mappedClass);
} else {
log.warning(String.format("%s loader has no '%s' file, is it really a %s Loader jar?",
variant.friendlyName, variant.modJsonName, variant.friendlyName));
return null;
}
log.info(String.format("Downloading %s metadata...", variant.friendlyName));
URL metaUrl = HttpRequest.url(
String.format(variant.metaUrl, manifest.getGameVersion(), loaderMod.getVersion()));
VersionManifest fabricManifest = HttpRequest.get(metaUrl)
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(VersionManifest.class);
for (Library library : fabricManifest.getLibraries()) {
// To quote a famous comment: "And here we come upon a sad state of affairs."
// Quilt's meta API returns broken data about how to launch the game. It specifies its own incomplete
// and ultimately broken set of intermediary mappings called "hashed". If the loader finds "hashed" in
// the classpath it tries to use it and blows up because it doesn't work.
// We work around this here by just throwing the hashed library out - they do now at least specify
// fabric's intermediary mappings in the library list, which DO work.
// Historical note: previously they didn't do this! Every launcher that added Quilt support had to add
// a hack that replaced hashed with intermediary! This is a lot of technical debt that is gonna come
// back to bite them in the ass later, because it's all still there!
// TODO pester Quilt again about fixing this....
if (library.getName().startsWith("org.quiltmc:hashed") && loaderMod instanceof QuiltMod) {
continue;
}
result.getLoaderLibraries().add(library);
log.info("Adding loader library " + library.getName());
}
String mainClass = fabricManifest.getMainClass();
if (mainClass != null) {
manifest.getVersionManifest().setMainClass(mainClass);
log.info("Using main class " + mainClass);
}
} catch (InterruptedException e) {
log.warning(String.format("HTTP request to %s metadata API was interrupted!", variant.friendlyName));
throw new IOException(e);
} finally {
closer.close();
jarFile.close();
}
return result;
}
@RequiredArgsConstructor
public enum Variant {
FABRIC("Fabric", "fabric.mod.json", "https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json", FabricMod.class),
QUILT("Quilt", "quilt.mod.json", "https://meta.quiltmc.org/v3/versions/loader/%s/%s/profile/json", QuiltMod.class);
private final String friendlyName;
private final String modJsonName;
private final String metaUrl;
private final Class<? extends Versionable> mappedClass;
}
}

View file

@ -0,0 +1,11 @@
package com.skcraft.launcher.builder.loaders;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.skcraft.launcher.model.modpack.Manifest;
import java.io.File;
import java.io.IOException;
public interface ILoaderProcessor {
LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException;
}

View file

@ -0,0 +1,15 @@
package com.skcraft.launcher.builder.loaders;
import com.google.common.collect.Lists;
import com.skcraft.launcher.model.minecraft.Library;
import lombok.Data;
import java.net.URL;
import java.util.List;
@Data
public class LoaderResult {
private final List<Library> loaderLibraries = Lists.newArrayList();
private final List<Library> processorLibraries = Lists.newArrayList();
private final List<URL> jarMavens = Lists.newArrayList();
}

View file

@ -0,0 +1,191 @@
package com.skcraft.launcher.builder.loaders;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;
import com.google.common.io.Closer;
import com.skcraft.launcher.builder.BuilderUtils;
import com.skcraft.launcher.model.loader.LoaderManifest;
import com.skcraft.launcher.model.loader.SidedData;
import com.skcraft.launcher.model.loader.VersionInfo;
import com.skcraft.launcher.model.loader.profiles.ModernForgeInstallProfile;
import com.skcraft.launcher.model.minecraft.GameArgument;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.Side;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.model.modpack.DownloadableFile;
import com.skcraft.launcher.model.modpack.Manifest;
import com.skcraft.launcher.util.FileUtils;
import lombok.extern.java.Log;
import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
@Log
public class ModernForgeLoaderProcessor implements ILoaderProcessor {
@Override
public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException {
JarFile jarFile = new JarFile(loaderJar);
Closer closer = Closer.create();
LoaderResult result = new LoaderResult();
try {
ZipEntry versionEntry = BuilderUtils.getZipEntry(jarFile, "version.json");
String loaderName = jarFile.getName();
if (versionEntry != null) {
InputStream stream = jarFile.getInputStream(versionEntry);
VersionInfo info = mapper.readValue(
BuilderUtils.readStringFromStream(closer.register(new InputStreamReader(stream))),
VersionInfo.class);
VersionManifest version = manifest.getVersionManifest();
if (info.getId() != null) {
loaderName = info.getId();
}
// Copy game arguments
List<GameArgument> gameArguments = info.getArguments().getGameArguments();
if (gameArguments != null) {
if (info.isOverridingArguments()) {
version.getArguments().getGameArguments().clear();
}
version.getArguments().getGameArguments().addAll(gameArguments);
}
// Copy JVM arguments
List<GameArgument> jvmArguments = info.getArguments().getJvmArguments();
if (jvmArguments != null) {
version.getArguments().getJvmArguments().addAll(jvmArguments);
}
// Add libraries
List<Library> libraries = info.getLibraries();
if (libraries != null) {
for (Library library : libraries) {
result.getLoaderLibraries().add(library);
log.info("Adding loader library " + library.getName());
}
}
// Copy logging config
SidedData<VersionManifest.LoggingConfig> loggingConfig = info.getLogging();
if (loggingConfig != null) {
version.setLogging(loggingConfig);
}
// Copy main class
String mainClass = info.getMainClass();
if (mainClass != null) {
version.setMainClass(mainClass);
log.info("Using " + mainClass + " as the main class");
}
} else {
log.warning("The loader " + loaderJar.getAbsolutePath() + " does not appear to have a " +
"version.json file inside -- is it actually an installer for Forge?");
}
ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json");
if (profileEntry != null) {
InputStream stream = jarFile.getInputStream(profileEntry);
String data = CharStreams.toString(closer.register(new InputStreamReader(stream)));
data = data.replace(",\\s*\\}", "}");
ModernForgeInstallProfile profile = mapper.readValue(data, ModernForgeInstallProfile.class);
if (!profile.getMinecraft().equals(manifest.getGameVersion())) {
// TODO: Ideally this would show up as a Problem when running the check command.
// Getting the data from here to there is quite difficult, however.
log.warning(String.format("The Forge installer inside loaders/ is for Minecraft version %s; your " +
"manifest is set to %s.", profile.getMinecraft(), manifest.getGameVersion()));
}
// Import the libraries for the installer
result.getProcessorLibraries().addAll(profile.getLibraries());
// Extract the data files
List<DownloadableFile> extraFiles = Lists.newArrayList();
File objectsDir = new File(baseDir, manifest.getObjectsLocation());
ZipEntry clientBinpatch = BuilderUtils.getZipEntry(jarFile, "data/client.lzma");
if (clientBinpatch != null) {
DownloadableFile entry = FileUtils.saveStreamToObjectsDir(
closer.register(jarFile.getInputStream(clientBinpatch)),
objectsDir);
entry.setName("client.lzma");
entry.setSide(Side.CLIENT);
extraFiles.add(entry);
profile.getData().get("BINPATCH").setClient("&" + entry.getName() + "&");
}
ZipEntry serverBinpatch = BuilderUtils.getZipEntry(jarFile, "data/server.lzma");
if (serverBinpatch != null) {
DownloadableFile entry = FileUtils.saveStreamToObjectsDir(
closer.register(jarFile.getInputStream(serverBinpatch)),
objectsDir);
entry.setName("server.lzma");
entry.setSide(Side.SERVER);
extraFiles.add(entry);
profile.getData().get("BINPATCH").setServer("&" + entry.getName() + "&");
}
// Forge install profile spec version 1 and above.
if (profile.getSpec() >= 1) {
// Add the installer itself to the extra files.
// This is for a server-only task like above, but hey.
DownloadableFile entry = FileUtils.saveStreamToObjectsDir(
closer.register(new FileInputStream(loaderJar)), objectsDir);
entry.setName(loaderJar.getName());
entry.setSide(Side.SERVER);
extraFiles.add(entry);
profile.getData().put("INSTALLER", SidedData.of("&" + entry.getName() + "&"));
}
// Add extra sided data
profile.getData().put("SIDE", SidedData.create("client", "server"));
// Add loader manifest to the map
manifest.getLoaders().put(loaderName, new LoaderManifest(profile.getLibraries(), profile.getData(), extraFiles));
// Find name of final patched library and mark it as excluded from download
// TODO: we should generalize this to all process outputs, really
SidedData<String> finalJars = profile.getData().get("PATCHED");
if (finalJars != null) {
String libraryName = finalJars.getClient();
libraryName = libraryName.substring(1, libraryName.length() - 1);
for (Library lib : result.getLoaderLibraries()) {
if (lib.matches(libraryName)) {
lib.setGenerated(true);
log.info(String.format("Setting generated flag on library '%s'", lib.getName()));
break;
}
}
}
// Add processors
manifest.getTasks().addAll(profile.toProcessorEntries(loaderName));
}
ZipEntry mavenEntry = BuilderUtils.getZipEntry(jarFile, "maven/");
if (mavenEntry != null) {
URL jarUrl = new URL("jar:file:" + loaderJar.getAbsolutePath() + "!/");
result.getJarMavens().add(new URL(jarUrl, "/maven/"));
}
} finally {
closer.close();
jarFile.close();
}
return result;
}
}

View file

@ -0,0 +1,112 @@
package com.skcraft.launcher.builder.loaders;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import com.google.common.io.Files;
import com.skcraft.launcher.builder.BuilderUtils;
import com.skcraft.launcher.model.loader.profiles.LegacyInstallProfile;
import com.skcraft.launcher.model.minecraft.GameArgument;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.MinecraftArguments;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.model.modpack.Manifest;
import lombok.extern.java.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
@Log
public class OldForgeLoaderProcessor implements ILoaderProcessor {
@Override
public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException {
JarFile jarFile = new JarFile(loaderJar);
LoaderResult result = new LoaderResult();
Closer closer = Closer.create();
try {
ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json");
if (profileEntry != null) {
InputStream stream = jarFile.getInputStream(profileEntry);
// Read file
String data = BuilderUtils.readStringFromStream(closer.register(new InputStreamReader(stream)));
LegacyInstallProfile profile = mapper.readValue(data, LegacyInstallProfile.class);
VersionManifest version = manifest.getVersionManifest();
// Copy tweak class arguments
MinecraftArguments args = profile.getVersionInfo().getArguments();
if (args != null) {
Iterator<GameArgument> iter = args.getGameArguments().iterator();
while (iter.hasNext()) {
GameArgument cur = iter.next();
if (cur.getValues().contains("--tweakClass")) {
String tweakClass = cur.getValues().size() > 1
? cur.getValues().get(1)
: iter.next().getJoinedValue();
List<GameArgument> gameArgs = manifest.getVersionManifest().getArguments().getGameArguments();
gameArgs.add(new GameArgument("--tweakClass"));
gameArgs.add(new GameArgument(tweakClass));
log.info(String.format("Adding tweak class '%s' to arguments", tweakClass));
}
}
}
// Add libraries
List<Library> libraries = profile.getVersionInfo().getLibraries();
if (libraries != null) {
for (Library library : libraries) {
if (!version.getLibraries().contains(library)) {
result.getLoaderLibraries().add(library);
}
}
}
// Copy main class
String mainClass = profile.getVersionInfo().getMainClass();
if (mainClass != null) {
version.setMainClass(mainClass);
log.info("Using " + mainClass + " as the main class");
}
// Extract the library
String filePath = profile.getInstallData().getFilePath();
String libraryPath = profile.getInstallData().getPath();
if (filePath != null && libraryPath != null) {
ZipEntry libraryEntry = BuilderUtils.getZipEntry(jarFile, filePath);
if (libraryEntry != null) {
File librariesDir = new File(baseDir, "libraries");
File extractPath = new File(librariesDir, Library.mavenNameToPath(libraryPath));
Files.createParentDirs(extractPath);
ByteStreams.copy(closer.register(jarFile.getInputStream(libraryEntry)),
Files.newOutputStreamSupplier(extractPath));
} else {
log.warning("Could not find the file '" + filePath + "' in "
+ loaderJar.getAbsolutePath()
+ ", which means that this mod loader will not work correctly");
}
}
} else {
log.warning("The file at " + loaderJar.getAbsolutePath() + " did not appear to have an " +
"install_profile.json file inside -- is it actually an installer for a mod loader?");
}
} finally {
closer.close();
jarFile.close();
}
return result;
}
}