Improve cache file detectors.
This commit is contained in:
parent
9f65af36b0
commit
29f602904a
9 changed files with 327 additions and 38 deletions
|
@ -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/).
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.")) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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=<libraries-path>".
|
||||
*/
|
||||
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=<installer-path>".
|
||||
*/
|
||||
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=<minecraft-path>".
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
io.github.zekerzhayard.forgewrapper.installer.detector.MultiMCFileDetector
|
Loading…
Reference in a new issue