diff --git a/README.md b/README.md index f490261..21e2b96 100644 --- a/README.md +++ b/README.md @@ -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/). diff --git a/build.gradle b/build.gradle index c501600..591fddd 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/ClientInstall4MultiMC.java b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/ClientInstall4MultiMC.java index f2db374..ea72564 100644 --- a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/ClientInstall4MultiMC.java +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/ClientInstall4MultiMC.java @@ -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 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); } } diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java index ad60ad7..1a822ce 100644 --- a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Installer.java @@ -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); } } diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java index bd97c19..c52c272 100644 --- a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Main.java @@ -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 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()); - // / /modlauncher /mods /cpw /libraries - return laucnher.getParentFile().getParentFile().getParentFile().getParentFile().getParentFile(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - public static Stream 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.")) { diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java new file mode 100644 index 0000000..369c4b5 --- /dev/null +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.java @@ -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 sl = ServiceLoader.load(IFileDetector.class); + HashMap detectors = new HashMap<>(); + for (IFileDetector detector : sl) { + detectors.put(detector.name(), detector); + } + + boolean enabled = false; + IFileDetector temp = null; + for (Map.Entry detector : detectors.entrySet()) { + HashMap 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; + } +} diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java new file mode 100644 index 0000000..b816708 --- /dev/null +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.java @@ -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 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(); + // / /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 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 libsMap = new HashMap<>(); + Map hashMap = new HashMap<>(); + + // Get all "data//client" elements. + for (Map.Entry entry : jo.entrySet()) { + String clientStr = getElement(entry.getValue().getAsJsonObject(), "client").getAsString(); + if (entry.getKey().endsWith("_SHA")) { + Pattern p = Pattern.compile("^'(?[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("^\\[(?[^:]*):(?[^:]*):(?[^:@]*)(:(?[^@]*))?(@(?[^]]*))?]$"); + 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 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> 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; + } +} diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java new file mode 100644 index 0000000..cb87d27 --- /dev/null +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.java @@ -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 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; + } +} diff --git a/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector b/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector new file mode 100644 index 0000000..44375f8 --- /dev/null +++ b/src/main/resources/META-INF/services/io.github.zekerzhayard.forgewrapper.installer.detector.IFileDetector @@ -0,0 +1 @@ +io.github.zekerzhayard.forgewrapper.installer.detector.MultiMCFileDetector \ No newline at end of file