diff --git a/README.md b/README.md index d9ed486..490f52d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ +### 1.13+ notice + +The launcher now supports modern Forge distributions (1.13+), plus Fabric! + SKCraft Launcher ================ +![Now with 1.13+ support!](readme/now_with_support.png) + Need to make it easy for people to install and play your modpack? The SKCraft Launcher platform may be for you if: :heavy_check_mark: You want your own logo and branding on the launcher, with your own news feed @@ -21,7 +27,7 @@ The launcher has all standard features that you'd expect like :one: resume of incomplete downloads, :two: incremental updates, and :three: file deduplication (saves disk space when you have files duplicated between updates or modpacks). -LiteLoader, Forge, and .jar mods are supported. You can put resource packs in, or really even random mod files that don't go in the configs or mods folder. +LiteLoader, Forge, Fabric, and custom .jar mods are supported. You can put resource packs in, or really even random mod files that don't go in the configs or mods folder. You do need some sort of website, but it does **not** need anything complicated like PHP. @@ -52,7 +58,7 @@ * src/**resourcepacks**/ * loaders/ -You'd put LiteLoader and Forge into the *loaders* folder. :ok_hand: +LiteLoader and Forge installers, or Fabric Loader, go into the *loaders* folder. :ok_hand: ### More Pictures diff --git a/build.gradle b/build.gradle index 8565ca4..4d47925 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,6 @@ repositories { mavenCentral() - maven { url "http://repo.maven.apache.org/maven2" } } if (JavaVersion.current().isJava8Compatible()) { diff --git a/creator-tools/build.gradle b/creator-tools/build.gradle index a99e096..c93a961 100644 --- a/creator-tools/build.gradle +++ b/creator-tools/build.gradle @@ -1,4 +1,5 @@ plugins { + id 'application' id "com.github.johnrengelman.shadow" } @@ -20,11 +21,7 @@ } } -jar { - manifest { - attributes("Main-Class": "com.skcraft.launcher.creator.Creator") - } -} +mainClassName = "com.skcraft.launcher.creator.Creator" shadowJar { } diff --git a/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServerBuilder.java b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServerBuilder.java index d8ad19d..2a02ad4 100644 --- a/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServerBuilder.java +++ b/creator-tools/src/main/java/com/skcraft/launcher/creator/server/TestServerBuilder.java @@ -11,6 +11,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; @@ -72,6 +73,8 @@ server.setHandler(gzip); gzip.setHandler(contexts); + server.addBean(new ErrorHandler()); + return new TestServer(server); } diff --git a/launcher-bootstrap/build.gradle b/launcher-bootstrap/build.gradle index 4777a69..4ffff0b 100644 --- a/launcher-bootstrap/build.gradle +++ b/launcher-bootstrap/build.gradle @@ -1,13 +1,10 @@ plugins { + id 'application' id "com.github.johnrengelman.shadow" id 'io.franzbecker.gradle-lombok' } -jar { - manifest { - attributes("Main-Class": "com.skcraft.launcher.Bootstrap") - } -} +mainClassName = "com.skcraft.launcher.Bootstrap" dependencies { compile 'com.googlecode.json-simple:json-simple:1.1.1' diff --git a/launcher-builder/build.gradle b/launcher-builder/build.gradle index 8c0e604..787e83c 100644 --- a/launcher-builder/build.gradle +++ b/launcher-builder/build.gradle @@ -1,12 +1,9 @@ plugins { + id 'application' id "com.github.johnrengelman.shadow" } -jar { - manifest { - attributes("Main-Class": "com.skcraft.launcher.builder.PackageBuilder") - } -} +mainClassName = "com.skcraft.launcher.builder.PackageBuilder" dependencies { compile project(':launcher') diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java index 091e63f..10999a7 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java @@ -7,8 +7,10 @@ package com.skcraft.launcher.builder; import com.beust.jcommander.internal.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.*; @@ -41,8 +43,15 @@ 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 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)); diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java index 303c63d..2b2e9a1 100644 --- a/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java @@ -12,16 +12,18 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; -import com.google.common.base.Strings; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; -import com.google.common.io.CharStreams; 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.model.loader.InstallProfile; +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.Environment; @@ -29,17 +31,17 @@ 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; import static com.google.common.base.Preconditions.checkNotNull; @@ -51,9 +53,6 @@ */ @Log public class PackageBuilder { - - private static final Pattern TWEAK_CLASS_ARG = Pattern.compile("--tweakClass\\s+([^\\s]+)"); - private final Properties properties; private final ObjectMapper mapper; private ObjectWriter writer; @@ -61,8 +60,14 @@ private final PropertiesApplicator applicator; @Getter private boolean prettyPrint = false; + + @Getter @Setter + private File baseDir; + private List loaderLibraries = Lists.newArrayList(); + private List installerLibraries = Lists.newArrayList(); private List mavenRepos; + private List jarMavens = Lists.newArrayList(); /** * Create a new package builder. @@ -142,74 +147,38 @@ 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)); - // Read file - String data = CharStreams.toString(closer.register(new InputStreamReader(stream))); - data = data.replaceAll(",\\s*\\}", "}"); // Fix issues with trailing commas + BasicInstallProfile basicProfile = mapper.readValue(BuilderUtils.readStringFromStream(reader), + BasicInstallProfile.class); - InstallProfile profile = mapper.readValue(data, InstallProfile.class); - VersionManifest version = manifest.getVersionManifest(); - - // Copy tweak class arguments - String args = profile.getVersionInfo().getMinecraftArguments(); - if (args != null) { - 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"); - } + if (basicProfile.isLegacy()) { + processor = new OldForgeLoaderProcessor(); + } else if (basicProfile.getProfile().equalsIgnoreCase("forge")) { + processor = new ModernForgeLoaderProcessor(); } - - // Add libraries - List libraries = profile.getVersionInfo().getLibraries(); - if (libraries != null) { - for (Library library : libraries) { - if (!version.getLibraries().contains(library)) { - loaderLibraries.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) { - 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?"); + } else if (BuilderUtils.getZipEntry(jarFile, "fabric-installer.json") != null) { + processor = new FabricLoaderProcessor(); } } finally { closer.close(); jarFile.close(); } + + if (processor != null) { + LoaderResult result = processor.process(file, manifest, mapper, baseDir); + + loaderLibraries.addAll(result.getLoaderLibraries()); + installerLibraries.addAll(result.getProcessorLibraries()); + jarMavens.addAll(result.getJarMavens()); + } } public void downloadLibraries(File librariesDir) throws IOException, InterruptedException { @@ -218,54 +187,39 @@ // TODO: Download libraries for different environments -- As of writing, this is not an issue Environment env = Environment.getInstance(); - for (Library library : loaderLibraries) { - File outputPath = new File(librariesDir, library.getPath(env)); + for (Library library : Iterables.concat(loaderLibraries, installerLibraries)) { + 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 sources = Lists.newArrayList(); - if (library.getBaseUrl() != null) { - sources.add(library.getBaseUrl()); + // Try just the URL, it might be a full URL to the file + 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 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); + // Assume artifact URL is a maven repository URL and try that + if (!found) { + URL url = LauncherUtils.concat(url(artifact.getUrl()), artifact.getPath()); + found = tryDownloadLibrary(library, artifact, url.toString(), outputPath); + } - 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 +229,71 @@ } } + 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 compressors = BuilderUtils.getCompressors(baseUrl); + for (Compressor compressor : Lists.reverse(compressors)) { + url = new URL(url, compressor.transformPathname(artifact.getPath())); + } + + 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()); + 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.emptyList()); + return true; + } + + private void writeLibraryToFile(File outputPath, File inputFile, List 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 +316,24 @@ 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 +404,7 @@ // From config builder.readConfig(options.getConfigPath()); builder.readVersionManifest(options.getVersionManifestPath()); + builder.setBaseDir(options.getOutputPath()); // From options manifest.updateName(options.getName()); diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java new file mode 100644 index 0000000..4d52840 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/FabricLoaderProcessor.java @@ -0,0 +1,113 @@ +package com.skcraft.launcher.builder.loaders; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Iterables; +import com.google.common.io.Closer; +import com.skcraft.launcher.builder.BuilderUtils; +import com.skcraft.launcher.model.loader.MavenUrl; +import com.skcraft.launcher.model.loader.profiles.FabricInstallProfile; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.modpack.Manifest; +import com.skcraft.launcher.util.HttpRequest; +import lombok.extern.java.Log; +import org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.List; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +@Log +public class FabricLoaderProcessor 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 installerEntry = BuilderUtils.getZipEntry(jarFile, "fabric-installer.json"); + + if (installerEntry != null) { + InputStreamReader reader = new InputStreamReader(jarFile.getInputStream(installerEntry)); + FabricInstallProfile profile = mapper.readValue( + BuilderUtils.readStringFromStream(closer.register(reader)), FabricInstallProfile.class); + + // Check version + if (profile.getVersion() != 1) { + log.warning(String.format("Fabric installer metadata version is %d - we expect version 1.", + profile.getVersion())); + } + + // Add libraries (TODO: Add server-only libraries to somewhere) + Iterable libraries = Iterables.concat(profile.getLibraries().getClient(), + profile.getLibraries().getCommon()); + for (Library library : libraries) { + result.getLoaderLibraries().add(library); + log.info("Adding loader library " + library.getName()); + } + + // Add actual loader jar into library path + if (profile.getLoader() != null) { + result.getLoaderLibraries().add(profile.getLoader()); + log.info(String.format("Adding Fabric Loader '%s'", profile.getLoader().getName())); + } else { + log.warning("Fabric loader metadata is missing a `loader` section, making up a fake library"); + Library loader = new Library(); + loader.setName("faked:loader:" + FilenameUtils.getBaseName(loaderJar.getName())); + + Library.Downloads downloads = new Library.Downloads(); + downloads.setArtifact(new Library.Artifact()); + downloads.getArtifact().setPath(loaderJar.getName()); + downloads.getArtifact().setUrl(""); + loader.setDownloads(downloads); + + result.getLoaderLibraries().add(loader); + // Little bit of a hack here, pretending the filesystem is a maven + result.getJarMavens().add(new URL("file:" + loaderJar.getParentFile().getAbsolutePath() + "/")); + } + + // Set main class + String mainClass = profile.getMainClass().getClient(); + if (mainClass != null) { + manifest.getVersionManifest().setMainClass(mainClass); + log.info("Using main class " + mainClass); + } + + // Add intermediary library + log.info("Downloading fabric metadata..."); + URL url = HttpRequest.url("https://meta.fabricmc.net/v2/versions/intermediary/" + + manifest.getVersionManifest().getId()); + List versions = HttpRequest.get(url) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(new TypeReference>() {}); + + if (versions != null && versions.size() > 0) { + MavenUrl intermediaryLib = versions.get(0); + + if (intermediaryLib.getUrl() == null) { + // FIXME temporary hack since maven URL is missing, hopefully can go away soon + // waiting on PR FabricMC/fabric-meta#9 + intermediaryLib.setUrl("https://maven.fabricmc.net/"); + } + + result.getLoaderLibraries().add(intermediaryLib.toLibrary()); + log.info("Added intermediary " + intermediaryLib.getName()); + } + } + } catch (InterruptedException e) { + log.warning("HTTP request to fabric metadata API was interrupted, this will probably not work!"); + } finally { + closer.close(); + jarFile.close(); + } + + return result; + } +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ILoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ILoaderProcessor.java new file mode 100644 index 0000000..3006b86 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ILoaderProcessor.java @@ -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; +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/LoaderResult.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/LoaderResult.java new file mode 100644 index 0000000..9fe2783 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/LoaderResult.java @@ -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 loaderLibraries = Lists.newArrayList(); + private final List processorLibraries = Lists.newArrayList(); + private final List jarMavens = Lists.newArrayList(); +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java new file mode 100644 index 0000000..0814bee --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/ModernForgeLoaderProcessor.java @@ -0,0 +1,143 @@ +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.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +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 (version.getId() != null) { + loaderName = version.getId(); + } + + // Copy game arguments + List gameArguments = info.getArguments().getGameArguments(); + if (gameArguments != null) { + if (info.isOverridingArguments()) { + version.getArguments().getGameArguments().clear(); + } + + version.getArguments().getGameArguments().addAll(gameArguments); + } + + // Add libraries + List libraries = info.getLibraries(); + if (libraries != null) { + for (Library library : libraries) { + result.getLoaderLibraries().add(library); + log.info("Adding loader library " + library.getName()); + } + } + + // 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); + + // Import the libraries for the installer + result.getProcessorLibraries().addAll(profile.getLibraries()); + + // Extract the data files + List 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"); + 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)), + new File(baseDir, manifest.getObjectsLocation())); + + entry.setName("server.lzma"); + entry.setSide(Side.SERVER); + 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.getLibraries(), 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:" + loaderJar.getAbsolutePath() + "!/"); + result.getJarMavens().add(new URL(jarUrl, "/maven/")); + } + } finally { + closer.close(); + jarFile.close(); + } + + return result; + } +} diff --git a/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java new file mode 100644 index 0000000..8daa555 --- /dev/null +++ b/launcher-builder/src/main/java/com/skcraft/launcher/builder/loaders/OldForgeLoaderProcessor.java @@ -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 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 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 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, manifest.getLibrariesLocation()); + 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; + } +} diff --git a/launcher-fancy/build.gradle b/launcher-fancy/build.gradle index e20e21a..bd0f5ab 100644 --- a/launcher-fancy/build.gradle +++ b/launcher-fancy/build.gradle @@ -1,16 +1,20 @@ plugins { + id 'application' id "com.github.johnrengelman.shadow" } -jar { - manifest { - attributes("Main-Class": "com.skcraft.launcher.FancyLauncher") +mainClassName = "com.skcraft.launcher.FancyLauncher" + +repositories { + maven { + name = 'obw maven' + url = 'https://maven.offbeatwit.ch/repository/snapshots' } } dependencies { compile project(':launcher') - compile 'com.github.insubstantial:substance:7.3' + compile 'io.github.cottonmc.insubstantial:substance:7.3.1-SNAPSHOT' } shadowJar { diff --git a/launcher/build.gradle b/launcher/build.gradle index 43dec87..0f11adf 100644 --- a/launcher/build.gradle +++ b/launcher/build.gradle @@ -1,13 +1,10 @@ plugins { + id 'application' id "com.github.johnrengelman.shadow" id 'io.franzbecker.gradle-lombok' } -jar { - manifest { - attributes("Main-Class": "com.skcraft.launcher.Launcher") - } -} +mainClassName = "com.skcraft.launcher.Launcher" dependencies { compile 'javax.xml.bind:jaxb-api:2.2.4' diff --git a/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java b/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java index 4127172..2bf64c7 100644 --- a/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java +++ b/launcher/src/main/java/com/skcraft/launcher/AssetsRoot.java @@ -50,7 +50,7 @@ * @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 @@ * @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) { diff --git a/launcher/src/main/java/com/skcraft/launcher/Launcher.java b/launcher/src/main/java/com/skcraft/launcher/Launcher.java index f6d5cce..58a25d9 100644 --- a/launcher/src/main/java/com/skcraft/launcher/Launcher.java +++ b/launcher/src/main/java/com/skcraft/launcher/Launcher.java @@ -16,10 +16,12 @@ 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; @@ -52,7 +54,7 @@ @Log public final class Launcher { - public static final int PROTOCOL_VERSION = 2; + public static final int PROTOCOL_VERSION = 3; @Getter private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); @@ -66,6 +68,7 @@ @Getter private final LaunchSupervisor launchSupervisor = new LaunchSupervisor(this); @Getter private final UpdateManager updateManager = new UpdateManager(this); @Getter private final InstanceTasks instanceTasks = new InstanceTasks(this); + private final Environment env = Environment.getInstance(); /** * Create a new launcher instance with the given base directory. @@ -136,6 +139,15 @@ } /** + * Get the launcher title. + * + * @return The launcher title. + */ + public String getTitle() { + return tr("launcher.appTitle"); + } + + /** * Get the launcher version. * * @return the launcher version @@ -265,6 +277,15 @@ } /** + * Fetch a library file. + * @param library Library to fetch + * @return File pointing to the library on disk. + */ + public File getLibraryFile(Library library) { + return new File(getLibrariesDir(), library.getPath(env)); + } + + /** * Get the directory to store versions. * * @return the versions directory diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java index c2f14e9..4fc027d 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/ProgressDialog.java @@ -50,7 +50,7 @@ public ProgressDialog(Window owner, String title, String message) { super(owner, title, ModalityType.DOCUMENT_MODAL); - setResizable(false); + setResizable(true); initComponents(); label.setText(message); defaultTitle = title; diff --git a/launcher/src/main/java/com/skcraft/launcher/install/FileCopy.java b/launcher/src/main/java/com/skcraft/launcher/install/FileCopy.java index eec2be6..49eb68b 100644 --- a/launcher/src/main/java/com/skcraft/launcher/install/FileCopy.java +++ b/launcher/src/main/java/com/skcraft/launcher/install/FileCopy.java @@ -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 @@ } @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); diff --git a/launcher/src/main/java/com/skcraft/launcher/install/FileMover.java b/launcher/src/main/java/com/skcraft/launcher/install/FileMover.java index c1331b8..6943129 100644 --- a/launcher/src/main/java/com/skcraft/launcher/install/FileMover.java +++ b/launcher/src/main/java/com/skcraft/launcher/install/FileMover.java @@ -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 @@ } @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(); diff --git a/launcher/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java b/launcher/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java index e26e858..5464c2a 100644 --- a/launcher/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java +++ b/launcher/src/main/java/com/skcraft/launcher/install/InstallLogFileMover.java @@ -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 @@ } @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(); diff --git a/launcher/src/main/java/com/skcraft/launcher/install/InstallTask.java b/launcher/src/main/java/com/skcraft/launcher/install/InstallTask.java index 8988002..2f7d5ab 100644 --- a/launcher/src/main/java/com/skcraft/launcher/install/InstallTask.java +++ b/launcher/src/main/java/com/skcraft/launcher/install/InstallTask.java @@ -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; } diff --git a/launcher/src/main/java/com/skcraft/launcher/install/Installer.java b/launcher/src/main/java/com/skcraft/launcher/install/Installer.java index ccfa522..22e224a 100644 --- a/launcher/src/main/java/com/skcraft/launcher/install/Installer.java +++ b/launcher/src/main/java/com/skcraft/launcher/install/Installer.java @@ -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 @@ @Getter private final File tempDir; private final HttpDownloader downloader; - private InstallTask running; - private int count = 0; - private int finished = 0; - private List queue = new ArrayList(); + 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 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 @@ @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 queue = new ArrayList(); + + 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; + } + } + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java b/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java new file mode 100644 index 0000000..4c7cf65 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/install/ProcessorTask.java @@ -0,0 +1,139 @@ +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 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 programArgs = processor.resolveArgs(resolver); + Map outputs = processor.resolveOutputs(resolver); + + message = "Finding libraries"; + Library execFile = loaderManifest.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 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(loaderManifest.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 parent; + try { + // in java 9+ we need the platform classloader for access to certain modules + parent = (ClassLoader) ClassLoader.class.getDeclaredMethod("getPlatformClassLoader") + .invoke(null); + } catch (Throwable ignored) { + // java 8 or below it's a-ok to have no delegate + parent = null; + } + + ClassLoader cl = new URLClassLoader(classpath.toArray(new URL[0]), parent); + 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 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); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java b/launcher/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java index 726cdbf..dbdaba0 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/JavaProcessBuilder.java @@ -106,9 +106,6 @@ command.add("-XX:MaxPermSize=" + String.valueOf(permGen) + "M"); } - command.add("-cp"); - command.add(buildClassPath()); - command.add(mainClass); for (String arg : args) { diff --git a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java index 833d733..c0cd5c5 100644 --- a/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java +++ b/launcher/src/main/java/com/skcraft/launcher/launch/Runner.java @@ -15,9 +15,7 @@ import com.skcraft.launcher.*; import com.skcraft.launcher.auth.Session; import com.skcraft.launcher.install.ZipExtract; -import com.skcraft.launcher.model.minecraft.AssetsIndex; -import com.skcraft.launcher.model.minecraft.Library; -import com.skcraft.launcher.model.minecraft.VersionManifest; +import com.skcraft.launcher.model.minecraft.*; import com.skcraft.launcher.persistence.Persistence; import com.skcraft.launcher.util.Environment; import com.skcraft.launcher.util.Platform; @@ -60,6 +58,7 @@ private Configuration config; private JavaProcessBuilder builder; private AssetsRoot assetsRoot; + private FeatureList.Mutable featureList; /** * Create a new instance launcher. @@ -75,6 +74,7 @@ this.instance = instance; this.session = session; this.extractDir = extractDir; + this.featureList = new FeatureList.Mutable(); } /** @@ -131,19 +131,18 @@ } progress = new DefaultProgress(0.9, SharedLocale.tr("runner.collectingArgs")); + builder.classPath(getJarPath()); + builder.setMainClass(versionManifest.getMainClass()); - addJvmArgs(); + addWindowArgs(); addLibraries(); + addJvmArgs(); addJarArgs(); addProxyArgs(); addServerArgs(); - addWindowArgs(); addPlatformArgs(); addLegacyArgs(); - builder.classPath(getJarPath()); - builder.setMainClass(versionManifest.getMainClass()); - callLaunchModifier(); ProcessBuilder processBuilder = new ProcessBuilder(builder.buildCommand()); @@ -175,11 +174,6 @@ builder.getFlags().add("-Xdock:name=Minecraft"); } } - - // Windows arguments - if (getEnvironment().getPlatform() == Platform.WINDOWS) { - builder.getFlags().add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"); - } } /** @@ -210,8 +204,6 @@ tr("runner.missingLibrary", instance.getTitle(), library.getName())); } } - - builder.getFlags().add("-Djava.library.path=" + extractDir.getAbsoluteFile()); } /** @@ -253,14 +245,23 @@ builder.tryJvmPath(new File(rawJvmPath)); } + List flags = builder.getFlags(); String rawJvmArgs = config.getJvmArgs(); if (!Strings.isNullOrEmpty(rawJvmArgs)) { - List flags = builder.getFlags(); - for (String arg : JavaProcessBuilder.splitArgs(rawJvmArgs)) { flags.add(arg); } } + + List javaArguments = versionManifest.getArguments().getJvmArguments(); + StrSubstitutor substitutor = new StrSubstitutor(getCommandSubstitutions()); + for (GameArgument arg : javaArguments) { + if (arg.shouldApply(environment, featureList)) { + for (String subArg : arg.getValues()) { + flags.add(substitutor.replace(subArg)); + } + } + } } /** @@ -271,10 +272,14 @@ private void addJarArgs() throws JsonProcessingException { List args = builder.getArgs(); - String[] rawArgs = versionManifest.getMinecraftArguments().split(" +"); + List rawArgs = versionManifest.getArguments().getGameArguments(); StrSubstitutor substitutor = new StrSubstitutor(getCommandSubstitutions()); - for (String arg : rawArgs) { - args.add(substitutor.replace(arg)); + for (GameArgument arg : rawArgs) { + if (arg.shouldApply(environment, featureList)) { + for (String subArg : arg.getValues()) { + args.add(substitutor.replace(subArg)); + } + } } } @@ -329,15 +334,10 @@ * Add window arguments. */ private void addWindowArgs() { - List args = builder.getArgs(); int width = config.getWindowWidth(); - int height = config.getWidowHeight(); if (width >= 10) { - args.add("--width"); - args.add(String.valueOf(width)); - args.add("--height"); - args.add(String.valueOf(height)); + featureList.addFeature("has_custom_resolution", true); } } @@ -345,7 +345,32 @@ * Add arguments to make legacy Minecraft work. */ private void addLegacyArgs() { - builder.getFlags().add("-Dminecraft.applet.TargetDirectory=" + instance.getContentDir()); + List flags = builder.getFlags(); + + if (versionManifest.getMinimumLauncherVersion() < 21) { + // Add bits that the legacy manifests don't + flags.add("-Djava.library.path=" + extractDir.getAbsoluteFile()); + flags.add("-cp"); + flags.add(builder.buildClassPath()); + + if (featureList.hasFeature("has_custom_resolution")) { + List args = builder.getArgs(); + args.add("--width"); + args.add(String.valueOf(config.getWindowWidth())); + args.add("--height"); + args.add(String.valueOf(config.getWidowHeight())); + } + + // Add old platform hacks that the new manifests already specify + if (getEnvironment().getPlatform() == Platform.WINDOWS) { + flags.add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"); + } + } + + if (versionManifest.getMinimumLauncherVersion() < 18) { + // TODO find out exactly what versions need this hack. + flags.add("-Dminecraft.applet.TargetDirectory=" + instance.getContentDir()); + } } /** @@ -358,6 +383,7 @@ Map map = new HashMap(); map.put("version_name", versionManifest.getId()); + map.put("version_type", launcher.getProperties().getProperty("launcherShortname")); map.put("auth_access_token", session.getAccessToken()); map.put("auth_session", session.getSessionToken()); @@ -371,7 +397,15 @@ 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()); + + map.put("resolution_width", String.valueOf(config.getWindowWidth())); + map.put("resolution_height", String.valueOf(config.getWidowHeight())); + + map.put("launcher_name", launcher.getTitle()); + map.put("launcher_version", launcher.getVersion()); + map.put("classpath", builder.buildClassPath()); + map.put("natives_directory", extractDir.getAbsolutePath()); return map; } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/BasicInstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/BasicInstallProfile.java new file mode 100644 index 0000000..b5f5f2f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/BasicInstallProfile.java @@ -0,0 +1,35 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class BasicInstallProfile { + private String profile; + private int spec; + + @JsonProperty("install") + private Legacy legacyProfile; + + @JsonIgnore + public boolean isLegacy() { + return getLegacyProfile() != null; + } + + public String resolveProfileName() { + if (isLegacy()) { + return getLegacyProfile().getProfileName(); + } else { + return getProfile(); + } + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Legacy { + private String profileName; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/ExtendedSidedData.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/ExtendedSidedData.java new file mode 100644 index 0000000..d83fc6b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/ExtendedSidedData.java @@ -0,0 +1,10 @@ +package com.skcraft.launcher.model.loader; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ExtendedSidedData extends SidedData { + private T common; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallData.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallData.java deleted file mode 100644 index c346dc6..0000000 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallData.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.model.loader; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Data; - -@Data -@JsonIgnoreProperties(ignoreUnknown = true) -public class InstallData { - - private String path; - private String filePath; - -} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java new file mode 100644 index 0000000..505a67b --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProcessor.java @@ -0,0 +1,35 @@ +package com.skcraft.launcher.model.loader; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +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 classpath; + private List args; + private Map outputs; + + public List resolveArgs(LoaderSubResolver resolver) { + return Lists.transform(getArgs(), resolver); + } + + public Map resolveOutputs(final LoaderSubResolver resolver) { + if (getOutputs() == null) return Collections.emptyMap(); + + HashMap result = new HashMap(); + + for (Map.Entry entry : getOutputs().entrySet()) { + result.put(resolver.apply(entry.getKey()), resolver.apply(entry.getValue())); + } + + return result; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProfile.java deleted file mode 100644 index 6205501..0000000 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/InstallProfile.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.model.loader; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; - -@Data -@JsonIgnoreProperties(ignoreUnknown = true) -public class InstallProfile { - - @JsonProperty("install") - private InstallData installData; - private VersionInfo versionInfo; - -} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java new file mode 100644 index 0000000..c1b567c --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderManifest.java @@ -0,0 +1,29 @@ +package com.skcraft.launcher.model.loader; + +import com.skcraft.launcher.model.minecraft.Library; +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 List libraries; + private Map> sidedData; + private List downloadableFiles; + + public Library findLibrary(String name) { + for (Library library : getLibraries()) { + if (library.getName().equals(name)) { + return library; + } + } + + return null; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java new file mode 100644 index 0000000..299aea7 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/LoaderSubResolver.java @@ -0,0 +1,69 @@ +package com.skcraft.launcher.model.loader; + +import com.google.common.base.Function; +import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.Side; +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 implements Function { + private final Manifest manifest; + private final LoaderManifest loader; + private final Environment env; + private final Side side; + private final File baseDir; + private final HashMap localFiles; + + public String getPathOf(String... rest) { + File file = baseDir; + for (String part : rest) { + file = new File(file, part); + } + + return file.getAbsolutePath(); + } + + @Override + public String apply(String arg) { + if (arg == null) return null; + + 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 = loader.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; + } + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/MavenUrl.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/MavenUrl.java new file mode 100644 index 0000000..fdc6ca4 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/MavenUrl.java @@ -0,0 +1,24 @@ +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 lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class MavenUrl { + @JsonProperty("maven") + private String name; + private String url; + private String version; + private boolean stable; + + public Library toLibrary() { + Library library = new Library(); + library.setName(name); + library.setUrl(url); + + return library; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java new file mode 100644 index 0000000..706d415 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/ProcessorEntry.java @@ -0,0 +1,42 @@ +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.minecraft.Side; +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 localFilesMap = Maps.newHashMap(); + for (DownloadableFile downloadableFile : loaderManifest.getDownloadableFiles()) { + if (downloadableFile.getSide() != Side.CLIENT) continue; + + DownloadableFile.LocalFile localFile = downloadableFile.download(installer, getManifest()); + + localFilesMap.put(localFile.getName(), localFile); + } + + installer.queueLate(new ProcessorTask(processor, loaderManifest, getManifest(), localFilesMap)); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/SidedData.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/SidedData.java new file mode 100644 index 0000000..ea06805 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/SidedData.java @@ -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 T client; + private T server; + + public T resolveFor(Side side) { + switch (side) { + case CLIENT: return client; + case SERVER: return server; + default: return null; + } + } + + public static SidedData of(T singleValue) { + return new SidedData(singleValue, singleValue); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java index f9872c4..7d383bc 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java @@ -6,18 +6,36 @@ package com.skcraft.launcher.model.loader; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.common.base.Splitter; +import com.skcraft.launcher.model.minecraft.GameArgument; import com.skcraft.launcher.model.minecraft.Library; +import com.skcraft.launcher.model.minecraft.MinecraftArguments; import lombok.Data; +import java.util.ArrayList; import java.util.List; @Data @JsonIgnoreProperties(ignoreUnknown = true) public class VersionInfo { - - private String minecraftArguments; + private String id; + private MinecraftArguments arguments; private String mainClass; private List libraries; + @JsonIgnore private transient boolean overridingArguments; + + public void setMinecraftArguments(String argumentString) { + MinecraftArguments minecraftArguments = new MinecraftArguments(); + minecraftArguments.setGameArguments(new ArrayList()); + + for (String arg : Splitter.on(' ').split(argumentString)) { + minecraftArguments.getGameArguments().add(new GameArgument(arg)); + } + + setArguments(minecraftArguments); + setOverridingArguments(true); + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/FabricInstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/FabricInstallProfile.java new file mode 100644 index 0000000..3219ebd --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/FabricInstallProfile.java @@ -0,0 +1,18 @@ +package com.skcraft.launcher.model.loader.profiles; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.skcraft.launcher.model.loader.ExtendedSidedData; +import com.skcraft.launcher.model.loader.SidedData; +import com.skcraft.launcher.model.minecraft.Library; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class FabricInstallProfile { + private int version; + private Library loader; + private ExtendedSidedData> libraries; + private SidedData mainClass; +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/LegacyInstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/LegacyInstallProfile.java new file mode 100644 index 0000000..8b0f1d5 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/LegacyInstallProfile.java @@ -0,0 +1,21 @@ +package com.skcraft.launcher.model.loader.profiles; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.skcraft.launcher.model.loader.VersionInfo; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class LegacyInstallProfile { + @JsonProperty("install") + private InstallData installData; + private VersionInfo versionInfo; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class InstallData { + private String path; + private String filePath; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java new file mode 100644 index 0000000..93594af --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/profiles/ModernForgeInstallProfile.java @@ -0,0 +1,36 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.loader.profiles; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.loader.InstallProcessor; +import com.skcraft.launcher.model.loader.ProcessorEntry; +import com.skcraft.launcher.model.loader.SidedData; +import com.skcraft.launcher.model.minecraft.Library; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModernForgeInstallProfile { + private List libraries; + private List processors; + private Map> data; + + public List toProcessorEntries(final String loaderName) { + return Lists.transform(getProcessors(), new Function() { + @Override + public ProcessorEntry apply(InstallProcessor input) { + return new ProcessorEntry(loaderName, input); + } + }); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/FeatureList.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/FeatureList.java new file mode 100644 index 0000000..a32bcc1 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/FeatureList.java @@ -0,0 +1,36 @@ +package com.skcraft.launcher.model.minecraft; + +import com.google.common.collect.Maps; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * List of enabled features for Minecraft feature rules. + */ +@NoArgsConstructor +public class FeatureList { + protected Map features = Maps.newHashMap(); + + public boolean doesMatch(Map features) { + for (Map.Entry entry : features.entrySet()) { + if (!entry.getValue().equals(this.features.get(entry.getKey()))) { + return false; + } + } + + return true; + } + + public boolean hasFeature(String key) { + return features.get(key) != null && features.get(key); + } + + public static class Mutable extends FeatureList { + public void addFeature(String key, boolean value) { + features.put(key, value); + } + } + + public static final FeatureList EMPTY = new FeatureList(); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/GameArgument.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/GameArgument.java new file mode 100644 index 0000000..5c4046d --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/GameArgument.java @@ -0,0 +1,54 @@ +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 com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.skcraft.launcher.model.minecraft.mapper.ArgumentValueDeserializer; +import com.skcraft.launcher.model.minecraft.mapper.ArgumentValueSerializer; +import com.skcraft.launcher.util.Environment; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +public class GameArgument { + @JsonProperty("value") + @JsonDeserialize(using = ArgumentValueDeserializer.class) + @JsonSerialize(using = ArgumentValueSerializer.class) + private List values; + private List rules; + + public GameArgument(List values) { + this.values = values; + } + + public GameArgument(String value) { + this.values = Lists.newArrayList(value); + } + + @JsonIgnore + public String getJoinedValue() { + return Joiner.on(' ').join(values); + } + + public boolean shouldApply(Environment environment, FeatureList featureList) { + if (getRules() == null) return true; + + boolean result = false; + + for (Rule rule : rules) { + if (rule.matches(environment, featureList)) { + result = rule.isAllowed(); + } + } + + return result; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java index ccb13de..a5e0047 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Library.java @@ -6,27 +6,21 @@ package com.skcraft.launcher.model.minecraft; -import com.fasterxml.jackson.annotation.*; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +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; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; @Data @JsonIgnoreProperties(ignoreUnknown = true) 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 natives; private Extract extract; private List rules; @@ -37,28 +31,13 @@ // 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; if (getRules() != null) { for (Rule rule : getRules()) { - if (rule.matches(environment)) { - allow = rule.getAction() == Action.ALLOW; + if (rule.matches(environment, FeatureList.EMPTY)) { + allow = rule.isAllowed(); } } } else { @@ -68,60 +47,64 @@ 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) { + public String getNativeString(Environment environment) { if (getNatives() != null) { - switch (platform) { + String nativeString; + + switch (environment.getPlatform()) { case LINUX: - return getNatives().get("linux"); + nativeString = getNatives().get("linux"); + break; case WINDOWS: - return getNatives().get("windows"); + nativeString = getNatives().get("windows"); + break; case MAC_OS_X: - return getNatives().get("osx"); + nativeString = getNatives().get("osx"); + break; default: return null; } + + return nativeString.replace("${arch}", environment.getArchBits()); } else { return null; } } - public String getFilename(Environment environment) { - String nativeString = getNativeString(environment.getPlatform()); - if (nativeString != null) { - return String.format("%s-%s-%s.jar", - getArtifact(), getVersion(), nativeString); + /** + * BACKWARDS COMPATIBILITY: + * Some library definitions only come with a "name" key and don't trigger any other compatibility measures. + * Therefore, if a library has no artifacts when this is called, we call {@link #setServerreq)} to trigger + * artifact generation that assumes the source is the Minecraft libraries URL. + * There is also some special handling for natives in this function; if we have no extra artifacts (newer specs + * put this in "classifiers" in the download list) then we make up an artifact by adding a maven classifier to + * the library name and using that. + */ + public Artifact getArtifact(Environment environment) { + if (getDownloads() == null) { + setServerreq(true); // BACKWARDS COMPATIBILITY } - return String.format("%s-%s.jar", getArtifact(), getVersion()); + String nativeString = getNativeString(environment); + + if (nativeString != null) { + if (getDownloads().getClassifiers() == null) { + // BACKWARDS COMPATIBILITY: make up a virtual artifact + Artifact virtualArtifact = new Artifact(); + virtualArtifact.setUrl(getDownloads().getArtifact().getUrl()); + virtualArtifact.setPath(mavenNameToPath(name + ":" + nativeString)); + + return virtualArtifact; + } + + 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 @@ -134,6 +117,10 @@ if (name != null ? !name.equals(library.name) : library.name != null) return false; + // If libraries have different natives lists, they should be separate. + if (natives != null ? !natives.equals(library.natives) : library.natives != null) + return false; + return true; } @@ -143,55 +130,81 @@ } @Data - public static class Rule { - private Action action; - private OS os; - - public boolean matches(Environment environment) { - if (getOs() == null) { - return true; - } else { - return getOs().matches(environment); - } - } - } - - @Data - public static class OS { - private Platform platform; - private Pattern version; - - @JsonProperty("name") - @JsonDeserialize(using = PlatformDeserializer.class) - @JsonSerialize(using = PlatformSerializer.class) - public Platform getPlatform() { - return platform; - } - - public boolean matches(Environment environment) { - return (getPlatform() == null || getPlatform().equals(environment.getPlatform())) && - (getVersion() == null || getVersion().matcher(environment.getPlatformVersion()).matches()); - } - } - - @Data public static class Extract { private List exclude; } - private enum Action { - ALLOW, - DISALLOW; + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Artifact { + private String path; + private String url; + private String sha1; + } - @JsonCreator - public static Action fromJson(String text) { - return valueOf(text.toUpperCase()); - } + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Downloads { + private Artifact artifact; + private Map classifiers; + } - @JsonValue - public String toJson() { - return name().toLowerCase(); + /** + * BACKWARDS COMPATIBILITY: + * Various sources use the old-style library specification, where there are two keys - "name" and "url", + * rather than the newer multiple-artifact style. This setter is called by Jackson when the "url" property + * is present, and uses it to create a "virtual" artifact using the URL given to us here plus the library + * name parsed out into a path. + */ + public void setUrl(String url) { + Artifact virtualArtifact = new Artifact(); + + virtualArtifact.setUrl(url); + virtualArtifact.setPath(mavenNameToPath(name)); + + Downloads downloads = new Downloads(); + downloads.setArtifact(virtualArtifact); + + setDownloads(downloads); + } + + /** + * BACKWARDS COMPATIBILITY: + * Some old Forge distributions use a parameter called "serverreq" to indicate that the dependency should + * be fetched from the Minecraft library source; this setter handles that. + */ + public void setServerreq(boolean value) { + if (value) { + setUrl("https://libraries.minecraft.net/"); // TODO get this from properties? } } + public static String mavenNameToPath(String mavenName) { + List 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 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); + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java new file mode 100644 index 0000000..5a5ba50 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/MinecraftArguments.java @@ -0,0 +1,25 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.skcraft.launcher.model.minecraft.mapper.MinecraftArgumentsDeserializer; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class MinecraftArguments { + @JsonProperty("game") + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + @JsonDeserialize(contentUsing = MinecraftArgumentsDeserializer.class) + private List gameArguments; + + @JsonProperty("jvm") + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + @JsonDeserialize(contentUsing = MinecraftArgumentsDeserializer.class) + private List jvmArguments; +} + diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/PlatformDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/PlatformDeserializer.java deleted file mode 100644 index 5eb8a51..0000000 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/PlatformDeserializer.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.model.minecraft; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.skcraft.launcher.util.Platform; - -import java.io.IOException; - -public class PlatformDeserializer extends JsonDeserializer { - - @Override - public Platform deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - String text = jsonParser.getText(); - if (text.equalsIgnoreCase("windows")) { - return Platform.WINDOWS; - } else if (text.equalsIgnoreCase("linux")) { - return Platform.LINUX; - } else if (text.equalsIgnoreCase("solaris")) { - return Platform.SOLARIS; - } else if (text.equalsIgnoreCase("osx")) { - return Platform.MAC_OS_X; - } else { - throw new IOException("Unknown platform: " + text); - } - } - -} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/PlatformSerializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/PlatformSerializer.java deleted file mode 100644 index 91a44b1..0000000 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/PlatformSerializer.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SK's Minecraft Launcher - * Copyright (C) 2010-2014 Albert Pham and contributors - * Please see LICENSE.txt for license information. - */ - -package com.skcraft.launcher.model.minecraft; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.skcraft.launcher.util.Platform; - -import java.io.IOException; - -public class PlatformSerializer extends JsonSerializer { - - @Override - public void serialize(Platform platform, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) - throws IOException, JsonProcessingException { - switch (platform) { - case WINDOWS: - jsonGenerator.writeString("windows"); - break; - case MAC_OS_X: - jsonGenerator.writeString("osx"); - break; - case LINUX: - jsonGenerator.writeString("linux"); - break; - case SOLARIS: - jsonGenerator.writeString("solaris"); - break; - case UNKNOWN: - jsonGenerator.writeNull(); - break; - } - } - -} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Rule.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Rule.java new file mode 100644 index 0000000..097292f --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Rule.java @@ -0,0 +1,77 @@ +package com.skcraft.launcher.model.minecraft; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.skcraft.launcher.model.minecraft.mapper.PlatformDeserializer; +import com.skcraft.launcher.model.minecraft.mapper.PlatformSerializer; +import com.skcraft.launcher.util.Environment; +import com.skcraft.launcher.util.Platform; +import lombok.Data; + +import java.util.Map; +import java.util.regex.Pattern; + +@Data +public class Rule { + private Action action; + private OS os; + private Map features; + + private boolean doesOsMatch(Environment environment) { + if (getOs() == null) { + return true; + } else { + return getOs().matches(environment); + } + } + + private boolean doFeaturesMatch(FeatureList match) { + if (getFeatures() == null) return true; + + return match.doesMatch(features); + } + + public boolean matches(Environment environment, FeatureList match) { + return doesOsMatch(environment) && doFeaturesMatch(match); + } + + @JsonIgnore + public boolean isAllowed() { + return action == Action.ALLOW; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class OS { + private Platform platform; + private Pattern version; + + @JsonProperty("name") + @JsonDeserialize(using = PlatformDeserializer.class) + @JsonSerialize(using = PlatformSerializer.class) + public Platform getPlatform() { + return platform; + } + + public boolean matches(Environment environment) { + return (getPlatform() == null || getPlatform().equals(environment.getPlatform())) && + (getVersion() == null || getVersion().matcher(environment.getPlatformVersion()).matches()); + } + } + + public enum Action { + ALLOW, + DISALLOW; + + @JsonCreator + public static Action fromJson(String text) { + return valueOf(text.toUpperCase()); + } + + @JsonValue + public String toJson() { + return name().toLowerCase(); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Side.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Side.java new file mode 100644 index 0000000..94079b2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Side.java @@ -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; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Version.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Version.java index c01f6cb..a580df2 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Version.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/Version.java @@ -20,6 +20,11 @@ @NonNull private String id; + @Getter + @Setter + @NonNull + private String url; + public Version() { } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java index 283bb8a..d0fad1a 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java @@ -6,12 +6,12 @@ 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 com.google.common.base.Splitter; import lombok.Data; -import java.util.Date; -import java.util.LinkedHashSet; +import java.util.*; @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -21,16 +21,56 @@ private Date time; private Date releaseTime; private String assets; + private AssetIndex assetIndex; private String type; - private String processArguments; - private String minecraftArguments; + private MinecraftArguments arguments; private String mainClass; private int minimumLauncherVersion; private LinkedHashSet libraries; + private Map downloads = new HashMap(); - @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; + } + + public void setMinecraftArguments(String minecraftArguments) { + MinecraftArguments result = new MinecraftArguments(); + result.setGameArguments(new ArrayList()); + result.setJvmArguments(new ArrayList()); + + for (String arg : Splitter.on(' ').split(minecraftArguments)) { + result.getGameArguments().add(new GameArgument(arg)); + } + + setArguments(result); + } + + @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; + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java new file mode 100644 index 0000000..64f218e --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueDeserializer.java @@ -0,0 +1,35 @@ +package com.skcraft.launcher.model.minecraft.mapper; + +import com.beust.jcommander.internal.Lists; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class ArgumentValueDeserializer extends StdDeserializer> { + protected ArgumentValueDeserializer() { + super(TypeFactory.defaultInstance().constructCollectionType(List.class, String.class)); + } + + @Override + public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (!jp.hasCurrentToken()) jp.nextToken(); + + if (jp.getCurrentToken() == JsonToken.START_ARRAY) { + String[] allValues = jp.readValueAs(String[].class); + return Arrays.asList(allValues); + } else if (jp.getCurrentToken() == JsonToken.VALUE_STRING) { + String value = jp.readValueAs(String.class); + return Lists.newArrayList(value); + } + + throw new InvalidFormatException("Invalid JSON type for deserializer (not string or array)", null, List.class); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueSerializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueSerializer.java new file mode 100644 index 0000000..94b00b2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/ArgumentValueSerializer.java @@ -0,0 +1,25 @@ +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import java.io.IOException; +import java.util.List; + +public class ArgumentValueSerializer extends StdSerializer> { + protected ArgumentValueSerializer() { + super(TypeFactory.defaultInstance().constructCollectionType(List.class, String.class)); + } + + @Override + public void serialize(List value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { + if (value.size() == 1) { + jgen.writeString(value.get(0)); + } else { + provider.defaultSerializeValue(value, jgen); + } + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java new file mode 100644 index 0000000..a6436a2 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/MinecraftArgumentsDeserializer.java @@ -0,0 +1,31 @@ +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.skcraft.launcher.model.minecraft.GameArgument; + +import java.io.IOException; + +public class MinecraftArgumentsDeserializer extends StdDeserializer { + protected MinecraftArgumentsDeserializer() { + super(GameArgument.class); + } + + @Override + public GameArgument deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (!jp.hasCurrentToken()) jp.nextToken(); + + if (jp.getCurrentToken() == JsonToken.START_OBJECT) { + return jp.readValueAs(GameArgument.class); + } else if (jp.getCurrentToken() == JsonToken.VALUE_STRING) { + String argument = jp.getValueAsString(); + return new GameArgument(argument); + } + + throw new InvalidFormatException("Invalid JSON type for deserializer (not string or object)", null, GameArgument.class); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformDeserializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformDeserializer.java new file mode 100644 index 0000000..6fd9dc7 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformDeserializer.java @@ -0,0 +1,35 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.skcraft.launcher.util.Platform; + +import java.io.IOException; + +public class PlatformDeserializer extends JsonDeserializer { + + @Override + public Platform deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + String text = jsonParser.getText(); + if (text.equalsIgnoreCase("windows")) { + return Platform.WINDOWS; + } else if (text.equalsIgnoreCase("linux")) { + return Platform.LINUX; + } else if (text.equalsIgnoreCase("solaris")) { + return Platform.SOLARIS; + } else if (text.equalsIgnoreCase("osx")) { + return Platform.MAC_OS_X; + } else { + throw new IOException("Unknown platform: " + text); + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformSerializer.java b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformSerializer.java new file mode 100644 index 0000000..d703734 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/minecraft/mapper/PlatformSerializer.java @@ -0,0 +1,41 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham and contributors + * Please see LICENSE.txt for license information. + */ + +package com.skcraft.launcher.model.minecraft.mapper; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.skcraft.launcher.util.Platform; + +import java.io.IOException; + +public class PlatformSerializer extends JsonSerializer { + + @Override + public void serialize(Platform platform, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException, JsonProcessingException { + switch (platform) { + case WINDOWS: + jsonGenerator.writeString("windows"); + break; + case MAC_OS_X: + jsonGenerator.writeString("osx"); + break; + case LINUX: + jsonGenerator.writeString("linux"); + break; + case SOLARIS: + jsonGenerator.writeString("solaris"); + break; + case UNKNOWN: + jsonGenerator.writeNull(); + break; + } + } + +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/DownloadableFile.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/DownloadableFile.java new file mode 100644 index 0000000..1eddaf0 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/DownloadableFile.java @@ -0,0 +1,34 @@ +package com.skcraft.launcher.model.modpack; + +import com.skcraft.launcher.install.Installer; +import com.skcraft.launcher.model.minecraft.Side; +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 Side side; + 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; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java index cac0f7f..49ec313 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/Manifest.java @@ -12,8 +12,9 @@ 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,13 +23,15 @@ 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) public class Manifest extends BaseManifest { - public static final int MIN_PROTOCOL_VERSION = 2; + public static final int MIN_PROTOCOL_VERSION = 3; private int minimumVersion; private URL baseUrl; @@ -43,6 +46,7 @@ @Getter @Setter @JsonIgnore private Installer installer; private VersionManifest versionManifest; + private Map loaders = new HashMap(); @JsonIgnore public URL getLibrariesUrl() { diff --git a/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java b/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java index 8d06fad..e4f503a 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/modpack/ManifestEntry.java @@ -12,6 +12,7 @@ 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 @@ 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") diff --git a/launcher/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java b/launcher/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java index 0557004..4155df6 100644 --- a/launcher/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java +++ b/launcher/src/main/java/com/skcraft/launcher/selfupdate/SelfUpdater.java @@ -48,7 +48,7 @@ installer.queue(new FileMover(tempFile, file)); progress = installer; - installer.execute(); + installer.execute(launcher); return file; } finally { diff --git a/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java b/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java index 61f3dd1..2af4d07 100644 --- a/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java +++ b/launcher/src/main/java/com/skcraft/launcher/update/BaseUpdater.java @@ -7,6 +7,7 @@ package com.skcraft.launcher.update; import com.google.common.base.Strings; +import com.google.common.collect.Iterables; import com.skcraft.launcher.AssetsRoot; import com.skcraft.launcher.Instance; import com.skcraft.launcher.Launcher; @@ -14,6 +15,7 @@ import com.skcraft.launcher.dialog.FeatureSelectionDialog; import com.skcraft.launcher.dialog.ProgressDialog; import com.skcraft.launcher.install.*; +import com.skcraft.launcher.model.loader.LoaderManifest; import com.skcraft.launcher.model.minecraft.Asset; import com.skcraft.launcher.model.minecraft.AssetsIndex; import com.skcraft.launcher.model.minecraft.Library; @@ -201,11 +203,17 @@ } protected void installLibraries(@NonNull Installer installer, - @NonNull VersionManifest versionManifest, + @NonNull Manifest manifest, @NonNull File librariesDir, @NonNull List sources) throws InterruptedException { + VersionManifest versionManifest = manifest.getVersionManifest(); - for (Library library : versionManifest.getLibraries()) { + Iterable allLibraries = versionManifest.getLibraries(); + for (LoaderManifest loader : manifest.getLoaders().values()) { + allLibraries = Iterables.concat(allLibraries, loader.getLibraries()); + } + + for (Library library : allLibraries) { if (library.matches(environment)) { checkInterrupted(); @@ -224,8 +232,8 @@ 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)); } } } diff --git a/launcher/src/main/java/com/skcraft/launcher/update/Updater.java b/launcher/src/main/java/com/skcraft/launcher/update/Updater.java index 70fa583..083dfde 100644 --- a/launcher/src/main/java/com/skcraft/launcher/update/Updater.java +++ b/launcher/src/main/java/com/skcraft/launcher/update/Updater.java @@ -14,6 +14,8 @@ import com.skcraft.launcher.Launcher; import com.skcraft.launcher.LauncherException; import com.skcraft.launcher.install.Installer; +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.persistence.Persistence; @@ -100,26 +102,48 @@ return instance; } + /** + * Check whether the package manifest contains an embedded version manifest, + * otherwise we'll have to download the one for the given Minecraft version. + * + * BACKWARDS COMPATIBILITY: + * Old manifests have an embedded version manifest without the minecraft JARs list present. + * If we find a manifest without that jar list, fetch the newer copy from launchermeta and use the list from that. + * We can't just replace the manifest outright because library versions might differ and that screws up Runner. + */ private VersionManifest readVersionManifest(Manifest manifest) throws IOException, InterruptedException { - // Check whether the package manifest contains an embedded version manifest, - // otherwise we'll have to download the one for the given Minecraft version VersionManifest version = manifest.getVersionManifest(); - if (version != null) { - mapper.writeValue(instance.getVersionPath(), version); - return version; - } else { - URL url = url(String.format( - launcher.getProperties().getProperty("versionManifestUrl"), - manifest.getGameVersion())); + URL url = url(launcher.getProperties().getProperty("versionManifestUrl")); - return HttpRequest - .get(url) - .execute() - .expectResponseCode(200) - .returnContent() - .saveContent(instance.getVersionPath()) - .asJson(VersionManifest.class); + if (version == null) { + version = fetchVersionManifest(url, manifest); } + + if (version.getDownloads().isEmpty()) { + // Backwards compatibility hack + VersionManifest otherManifest = fetchVersionManifest(url, manifest); + + version.setDownloads(otherManifest.getDownloads()); + version.setAssetIndex(otherManifest.getAssetIndex()); + } + + mapper.writeValue(instance.getVersionPath(), version); + return version; + } + + private static VersionManifest fetchVersionManifest(URL url, Manifest manifest) throws IOException, InterruptedException { + ReleaseList releases = HttpRequest.get(url) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(ReleaseList.class); + + Version relVersion = releases.find(manifest.getGameVersion()); + return HttpRequest.get(url(relVersion.getUrl())) + .execute() + .expectResponseCode(200) + .returnContent() + .asJson(VersionManifest.class); } /** @@ -152,7 +176,7 @@ // 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,16 +186,16 @@ 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")); - installLibraries(installer, version, launcher.getLibrariesDir(), librarySources); + installLibraries(installer, manifest, launcher.getLibrariesDir(), librarySources); // 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 +203,9 @@ log.info("Executing install phase..."); progress = ProgressFilter.between(installer, 0.98, 1); - installer.execute(); + installer.execute(launcher); + + installer.executeLate(launcher); log.info("Completing..."); complete(); diff --git a/launcher/src/main/java/com/skcraft/launcher/util/FileUtils.java b/launcher/src/main/java/com/skcraft/launcher/util/FileUtils.java new file mode 100644 index 0000000..35a9227 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/util/FileUtils.java @@ -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; + } +} diff --git a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties index 99c31e4..23b54eb 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -146,6 +146,7 @@ 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. diff --git a/launcher/src/main/resources/com/skcraft/launcher/launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties index f434d2c..67119ca 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/launcher.properties @@ -6,12 +6,11 @@ version=${project.version} agentName=Minecraft +launcherShortname=SKCLauncher 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 assetsSource=http://resources.download.minecraft.net/ yggdrasilAuthUrl=https://authserver.mojang.com/authenticate resetPasswordUrl=https://minecraft.net/resetpassword diff --git a/launcher/src/main/resources/com/skcraft/launcher/maven_repos.json b/launcher/src/main/resources/com/skcraft/launcher/maven_repos.json index e7ad706..ec8063e 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/maven_repos.json +++ b/launcher/src/main/resources/com/skcraft/launcher/maven_repos.json @@ -1,5 +1,7 @@ [ "https://libraries.minecraft.net/", "https://repo1.maven.org/maven2/", + "https://files.minecraftforge.net/maven/", + "https://maven.fabricmc.net/", "http://maven.apache.org/" ] diff --git a/readme/now_with_support.png b/readme/now_with_support.png new file mode 100644 index 0000000..929dfba --- /dev/null +++ b/readme/now_with_support.png Binary files differ