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 422248f7d..dd208777d 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 @@ -168,7 +168,9 @@ private void processLoader(LinkedHashSet loaderLibraries, File file, Fi processor = new ModernForgeLoaderProcessor(); } } else if (BuilderUtils.getZipEntry(jarFile, "fabric-installer.json") != null) { - processor = new FabricLoaderProcessor(); + processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.FABRIC); + } else if (BuilderUtils.getZipEntry(jarFile, "quilt_installer.json") != null) { + processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.QUILT); } } finally { closer.close(); @@ -205,8 +207,8 @@ public void downloadLibraries(File librariesDir) throws IOException, Interrupted Files.createParentDirs(outputPath); boolean found = false; - // Try just the URL, it might be a full URL to the file - if (!artifact.getUrl().isEmpty()) { + // If URL doesn't end with a /, it might be the direct file + if (!artifact.getUrl().endsWith("/")) { found = tryDownloadLibrary(library, artifact, artifact.getUrl(), outputPath); } @@ -261,7 +263,9 @@ private boolean tryDownloadLibrary(Library library, Library.Artifact artifact, S try { log.info("Downloading library " + library.getName() + " from " + url + "..."); - HttpRequest.get(url).execute().expectResponseCode(200).saveContent(tempFile); + HttpRequest.get(url).execute().expectResponseCode(200) + .expectContentType("application/java-archive", "application/octet-stream") + .saveContent(tempFile); } catch (IOException e) { log.info("Could not get file from " + url + ": " + e.getMessage()); return false; 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 index a5d3cfb28..f3362a6a3 100644 --- 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 @@ -4,10 +4,13 @@ import com.google.common.io.Closer; import com.skcraft.launcher.builder.BuilderUtils; import com.skcraft.launcher.model.loader.FabricMod; +import com.skcraft.launcher.model.loader.QuiltMod; +import com.skcraft.launcher.model.loader.Versionable; import com.skcraft.launcher.model.minecraft.Library; import com.skcraft.launcher.model.minecraft.VersionManifest; import com.skcraft.launcher.model.modpack.Manifest; import com.skcraft.launcher.util.HttpRequest; +import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; import java.io.File; @@ -18,7 +21,10 @@ import java.util.zip.ZipEntry; @Log +@RequiredArgsConstructor public class FabricLoaderProcessor implements ILoaderProcessor { + private final Variant variant; + @Override public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException { JarFile jarFile = new JarFile(loaderJar); @@ -26,24 +32,23 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp Closer closer = Closer.create(); try { - String loaderVersion; + Versionable loaderMod; - ZipEntry modEntry = BuilderUtils.getZipEntry(jarFile, "fabric.mod.json"); + ZipEntry modEntry = BuilderUtils.getZipEntry(jarFile, variant.modJsonName); if (modEntry != null) { InputStreamReader reader = new InputStreamReader(jarFile.getInputStream(modEntry)); - FabricMod loaderMod = mapper.readValue( - BuilderUtils.readStringFromStream(closer.register(reader)), FabricMod.class); - loaderVersion = loaderMod.getVersion(); + loaderMod = mapper.readValue( + BuilderUtils.readStringFromStream(closer.register(reader)), variant.mappedClass); } else { - log.warning("Fabric loader has no 'fabric.mod.json' file, is it really a Fabric Loader jar?"); + log.warning(String.format("%s loader has no '%s' file, is it really a %s Loader jar?", + variant.friendlyName, variant.modJsonName, variant.friendlyName)); return null; } - log.info("Downloading fabric metadata..."); + log.info(String.format("Downloading %s metadata...", variant.friendlyName)); URL metaUrl = HttpRequest.url( - String.format("https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json", - manifest.getGameVersion(), loaderVersion)); + String.format(variant.metaUrl, manifest.getGameVersion(), loaderMod.getVersion())); VersionManifest fabricManifest = HttpRequest.get(metaUrl) .execute() .expectResponseCode(200) @@ -51,6 +56,20 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp .asJson(VersionManifest.class); for (Library library : fabricManifest.getLibraries()) { + // To quote a famous comment: "And here we come upon a sad state of affairs." + // Quilt's meta API returns broken data about how to launch the game. It specifies its own incomplete + // and ultimately broken set of intermediary mappings called "hashed". If the loader finds "hashed" in + // the classpath it tries to use it and blows up because it doesn't work. + // We work around this here by just throwing the hashed library out - they do now at least specify + // fabric's intermediary mappings in the library list, which DO work. + // Historical note: previously they didn't do this! Every launcher that added Quilt support had to add + // a hack that replaced hashed with intermediary! This is a lot of technical debt that is gonna come + // back to bite them in the ass later, because it's all still there! + // TODO pester Quilt again about fixing this.... + if (library.getName().startsWith("org.quiltmc:hashed") && loaderMod instanceof QuiltMod) { + continue; + } + result.getLoaderLibraries().add(library); log.info("Adding loader library " + library.getName()); } @@ -61,7 +80,8 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp log.info("Using main class " + mainClass); } } catch (InterruptedException e) { - log.warning("HTTP request to fabric metadata API was interrupted, this will probably not work!"); + log.warning(String.format("HTTP request to %s metadata API was interrupted!", variant.friendlyName)); + throw new IOException(e); } finally { closer.close(); jarFile.close(); @@ -69,4 +89,15 @@ public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapp return result; } + + @RequiredArgsConstructor + public enum Variant { + FABRIC("Fabric", "fabric.mod.json", "https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json", FabricMod.class), + QUILT("Quilt", "quilt.mod.json", "https://meta.quiltmc.org/v3/versions/loader/%s/%s/profile/json", QuiltMod.class); + + private final String friendlyName; + private final String modJsonName; + private final String metaUrl; + private final Class mappedClass; + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java index c354964f0..0afbb91f0 100644 --- a/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/FabricMod.java @@ -5,7 +5,7 @@ @Data @JsonIgnoreProperties(ignoreUnknown = true) -public class FabricMod { +public class FabricMod implements Versionable { private String id; private String name; private String version; diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java new file mode 100644 index 000000000..6d0169b39 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/QuiltMod.java @@ -0,0 +1,29 @@ +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 QuiltMod implements Versionable { + @JsonProperty("schema_version") + private int schemaVersion; + + @JsonProperty("quilt_loader") + private Mod meta; + + @Override + public String getVersion() { + return meta.getVersion(); + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Mod { + private String version; + + @JsonProperty("intermediate_mappings") + private String intermediateMappings; + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java b/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java new file mode 100644 index 000000000..b2faa2b73 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/model/loader/Versionable.java @@ -0,0 +1,5 @@ +package com.skcraft.launcher.model.loader; + +public interface Versionable { + String getVersion(); +} diff --git a/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java index e97b55814..e1b94d42d 100644 --- a/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java +++ b/launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java @@ -232,6 +232,28 @@ public HttpRequest expectResponseCodeOr(int code, HttpFunc throw exc; } + /** + * Continue if the content type matches, otherwise throw an exception + * + * @param expectedTypes Expected content-type(s) + * @return this object + * @throws IOException Unexpected content-type or other error + */ + public HttpRequest expectContentType(String... expectedTypes) throws IOException { + if (conn == null) throw new IllegalArgumentException("No connection has been made!"); + + String contentType = conn.getHeaderField("Content-Type"); + for (String expectedType : expectedTypes) { + if (expectedType.equals(contentType)) { + return this; + } + } + + close(); + throw new IOException(String.format("Did not get expected content type '%s', instead got '%s'.", + String.join(" | ", expectedTypes), contentType)); + } + /** * Get the response code. *