1
0
mirror of https://github.com/SKCraft/Launcher.git synced 2024-11-30 13:13:58 +01:00

Initial 1.16 update

oh lord okay
- Update to new launchermeta endpoint for version manifests
- Fix library handling (libraries now seem to give us the URL most of the time)
- Write entire new system for handling the forge installer process
- Fix asset handling, mostly (still TODO is remove all traces of assetSource, it's given to us by launchermeta now)
This commit is contained in:
Henry Le Grys 2020-12-01 18:41:44 +00:00
parent 40b5fb838c
commit 410031a86f
33 changed files with 819 additions and 182 deletions

View File

@ -42,7 +42,7 @@ public final class BuilderUtils {
}
public static List<Compressor> getCompressors(String repoUrl) {
if (repoUrl.matches("^https?://files.minecraftforge.net/maven/")) {
if (repoUrl.matches("^https?://files.minecraftforge.net/maven/?")) {
return Lists.newArrayList(
new Compressor("xz", CompressorStreamFactory.XZ),
new Compressor("pack", CompressorStreamFactory.PACK200));

View File

@ -12,6 +12,7 @@ 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.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
@ -21,24 +22,32 @@ import com.google.common.io.Files;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.LauncherUtils;
import com.skcraft.launcher.model.loader.InstallProfile;
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.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.DownloadableFile;
import com.skcraft.launcher.model.modpack.Manifest;
import com.skcraft.launcher.util.Environment;
import com.skcraft.launcher.util.FileUtils;
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.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
@ -61,8 +70,13 @@ public class PackageBuilder {
private final PropertiesApplicator applicator;
@Getter
private boolean prettyPrint = false;
@Getter @Setter
private File baseDir;
private List<Library> loaderLibraries = Lists.newArrayList();
private List<String> mavenRepos;
private List<URL> jarMavens = Lists.newArrayList();
/**
* Create a new package builder.
@ -144,67 +158,101 @@ public class PackageBuilder {
Closer closer = Closer.create();
try {
ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json");
ZipEntry manifestEntry = BuilderUtils.getZipEntry(jarFile, "version.json");
String loaderName = file.getName();
if (profileEntry != null) {
InputStream stream = jarFile.getInputStream(profileEntry);
if (manifestEntry != null) {
InputStream stream = jarFile.getInputStream(manifestEntry);
// Read file
String data = CharStreams.toString(closer.register(new InputStreamReader(stream)));
data = data.replaceAll(",\\s*\\}", "}"); // Fix issues with trailing commas
InstallProfile profile = mapper.readValue(data, InstallProfile.class);
VersionInfo info = mapper.readValue(data, VersionInfo.class);
VersionManifest version = manifest.getVersionManifest();
if (version.getId() != null) {
loaderName = version.getId();
}
// Copy tweak class arguments
String args = profile.getVersionInfo().getMinecraftArguments();
if (args != null) {
List<String> gameArguments = info.getMinecraftArguments().getGameArguments();
if (gameArguments != null) {
String args = Joiner.on(' ').join(gameArguments);
String existingArgs = Strings.nullToEmpty(version.getMinecraftArguments());
Matcher m = TWEAK_CLASS_ARG.matcher(args);
while (m.find()) {
version.setMinecraftArguments(existingArgs + " " + m.group());
log.info("Adding " + m.group() + " to launch arguments");
}
version.setMinecraftArguments(Joiner.on(' ').join(existingArgs, args));
}
// Add libraries
List<Library> libraries = profile.getVersionInfo().getLibraries();
List<Library> libraries = info.getLibraries();
if (libraries != null) {
for (Library library : libraries) {
if (!version.getLibraries().contains(library)) {
loaderLibraries.add(library);
}
loaderLibraries.add(library);
log.info("Adding loader library " + library.getName());
}
}
// Copy main class
String mainClass = profile.getVersionInfo().getMainClass();
String mainClass = info.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) {
Library library = new Library();
library.setName(libraryPath);
File extractPath = new File(librariesDir, library.getPath(Environment.getInstance()));
Files.createParentDirs(extractPath);
ByteStreams.copy(closer.register(jarFile.getInputStream(libraryEntry)), Files.newOutputStreamSupplier(extractPath));
} else {
log.warning("Could not find the file '" + filePath + "' in " + file.getAbsolutePath() + ", which means that this mod loader will not work correctly");
}
}
} else {
log.warning("The file at " + file.getAbsolutePath() + " did not appear to have an " +
"install_profile.json file inside -- is it actually an installer for a mod loader?");
"version.json file inside -- is it actually an installer for a mod loader?");
}
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*\\}", "}");
InstallProfile profile = mapper.readValue(data, InstallProfile.class);
// Import the libraries for the installer
loaderLibraries.addAll(profile.getLibraries());
// Extract the data files
List<DownloadableFile> extraFiles = Lists.newArrayList();
ZipEntry clientBinpatch = BuilderUtils.getZipEntry(jarFile, "data/client.lzma");
if (clientBinpatch != null) {
DownloadableFile entry = FileUtils.saveStreamToObjectsDir(
closer.register(jarFile.getInputStream(clientBinpatch)),
new File(baseDir, manifest.getObjectsLocation()));
entry.setName("client.lzma");
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)),
new File(baseDir, manifest.getObjectsLocation()));
entry.setName("server.lzma");
extraFiles.add(entry);
profile.getData().get("BINPATCH").setServer("&" + 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.getData(), extraFiles));
// Add processors
manifest.getTasks().addAll(profile.toProcessorEntries(loaderName));
}
ZipEntry mavenEntry = BuilderUtils.getZipEntry(jarFile, "maven/");
if (mavenEntry != null) {
URL jarUrl = new URL("jar:file:" + file.getAbsolutePath() + "!/");
jarMavens.add(new URL(jarUrl, "/maven/"));
}
} finally {
closer.close();
@ -219,53 +267,31 @@ public class PackageBuilder {
Environment env = Environment.getInstance();
for (Library library : loaderLibraries) {
File outputPath = new File(librariesDir, library.getPath(env));
Library.Artifact artifact = library.getArtifact(env);
File outputPath = new File(librariesDir, artifact.getPath());
if (!outputPath.exists()) {
Files.createParentDirs(outputPath);
boolean found = false;
// Gather a list of repositories to download from
List<String> sources = Lists.newArrayList();
if (library.getBaseUrl() != null) {
sources.add(library.getBaseUrl());
if (!artifact.getUrl().isEmpty()) {
found = tryDownloadLibrary(library, artifact, artifact.getUrl(), outputPath);
}
sources.addAll(mavenRepos);
// Try each repository
for (String baseUrl : sources) {
String pathname = library.getPath(env);
// Some repositories compress their files
List<Compressor> compressors = BuilderUtils.getCompressors(baseUrl);
for (Compressor compressor : Lists.reverse(compressors)) {
pathname = compressor.transformPathname(pathname);
// Look inside the loader JARs
if (!found) {
for (URL base : jarMavens) {
found = tryFetchLibrary(library, new URL(base, artifact.getPath()), outputPath);
if (found) break;
}
}
URL url = new URL(baseUrl + pathname);
File tempFile = File.createTempFile("launcherlib", null);
try {
log.info("Downloading library " + library.getName() + " from " + url + "...");
HttpRequest.get(url).execute().expectResponseCode(200).saveContent(tempFile);
} catch (IOException e) {
log.info("Could not get file from " + url + ": " + e.getMessage());
continue;
// Try each repository if not found yet
if (!found) {
for (String baseUrl : mavenRepos) {
found = tryDownloadLibrary(library, artifact, baseUrl + artifact.getPath(), outputPath);
if (found) break;
}
// Decompress (if needed) and write to file
Closer closer = Closer.create();
InputStream inputStream = closer.register(new FileInputStream(tempFile));
inputStream = closer.register(new BufferedInputStream(inputStream));
for (Compressor compressor : compressors) {
inputStream = closer.register(compressor.createInputStream(inputStream));
}
ByteStreams.copy(inputStream, closer.register(new FileOutputStream(outputPath)));
tempFile.delete();
found = true;
break;
}
if (!found) {
@ -275,6 +301,65 @@ public class PackageBuilder {
}
}
private boolean tryDownloadLibrary(Library library, Library.Artifact artifact, String baseUrl, File outputPath)
throws IOException, InterruptedException {
URL url = new URL(baseUrl);
File tempFile = File.createTempFile("launcherlib", null);
// Some repositories compress their files
List<Compressor> compressors = BuilderUtils.getCompressors(baseUrl);
for (Compressor compressor : Lists.reverse(compressors)) {
url = new URL(url, compressor.transformPathname(artifact.getPath()));
}
try {
log.info("Downloading library " + library.getName() + " from " + url + "...");
HttpRequest.get(url).execute().expectResponseCode(200).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");
@ -297,18 +382,24 @@ public class PackageBuilder {
log.info("Loaded version manifest from " + path.getAbsolutePath());
} else {
URL url = url(String.format(
properties.getProperty("versionManifestUrl"),
manifest.getGameVersion()));
URL url = url(properties.getProperty("versionManifestUrl"));
log.info("Fetching version manifest from " + url + "...");
manifest.setVersionManifest(HttpRequest
.get(url)
ReleaseList releases = HttpRequest.get(url)
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(VersionManifest.class));
.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);
}
}
@ -379,6 +470,7 @@ public class PackageBuilder {
// From config
builder.readConfig(options.getConfigPath());
builder.readVersionManifest(options.getVersionManifestPath());
builder.setBaseDir(options.getOutputPath());
// From options
manifest.updateName(options.getName());

View File

@ -50,7 +50,7 @@ public class AssetsRoot {
* @return the file, which may not exist
*/
public File getIndexPath(VersionManifest versionManifest) {
return new File(dir, "indexes/" + versionManifest.getAssetsIndex() + ".json");
return new File(dir, "indexes/" + versionManifest.getAssetId() + ".json");
}
/**
@ -75,7 +75,7 @@ public class AssetsRoot {
* @throws LauncherException
*/
public AssetsTreeBuilder createAssetsBuilder(@NonNull VersionManifest versionManifest) throws LauncherException {
String indexId = versionManifest.getAssetsIndex();
String indexId = versionManifest.getAssetId();
File path = getIndexPath(versionManifest);
AssetsIndex index = Persistence.read(path, AssetsIndex.class, true);
if (index == null || index.getObjects() == null) {

View File

@ -16,10 +16,12 @@ import com.skcraft.launcher.auth.AccountList;
import com.skcraft.launcher.auth.LoginService;
import com.skcraft.launcher.auth.YggdrasilLoginService;
import com.skcraft.launcher.launch.LaunchSupervisor;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.persistence.Persistence;
import com.skcraft.launcher.swing.SwingHelper;
import com.skcraft.launcher.update.UpdateManager;
import com.skcraft.launcher.util.Environment;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SharedLocale;
import com.skcraft.launcher.util.SimpleLogFormatter;
@ -264,6 +266,16 @@ public final class Launcher {
return new File(getCommonDataDir(), "libraries");
}
/**
* Fetch a library file.
* @param library Library to fetch
* @return File pointing to the library on disk.
*/
public File getLibraryFile(Library library) {
Environment env = Environment.getInstance();
return new File(getLibrariesDir(), library.getPath(env));
}
/**
* Get the directory to store versions.
*

View File

@ -7,6 +7,7 @@
package com.skcraft.launcher.install;
import com.google.common.io.Files;
import com.skcraft.launcher.Launcher;
import lombok.NonNull;
import lombok.extern.java.Log;
@ -28,7 +29,7 @@ public class FileCopy implements InstallTask {
}
@Override
public void execute() throws IOException {
public void execute(Launcher launcher) throws IOException {
log.log(Level.INFO, "Copying to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()});
to.getParentFile().mkdirs();
Files.copy(from, to);

View File

@ -6,6 +6,7 @@
package com.skcraft.launcher.install;
import com.skcraft.launcher.Launcher;
import lombok.NonNull;
import lombok.extern.java.Log;
@ -27,7 +28,7 @@ public class FileMover implements InstallTask {
}
@Override
public void execute() throws IOException {
public void execute(Launcher launcher) throws IOException {
log.log(Level.INFO, "Moving to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()});
to.getParentFile().mkdirs();
to.delete();

View File

@ -6,6 +6,7 @@
package com.skcraft.launcher.install;
import com.skcraft.launcher.Launcher;
import lombok.NonNull;
import lombok.extern.java.Log;
@ -29,7 +30,7 @@ public class InstallLogFileMover implements InstallTask {
}
@Override
public void execute() throws IOException {
public void execute(Launcher launcher) throws IOException {
InstallLogFileMover.log.log(Level.INFO, "Installing to {0} (from {1})...", new Object[]{to.getAbsoluteFile(), from.getName()});
to.getParentFile().mkdirs();
to.delete();

View File

@ -7,9 +7,10 @@
package com.skcraft.launcher.install;
import com.skcraft.concurrency.ProgressObservable;
import com.skcraft.launcher.Launcher;
public interface InstallTask extends ProgressObservable {
void execute() throws Exception;
void execute(Launcher launcher) throws Exception;
}

View File

@ -7,6 +7,7 @@
package com.skcraft.launcher.install;
import com.skcraft.concurrency.ProgressObservable;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.util.SharedLocale;
import lombok.Getter;
import lombok.NonNull;
@ -26,11 +27,11 @@ public class Installer implements ProgressObservable {
@Getter private final File tempDir;
private final HttpDownloader downloader;
private InstallTask running;
private int count = 0;
private int finished = 0;
private List<InstallTask> queue = new ArrayList<InstallTask>();
private TaskQueue mainQueue = new TaskQueue();
private TaskQueue lateQueue = new TaskQueue();
private transient TaskQueue activeQueue;
public Installer(@NonNull File tempDir) {
this.tempDir = tempDir;
@ -38,27 +39,27 @@ public class Installer implements ProgressObservable {
}
public synchronized void queue(@NonNull InstallTask runnable) {
queue.add(runnable);
count++;
mainQueue.queue(runnable);
}
public synchronized void queueLate(@NonNull InstallTask runnable) {
lateQueue.queue(runnable);
}
public void download() throws IOException, InterruptedException {
downloader.execute();
}
public synchronized void execute() throws Exception {
queue = Collections.unmodifiableList(queue);
public synchronized void execute(Launcher launcher) throws Exception {
activeQueue = mainQueue;
mainQueue.execute(launcher);
activeQueue = null;
}
try {
for (InstallTask runnable : queue) {
checkInterrupted();
running = runnable;
runnable.execute();
finished++;
}
} finally {
running = null;
}
public synchronized void executeLate(Launcher launcher) throws Exception {
activeQueue = lateQueue;
lateQueue.execute(launcher);
activeQueue = null;
}
public Downloader getDownloader() {
@ -67,20 +68,50 @@ public class Installer implements ProgressObservable {
@Override
public double getProgress() {
return finished / (double) count;
if (activeQueue == null) return 0.0;
return activeQueue.finished / (double) activeQueue.count;
}
@Override
public String getStatus() {
InstallTask running = this.running;
if (running != null) {
if (activeQueue != null && activeQueue.running != null) {
InstallTask running = activeQueue.running;
String status = running.getStatus();
if (status == null) {
status = running.toString();
}
return tr("installer.executing", count - finished) + "\n" + status;
return tr("installer.executing", activeQueue.count - activeQueue.finished) + "\n" + status;
} else {
return SharedLocale.tr("installer.installing");
}
}
public static class TaskQueue {
private List<InstallTask> queue = new ArrayList<InstallTask>();
private int count = 0;
private int finished = 0;
private InstallTask running;
public synchronized void queue(@NonNull InstallTask runnable) {
queue.add(runnable);
count++;
}
public synchronized void execute(Launcher launcher) throws Exception {
queue = Collections.unmodifiableList(queue);
try {
for (InstallTask runnable : queue) {
checkInterrupted();
running = runnable;
runnable.execute(launcher);
finished++;
}
} finally {
running = null;
}
}
}
}

View File

@ -0,0 +1,129 @@
package com.skcraft.launcher.install;
import com.google.common.collect.Lists;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.model.loader.InstallProcessor;
import com.skcraft.launcher.model.loader.LoaderManifest;
import com.skcraft.launcher.model.loader.LoaderSubResolver;
import com.skcraft.launcher.model.loader.SidedData;
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.Environment;
import com.skcraft.launcher.util.FileUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.java.Log;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import static com.skcraft.launcher.util.SharedLocale.tr;
@RequiredArgsConstructor
@Log
public class ProcessorTask implements InstallTask {
private final InstallProcessor processor;
private final LoaderManifest loaderManifest;
private final Manifest manifest;
private final HashMap<String, DownloadableFile.LocalFile> localFiles;
private transient String message = "";
private transient double progress = 0;
@Override
public void execute(Launcher launcher) throws Exception {
VersionManifest versionManifest = manifest.getVersionManifest();
loaderManifest.getSidedData().put("MINECRAFT_JAR", SidedData.of(launcher.getJarPath(versionManifest).getAbsolutePath()));
LoaderSubResolver resolver = new LoaderSubResolver(manifest, loaderManifest,
Environment.getInstance(), Side.CLIENT, launcher.getBaseDir(), localFiles);
message = "Resolving parameters";
List<String> programArgs = processor.resolveArgs(resolver);
Map<String, String> outputs = processor.resolveOutputs(resolver);
message = "Finding libraries";
Library execFile = versionManifest.findLibrary(processor.getJar());
File jar = launcher.getLibraryFile(execFile);
JarFile jarFile = new JarFile(jar);
String mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
jarFile.close();
if (mainClass == null || mainClass.isEmpty()) {
throw new RuntimeException(String.format("Processor jar file '%s' has no main class!", processor.getJar()));
}
List<URL> classpath = Lists.newArrayList(jar.toURI().toURL());
int i = 0;
int total = processor.getClasspath().size();
for (String libraryName : processor.getClasspath()) {
message = "Adding library " + libraryName;
File libraryFile = launcher.getLibraryFile(versionManifest.findLibrary(libraryName));
if (!libraryFile.exists()) {
throw new RuntimeException(String.format("Missing library '%s' for processor '%s'",
libraryName, processor.getJar()));
}
classpath.add(libraryFile.toURI().toURL());
i++;
progress = (double) i / total;
}
progress = 0.0;
message = "Executing";
log.info(String.format("Running processor '%s' with %d args", processor.getJar(), programArgs.size()));
ClassLoader cl = new URLClassLoader(classpath.toArray(new URL[0]), null);
try {
Class<?> mainClazz = Class.forName(mainClass, true, cl);
Method main = mainClazz.getDeclaredMethod("main", String[].class);
main.invoke(null, (Object) programArgs.toArray(new String[0]));
} catch (Throwable e) {
throw new RuntimeException(e);
}
message = "Verifying";
progress = 1.0;
if (!outputs.isEmpty()) {
progress = 0.0;
i = 0;
total = outputs.size();
for (Map.Entry<String, String> output : outputs.entrySet()) {
File artifact = new File(output.getKey());
if (!artifact.exists()) {
throw new RuntimeException(String.format("Artifact '%s' missing", output.getKey()));
}
if (!FileUtils.getShaHash(artifact).equals(output.getValue())) {
throw new RuntimeException(String.format("Artifact '%s' has invalid hash!", output.getKey()));
}
i++;
progress = (double) i / total;
}
}
}
@Override
public double getProgress() {
return progress;
}
@Override
public String getStatus() {
return tr("installer.runningProcessor", processor.getJar(), message);
}
}

View File

@ -371,7 +371,7 @@ public class Runner implements Callable<Process>, ProgressObservable {
map.put("game_directory", instance.getContentDir().getAbsolutePath());
map.put("game_assets", virtualAssetsDir.getAbsolutePath());
map.put("assets_root", launcher.getAssets().getDir().getAbsolutePath());
map.put("assets_index_name", versionManifest.getAssetsIndex());
map.put("assets_index_name", versionManifest.getAssetId());
return map;
}

View File

@ -0,0 +1,41 @@
package com.skcraft.launcher.model.loader;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import lombok.Data;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class InstallProcessor {
private String jar;
private List<String> classpath;
private List<String> args;
private Map<String, String> outputs;
public List<String> resolveArgs(final LoaderSubResolver resolver) {
return Lists.transform(getArgs(), new Function<String, String>() {
@Override
public String apply(String input) {
return resolver.transform(input);
}
});
}
public Map<String, String> resolveOutputs(final LoaderSubResolver resolver) {
if (getOutputs() == null) return Collections.emptyMap();
HashMap<String, String> result = new HashMap<String, String>();
for (Map.Entry<String, String> entry : getOutputs().entrySet()) {
result.put(resolver.transform(entry.getKey()), resolver.transform(entry.getValue()));
}
return result;
}
}

View File

@ -7,15 +7,27 @@
package com.skcraft.launcher.model.loader;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.skcraft.launcher.model.minecraft.Library;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class InstallProfile {
private List<Library> libraries;
private List<InstallProcessor> processors;
private Map<String, SidedData> data;
@JsonProperty("install")
private InstallData installData;
private VersionInfo versionInfo;
public List<ProcessorEntry> toProcessorEntries(final String loaderName) {
return Lists.transform(getProcessors(), new Function<InstallProcessor, ProcessorEntry>() {
@Override
public ProcessorEntry apply(InstallProcessor input) {
return new ProcessorEntry(loaderName, input);
}
});
}
}

View File

@ -0,0 +1,17 @@
package com.skcraft.launcher.model.loader;
import com.skcraft.launcher.model.modpack.DownloadableFile;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoaderManifest {
private Map<String, SidedData> sidedData;
private List<DownloadableFile> downloadableFiles;
}

View File

@ -0,0 +1,68 @@
package com.skcraft.launcher.model.loader;
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.Environment;
import lombok.RequiredArgsConstructor;
import java.io.File;
import java.util.HashMap;
@RequiredArgsConstructor
public class LoaderSubResolver {
private final Manifest manifest;
private final LoaderManifest loader;
private final Environment env;
private final Side side;
private final File baseDir;
private final HashMap<String, DownloadableFile.LocalFile> localFiles;
public String getPathOf(String... rest) {
File file = baseDir;
for (String part : rest) {
file = new File(file, part);
}
return file.getAbsolutePath();
}
public String transform(String arg) {
VersionManifest version = manifest.getVersionManifest();
while (true) {
char start = arg.charAt(0);
int bound = arg.length() - 1;
char end = arg.charAt(bound);
if (start == '{' && end == '}') {
SidedData sidedData = loader.getSidedData().get(arg.substring(1, bound));
if (sidedData != null) {
arg = sidedData.resolveFor(side);
}
} else if (start == '[' && end == ']') {
String libraryName = arg.substring(1, bound);
Library library = version.findLibrary(libraryName);
if (library != null) {
arg = getPathOf(manifest.getLibrariesLocation(), library.getPath(env));
} else {
arg = getPathOf(manifest.getLibrariesLocation(), Library.mavenNameToPath(libraryName));
}
} else if (start == '&' && end == '&') {
String localFileName = arg.substring(1, bound);
if (localFiles.containsKey(localFileName)) {
arg = localFiles.get(localFileName).getLocation().getAbsolutePath();
} else {
arg = localFileName;
}
} else if (start == '\'' && end == '\'') {
arg = arg.substring(1, bound);
} else {
return arg;
}
}
}
}

View File

@ -0,0 +1,39 @@
package com.skcraft.launcher.model.loader;
import com.google.common.collect.Maps;
import com.skcraft.launcher.install.InstallLog;
import com.skcraft.launcher.install.Installer;
import com.skcraft.launcher.install.ProcessorTask;
import com.skcraft.launcher.install.UpdateCache;
import com.skcraft.launcher.model.modpack.DownloadableFile;
import com.skcraft.launcher.model.modpack.ManifestEntry;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.File;
import java.util.HashMap;
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class ProcessorEntry extends ManifestEntry {
private String loaderName;
private InstallProcessor processor;
@Override
public void install(Installer installer, InstallLog log, UpdateCache cache, File contentDir) throws Exception {
LoaderManifest loaderManifest = getManifest().getLoaders().get(loaderName);
HashMap<String, DownloadableFile.LocalFile> localFilesMap = Maps.newHashMap();
for (DownloadableFile downloadableFile : loaderManifest.getDownloadableFiles()) {
DownloadableFile.LocalFile localFile = downloadableFile.download(installer, getManifest());
localFilesMap.put(localFile.getName(), localFile);
}
installer.queueLate(new ProcessorTask(processor, loaderManifest, getManifest(), localFilesMap));
}
}

View File

@ -0,0 +1,28 @@
package com.skcraft.launcher.model.loader;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.skcraft.launcher.model.minecraft.Side;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor(staticName = "create")
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class SidedData {
private String client;
private String server;
public String resolveFor(Side side) {
switch (side) {
case CLIENT: return client;
case SERVER: return server;
default: return null;
}
}
public static SidedData of(String singleValue) {
return new SidedData(singleValue, singleValue);
}
}

View File

@ -7,7 +7,9 @@
package com.skcraft.launcher.model.loader;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.MinecraftArguments;
import lombok.Data;
import java.util.List;
@ -15,8 +17,9 @@ import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class VersionInfo {
private String minecraftArguments;
private String id;
@JsonProperty("arguments")
private MinecraftArguments minecraftArguments;
private String mainClass;
private List<Library> libraries;

View File

@ -6,9 +6,14 @@
package com.skcraft.launcher.model.minecraft;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.skcraft.launcher.util.Environment;
import com.skcraft.launcher.util.Platform;
import lombok.Data;
@ -22,11 +27,7 @@ import java.util.regex.Pattern;
public class Library {
private String name;
private transient String group;
private transient String artifact;
private transient String version;
@JsonProperty("url")
private String baseUrl;
private Downloads downloads;
private Map<String, String> natives;
private Extract extract;
private List<Rule> rules;
@ -37,21 +38,6 @@ public class Library {
// Custom
private boolean locallyAvailable;
public void setName(String name) {
this.name = name;
if (name != null) {
String[] parts = name.split(":");
this.group = parts[0];
this.artifact = parts[1];
this.version = parts[2];
} else {
this.group = null;
this.artifact = null;
this.version = null;
}
}
public boolean matches(Environment environment) {
boolean allow = false;
@ -68,21 +54,6 @@ public class Library {
return allow;
}
@JsonIgnore
public String getGroup() {
return group;
}
@JsonIgnore
public String getArtifact() {
return artifact;
}
@JsonIgnore
public String getVersion() {
return version;
}
public String getNativeString(Platform platform) {
if (getNatives() != null) {
switch (platform) {
@ -100,28 +71,18 @@ public class Library {
}
}
public String getFilename(Environment environment) {
public Artifact getArtifact(Environment environment) {
String nativeString = getNativeString(environment.getPlatform());
if (nativeString != null) {
return String.format("%s-%s-%s.jar",
getArtifact(), getVersion(), nativeString);
}
return String.format("%s-%s.jar", getArtifact(), getVersion());
if (nativeString != null) {
return getDownloads().getClassifiers().get(nativeString);
} else {
return getDownloads().getArtifact();
}
}
public String getPath(Environment environment) {
StringBuilder builder = new StringBuilder();
builder.append(getGroup().replace('.', '/'));
builder.append("/");
builder.append(getArtifact());
builder.append("/");
builder.append(getVersion());
builder.append("/");
builder.append(getFilename(environment));
String path = builder.toString();
path = path.replace("${arch}", environment.getArchBits());
return path;
return getArtifact(environment).getPath();
}
@Override
@ -179,6 +140,21 @@ public class Library {
private List<String> exclude;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Artifact {
private String path;
private String url;
private String sha1;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Downloads {
private Artifact artifact;
private Map<String, Artifact> classifiers;
}
private enum Action {
ALLOW,
DISALLOW;
@ -194,4 +170,32 @@ public class Library {
}
}
public static String mavenNameToPath(String mavenName) {
List<String> split = Splitter.on(':').splitToList(mavenName);
int size = split.size();
String group = split.get(0);
String name = split.get(1);
String version = split.get(2);
String extension = "jar";
String fileName = name + "-" + version;
if (size > 3) {
String classifier = split.get(3);
if (classifier.indexOf("@") != -1) {
List<String> parts = Splitter.on('@').splitToList(classifier);
classifier = parts.get(0);
extension = parts.get(1);
}
fileName += "-" + classifier;
}
fileName += "." + extension;
return Joiner.on('/').join(group.replace('.', '/'), name, version, fileName);
}
}

View File

@ -0,0 +1,15 @@
package com.skcraft.launcher.model.minecraft;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class MinecraftArguments {
@JsonProperty("game")
private List<String> gameArguments;
}

View File

@ -0,0 +1,21 @@
package com.skcraft.launcher.model.minecraft;
public enum Side {
CLIENT("client"),
SERVER("server");
private String name;
Side(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
}

View File

@ -20,6 +20,11 @@ public class Version {
@NonNull
private String id;
@Getter
@Setter
@NonNull
private String url;
public Version() {
}

View File

@ -6,12 +6,14 @@
package com.skcraft.launcher.model.minecraft;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@ -21,16 +23,45 @@ public class VersionManifest {
private Date time;
private Date releaseTime;
private String assets;
private AssetIndex assetIndex;
private String type;
private String processArguments;
private String minecraftArguments;
private String mainClass;
private int minimumLauncherVersion;
private LinkedHashSet<Library> libraries;
private Map<String, Artifact> downloads = new HashMap<String, Artifact>();
@JsonIgnore
public String getAssetsIndex() {
return getAssets() != null ? getAssets() : "legacy";
public String getAssetId() {
return getAssetIndex() != null
? getAssetIndex().getId()
: "legacy";
}
public Library findLibrary(String name) {
for (Library library : getLibraries()) {
if (library.getName().equals(name)) {
return library;
}
}
return null;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Artifact {
private String url;
private int size;
@JsonProperty("sha1")
private String hash;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class AssetIndex {
private String id;
private String url;
}
}

View File

@ -0,0 +1,32 @@
package com.skcraft.launcher.model.modpack;
import com.skcraft.launcher.install.Installer;
import lombok.Data;
import lombok.NonNull;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import static com.skcraft.launcher.LauncherUtils.concat;
@Data
public class DownloadableFile {
private String name;
private String hash;
private String location;
private int size;
public LocalFile download(@NonNull Installer installer, Manifest manifest) throws MalformedURLException {
URL url = concat(manifest.getObjectsUrl(), getLocation());
File local = installer.getDownloader().download(url, hash, size, name);
return new LocalFile(local, name);
}
@Data
public static class LocalFile {
private final File location;
private final String name;
}
}

View File

@ -12,8 +12,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Strings;
import com.skcraft.launcher.Instance;
import com.skcraft.launcher.LauncherUtils;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.install.Installer;
import com.skcraft.launcher.model.loader.LoaderManifest;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@ -22,7 +23,9 @@ import lombok.Setter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
@ -43,6 +46,7 @@ public class Manifest extends BaseManifest {
@Getter @Setter @JsonIgnore
private Installer installer;
private VersionManifest versionManifest;
private Map<String, LoaderManifest> loaders = new HashMap<String, LoaderManifest>();
@JsonIgnore
public URL getLibrariesUrl() {

View File

@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.skcraft.launcher.install.InstallLog;
import com.skcraft.launcher.install.Installer;
import com.skcraft.launcher.install.UpdateCache;
import com.skcraft.launcher.model.loader.ProcessorEntry;
import lombok.Data;
import lombok.ToString;
@ -23,7 +24,8 @@ import java.io.File;
property = "type",
defaultImpl = FileInstall.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = FileInstall.class, name = "file")
@JsonSubTypes.Type(value = FileInstall.class, name = "file"),
@JsonSubTypes.Type(value = ProcessorEntry.class, name = "process")
})
@Data
@ToString(exclude = "manifest")

View File

@ -48,7 +48,7 @@ public class SelfUpdater implements Callable<File>, ProgressObservable {
installer.queue(new FileMover(tempFile, file));
progress = installer;
installer.execute();
installer.execute(launcher);
return file;
} finally {

View File

@ -224,8 +224,8 @@ public abstract class BaseUpdater {
File tempFile = installer.getDownloader().download(urls, "", LIBRARY_SIZE_ESTIMATE,
library.getName() + ".jar");
installer.queue(new FileMover( tempFile, targetFile));
log.info("Fetching " + path + " from " + urls);
installer.queue(new FileMover( tempFile, targetFile));
}
}
}

View File

@ -152,7 +152,7 @@ public class Updater extends BaseUpdater implements Callable<Instance>, Progress
// Install the .jar
File jarPath = launcher.getJarPath(version);
URL jarSource = launcher.propUrl("jarUrl", version.getId());
URL jarSource = url(version.getDownloads().get("client").getUrl());
log.info("JAR at " + jarPath.getAbsolutePath() + ", fetched from " + jarSource);
installJar(installer, jarPath, jarSource);
@ -162,7 +162,7 @@ public class Updater extends BaseUpdater implements Callable<Instance>, Progress
URL url = manifest.getLibrariesUrl();
if (url != null) {
log.info("Added library source: " + url);
librarySources.add(url);
librarySources.add(0, url);
}
progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.collectingLibraries"));
@ -171,7 +171,7 @@ public class Updater extends BaseUpdater implements Callable<Instance>, Progress
// Download assets
log.info("Enumerating assets to download...");
progress = new DefaultProgress(-1, SharedLocale.tr("instanceUpdater.collectingAssets"));
installAssets(installer, version, launcher.propUrl("assetsIndexUrl", version.getAssetsIndex()), assetsSources);
installAssets(installer, version, url(version.getAssetIndex().getUrl()), assetsSources);
log.info("Executing download phase...");
progress = ProgressFilter.between(installer.getDownloader(), 0, 0.98);
@ -179,7 +179,9 @@ public class Updater extends BaseUpdater implements Callable<Instance>, Progress
log.info("Executing install phase...");
progress = ProgressFilter.between(installer, 0.98, 1);
installer.execute();
installer.execute(launcher);
installer.executeLate(launcher);
log.info("Completing...");
complete();

View File

@ -0,0 +1,43 @@
package com.skcraft.launcher.util;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.skcraft.launcher.model.modpack.DownloadableFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileUtils {
public static DownloadableFile saveStreamToObjectsDir(InputStream stream, File outputDir) throws IOException {
byte[] input = ByteStreams.toByteArray(stream);
HashFunction hf = Hashing.sha1();
String fileHash = hf.hashBytes(input).toString();
String filePath = fileHash.substring(0, 2) + "/" + fileHash.substring(2, 4) + "/" + fileHash;
File dest = new File(outputDir, filePath);
dest.getParentFile().mkdirs();
Files.write(input, dest);
DownloadableFile entry = new DownloadableFile();
entry.setLocation(filePath);
entry.setHash(fileHash);
entry.setSize(input.length);
return entry;
}
public static String getShaHash(File file) throws IOException {
FileInputStream stream = new FileInputStream(file);
byte[] input = ByteStreams.toByteArray(stream);
String res = Hashing.sha1().hashBytes(input).toString();
stream.close();
return res;
}
}

View File

@ -146,6 +146,7 @@ installer.installing=Installing...
installer.executing=Executing tasks... ({0} remaining)
installer.copyingFile=Copying from {0} to {1}
installer.movingFile=Moving {0} to {1}
installer.runningProcessor=Running processor {0}: {1}
updater.updating=Updating launcher...
updater.updateRequiredButOffline=An update is required but you need to be in online mode.

View File

@ -8,7 +8,7 @@ version=${project.version}
agentName=Minecraft
offlinePlayerName=Player
versionManifestUrl=https://s3.amazonaws.com/Minecraft.Download/versions/%1$s/%1$s.json
versionManifestUrl=https://launchermeta.mojang.com/mc/game/version_manifest.json
librariesSource=https://libraries.minecraft.net/
jarUrl=https://s3.amazonaws.com/Minecraft.Download/versions/%1$s/%1$s.jar
assetsIndexUrl=https://s3.amazonaws.com/Minecraft.Download/indexes/%s.json

View File

@ -1,5 +1,6 @@
[
"https://libraries.minecraft.net/",
"https://repo1.maven.org/maven2/",
"https://files.minecraftforge.net/maven/",
"http://maven.apache.org/"
]