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")