Skip to content

Commit

Permalink
SKCraftGH-481 Add Quilt loader support
Browse files Browse the repository at this point in the history
... with a caveat that it may break in future due to Quilt's broken
metadata
  • Loading branch information
hedgehog1029 committed Aug 7, 2022
1 parent 16f44a8 commit 2a6c5ee
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ private void processLoader(LinkedHashSet<Library> 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();
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,39 +21,55 @@
import java.util.zip.ZipEntry;

@Log
@RequiredArgsConstructor
public class FabricLoaderProcessor implements ILoaderProcessor {
private final Variant variant;

@Override
public LoaderResult process(File loaderJar, Manifest manifest, ObjectMapper mapper, File baseDir) throws IOException {
JarFile jarFile = new JarFile(loaderJar);
LoaderResult result = new LoaderResult();
Closer closer = Closer.create();

try {
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)
.returnContent()
.asJson(VersionManifest.class);

for (Library library : fabricManifest.getLibraries()) {
// To quote a famous comment: "And here we come upon a sad state of affairs."
// Quilt's meta API returns broken data about how to launch the game. It specifies its own incomplete
// and ultimately broken set of intermediary mappings called "hashed". If the loader finds "hashed" in
// the classpath it tries to use it and blows up because it doesn't work.
// We work around this here by just throwing the hashed library out - they do now at least specify
// fabric's intermediary mappings in the library list, which DO work.
// Historical note: previously they didn't do this! Every launcher that added Quilt support had to add
// a hack that replaced hashed with intermediary! This is a lot of technical debt that is gonna come
// back to bite them in the ass later, because it's all still there!
// TODO pester Quilt again about fixing this....
if (library.getName().startsWith("org.quiltmc:hashed") && loaderMod instanceof QuiltMod) {
continue;
}

result.getLoaderLibraries().add(library);
log.info("Adding loader library " + library.getName());
}
Expand All @@ -61,12 +80,24 @@ 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();
}

return result;
}

@RequiredArgsConstructor
public enum Variant {
FABRIC("Fabric", "fabric.mod.json", "https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json", FabricMod.class),
QUILT("Quilt", "quilt.mod.json", "https://meta.quiltmc.org/v3/versions/loader/%s/%s/profile/json", QuiltMod.class);

private final String friendlyName;
private final String modJsonName;
private final String metaUrl;
private final Class<? extends Versionable> mappedClass;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.skcraft.launcher.model.loader;

public interface Versionable {
String getVersion();
}
22 changes: 22 additions & 0 deletions launcher/src/main/java/com/skcraft/launcher/util/HttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,28 @@ public <E extends Exception> 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.
*
Expand Down

0 comments on commit 2a6c5ee

Please sign in to comment.