diff --git a/README.md b/README.md index db9aed1..5e9d2b5 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,27 @@ Allow MultiMC to launch Minecraft 1.13+ with Forge. ## How to use -1. Download Forge installer for Minecraft 1.13+ at [https://files.minecraftforge.net/]. +### Install Forge Only +1. Download Forge installer for Minecraft 1.13+ [here](https://files.minecraftforge.net/). 2. Download ForgeWrapper jar file at the [release](https://github.com/ZekerZhayard/ForgeWrapper/releases) page. 3. Run the below command in terminal: ``` - java -jar [--installer] [--instance ] + java -jar --installer= [--instance=] ``` *Notice: If you don't specify a MultiMC instance path, ForgeWrapper will create the instance folder in current working space.* 4. If the instance folder which just created is not in `MultiMC/instances` folder, you just need to move to the `MultiMC/instances` folder. -5. Run MultiMC, and you will see a new instance named `forge--`. \ No newline at end of file +5. Run MultiMC, and you will see a new instance named `forge--`. + +### Install CurseForge Modpack +1. Download the modpack zip file. +2. Download ForgeWrapper jar file at the [release](https://github.com/ZekerZhayard/ForgeWrapper/releases) page. +3. Run the below command in terminal: + ``` + java -jar --cursepack= [--instance=] + ``` + *Notice: If you don't specify a MultiMC instance path, ForgeWrapper will create the instance folder in current working space.* + +4. If the instance folder which just created is not in `MultiMC/instances` folder, you just need to move to the `MultiMC/instances` folder. +5. Run MultiMC, and you will see a new instance named `-`. +*Notice: CurseForge modpack will be installed on first launch by [cursepacklocator](https://github.com/cpw/cursepacklocator), it will take a few minutes.* \ No newline at end of file diff --git a/build.gradle b/build.gradle index fa85405..13e5703 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ apply plugin: "idea" sourceCompatibility = targetCompatibility = 1.8 -version = "1.1.0" +version = "1.2.0" group = "io.github.zekerzhayard" archivesBaseName = rootProject.name @@ -19,7 +19,6 @@ repositories { } dependencies { - compile "commons-codec:commons-codec:1.10" compile "cpw.mods:modlauncher:4.1.0" compile "net.minecraftforge:forge:1.14.4-28.2.0:installer" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 290541c..d193c61 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Thu Mar 12 20:06:29 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip diff --git a/src/main/java/cpw/mods/forge/cursepacklocator/Murmur2.java b/src/main/java/cpw/mods/forge/cursepacklocator/Murmur2.java new file mode 100644 index 0000000..f754e18 --- /dev/null +++ b/src/main/java/cpw/mods/forge/cursepacklocator/Murmur2.java @@ -0,0 +1,166 @@ +/** + * Copyright 2014 Prasanth Jayachandran + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cpw.mods.forge.cursepacklocator; + +/** + * Murmur2 32 and 64 bit variants. + * 32-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash2.cpp#37 + * 64-bit Java port of https://code.google.com/p/smhasher/source/browse/trunk/MurmurHash2.cpp#96 + */ +public class Murmur2 { + // Constants for 32-bit variant + private static final int M_32 = 0x5bd1e995; + private static final int R_32 = 24; + + // Constants for 64-bit variant + private static final long M_64 = 0xc6a4a7935bd1e995L; + private static final int R_64 = 47; + private static final int DEFAULT_SEED = 0; + + /** + * Murmur2 32-bit variant. + * + * @param data - input byte array + * @return - hashcode + */ + public static int hash32(byte[] data) { + return hash32(data, data.length, DEFAULT_SEED); + } + + /** + * Murmur2 32-bit variant. + * + * @param data - input byte array + * @param length - length of array + * @param seed - seed. (default 0) + * @return - hashcode + */ + public static int hash32(byte[] data, int length, int seed) { + int h = seed ^ length; + int len_4 = length >> 2; + + // body + for (int i = 0; i < len_4; i++) { + int i_4 = i << 2; + int k = (data[i_4] & 0xff) + | ((data[i_4 + 1] & 0xff) << 8) + | ((data[i_4 + 2] & 0xff) << 16) + | ((data[i_4 + 3] & 0xff) << 24); + + // mix functions + k *= M_32; + k ^= k >>> R_32; + k *= M_32; + h *= M_32; + h ^= k; + } + + // tail + int len_m = len_4 << 2; + int left = length - len_m; + if (left != 0) { + // see https://github.com/cpw/cursepacklocator/pull/3 + if (left >= 3) { + h ^= (int) data[length - (left - 2)] << 16; + } + if (left >= 2) { + h ^= (int) data[length - (left - 1)] << 8; + } + if (left >= 1) { + h ^= (int) data[length - left]; + } + + h *= M_32; + } + + // finalization + h ^= h >>> 13; + h *= M_32; + h ^= h >>> 15; + + return h; + } + + /** + * Murmur2 64-bit variant. + * + * @param data - input byte array + * @return - hashcode + */ + public static long hash64(final byte[] data) { + return hash64(data, data.length, DEFAULT_SEED); + } + + /** + * Murmur2 64-bit variant. + * + * @param data - input byte array + * @param length - length of array + * @param seed - seed. (default 0) + * @return - hashcode + */ + public static long hash64(final byte[] data, int length, int seed) { + long h = (seed & 0xffffffffl) ^ (length * M_64); + int length8 = length >> 3; + + // body + for (int i = 0; i < length8; i++) { + final int i8 = i << 3; + long k = ((long) data[i8] & 0xff) + | (((long) data[i8 + 1] & 0xff) << 8) + | (((long) data[i8 + 2] & 0xff) << 16) + | (((long) data[i8 + 3] & 0xff) << 24) + | (((long) data[i8 + 4] & 0xff) << 32) + | (((long) data[i8 + 5] & 0xff) << 40) + | (((long) data[i8 + 6] & 0xff) << 48) + | (((long) data[i8 + 7] & 0xff) << 56); + + // mix functions + k *= M_64; + k ^= k >>> R_64; + k *= M_64; + h ^= k; + h *= M_64; + } + + // tail + int tailStart = length8 << 3; + switch (length - tailStart) { + case 7: + h ^= (long) (data[tailStart + 6] & 0xff) << 48; + case 6: + h ^= (long) (data[tailStart + 5] & 0xff) << 40; + case 5: + h ^= (long) (data[tailStart + 4] & 0xff) << 32; + case 4: + h ^= (long) (data[tailStart + 3] & 0xff) << 24; + case 3: + h ^= (long) (data[tailStart + 2] & 0xff) << 16; + case 2: + h ^= (long) (data[tailStart + 1] & 0xff) << 8; + case 1: + h ^= (long) (data[tailStart] & 0xff); + h *= M_64; + } + + // finalization + h ^= h >>> R_64; + h *= M_64; + h ^= h >>> R_64; + + return h; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Converter.java b/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Converter.java index f9737eb..0fe1262 100644 --- a/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Converter.java +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Converter.java @@ -8,8 +8,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; import java.util.ArrayList; +import java.util.Enumeration; import java.util.List; import java.util.Map; import java.util.Optional; @@ -21,26 +21,32 @@ import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import io.github.zekerzhayard.forgewrapper.installer.Download; public class Converter { - public static void convert(Path installerPath, Path targetDir) throws Exception { - JsonObject installer = getInstallerJson(installerPath); + public static void convert(Path installerPath, Path targetDir, String cursepack) throws Exception { + if (cursepack != null) { + installerPath = getForgeInstallerFromCursePack(cursepack); + } + + JsonObject installer = getJsonFromZip(installerPath, "version.json"); List arguments = getAdditionalArgs(installer); String mcVersion = arguments.get(arguments.indexOf("--fml.mcVersion") + 1); String forgeVersion = arguments.get(arguments.indexOf("--fml.forgeVersion") + 1); String forgeFullVersion = "forge-" + mcVersion + "-" + forgeVersion; + String instanceName = cursepack == null ? forgeFullVersion : installerPath.toFile().getName().replace("-installer.jar", ""); StringBuilder wrapperVersion = new StringBuilder(); JsonObject pack = convertPackJson(mcVersion); - JsonObject patches = convertPatchesJson(installer, mcVersion, forgeVersion, wrapperVersion); + JsonObject patches = convertPatchesJson(installer, mcVersion, forgeVersion, wrapperVersion, cursepack); Files.createDirectories(targetDir); // Copy mmc-pack.json and instance.cfg to folder. - Path instancePath = targetDir.resolve(forgeFullVersion); + Path instancePath = targetDir.resolve(instanceName); Files.createDirectories(instancePath); Files.copy(new ByteArrayInputStream(pack.toString().getBytes(StandardCharsets.UTF_8)), instancePath.resolve("mmc-pack.json"), StandardCopyOption.REPLACE_EXISTING); - Files.copy(new ByteArrayInputStream(("InstanceType=OneSix\nname=" + forgeFullVersion).getBytes(StandardCharsets.UTF_8)), instancePath.resolve("instance.cfg"), StandardCopyOption.REPLACE_EXISTING); + Files.copy(new ByteArrayInputStream(("InstanceType=OneSix\nname=" + instanceName).getBytes(StandardCharsets.UTF_8)), instancePath.resolve("instance.cfg"), StandardCopyOption.REPLACE_EXISTING); // Copy ForgeWrapper to /libraries folder. Path librariesPath = instancePath.resolve("libraries"); @@ -56,10 +62,22 @@ public class Converter { Path forgeWrapperPath = instancePath.resolve(".minecraft").resolve(".forgewrapper"); Files.createDirectories(forgeWrapperPath); Files.copy(installerPath, forgeWrapperPath.resolve(forgeFullVersion + "-installer.jar"), StandardCopyOption.REPLACE_EXISTING); + + // Extract all curse pack entries to /.minecraft folder. + if (cursepack != null) { + ZipFile zip = new ZipFile(cursepack); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + Path targetFolder = forgeWrapperPath.getParent().resolve(entry.getName()); + Files.createDirectories(targetFolder.getParent()); + Files.copy(zip.getInputStream(entry), targetFolder, StandardCopyOption.REPLACE_EXISTING); + } + } } public static List getAdditionalArgs(Path installerPath) { - JsonObject installer = getInstallerJson(installerPath); + JsonObject installer = getJsonFromZip(installerPath, "version.json"); return getAdditionalArgs(installer); } @@ -69,12 +87,12 @@ public class Converter { return args; } - public static JsonObject getInstallerJson(Path installerPath) { + public static JsonObject getJsonFromZip(Path path, String json) { try { - ZipFile zf = new ZipFile(installerPath.toFile()); - ZipEntry versionFile = zf.getEntry("version.json"); + ZipFile zf = new ZipFile(path.toFile()); + ZipEntry versionFile = zf.getEntry(json); if (versionFile == null) { - throw new RuntimeException("The forge installer is invalid!"); + throw new RuntimeException("The zip file is invalid!"); } InputStreamReader isr = new InputStreamReader(zf.getInputStream(versionFile), StandardCharsets.UTF_8); return new JsonParser().parse(isr).getAsJsonObject(); @@ -83,6 +101,28 @@ public class Converter { } } + private static Path getForgeInstallerFromCursePack(String cursepack) throws Exception { + JsonObject manifest = getJsonFromZip(Paths.get(cursepack), "manifest.json"); + JsonObject minecraft = getElement(manifest, "minecraft").getAsJsonObject(); + String mcVersion = getElement(minecraft, "version").getAsString(); + String forgeVersion = null; + for (JsonElement element : getElement(minecraft, "modLoaders").getAsJsonArray()) { + String id = getElement(element.getAsJsonObject(), "id").getAsString(); + if (id.startsWith("forge-")) { + forgeVersion = id.replace("forge-", ""); + break; + } + } + if (forgeVersion == null) { + throw new RuntimeException("The curse pack is invalid!"); + } + String packName = getElement(manifest, "name").getAsString(); + String packVersion = getElement(manifest, "version").getAsString(); + Path installer = Paths.get(System.getProperty("java.io.tmpdir", "."), String.format("%s-%s-installer.jar", packName, packVersion)); + Download.download(String.format("https://files.minecraftforge.net/maven/net/minecraftforge/forge/%s-%s/forge-%s-%s-installer.jar", mcVersion, forgeVersion, mcVersion, forgeVersion), installer.toString()); + return installer; + } + // Convert mmc-pack.json: // - Replace Minecraft version private static JsonObject convertPackJson(String mcVersion) { @@ -102,7 +142,7 @@ public class Converter { // - Add libraries // - Add forge-launcher url // - Replace Minecraft & Forge versions - private static JsonObject convertPatchesJson(JsonObject installer, String mcVersion, String forgeVersion, StringBuilder wrapperVersion) { + private static JsonObject convertPatchesJson(JsonObject installer, String mcVersion, String forgeVersion, StringBuilder wrapperVersion, String cursepack) { JsonObject patches = new JsonParser().parse(new InputStreamReader(Converter.class.getResourceAsStream("/patches/net.minecraftforge.json"))).getAsJsonObject(); JsonArray libraries = getElement(patches, "libraries").getAsJsonArray(); @@ -112,10 +152,16 @@ public class Converter { wrapperVersion.append(getElement(lib.getAsJsonObject(), "MMC-filename").getAsString()); } } + if (cursepack != null) { + JsonObject cursepacklocator = new JsonObject(); + cursepacklocator.addProperty("name", "cpw.mods.forge:cursepacklocator:1.2.0"); + cursepacklocator.addProperty("url", "https://files.minecraftforge.net/maven/"); + libraries.add(cursepacklocator); + } for (JsonElement lib : getElement(installer ,"libraries").getAsJsonArray()) { JsonObject artifact = getElement(getElement(lib.getAsJsonObject(), "downloads").getAsJsonObject(), "artifact").getAsJsonObject(); String path = getElement(artifact, "path").getAsString(); - if (path.startsWith("net/minecraftforge/forge/")) { + if (path.equals(String.format("net/minecraftforge/forge/%s-%s/forge-%s-%s.jar", mcVersion, forgeVersion, mcVersion, forgeVersion))) { artifact.getAsJsonObject().addProperty("url", "https://files.minecraftforge.net/maven/" + path.replace(".jar", "-launcher.jar")); } libraries.add(lib); diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Main.java b/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Main.java index a9d4c38..31f84be 100644 --- a/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Main.java +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/converter/Main.java @@ -4,21 +4,30 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; + +import io.github.zekerzhayard.forgewrapper.installer.Download; public class Main { public static void main(String[] args) { - ArrayList argsList = new ArrayList<>(Arrays.asList(args)); - Path installer, instance; + Path installer = null, instance = Paths.get("."); + String cursepack = null; try { - installer = Paths.get(argsList.get(argsList.indexOf("--installer") + 1)); - instance = Paths.get("."); - if (argsList.contains("--instance")) { - instance = Paths.get(argsList.get(argsList.indexOf("--instance") + 1)); + HashMap argsMap = parseArgs(args); + if (argsMap.containsKey("--installer")) { + installer = Paths.get(argsMap.get("--installer")); + } else { + installer = Paths.get(System.getProperty("java.io.tmpdir", "."), "gson-2.8.6.jar"); + Download.download("https://repo1.maven.org/maven2/com/google/code/gson/gson/2.8.6/gson-2.8.6.jar", installer.toString()); + } + if (argsMap.containsKey("--instance")) { + instance = Paths.get(argsMap.get("--instance")); + } + if (argsMap.containsKey("--cursepack")) { + cursepack = argsMap.get("--cursepack"); } } catch (Exception e) { - System.out.println("Invalid arguments! Use: java -jar [--installer] [--instance ]"); + System.out.println("Invalid arguments! Use: java -jar [--installer= | --cursepack=] [--instance=]"); throw new RuntimeException(e); } @@ -27,11 +36,28 @@ public class Main { Converter.class.getProtectionDomain().getCodeSource().getLocation(), installer.toUri().toURL() }, null); - ucl.loadClass("io.github.zekerzhayard.forgewrapper.converter.Converter").getMethod("convert", Path.class, Path.class).invoke(null, installer, instance); + ucl.loadClass("io.github.zekerzhayard.forgewrapper.converter.Converter").getMethod("convert", Path.class, Path.class, String.class).invoke(null, installer, instance, cursepack); System.out.println("Successfully install Forge for MultiMC!"); } catch (Exception e) { System.out.println("Failed to install Forge!"); throw new RuntimeException(e); } } + + /** + * @return installer -- The path of forge installer.
+ * instance -- The instance folder of MultiMC.
+ * cursepack -- The version of cursepacklocator.
+ */ + private static HashMap parseArgs(String[] args) { + HashMap map = new HashMap<>(); + for (String arg : args) { + String[] params = arg.split("=", 2); + map.put(params[0], params[1]); + } + if (!map.containsKey("--installer") && !map.containsKey("--cursepack")) { + throw new IllegalArgumentException(); + } + return map; + } } diff --git a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Download.java b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Download.java index 38846f5..c89687f 100644 --- a/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Download.java +++ b/src/main/java/io/github/zekerzhayard/forgewrapper/installer/Download.java @@ -1,26 +1,28 @@ package io.github.zekerzhayard.forgewrapper.installer; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStreamReader; import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Locale; -import org.apache.commons.codec.digest.DigestUtils; - public class Download { - public static void download(String url, String location) throws IOException { + private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + public static void download(String url, String location) throws Exception { File localFile = new File(location); localFile.getParentFile().mkdirs(); if (localFile.isFile()) { try { System.out.println("Checking Fingerprints of installer..."); - Files.copy(new URL(url + ".md5").openConnection().getInputStream(), Paths.get(location + ".md5"), StandardCopyOption.REPLACE_EXISTING); - Files.copy(new URL(url + ".sha1").openConnection().getInputStream(), Paths.get(location + ".sha1"), StandardCopyOption.REPLACE_EXISTING); - String md5 = new String(Files.readAllBytes(Paths.get(location + ".md5"))); - String sha1 = new String(Files.readAllBytes(Paths.get(location + ".sha1"))); + String md5 = new BufferedReader(new InputStreamReader(new URL(url + ".md5").openConnection().getInputStream())).readLine(); + String sha1 = new BufferedReader(new InputStreamReader(new URL(url + ".sha1").openConnection().getInputStream())).readLine(); if (!checkMD5(location, md5) || !checkSHA1(location, sha1)) { System.out.println("Fingerprints do not match!"); localFile.delete(); @@ -33,19 +35,31 @@ public class Download { if (localFile.isDirectory()) { throw new RuntimeException(location + " must be a file!"); } - System.out.println("Downloading forge installer..."); + System.out.println("Downloading forge installer... (" + url + " ---> " + location + ")"); Files.copy(new URL(url).openConnection().getInputStream(), Paths.get(location), StandardCopyOption.REPLACE_EXISTING); download(url, location); } } - public static boolean checkMD5(String path, String hash) throws IOException { - String md5 = DigestUtils.md5Hex(Files.readAllBytes(Paths.get(path))); + public static boolean checkMD5(String path, String hash) throws IOException, NoSuchAlgorithmException { + String md5 = new String(encodeHex(MessageDigest.getInstance("MD5").digest(Files.readAllBytes(Paths.get(path))))); + System.out.println("MD5: " + hash + " ---> " + md5); return md5.toLowerCase(Locale.ENGLISH).equals(hash.toLowerCase(Locale.ENGLISH)); } - public static boolean checkSHA1(String path, String hash) throws IOException { - String sha1 = DigestUtils.sha1Hex(Files.readAllBytes(Paths.get(path))); + public static boolean checkSHA1(String path, String hash) throws IOException, NoSuchAlgorithmException { + String sha1 = new String(encodeHex(MessageDigest.getInstance("SHA-1").digest(Files.readAllBytes(Paths.get(path))))); + System.out.println("SHA-1: " + hash + " ---> " + sha1); return sha1.toLowerCase(Locale.ENGLISH).equals(hash.toLowerCase(Locale.ENGLISH)); } + + private static char[] encodeHex(final byte[] data) { + final int l = data.length; + final char[] out = new char[l << 1]; + for (int i = 0, j = 0; i < l; i++) { + out[j++] = DIGITS[(0xF0 & data[i]) >>> 4]; + out[j++] = DIGITS[0x0F & data[i]]; + } + return out; + } }