Fix: 无法安装带有 Fabric 的非标准 MMC 整合包 (#4034)

- 修复无法安装带有 Fabric 的非标准 MMC 整合包的漏洞
- 在安装时写入当前安装器实现信息和启动器信息,以在后续调试时获得更多信息

Fix #4049
This commit is contained in:
Burning_TNT 2025-09-14 20:50:53 +08:00 committed by GitHub
parent a10e9a04b1
commit a39a23f938
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 290 additions and 22 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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);

View File

@ -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)
}
}
}

View File

@ -241,6 +241,10 @@ public class Library implements Comparable<Library>, Validation {
return hint;
}
public Library withoutCommunityFields() {
return new Library(artifact, url, downloads, checksums, extract, natives, rules, null, null);
}
/**
* Available when hint is "local"
*

View File

@ -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<String, String> 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<String, String> 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<String, LibraryAnalyzer.LibraryType> 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));
}
}

View File

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

View File

@ -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;
* <p>A task transforming MultiMC Modpack Scheme to Official Launcher Scheme.
* The transforming process contains 7 stage:
* <ul>
* <li>General Setup</li>
* <li>Load Components</li>
* <li>Resolve Json-Patch</li>
* <li>Build Artifact</li>
* <li>Copy Embedded Files</li>
* <li>Assemble Game</li>
* <li>Download Game</li>
* <li>Apply JAR mods</li>
* <li>General Setup: Compute checksum and copy 'overrides' files.</li>
* <li>Load Components: Parse all local Json-Patch and prepare to fetch others from Internet.</li>
* <li>Resolve Json-Patch: Fetch remote Json-Patch and their dependencies.</li>
* <li>Build Artifact: Transform Json-Patch to Official Scheme lossily, without original structure.</li>
* <li>Copy Embedded Files: Copy embedded libraries and icon.</li>
* <li>Assemble Game: Prepare to download main jar, libraries and assets.</li>
* <li>Download Game: Download files.</li>
* <li>Apply JAR mods: Apply JAR mods into main jar.</li>
* </ul>
* 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.
* <b>FUNCTIONS OF GAMES MIGHT NOT BE COMPLETELY THE SAME WITH MMC.</b>
* </p>
*/
public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.ResolvedInstance> {
@ -133,10 +142,23 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
try (FileSystem fs = openModpack()) {
Path root = getRootPath(fs);
List<Task<MultiMCInstancePatch>> patches = new ArrayList<>();
for (MultiMCManifest.MultiMCManifestComponent component : Objects.requireNonNull(
List<MultiMCManifest.MultiMCManifestComponent> components = Objects.requireNonNull(
Objects.requireNonNull(manifest.getMmcPack(), "mmc-pack.json").getComponents(), "components"
)) {
);
List<Task<MultiMCInstancePatch>> 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<MultiMCInstancePatch.R
patches.add(Task.supplyAsync(() -> 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<List<MultiMCInstancePatch>> {
private final List<Task<MultiMCInstancePatch>> patches;
private final String mcVersion;
public MMCInstancePatchesAssembleTask(List<Task<MultiMCInstancePatch>> patches) {
public MMCInstancePatchesAssembleTask(List<Task<MultiMCInstancePatch>> patches, String mcVersion) {
this.patches = patches;
this.mcVersion = mcVersion;
}
@Override
@ -172,7 +196,7 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
@Override
public void execute() throws Exception {
Map<String, MultiMCInstancePatch> existed = new HashMap<>();
Map<String, MultiMCInstancePatch> existed = new LinkedHashMap<>();
for (Task<MultiMCInstancePatch> patch : patches) {
MultiMCInstancePatch result = patch.getResult();
@ -186,7 +210,7 @@ public final class MultiMCModpackInstallTask extends Task<MultiMCInstancePatch.R
String componentID = require.getID();
if (!existed.containsKey(componentID)) {
Task<MultiMCInstancePatch> 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<MultiMCInstancePatch.R
if (Files.exists(libraries))
FileUtils.copyDirectory(libraries, repository.getVersionRoot(name).toPath().resolve("libraries"));
for (Library library : artifact.getVersion().getLibraries()) {
if ("local".equals(library.getHint())) {
/* TODO: Determine whether we should erase community fields, like 'hint' and 'filename' from version json.
Retain them will facilitate compatibility, as some embedded libraries may check where their JAR is.
Meanwhile, potential compatibility issue with other launcher which never supports these fields might occur.
Here, we make the file stored twice, to keep maximum compatibility. */
Path from = repository.getLibraryFile(artifact.getVersion(), library).toPath();
Path target = repository.getLibraryFile(artifact.getVersion(), library.withoutCommunityFields()).toPath();
Files.createDirectories(target.getParent());
Files.copy(from, target, StandardCopyOption.REPLACE_EXISTING);
}
}
try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/HMCLMultiMCBootstrap-1.0.jar")) {
Path libraryPath = repository.getLibraryFile(artifact.getVersion(), MultiMCInstancePatch.BOOTSTRAP_LIBRARY).toPath();
Files.createDirectories(libraryPath.getParent());
Files.copy(Objects.requireNonNull(input, "Bundled HMCLMultiMCBootstrap is missing."), libraryPath, StandardCopyOption.REPLACE_EXISTING);
}
String iconKey = this.manifest.getIconKey();
if (iconKey != null) {
Path iconFile = root.resolve(iconKey + ".png");

View File

@ -0,0 +1,15 @@
version = "1.0"
tasks.compileJava {
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
}
tasks.jar {
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Implementation-Version" to project.version
)
}
}

View File

@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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<String, String> 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<String, String> parseQuery(String queryParameterString) {
if (queryParameterString == null) return Collections.emptyMap();
Map<String, String> 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);
}
}
}

View File

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