Improve cache file detectors.

This commit is contained in:
ZekerZhayard 2020-07-14 03:56:34 +08:00
parent 9f65af36b0
commit 29f602904a
9 changed files with 327 additions and 38 deletions

View file

@ -4,6 +4,14 @@ Allow [MultiMC](https://github.com/MultiMC/MultiMC5) to launch Minecraft 1.13+ w
**ForgeWrapper has been adopted by MultiMC, you do not need to perform the following steps manually. (2020-03-29)**
## For other launchers
1. ForgeWrapper provides some java properties since 1.4.2:
- `forgewrapper.librariesDir` : a path to libraries folder (e.g. -Dforgewrapper.librariesDir=/home/xxx/.minecraft/libraries)
- `forgewrapper.installer` : a path to forge installer (e.g. -Dforgewrapper.installer=/home/xxx/forge-1.14.4-28.2.0-installer.jar)
- `forgewrapper.minecraft` : a path to the vanilla minecraft jar (e.g. -Dforgewrapper.minecraft=/home/xxx/.minecraft/versions/1.14.4/1.14.4.jar)
2. ForgeWrapper also provides an interface [`IFileDetector`](https://github.com/ZekerZhayard/ForgeWrapper/blob/master/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java), you can implement it and custom your own detecting rules. To load it, you should make another jar which contains `META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector` within the full implementation class name and add the jar to class path.
## How to use (Outdated)
1. Download Forge installer for Minecraft 1.13+ [here](https://files.minecraftforge.net/).

View file

@ -5,7 +5,7 @@ apply plugin: "idea"
sourceCompatibility = targetCompatibility = 1.8
version = "1.4.1"
version = "1.4.2"
group = "io.github.zekerzhayard"
archivesBaseName = rootProject.name

View file

@ -8,14 +8,17 @@ import net.minecraftforge.installer.actions.ProgressCallback;
import net.minecraftforge.installer.json.Install;
public class ClientInstall4MultiMC extends ClientInstall {
public ClientInstall4MultiMC(Install profile, ProgressCallback monitor) {
protected File libraryDir;
protected File minecraftJar;
public ClientInstall4MultiMC(Install profile, ProgressCallback monitor, File libraryDir, File minecraftJar) {
super(profile, monitor);
this.libraryDir = libraryDir;
this.minecraftJar = minecraftJar;
}
@Override
public boolean run(File target, Predicate<String> optionals) {
File librariesDir = Main.getLibrariesDir();
File clientTarget = new File(String.format("%s/com/mojang/minecraft/%s/minecraft-%s-client.jar", librariesDir.getAbsolutePath(), this.profile.getMinecraft(), this.profile.getMinecraft()));
return this.processors.process(librariesDir, clientTarget);
return this.processors.process(this.libraryDir, this.minecraftJar);
}
}

View file

@ -1,11 +1,13 @@
package io.github.zekerzhayard.forgewrapper.installer;
import java.io.File;
import net.minecraftforge.installer.actions.ProgressCallback;
import net.minecraftforge.installer.json.Install;
import net.minecraftforge.installer.json.Util;
public class Installer {
public static boolean install() {
public static boolean install(File libraryDir, File minecraftJar) {
ProgressCallback monitor = ProgressCallback.withOutputs(System.out);
Install install = Util.loadInstallProfile();
if (System.getProperty("java.net.preferIPv4Stack") == null) {
@ -16,6 +18,6 @@ public class Installer {
String jvmVersion = System.getProperty("java.vm.version", "missing jvm version");
monitor.message(String.format("JVM info: %s - %s - %s", vendor, javaVersion, jvmVersion));
monitor.message("java.net.preferIPv4Stack=" + System.getProperty("java.net.preferIPv4Stack"));
return new ClientInstall4MultiMC(install, monitor).run(null, input -> true);
return new ClientInstall4MultiMC(install, monitor, libraryDir, minecraftJar).run(null, input -> true);
}
}

View file

@ -1,7 +1,6 @@
package io.github.zekerzhayard.forgewrapper.installer;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
@ -11,52 +10,46 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import cpw.mods.modlauncher.Launcher;
import io.github.zekerzhayard.forgewrapper.installer.detector.DetectorLoader;
import io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector;
public class Main {
public static void main(String[] args) throws Exception {
List<String> argsList = Stream.of(args).collect(Collectors.toList());
String mcVersion = argsList.get(argsList.indexOf("--fml.mcVersion") + 1);
String mcpFullVersion = mcVersion + "-" + argsList.get(argsList.indexOf("--fml.mcpVersion") + 1);
String forgeFullVersion = mcVersion + "-" + argsList.get(argsList.indexOf("--fml.forgeVersion") + 1);
Path librariesDir = getLibrariesDir().toPath();
Path minecraftDir = librariesDir.resolve("net").resolve("minecraft").resolve("client");
Path forgeDir = librariesDir.resolve("net").resolve("minecraftforge").resolve("forge").resolve(forgeFullVersion);
if (getAdditionalLibraries(minecraftDir, forgeDir, mcVersion, forgeFullVersion, mcpFullVersion).anyMatch(path -> !Files.exists(path))) {
IFileDetector detector = DetectorLoader.loadDetector();
if (!detector.checkExtraFiles(forgeFullVersion)) {
System.out.println("Some extra libraries are missing! Run the installer to generate them now.");
URLClassLoader ucl = URLClassLoader.newInstance(new URL[] {
// Check installer jar.
Path installerJar = detector.getInstallerJar(forgeFullVersion);
if (!IFileDetector.isFile(installerJar)) {
throw new RuntimeException("Can't detect the forge installer!");
}
// Check vanilla Minecraft jar.
Path minecraftJar = detector.getMinecraftJar(mcVersion);
if (!IFileDetector.isFile(minecraftJar)) {
throw new RuntimeException("Can't detect the Minecraft jar!");
}
try (URLClassLoader ucl = URLClassLoader.newInstance(new URL[] {
Main.class.getProtectionDomain().getCodeSource().getLocation(),
Launcher.class.getProtectionDomain().getCodeSource().getLocation(),
forgeDir.resolve("forge-" + forgeFullVersion + "-installer.jar").toUri().toURL()
}, getParentClassLoader());
Class<?> installer = ucl.loadClass("io.github.zekerzhayard.forgewrapper.installer.Installer");
if (!(boolean) installer.getMethod("install").invoke(null)) {
return;
installerJar.toUri().toURL()
}, getParentClassLoader())) {
Class<?> installer = ucl.loadClass("io.github.zekerzhayard.forgewrapper.installer.Installer");
if (!(boolean) installer.getMethod("install", File.class, File.class).invoke(null, detector.getLibraryDir().toFile(), minecraftJar.toFile())) {
return;
}
}
}
Launcher.main(args);
}
public static File getLibrariesDir() {
try {
File laucnher = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().toURI());
// /<version> /modlauncher /mods /cpw /libraries
return laucnher.getParentFile().getParentFile().getParentFile().getParentFile().getParentFile();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
public static Stream<Path> getAdditionalLibraries(Path minecraftDir, Path forgeDir, String mcVersion, String forgeFullVersion, String mcpFullVersion) {
return Stream.of(
forgeDir.resolve("forge-" + forgeFullVersion + "-client.jar"),
minecraftDir.resolve(mcVersion).resolve("client-" + mcVersion + "-extra.jar"),
minecraftDir.resolve(mcpFullVersion).resolve("client-" + mcpFullVersion + "-srg.jar")
);
}
// https://github.com/MinecraftForge/Installer/blob/fe18a164b5ebb15b5f8f33f6a149cc224f446dc2/src/main/java/net/minecraftforge/installer/actions/PostProcessors.java#L287-L303
private static ClassLoader getParentClassLoader() {
if (!System.getProperty("java.version").startsWith("1.")) {

View file

@ -0,0 +1,35 @@
package io.github.zekerzhayard.forgewrapper.installer.detector;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
public class DetectorLoader {
public static IFileDetector loadDetector() {
ServiceLoader<IFileDetector> sl = ServiceLoader.load(IFileDetector.class);
HashMap<String, IFileDetector> detectors = new HashMap<>();
for (IFileDetector detector : sl) {
detectors.put(detector.name(), detector);
}
boolean enabled = false;
IFileDetector temp = null;
for (Map.Entry<String, IFileDetector> detector : detectors.entrySet()) {
HashMap<String, IFileDetector> others = new HashMap<>(detectors);
others.remove(detector.getKey());
if (!enabled) {
enabled = detector.getValue().enabled(others);
if (enabled) {
temp = detector.getValue();
}
} else if (detector.getValue().enabled(others)) {
throw new RuntimeException("There are two or more file detectors are enabled! (" + temp.toString() + ", " + detector.toString() + ")");
}
}
if (temp == null) {
throw new RuntimeException("No file detector is enabled!");
}
return temp;
}
}

View file

@ -0,0 +1,201 @@
package io.github.zekerzhayard.forgewrapper.installer.detector;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import cpw.mods.modlauncher.Launcher;
public interface IFileDetector {
/**
* @return The name of the detector.
*/
String name();
/**
* If there are two or more detectors are enabled, an exception will be thrown. Removing anything from the map is in vain.
* @param others Other detectors.
* @return True represents enabled.
*/
boolean enabled(HashMap<String, IFileDetector> others);
/**
* @return The ".minecraft/libraries" folder for normal. It can also be defined by JVM argument "-Dforgewrapper.librariesDir=&lt;libraries-path&gt;".
*/
default Path getLibraryDir() {
String libraryDir = System.getProperty("forgewrapper.librariesDir");
if (libraryDir != null) {
return Paths.get(libraryDir).toAbsolutePath();
}
try {
Path launcher = Paths.get(Launcher.class.getProtectionDomain().getCodeSource().getLocation().toURI()).toAbsolutePath();
// /<version> /modlauncher/mods /cpw /libraries
return launcher.getParent().getParent().getParent().getParent().getParent().toAbsolutePath();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
/**
* @param forgeFullVersion Forge full version (e.g. 1.14.4-28.2.0).
* @return The forge installer jar path. It can also be defined by JVM argument "-Dforgewrapper.installer=&lt;installer-path&gt;".
*/
default Path getInstallerJar(String forgeFullVersion) {
String installer = System.getProperty("forgewrapper.installer");
if (installer != null) {
return Paths.get(installer).toAbsolutePath();
}
return null;
}
/**
* @param mcVersion Minecraft version (e.g. 1.14.4).
* @return The minecraft client jar path. It can also be defined by JVM argument "-Dforgewrapper.minecraft=&lt;minecraft-path&gt;".
*/
default Path getMinecraftJar(String mcVersion) {
String minecraft = System.getProperty("forgewrapper.minecraft");
if (minecraft != null) {
return Paths.get(minecraft).toAbsolutePath();
}
return null;
}
/**
* @param forgeFullVersion Forge full version (e.g. 1.14.4-28.2.0).
* @return The json object in the-installer-jar-->install_profile.json-->data-->xxx-->client.
*/
default JsonObject getInstallProfileExtraData(String forgeFullVersion) {
Path installer = this.getInstallerJar(forgeFullVersion);
if (isFile(installer)) {
try (ZipFile zf = new ZipFile(installer.toFile())) {
ZipEntry ze = zf.getEntry("install_profile.json");
if (ze != null) {
try (
InputStream is = zf.getInputStream(ze);
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8)
) {
for (Map.Entry<String, JsonElement> entry : new JsonParser().parse(isr).getAsJsonObject().entrySet()) {
if (entry.getKey().equals("data")) {
return entry.getValue().getAsJsonObject();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
throw new RuntimeException("Can't detect the forge installer!");
}
return null;
}
/**
* Check all cached files.
* @param forgeFullVersion Forge full version (e.g. 1.14.4-28.2.0).
* @return True represents all files are ready.
*/
default boolean checkExtraFiles(String forgeFullVersion) {
JsonObject jo = this.getInstallProfileExtraData(forgeFullVersion);
if (jo != null) {
Map<String, Path> libsMap = new HashMap<>();
Map<String, String> hashMap = new HashMap<>();
// Get all "data/<name>/client" elements.
for (Map.Entry<String, JsonElement> entry : jo.entrySet()) {
String clientStr = getElement(entry.getValue().getAsJsonObject(), "client").getAsString();
if (entry.getKey().endsWith("_SHA")) {
Pattern p = Pattern.compile("^'(?<sha1>[A-Za-z0-9]{40})'$");
Matcher m = p.matcher(clientStr);
if (m.find()) {
hashMap.put(entry.getKey(), m.group("sha1"));
}
} else {
Pattern p = Pattern.compile("^\\[(?<groupId>[^:]*):(?<artifactId>[^:]*):(?<version>[^:@]*)(:(?<prefix>[^@]*))?(@(?<type>[^]]*))?]$");
Matcher m = p.matcher(clientStr);
if (m.find()) {
String groupId = nullToDefault(m.group("groupId"), "");
String artifactId = nullToDefault(m.group("artifactId"), "");
String version = nullToDefault(m.group("version"), "");
String prefix = nullToDefault(m.group("prefix"), "");
String type = nullToDefault(m.group("type"), "jar");
libsMap.put(entry.getKey(), this.getLibraryDir()
.resolve(groupId.replace('.', File.separatorChar))
.resolve(artifactId)
.resolve(version)
.resolve(artifactId + "-" + version + (prefix.equals("") ? "" : "-") + prefix + "." + type).toAbsolutePath());
}
}
}
// Check all cached libraries.
boolean checked = true;
for (Map.Entry<String, Path> entry : libsMap.entrySet()) {
checked = checked && this.checkExtraFile(entry.getValue(), hashMap.get(entry.getKey() + "_SHA"));
}
return checked;
}
// Skip installing process if installer profile doesn't exist.
return true;
}
/**
* Check the exact file.
* @param path The path of the file to check.
* @param sha1 The sha1 defined in installer.
* @return True represents the file is ready.
*/
default boolean checkExtraFile(Path path, String sha1) {
return Files.isRegularFile(path) && (sha1 == null || sha1.equals("") || sha1.toLowerCase(Locale.ENGLISH).equals(getFileSHA1(path)));
}
static boolean isFile(Path path) {
return path != null && Files.isRegularFile(path);
}
static JsonElement getElement(JsonObject object, String property) {
Optional<Map.Entry<String, JsonElement>> first = object.entrySet().stream().filter(e -> e.getKey().equals(property)).findFirst();
if (first.isPresent()) {
return first.get().getValue();
}
return JsonNull.INSTANCE;
}
static String getFileSHA1(Path path) {
try {
StringBuilder sha1 = new StringBuilder(new BigInteger(1, MessageDigest.getInstance("SHA-1").digest(Files.readAllBytes(path))).toString(16));
while (sha1.length() < 40) {
sha1.insert(0, "0");
}
return sha1.toString().toLowerCase(Locale.ENGLISH);
} catch (IOException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
static String nullToDefault(String string, String defaultValue) {
return string == null ? defaultValue : string;
}
}

View file

@ -0,0 +1,46 @@
package io.github.zekerzhayard.forgewrapper.installer.detector;
import java.nio.file.Path;
import java.util.HashMap;
public class MultiMCFileDetector implements IFileDetector {
protected Path libraryDir = null;
protected Path installerJar = null;
protected Path minecraftJar = null;
@Override
public String name() {
return "MultiMC";
}
@Override
public boolean enabled(HashMap<String, IFileDetector> others) {
return others.size() == 0;
}
@Override
public Path getLibraryDir() {
if (this.libraryDir == null) {
this.libraryDir = IFileDetector.super.getLibraryDir();
}
return this.libraryDir;
}
@Override
public Path getInstallerJar(String forgeFullVersion) {
Path path = IFileDetector.super.getInstallerJar(forgeFullVersion);
if (path == null) {
return this.installerJar != null ? this.installerJar : (this.installerJar = this.getLibraryDir().resolve("net").resolve("minecraftforge").resolve("forge").resolve(forgeFullVersion).resolve("forge-" + forgeFullVersion + "-installer.jar").toAbsolutePath());
}
return path;
}
@Override
public Path getMinecraftJar(String mcVersion) {
Path path = IFileDetector.super.getMinecraftJar(mcVersion);
if (path == null) {
return this.minecraftJar != null ? this.minecraftJar : (this.minecraftJar = this.getLibraryDir().resolve("com").resolve("mojang").resolve("minecraft").resolve(mcVersion).resolve("minecraft-" + mcVersion + "-client.jar").toAbsolutePath());
}
return path;
}
}

View file

@ -0,0 +1 @@
io.github.zekerzhayard.forgewrapper.installer.detector.MultiMCFileDetector