diff --git a/.gitignore b/.gitignore
index fc25cc840..082849356 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,8 +20,8 @@ hmcl-exported-logs-*
/HMCL/build/
/HMCLCore/build/
/HMCLBoot/build/
-/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
+/minecraft/libraries/HMCLMultiMCBootstrap/build/
/buildSrc/build/
# idea
@@ -30,12 +30,14 @@ hmcl-exported-logs-*
/HMCL/out/
/HMCLCore/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
+/minecraft/libraries/HMCLMultiMCBootstrap/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
+/minecraft/libraries/HMCLMultiMCBootstrap/bin/
.classpath
.project
.settings
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java
index 7d189d253..ff8763ac7 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java
@@ -19,11 +19,13 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import kala.compress.archivers.zip.ZipArchiveReader;
+import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
+import org.jackhuang.hmcl.mod.multimc.MultiMCComponents;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
@@ -69,6 +71,10 @@ public final class ModpackHelper {
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
);
+ static {
+ MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
+ }
+
@Nullable
public static ModpackProvider getProviderByType(String type) {
return providers.get(type);
diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts
index e1a8c9212..86ca2bde9 100644
--- a/HMCLCore/build.gradle.kts
+++ b/HMCLCore/build.gradle.kts
@@ -32,3 +32,16 @@ dependencies {
testImplementation(libs.jna.platform)
testImplementation(libs.jimfs)
}
+
+tasks.processResources {
+ listOf(
+ "HMCLTransformerDiscoveryService",
+ "HMCLMultiMCBootstrap"
+ ).map { project(":$it").tasks["jar"] as Jar }.forEach { task ->
+ dependsOn(task)
+
+ into("assets/game") {
+ from(task.outputs.files)
+ }
+ }
+}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java
index ae5ce6864..dc253ca8e 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java
@@ -241,6 +241,10 @@ public class Library implements Comparable, Validation {
return hint;
}
+ public Library withoutCommunityFields() {
+ return new Library(artifact, url, downloads, checksums, extract, natives, rules, null, null);
+ }
+
/**
* Available when hint is "local"
*
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCComponents.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCComponents.java
index c9452d01f..b4c9c78a9 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCComponents.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCComponents.java
@@ -4,7 +4,9 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public final class MultiMCComponents {
@@ -12,6 +14,41 @@ public final class MultiMCComponents {
private MultiMCComponents() {
}
+ private static final Map INSTALLER_PROFILE = new ConcurrentHashMap<>();
+
+ static {
+ // Please append a phrase below while fixing bugs or implementing new features for Instance Format transformer
+ INSTALLER_PROFILE.put("Patches", "recursive install, fabric & quilt intermediary");
+
+ // Check whether MultiMCComponents is 'org.jackhuang.hmcl.mod.multimc.MultiMCComponents'.
+ // We use a base64-encoded value here to prevent string literals from being replaced by IDE if users trigger the 'Refactor' feature.
+ if (new String(
+ Base64.getDecoder().decode("b3JnLmphY2todWFuZy5obWNsLm1vZC5tdWx0aW1jLk11bHRpTUNDb21wb25lbnRz"),
+ StandardCharsets.UTF_8
+ ).equals(MultiMCComponents.class.getName())) {
+ INSTALLER_PROFILE.put("Implementation", "Probably vanilla. Class location is not modified (org.jackhuang.hmcl.mod.multimc.MultiMCComponents).");
+ } else {
+ INSTALLER_PROFILE.put("Implementation", "Not vanilla. Class location is " + MultiMCComponents.class.getName());
+ }
+ }
+
+ public static void setImplementation(String implementation) {
+ INSTALLER_PROFILE.put("Implementation", implementation);
+ }
+
+ public static String getInstallerProfile() {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry entry : INSTALLER_PROFILE.entrySet()) {
+ builder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
+ }
+
+ if (builder.length() != 0) {
+ builder.setLength(builder.length() - 1);
+ }
+
+ return builder.toString();
+ }
+
private static final Map ID_TYPE = new HashMap<>();
static {
@@ -46,7 +83,25 @@ public final class MultiMCComponents {
return PAIRS;
}
- public static URI getMetaURL(String componentID, String version) {
+ public static URI getMetaURL(String componentID, String version, String mcVersion) {
+ if (version == null) {
+ switch (componentID) {
+ case "org.lwjgl": {
+ version = "2.9.1";
+ break;
+ }
+ case "org.lwjgl3": {
+ version = "3.1.2";
+ break;
+ }
+ case "net.fabricmc.intermediary":
+ case "org.quiltmc.hashed": {
+ version = mcVersion;
+ break;
+ }
+ }
+ }
+
return NetworkUtils.toURI(String.format("https://meta.multimc.org/v1/%s/%s.json", componentID, version));
}
}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstancePatch.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstancePatch.java
index 176a0c32c..51483e4a3 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstancePatch.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstancePatch.java
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonMap;
import org.jackhuang.hmcl.util.gson.JsonUtils;
+import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -45,6 +46,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -54,6 +56,8 @@ import java.util.stream.Collectors;
*/
@Immutable
public final class MultiMCInstancePatch {
+ public static final Library BOOTSTRAP_LIBRARY = new Library(new Artifact("org.jackhuang.hmcl", "mmc-bootstrap", "1.0"));
+
private final int formatVersion;
@SerializedName("uid")
@@ -390,6 +394,15 @@ public final class MultiMCInstancePatch {
}
}
+ {
+ libraries.add(0, BOOTSTRAP_LIBRARY);
+ jvmArguments.add(new StringArgument("-Dhmcl.mmc.bootstrap=" + NetworkUtils.withQuery("hmcl:///bootstrap_profile_v1/", Map.of(
+ "main_class", mainClass,
+ "installer", MultiMCComponents.getInstallerProfile()
+ ))));
+ mainClass = "org.jackhuang.hmcl.HMCLMultiMCBootstrap";
+ }
+
Version version = new Version(versionID)
.setArguments(new Arguments().addGameArguments(minecraftArguments).addJVMArgumentsDirect(jvmArguments))
.setMainClass(mainClass)
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java
index de756c11a..e3db99fc5 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java
@@ -19,11 +19,14 @@ package org.jackhuang.hmcl.mod.multimc;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
+import org.jackhuang.hmcl.download.LibraryAnalyzer;
+import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.DefaultGameRepository;
+import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
@@ -38,14 +41,16 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -54,16 +59,20 @@ import java.util.Objects;
* A task transforming MultiMC Modpack Scheme to Official Launcher Scheme.
* The transforming process contains 7 stage:
*
- * - General Setup
- * - Load Components
- * - Resolve Json-Patch
- * - Build Artifact
- * - Copy Embedded Files
- * - Assemble Game
- * - Download Game
- * - Apply JAR mods
+ * - General Setup: Compute checksum and copy 'overrides' files.
+ * - Load Components: Parse all local Json-Patch and prepare to fetch others from Internet.
+ * - Resolve Json-Patch: Fetch remote Json-Patch and their dependencies.
+ * - Build Artifact: Transform Json-Patch to Official Scheme lossily, without original structure.
+ * - Copy Embedded Files: Copy embedded libraries and icon.
+ * - Assemble Game: Prepare to download main jar, libraries and assets.
+ * - Download Game: Download files.
+ * - Apply JAR mods: Apply JAR mods into main jar.
*
* See codes below for detailed implementation.
+ *
+ * @implNote To guarantee all features of MultiMC Modpack Scheme is super hard.
+ * As f*** MMC never provides a detailed API docs, most codes below is guessed from its source code.
+ * FUNCTIONS OF GAMES MIGHT NOT BE COMPLETELY THE SAME WITH MMC.
*
*/
public final class MultiMCModpackInstallTask extends Task {
@@ -133,10 +142,23 @@ public final class MultiMCModpackInstallTask extends Task> patches = new ArrayList<>();
- for (MultiMCManifest.MultiMCManifestComponent component : Objects.requireNonNull(
+ List components = Objects.requireNonNull(
Objects.requireNonNull(manifest.getMmcPack(), "mmc-pack.json").getComponents(), "components"
- )) {
+ );
+ List> patches = new ArrayList<>();
+
+ String mcVersion = null;
+ for (MultiMCManifest.MultiMCManifestComponent component : components) {
+ if (MultiMCComponents.getComponent(component.getUid()) == LibraryAnalyzer.LibraryType.MINECRAFT) {
+ mcVersion = component.getVersion();
+ break;
+ }
+ }
+ if (mcVersion == null) {
+ throw new IllegalStateException("Cannot load modpacks without Minecraft.");
+ }
+
+ for (MultiMCManifest.MultiMCManifestComponent component : components) {
String componentID = Objects.requireNonNull(component.getUid(), "Component ID");
Path patchPath = root.resolve(String.format("patches/%s.json", componentID));
@@ -149,20 +171,22 @@ public final class MultiMCModpackInstallTask extends Task patch)); // TODO: Task.completed has unclear compatibility issue.
} else {
patches.add(
- new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion()))
+ new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion(), mcVersion))
.thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s))
);
}
}
- dependents.add(new MMCInstancePatchesAssembleTask(patches));
+ dependents.add(new MMCInstancePatchesAssembleTask(patches, mcVersion));
}
}
private static final class MMCInstancePatchesAssembleTask extends Task> {
private final List> patches;
+ private final String mcVersion;
- public MMCInstancePatchesAssembleTask(List> patches) {
+ public MMCInstancePatchesAssembleTask(List> patches, String mcVersion) {
this.patches = patches;
+ this.mcVersion = mcVersion;
}
@Override
@@ -172,7 +196,7 @@ public final class MultiMCModpackInstallTask extends Task existed = new HashMap<>();
+ Map existed = new LinkedHashMap<>();
for (Task patch : patches) {
MultiMCInstancePatch result = patch.getResult();
@@ -186,7 +210,7 @@ public final class MultiMCModpackInstallTask extends Task task = new GetTask(MultiMCComponents.getMetaURL(
- componentID, Lang.requireNonNullElse(require.getEqualsVersion(), require.getSuggests())
+ componentID, Lang.requireNonNullElse(require.getEqualsVersion(), require.getSuggests()), mcVersion
)).thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s));
task.run();
@@ -231,6 +255,26 @@ public final class MultiMCModpackInstallTask extends Task and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.jackhuang.hmcl;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Scanner;
+
+public final class HMCLMultiMCBootstrap {
+ private HMCLMultiMCBootstrap() {
+ }
+
+ public static void main(String[] args) throws Throwable {
+ String profile = System.getProperty("hmcl.mmc.bootstrap");
+ if (profile == null) {
+ launchLegacy(args);
+ return;
+ }
+
+ URI uri = URI.create(profile);
+ if (Objects.equals(uri.getPath(), "/bootstrap_profile_v1/")) {
+ launchV1(parseQuery(uri.getRawQuery()), args);
+ }
+ }
+
+ private static void launchV1(Map arguments, String[] args) throws Throwable {
+ String mainClass = arguments.get("main_class");
+ String installerInfo = arguments.get("installer");
+
+ launch(installerInfo, mainClass, args);
+ }
+
+ private static void launchLegacy(String[] args) throws Throwable {
+ String mainClass = new String(Base64.getUrlDecoder().decode(System.getProperty("hmcl.mmc.bootstrap.main")), StandardCharsets.UTF_8);
+ String installerInfo = new String(Base64.getUrlDecoder().decode(System.getProperty("hmcl.mmc.bootstrap.installer")), StandardCharsets.UTF_8);
+
+ launch(installerInfo, mainClass, args);
+ }
+
+ private static void launch(String installerInfo, String mainClass, String[] args) throws Throwable {
+ System.out.println("This version is installed by HMCLCore's MultiMC combat layer.");
+ System.out.println("Installer Properties:");
+ System.out.println(installerInfo);
+ System.out.println("Main Class: " + mainClass);
+ System.out.println("GAME MAY CRASH DUE TO BUGS. TEST YOUR GAME ON OFFICIAL MMC BEFORE REPORTING BUGS TO AUTHORS.");
+
+ Method[] methods = Class.forName(mainClass).getMethods();
+ for (Method method : methods) {
+ // https://docs.oracle.com/javase/specs/jls/se21/html/jls-12.html#jls-12.1.4
+ if ("main".equals(method.getName()) &&
+ Modifier.isStatic(method.getModifiers()) &&
+ method.getReturnType() == void.class &&
+ method.getParameterCount() == 1 &&
+ method.getParameters()[0].getType() == String[].class
+ ) {
+ method.invoke(null, (Object) args);
+ return;
+ }
+ }
+
+ throw new IllegalArgumentException("Cannot find method 'main(String[])' in " + mainClass);
+ }
+
+ private static Map parseQuery(String queryParameterString) {
+ if (queryParameterString == null) return Collections.emptyMap();
+
+ Map result = new HashMap<>();
+
+ try (Scanner scanner = new Scanner(queryParameterString)) {
+ scanner.useDelimiter("&");
+ while (scanner.hasNext()) {
+ String[] nameValue = scanner.next().split("=");
+ if (nameValue.length == 0 || nameValue.length > 2) {
+ throw new IllegalArgumentException("bad query string");
+ }
+
+ String name = decodeURL(nameValue[0]);
+ String value = nameValue.length == 2 ? decodeURL(nameValue[1]) : null;
+ result.put(name, value);
+ }
+ }
+ return result;
+ }
+
+ private static String decodeURL(String value) {
+ try {
+ return URLDecoder.decode(value, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 7831e3da1..a1a710b39 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -2,11 +2,11 @@ rootProject.name = "HMCL3"
include(
"HMCL",
"HMCLCore",
- "HMCLBoot",
- "HMCLTransformerDiscoveryService"
+ "HMCLBoot"
)
-val minecraftLibraries = listOf("HMCLTransformerDiscoveryService")
+val minecraftLibraries = listOf("HMCLTransformerDiscoveryService", "HMCLMultiMCBootstrap")
+include(minecraftLibraries)
for (library in minecraftLibraries) {
project(":$library").projectDir = file("minecraft/libraries/$library")