From 7d7dbb9eb5965ff3d44c8e576f7ac76471e544db Mon Sep 17 00:00:00 2001 From: Burning_TNT Date: Sun, 8 Jun 2025 20:59:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20MultiMC=20=E6=95=B4?= =?UTF-8?q?=E5=90=88=E5=8C=85=E5=85=BC=E5=AE=B9=E5=8A=9F=E8=83=BD=20(#3547?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #3540 Fix #3706 --- .../org/jackhuang/hmcl/ui/InstallerItem.java | 13 +- .../download/fabric/FabricInstallTask.java | 2 +- .../download/forge/ForgeNewInstallTask.java | 2 +- .../download/forge/ForgeOldInstallTask.java | 2 +- .../hmcl/download/game/GameInstallTask.java | 2 +- .../neoforge/NeoForgeOldInstallTask.java | 2 +- .../hmcl/download/quilt/QuiltInstallTask.java | 2 +- .../org/jackhuang/hmcl/game/Arguments.java | 8 +- .../hmcl/game/DefaultGameRepository.java | 13 +- .../java/org/jackhuang/hmcl/game/Library.java | 103 ++++- .../jackhuang/hmcl/game/OSRestriction.java | 35 +- .../java/org/jackhuang/hmcl/game/Version.java | 26 +- .../hmcl/game/tlauncher/TLauncherLibrary.java | 7 +- .../hmcl/mod/multimc/MultiMCComponents.java | 52 +++ .../mod/multimc/MultiMCInstancePatch.java | 384 ++++++++++++++++-- .../mod/multimc/MultiMCModpackExportTask.java | 22 +- .../multimc/MultiMCModpackInstallTask.java | 272 ++++++++----- .../mod/multimc/MultiMCModpackProvider.java | 16 - 18 files changed, 758 insertions(+), 205 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCComponents.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java index c071abd10..a57f27bbc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java @@ -281,7 +281,13 @@ public class InstallerItem extends Control { for (InstallerItem item : all) { if (!item.resolvedStateProperty.isBound()) { - item.resolvedStateProperty.bind(item.versionProperty); + item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> { + InstalledState itemVersion = item.versionProperty.get(); + if (itemVersion != null) { + return itemVersion; + } + return InstallableState.INSTANCE; + }, item.versionProperty)); } } @@ -385,7 +391,10 @@ public class InstallerItem extends Control { if (control.id.equals(MINECRAFT.getPatchId())) { removeButton.setVisible(false); } else { - removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty)); + removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> { + State state = control.resolvedStateProperty.get(); + return state instanceof InstalledState && !((InstalledState) state).external; + }, control.resolvedStateProperty)); } removeButton.managedProperty().bind(removeButton.visibleProperty()); removeButton.setOnAction(e -> { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java index 96d7d3838..458d8dd9b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java @@ -124,7 +124,7 @@ public final class FabricInstallTask extends Task { libraries.add(new Library(Artifact.fromDescriptor(fabricInfo.intermediary.maven), "https://maven.fabricmc.net/", null)); libraries.add(new Library(Artifact.fromDescriptor(fabricInfo.loader.maven), "https://maven.fabricmc.net/", null)); - return new Version(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), loaderVersion, 30000, arguments, mainClass, libraries); + return new Version(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries); } public static class FabricInfo { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java index 5182e3d5a..cee6806b8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java @@ -410,7 +410,7 @@ public class ForgeNewInstallTask extends Task { dependencyManager.checkLibraryCompletionAsync(forgeVersion, true))); setResult(forgeVersion - .setPriority(30000) + .setPriority(Version.PRIORITY_LOADER) .setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId()) .setVersion(selfVersion)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java index a1abeba25..df3b919ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java @@ -82,7 +82,7 @@ public class ForgeOldInstallTask extends Task { } setResult(installProfile.getVersionInfo() - .setPriority(30000) + .setPriority(Version.PRIORITY_LOADER) .setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId()) .setVersion(selfVersion)); dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo(), true)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java index 2fd3777c6..aea0e0454 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java @@ -65,7 +65,7 @@ public class GameInstallTask extends Task { @Override public void execute() throws Exception { Version patch = JsonUtils.fromNonNullJson(downloadTask.getResult(), Version.class) - .setId(MINECRAFT.getPatchId()).setVersion(remote.getGameVersion()).setJar(null).setPriority(0); + .setId(MINECRAFT.getPatchId()).setVersion(remote.getGameVersion()).setJar(null).setPriority(Version.PRIORITY_MC); setResult(patch); Version version = new Version(this.version.getId()).addPatch(patch); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java index 6bc1ae407..444993cde 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java @@ -406,7 +406,7 @@ public class NeoForgeOldInstallTask extends Task { dependencyManager.checkLibraryCompletionAsync(neoForgeVersion, true))); setResult(neoForgeVersion - .setPriority(30000) + .setPriority(Version.PRIORITY_LOADER) .setId(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId()) .setVersion(selfVersion)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java index 81f3a84f0..c0bd51290 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java @@ -120,7 +120,7 @@ public final class QuiltInstallTask extends Task { libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.intermediary.maven), getMavenRepositoryByGroup(quiltInfo.intermediary.maven), null)); libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.loader.maven), getMavenRepositoryByGroup(quiltInfo.loader.maven), null)); - return new Version(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, 30000, arguments, mainClass, libraries); + return new Version(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries); } private static String getMavenRepositoryByGroup(String maven) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java index 7d26e7101..0f9aa093f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Arguments.java @@ -79,8 +79,12 @@ public final class Arguments { } public Arguments addJVMArguments(List jvmArguments) { - List list = jvmArguments.stream().map(StringArgument::new).collect(Collectors.toList()); - return new Arguments(getGame(), Lang.merge(getJvm(), list)); + return addJVMArgumentsDirect(jvmArguments.stream().map(StringArgument::new).collect(Collectors.toList())); + } + + // TODO: How to distinguish addJVMArgumentsDirect from addJVMArguments? Naming is hard :) + public Arguments addJVMArgumentsDirect(List jvmArguments) { + return new Arguments(getGame(), Lang.merge(getJvm(), jvmArguments)); } public static Arguments merge(Arguments a, Arguments b) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index 98ce5eb69..dfb082ac2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -95,10 +95,15 @@ public class DefaultGameRepository implements GameRepository { @Override public File getLibraryFile(Version version, Library lib) { - if ("local".equals(lib.getHint()) && lib.getFileName() != null) - return new File(getVersionRoot(version.getId()), "libraries/" + lib.getFileName()); - else - return new File(getLibrariesDirectory(version), lib.getPath()); + if ("local".equals(lib.getHint())) { + if (lib.getFileName() != null) { + return new File(getVersionRoot(version.getId()), "libraries/" + lib.getFileName()); + } + + return new File(getVersionRoot(version.getId()), "libraries/" + lib.getArtifact().getFileName()); + } + + return new File(getLibrariesDirectory(version), lib.getPath()); } public Path getArtifactFile(Version version, Artifact artifact) { 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 6df4a6ea4..e77a5e861 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java @@ -17,10 +17,11 @@ */ package org.jackhuang.hmcl.game; -import com.google.gson.*; +import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.gson.TolerableValidationException; import org.jackhuang.hmcl.util.gson.Validation; @@ -28,10 +29,7 @@ import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; /** * A class that describes a Minecraft dependency. @@ -40,6 +38,40 @@ import java.util.Optional; */ @Immutable public class Library implements Comparable, Validation { + /** + *

A possible native descriptors can be: [variant-]os[-key]

+ * + *

+ * Variant can be empty string, 'native', or 'natives'. + * Key can be empty string, system arch, or system arch bit count. + *

+ */ + private static final String[] POSSIBLE_NATIVE_DESCRIPTORS; + + static { + String[] keys = { + "", + Architecture.SYSTEM_ARCH.name().toLowerCase(Locale.ROOT), + Architecture.SYSTEM_ARCH.getBits().getBit() + }, variants = {"", "native", "natives"}; + + POSSIBLE_NATIVE_DESCRIPTORS = new String[keys.length * variants.length]; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < keys.length; i++) { + for (int j = 0; j < variants.length; j++) { + if (!variants[j].isEmpty()) { + builder.append(variants[j]).append('-'); + } + builder.append(OperatingSystem.CURRENT_OS.getMojangName()); + if (!keys[i].isEmpty()) { + builder.append('-').append(keys[i]); + } + + POSSIBLE_NATIVE_DESCRIPTORS[i * variants.length + j] = builder.toString(); + builder.setLength(0); + } + } + } @SerializedName("name") private final Artifact artifact; @@ -93,13 +125,27 @@ public class Library implements Comparable, Validation { } public String getClassifier() { - if (artifact.getClassifier() == null) - if (natives != null && natives.containsKey(OperatingSystem.CURRENT_OS.getMojangName())) - return natives.get(OperatingSystem.CURRENT_OS.getMojangName()).replace("${arch}", Architecture.SYSTEM_ARCH.getBits().getBit()); - else - return null; - else + if (artifact.getClassifier() == null) { + if (natives != null) { + for (String nativeDescriptor : POSSIBLE_NATIVE_DESCRIPTORS) { + String nd = natives.get(nativeDescriptor); + if (nd != null) { + return nd.replace("${arch}", Architecture.SYSTEM_ARCH.getBits().getBit()); + } + } + } else if (downloads != null && downloads.getClassifiers() != null) { + for (String nativeDescriptor : POSSIBLE_NATIVE_DESCRIPTORS) { + LibraryDownloadInfo info = downloads.getClassifiers().get(nativeDescriptor); + if (info != null) { + return nativeDescriptor; + } + } + } + + return null; + } else { return artifact.getClassifier(); + } } public ExtractRules getExtract() { @@ -111,10 +157,17 @@ public class Library implements Comparable, Validation { } public boolean isNative() { - return natives != null && appliesToCurrentEnvironment(); + if (!appliesToCurrentEnvironment()) { + return false; + } + if (natives != null) { + return true; + } + + return downloads != null && downloads.getClassifiers().keySet().stream().anyMatch(s -> s.startsWith("native")); } - protected LibraryDownloadInfo getRawDownloadInfo() { + public LibraryDownloadInfo getRawDownloadInfo() { if (downloads != null) { if (isNative()) return downloads.getClassifiers().get(getClassifier()); @@ -125,6 +178,10 @@ public class Library implements Comparable, Validation { } } + public Artifact getArtifact() { + return artifact; + } + public String getPath() { LibraryDownloadInfo temp = getRawDownloadInfo(); if (temp != null && temp.getPath() != null) @@ -137,12 +194,28 @@ public class Library implements Comparable, Validation { LibraryDownloadInfo temp = getRawDownloadInfo(); String path = getPath(); return new LibraryDownloadInfo(path, - Optional.ofNullable(temp).map(LibraryDownloadInfo::getUrl).orElse(Optional.ofNullable(url).orElse(Constants.DEFAULT_LIBRARY_URL) + path), + computePath(temp, path), temp != null ? temp.getSha1() : null, temp != null ? temp.getSize() : 0 ); } + private String computePath(LibraryDownloadInfo raw, String path) { + if (raw != null) { + String url = raw.getUrl(); + if (url != null) { + return url; + } + } + + String repo = Lang.requireNonNullElse(url, Constants.DEFAULT_LIBRARY_URL); + if (!repo.endsWith("/")) { + repo += '/'; + } + + return repo + path; + } + public boolean hasDownloadURL() { LibraryDownloadInfo temp = getRawDownloadInfo(); if (temp != null) return temp.getUrl() != null; @@ -159,6 +232,7 @@ public class Library implements Comparable, Validation { /** * Hint for how to locate the library file. + * * @return null for default, "local" for location in version/<version>/libraries/filename */ @Nullable @@ -168,6 +242,7 @@ public class Library implements Comparable, Validation { /** * Available when hint is "local" + * * @return the filename of the local library in version/<version>/libraries/$filename */ @Nullable diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/OSRestriction.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/OSRestriction.java index ea614b721..98f91f0b6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/OSRestriction.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/OSRestriction.java @@ -24,7 +24,6 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem; import java.util.regex.Pattern; /** - * * @author huangyuhui */ public final class OSRestriction { @@ -57,19 +56,28 @@ public final class OSRestriction { this.arch = arch; } - public String getName() { - return name; - } - - public String getVersion() { - return version; - } - - public String getArch() { - return arch; - } - public boolean allow() { + // Some modpacks directly use { name: "win-x86" } + if (name != null) { + String[] parts = name.split("-", 3); + if (parts.length == 2) { + OperatingSystem os = OperatingSystem.parseOSName(parts[0]); + Architecture arch = Architecture.parseArchName(parts[1]); + + if (os != OperatingSystem.UNKNOWN && arch != Architecture.UNKNOWN) { + if (os != OperatingSystem.CURRENT_OS && !(os == OperatingSystem.LINUX && OperatingSystem.CURRENT_OS.isLinuxOrBSD())) { + return false; + } + + if (arch != Architecture.SYSTEM_ARCH) { + return false; + } + + return true; + } + } + } + OperatingSystem os = OperatingSystem.parseOSName(name); if (os != OperatingSystem.UNKNOWN && os != OperatingSystem.CURRENT_OS @@ -85,5 +93,4 @@ public final class OSRestriction { return true; } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java index e7e808be8..26b037822 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java @@ -36,6 +36,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; @Immutable public class Version implements Comparable, Validation { + /** + * Patches with higher priority can override info from other patches, such as mainClass. + */ + public static final int PRIORITY_MC = 0, PRIORITY_LOADER = 30000; + private final String id; private final String version; private final Integer priority; @@ -302,7 +307,8 @@ public class Version implements Comparable, Validation { if (inheritsFrom == null) { if (isRoot()) { - thisVersion = new Version(id).setPatches(patches); + // TODO: Breaking change, require much testing on versions installed with external installer, other launchers, and all kinds of versions. + thisVersion = patches != null ? new Version(id).setPatches(patches) : this; } else { thisVersion = this; } @@ -368,7 +374,7 @@ public class Version implements Comparable, Validation { return thisVersion.setId(id).setJar(resolve(provider).getJar()); } - private Version markAsResolved() { + public Version markAsResolved() { return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } @@ -380,6 +386,10 @@ public class Version implements Comparable, Validation { return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } + public Version setRoot(Boolean root) { + return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); + } + public Version setId(String id) { return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } @@ -396,6 +406,10 @@ public class Version implements Comparable, Validation { return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } + public Version setJavaVersion(GameJavaVersion javaVersion) { + return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); + } + public Version setArguments(Arguments arguments) { return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } @@ -412,10 +426,18 @@ public class Version implements Comparable, Validation { return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } + public Version setAssetIndex(AssetIndexInfo assetIndex) { + return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); + } + public Version setLibraries(List libraries) { return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } + public Version setDownload(JsonMap downloads) { + return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); + } + public Version setLogging(Map logging) { return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherLibrary.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherLibrary.java index 8a6bcf429..3475fb310 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherLibrary.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherLibrary.java @@ -18,7 +18,12 @@ package org.jackhuang.hmcl.game.tlauncher; import com.google.gson.annotations.SerializedName; -import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.game.Artifact; +import org.jackhuang.hmcl.game.CompatibilityRule; +import org.jackhuang.hmcl.game.ExtractRules; +import org.jackhuang.hmcl.game.LibrariesDownloadInfo; +import org.jackhuang.hmcl.game.Library; +import org.jackhuang.hmcl.game.LibraryDownloadInfo; import org.jackhuang.hmcl.util.Immutable; import java.util.List; 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 new file mode 100644 index 000000000..dafd01dd7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCComponents.java @@ -0,0 +1,52 @@ +package org.jackhuang.hmcl.mod.multimc; + +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.util.io.NetworkUtils; + +import java.net.URL; +import java.util.*; +import java.util.stream.Collectors; + +public final class MultiMCComponents { + + private MultiMCComponents() { + } + + private static final Map ID_TYPE = new HashMap<>(); + + static { + ID_TYPE.put("net.minecraft", LibraryAnalyzer.LibraryType.MINECRAFT); + ID_TYPE.put("net.minecraftforge", LibraryAnalyzer.LibraryType.FORGE); + ID_TYPE.put("net.neoforged", LibraryAnalyzer.LibraryType.NEO_FORGE); + ID_TYPE.put("com.mumfrey.liteloader", LibraryAnalyzer.LibraryType.LITELOADER); + ID_TYPE.put("net.fabricmc.fabric-loader", LibraryAnalyzer.LibraryType.FABRIC); + ID_TYPE.put("org.quiltmc.quilt-loader", LibraryAnalyzer.LibraryType.QUILT); + } + + private static final Map TYPE_ID = + ID_TYPE.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); + + private static final Collection> PAIRS = Collections.unmodifiableCollection(ID_TYPE.entrySet()); + + static { + if (TYPE_ID.isEmpty()) { + throw new AssertionError("Please make sure TYPE_ID and PAIRS is initialized after ID_TYPE!"); + } + } + + public static String getComponent(LibraryAnalyzer.LibraryType type) { + return TYPE_ID.get(type); + } + + public static LibraryAnalyzer.LibraryType getComponent(String type) { + return ID_TYPE.get(type); + } + + public static Collection> getPairs() { + return PAIRS; + } + + public static URL getMetaURL(String componentID, String version) { + return NetworkUtils.toURL(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 7a9f555eb..55869eb12 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 @@ -17,80 +17,408 @@ */ package org.jackhuang.hmcl.mod.multimc; +import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.Argument; +import org.jackhuang.hmcl.game.Arguments; +import org.jackhuang.hmcl.game.Artifact; +import org.jackhuang.hmcl.game.AssetIndexInfo; +import org.jackhuang.hmcl.game.CompatibilityRule; +import org.jackhuang.hmcl.game.DownloadType; +import org.jackhuang.hmcl.game.GameJavaVersion; import org.jackhuang.hmcl.game.Library; +import org.jackhuang.hmcl.game.OSRestriction; +import org.jackhuang.hmcl.game.RuledArgument; +import org.jackhuang.hmcl.game.StringArgument; +import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.util.Immutable; 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.logging.Logger; +import org.jackhuang.hmcl.util.platform.OperatingSystem; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; /** - * * @author huangyuhui */ @Immutable public final class MultiMCInstancePatch { + private final int formatVersion; - private final String name; + @SerializedName("uid") + private final String id; + + @SerializedName("version") private final String version; - @SerializedName("mcVersion") - private final String gameVersion; + @SerializedName("assetIndex") + private final AssetIndexInfo assetIndex; + + @SerializedName("minecraftArguments") + private final String minecraftArguments; + + @SerializedName("+jvmArgs") + private final List jvmArgs; + + @SerializedName("mainClass") private final String mainClass; - private final String fileId; + + @SerializedName("compatibleJavaMajors") + private final int[] javaMajors; + + @SerializedName("mainJar") + private final Library mainJar; + + @SerializedName("+traits") + private final List traits; @SerializedName("+tweakers") private final List tweakers; - @SerializedName("+libraries") - private final List _libraries; + @SerializedName(value = "+libraries") + private final List libraries0; + @SerializedName(value = "libraries") + private final List libraries1; + @SerializedName(value = "mavenFiles") + private final List mavenFiles; - @SerializedName("libraries") - private final List libraries; + @SerializedName("jarMods") + private final List jarMods; - public MultiMCInstancePatch() { - this("", "", "", "", "", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - } - - public MultiMCInstancePatch(String name, String version, String gameVersion, String mainClass, String fileId, List tweakers, List _libraries, List libraries) { - this.name = name; + public MultiMCInstancePatch(int formatVersion, String id, String version, AssetIndexInfo assetIndex, String minecraftArguments, List jvmArgs, String mainClass, int[] javaMajors, Library mainJar, List traits, List tweakers, List libraries0, List libraries1, List mavenFiles, List jarMods) { + this.formatVersion = formatVersion; + this.id = id; this.version = version; - this.gameVersion = gameVersion; + this.assetIndex = assetIndex; + this.minecraftArguments = minecraftArguments; + this.jvmArgs = jvmArgs; this.mainClass = mainClass; - this.fileId = fileId; - this.tweakers = new ArrayList<>(tweakers); - this._libraries = new ArrayList<>(_libraries); - this.libraries = new ArrayList<>(libraries); + this.javaMajors = javaMajors; + this.mainJar = mainJar; + this.traits = traits; + this.tweakers = tweakers; + this.libraries0 = libraries0; + this.libraries1 = libraries1; + this.mavenFiles = mavenFiles; + this.jarMods = jarMods; } - public String getName() { - return name; + public int getFormatVersion() { + return formatVersion; + } + + public String getID() { + return id; } public String getVersion() { return version; } - public String getGameVersion() { - return gameVersion; + public AssetIndexInfo getAssetIndex() { + return assetIndex; + } + + public String getMinecraftArguments() { + return minecraftArguments; + } + + public List getJvmArgs() { + return nonNullOrEmpty(jvmArgs); } public String getMainClass() { return mainClass; } - public String getFileId() { - return fileId; + public int[] getJavaMajors() { + return javaMajors; + } + + public Library getMainJar() { + return mainJar; + } + + public List getTraits() { + return nonNullOrEmpty(traits); } public List getTweakers() { - return Collections.unmodifiableList(tweakers); + return nonNullOrEmpty(tweakers); } public List getLibraries() { - return Lang.merge(_libraries, libraries); + List list = new ArrayList<>(); + if (libraries0 != null) { + list.addAll(libraries0); + } + if (libraries1 != null) { + list.addAll(libraries1); + } + return nonNullOrEmpty(list); } + public List getMavenOnlyFiles() { + return nonNullOrEmpty(mavenFiles); + } + + public List getJarMods() { + return nonNullOrEmpty(jarMods); + } + + private static List nonNullOrEmpty(List value) { + return value != null && !value.isEmpty() ? value : Collections.emptyList(); + } + + private static List dropDuplicate(List original, Function mapper) { + Set values = new HashSet<>(); + List result = new ArrayList<>(); + + for (T item : original) { + if (values.add(mapper.apply(item))) { + result.add(item); + } + } + + return result; + } + + public static MultiMCInstancePatch read(String componentID, String text) { + try { + return JsonUtils.fromNonNullJson(text, MultiMCInstancePatch.class); + } catch (JsonParseException e) { + throw new IllegalArgumentException("Illegal Json-Patch: " + componentID); + } + } + + public static final class ResolvedInstance { + private final Version version; + + private final String gameVersion; + + private final Library mainJar; + + private final List jarModFileNames; + private final List mavenOnlyFiles; + + public ResolvedInstance(Version version, String gameVersion, Library mainJar, List jarModFileNames, List mavenOnlyFiles) { + this.version = version; + this.gameVersion = gameVersion; + this.mainJar = mainJar; + this.jarModFileNames = jarModFileNames; + this.mavenOnlyFiles = mavenOnlyFiles; + } + + public Version getVersion() { + return version; + } + + public String getGameVersion() { + return gameVersion; + } + + public Library getMainJar() { + return mainJar; + } + + public List getJarModFileNames() { + return jarModFileNames; + } + + public List getMavenOnlyFiles() { + return mavenOnlyFiles; + } + } + + /** + *

Core methods transforming MultiMCModpack to Official Version Scheme.

+ * + *

Mose of the information can be transformed in a lossless manner, except for some inputs. + * See to do marks below for more information

+ * + * @param patches List of all Json-Patch. + * @param versionID the version ID. Used when constructing a Version. + * @return The resolved instance. + */ + public static ResolvedInstance resolveArtifact(List patches, String versionID) { + if (patches.isEmpty()) { + throw new IllegalArgumentException("Empty components."); + } + + for (MultiMCInstancePatch patch : patches) { + if (patch.getFormatVersion() != 1) { + throw new UnsupportedOperationException( + String.format("Unsupported JSON-Patch[%s] format version: %d", patch.getID(), patch.getFormatVersion()) + ); + } + } + + StringBuilder message = new StringBuilder(); + + List minecraftArguments; + ArrayList jvmArguments = new ArrayList<>(Arguments.DEFAULT_JVM_ARGUMENTS); + String mainClass; + AssetIndexInfo assetIndex; + int[] javaMajors; + Library mainJar; + List traits; + List tweakers; + List libraries; + List mavenOnlyFiles; + List jarModFileNames; + + { + MultiMCInstancePatch last = patches.get(patches.size() - 1); + minecraftArguments = last.getMinecraftArguments() == null ? null : StringUtils.tokenize(last.getMinecraftArguments()); + mainClass = last.getMainClass(); + assetIndex = last.getAssetIndex(); + javaMajors = last.getJavaMajors(); + mainJar = last.getMainJar(); + traits = last.getTraits(); + tweakers = last.getTweakers(); + libraries = last.getLibraries(); + mavenOnlyFiles = last.getMavenOnlyFiles(); + jarModFileNames = last.getJarMods().stream().map(Library::getFileName).collect(Collectors.toList()); + } + + for (int i = patches.size() - 2; i >= 0; i--) { + MultiMCInstancePatch patch = patches.get(i); + if (minecraftArguments == null & patch.getMinecraftArguments() != null) { + minecraftArguments = StringUtils.tokenize(patch.getMinecraftArguments()); + } + for (String jvmArg : patch.getJvmArgs()) { + jvmArguments.add(new StringArgument(jvmArg)); + } + mainClass = Lang.requireNonNullElse(mainClass, patch.getMainClass()); + assetIndex = Lang.requireNonNullElse(patch.getAssetIndex(), assetIndex); + javaMajors = Lang.requireNonNullElse(patch.getJavaMajors(), javaMajors); + mainJar = Lang.requireNonNullElse(patch.getMainJar(), mainJar); + traits = Lang.merge(patch.getTraits(), traits); + tweakers = Lang.merge(patch.getTweakers(), tweakers); + libraries = Lang.merge(patch.getLibraries(), libraries); + mavenOnlyFiles = Lang.merge(patch.getMavenOnlyFiles(), mavenOnlyFiles); + jarModFileNames = Lang.merge(patch.getJarMods().stream().map(Library::getFileName).collect(Collectors.toList()), jarModFileNames); + } + + mainClass = Lang.requireNonNullElse(mainClass, "net.minecraft.client.Minecraft"); + + if (minecraftArguments == null) { + minecraftArguments = new ArrayList<>(); + } + + // '--tweakClass' can't be the last argument. + for (int i = minecraftArguments.size() - 2; i >= 0; i--) { + if ("--tweakClass".equals(minecraftArguments.get(i))) { + tweakers.add(minecraftArguments.get(i + 1)); + + minecraftArguments.remove(i); + minecraftArguments.remove(i); + } + } + + traits = dropDuplicate(traits, Function.identity()); + tweakers = dropDuplicate(tweakers, Function.identity()); + libraries = dropDuplicate(libraries, Library::getName); + jarModFileNames = dropDuplicate(jarModFileNames, Function.identity()); + + for (String tweaker : tweakers) { + minecraftArguments.add("--tweakClass"); + minecraftArguments.add(tweaker); + } + + for (String trait : traits) { + switch (trait) { + case "FirstThreadOnMacOS": { + jvmArguments.add(new RuledArgument( + Collections.singletonList( + new CompatibilityRule(CompatibilityRule.Action.ALLOW, new OSRestriction(OperatingSystem.MACOS)) + ), + Collections.singletonList("-XstartOnFirstThread") + )); + break; + } + case "XR:Initial": // Flag for chat report. See https://discord.com/channels/132965178051526656/134843027553255425/1380885829702127616 + case "texturepacks": // HMCL hasn't support checking whether a game version supports texture packs. + case "no-texturepacks": { + break; + } + default: { + message.append(" - Trait: ").append(trait).append('\n'); + break; + } + } + } + + for (Library library : libraries) { + Artifact artifact = library.getArtifact(); + if ("io.github.zekerzhayard".equals(artifact.getGroup()) && "ForgeWrapper".equals(artifact.getName())) { + jvmArguments.add(new StringArgument("-Dforgewrapper.librariesDir=${library_directory}")); + jvmArguments.add(new StringArgument("-Dforgewrapper.minecraft=${primary_jar}")); + + for (Library lib : libraries) { + Artifact ar = lib.getArtifact(); + if ("net.neoforged".equals(ar.getGroup()) && "neoforge".equals(ar.getName()) && "installer".equals(ar.getClassifier()) || + "net.minecraftforge".equals(ar.getGroup()) && "forge".equals(ar.getName()) && "installer".equals(ar.getClassifier()) + ) { + jvmArguments.add(new StringArgument("-Dforgewrapper.installer=${library_directory}/" + ar.getPath())); + } + } + } + } + + Version version = new Version(versionID) + .setArguments(new Arguments().addGameArguments(minecraftArguments).addJVMArgumentsDirect(jvmArguments)) + .setMainClass(mainClass) + .setLibraries(libraries) + .setAssetIndex(assetIndex) + .setDownload(new JsonMap<>(Collections.singletonMap(DownloadType.CLIENT, mainJar.getRawDownloadInfo()))); + + /* TODO: Official Version-Json can only store one pre-defined GameJavaVersion, including 8, 11, 16, 17 and 21. + An array of all suitable java versions are NOT supported. + For compatibility with official launcher and any other launchers, a transform is made between int[] and GameJavaVersion. */ + javaMajors: + if (javaMajors != null) { + javaMajors = javaMajors.clone(); + Arrays.sort(javaMajors); + + for (int i = javaMajors.length - 1; i >= 0; i--) { + GameJavaVersion jv = GameJavaVersion.get(javaMajors[i]); + if (jv != null) { + version = version.setJavaVersion(jv); + break javaMajors; + } + } + + message.append(" - Java Version Range: ").append(Arrays.toString(javaMajors)).append('\n'); + } + + version = version.markAsResolved(); + + String gameVersion = null; + for (MultiMCInstancePatch patch : patches) { + if (MultiMCComponents.getComponent(patch.getID()) == LibraryAnalyzer.LibraryType.MINECRAFT) { + gameVersion = patch.getVersion(); + break; + } + } + + if (message.length() != 0) { + if (message.charAt(message.length() - 1) == '\n') { + message.setLength(message.length() - 1); + } + Logger.LOG.warning("Cannot fully parse MultiMC modpack with following unsupported features: \n" + message); + } + return new ResolvedInstance(version, gameVersion, mainJar, jarModFileNames, mavenOnlyFiles); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackExportTask.java index 99ff9ab35..93670a4d5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackExportTask.java @@ -31,6 +31,7 @@ import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; +import java.util.Map; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -74,17 +75,16 @@ public class MultiMCModpackExportTask extends Task { .orElseThrow(() -> new IOException("Cannot parse the version of " + versionId)); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(versionId), gameVersion); List components = new ArrayList<>(); - components.add(new MultiMCManifest.MultiMCManifestComponent(true, false, "net.minecraft", gameVersion)); - analyzer.getVersion(FORGE).ifPresent(forgeVersion -> - components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.minecraftforge", forgeVersion))); - analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> - components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.neoforged", neoForgeVersion))); - analyzer.getVersion(LITELOADER).ifPresent(liteLoaderVersion -> - components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "com.mumfrey.liteloader", liteLoaderVersion))); - analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> - components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.fabricmc.fabric-loader", fabricVersion))); - analyzer.getVersion(QUILT).ifPresent(quiltVersion -> - components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "org.quiltmc.quilt-loader", quiltVersion))); + components.add(new MultiMCManifest.MultiMCManifestComponent(true, false, MultiMCComponents.getComponent(MINECRAFT), gameVersion)); + + for (Map.Entry pair : MultiMCComponents.getPairs()) { + if (pair.getValue().isModLoader()) { + analyzer.getVersion(pair.getValue()).ifPresent( + v -> components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, pair.getKey(), v)) + ); + } + } + MultiMCManifest mmcPack = new MultiMCManifest(1, components); zip.putTextFile(JsonUtils.GSON.toJson(mmcPack), "mmc-pack.json"); 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 900f3a01a..64c531355 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,21 +19,26 @@ package org.jackhuang.hmcl.mod.multimc; import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.GameBuilder; -import org.jackhuang.hmcl.game.Arguments; +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.Version; import org.jackhuang.hmcl.mod.MinecraftInstanceTask; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackInstallTask; +import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -41,79 +46,47 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Optional; +import java.util.Objects; +import java.util.stream.Collectors; /** - * - * @author huangyuhui + *

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 and Apply JAR mods. + * See codes below for detailed implementation. + *

*/ -public final class MultiMCModpackInstallTask extends Task { +public final class MultiMCModpackInstallTask extends Task { private final File zipFile; private final Modpack modpack; private final MultiMCInstanceConfiguration manifest; private final String name; private final DefaultGameRepository repository; - private final List> dependencies = new ArrayList<>(1); - private final List> dependents = new ArrayList<>(4); + private final List> patches = new ArrayList<>(); + private final List> dependents = new ArrayList<>(); + private final List> dependencies = new ArrayList<>(); + private final DefaultDependencyManager dependencyManager; public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, String name) { this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; this.name = name; + this.dependencyManager = dependencyManager; this.repository = dependencyManager.getGameRepository(); File json = repository.getModpackConfiguration(name); if (repository.hasVersion(name) && !json.exists()) throw new IllegalArgumentException("Version " + name + " already exists."); - GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.getGameVersion()); - - if (manifest.getMmcPack() != null) { - Optional forge = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.minecraftforge")).findAny(); - forge.ifPresent(c -> { - if (c.getVersion() != null) - builder.version("forge", c.getVersion()); - }); - - Optional neoForge = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.neoforged")).findAny(); - neoForge.ifPresent(c -> { - if (c.getVersion() != null) - builder.version("neoforge", c.getVersion()); - }); - - Optional liteLoader = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("com.mumfrey.liteloader")).findAny(); - liteLoader.ifPresent(c -> { - if (c.getVersion() != null) - builder.version("liteloader", c.getVersion()); - }); - - Optional fabric = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.fabricmc.fabric-loader")).findAny(); - fabric.ifPresent(c -> { - if (c.getVersion() != null) - builder.version("fabric", c.getVersion()); - }); - - Optional quilt = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("org.quiltmc.quilt-loader")).findAny(); - quilt.ifPresent(c -> { - if (c.getVersion() != null) - builder.version("quilt", c.getVersion()); - }); - } - - dependents.add(builder.buildAsync()); onDone().register(event -> { if (event.isFailed()) repository.removeVersionFromDisk(name); }); } - @Override - public List> getDependencies() { - return dependencies; - } - @Override public boolean doPreExecute() { return true; @@ -121,85 +94,85 @@ public final class MultiMCModpackInstallTask extends Task { @Override public void preExecute() throws Exception { - File run = repository.getRunDirectory(name); - File json = repository.getModpackConfiguration(name); + // Stage #0: General Setup + { + File run = repository.getRunDirectory(name); + File json = repository.getModpackConfiguration(name); - ModpackConfiguration config = null; - try { - if (json.exists()) { - config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class)); + ModpackConfiguration config = null; + try { + if (json.exists()) { + config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class)); - if (!MultiMCModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a MultiMC modpack. Cannot update this version."); + if (!MultiMCModpackProvider.INSTANCE.getName().equals(config.getType())) + throw new IllegalArgumentException("Version " + name + " is not a MultiMC modpack. Cannot update this version."); + } + } catch (JsonParseException | IOException ignore) { } - } catch (JsonParseException | IOException ignore) { + + String mcDirectory; + try (FileSystem fs = openModpack()) { + mcDirectory = getRootPath(fs).resolve(".minecraft").toAbsolutePath().normalize().toString(); + } + + // TODO: Optimize unbearably slow ModpackInstallTask + dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(mcDirectory), any -> true, config).withStage("hmcl.modpack")); + dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(mcDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); } - String subDirectory; + // Stage #1: Load all related Json-Patch from meta maven or local mod pack. - try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setEncoding(modpack.getEncoding()).build()) { - // /.minecraft - if (Files.exists(fs.getPath("/.minecraft"))) { - subDirectory = "/.minecraft"; - // /minecraft - } else if (Files.exists(fs.getPath("/minecraft"))) { - subDirectory = "/minecraft"; - // /[name]/.minecraft - } else if (Files.exists(fs.getPath("/" + manifest.getName() + "/.minecraft"))) { - subDirectory = "/" + manifest.getName() + "/.minecraft"; - // /[name]/minecraft - } else if (Files.exists(fs.getPath("/" + manifest.getName() + "/minecraft"))) { - subDirectory = "/" + manifest.getName() + "/minecraft"; - } else { - subDirectory = "/" + manifest.getName() + "/.minecraft"; + try (FileSystem fs = openModpack()) { + Path root = getRootPath(fs); + + for (MultiMCManifest.MultiMCManifestComponent component : Objects.requireNonNull( + Objects.requireNonNull(manifest.getMmcPack(), "mmc-pack.json").getComponents(), "components" + )) { + String componentID = Objects.requireNonNull(component.getUid(), "Component ID"); + Path patchPath = root.resolve(String.format("patches/%s.json", componentID)); + + Task task; + if (Files.exists(patchPath)) { + if (!Files.isRegularFile(patchPath)) { + throw new IllegalArgumentException("Json-Patch isn't a file: " + componentID); + } + + // TODO: Task.completed has unclear compatibility issue. + String text = FileUtils.readText(patchPath, StandardCharsets.UTF_8); + task = Task.supplyAsync(() -> text); + } else { + task = new GetTask(MultiMCComponents.getMetaURL(componentID, component.getVersion())); + } + + Task task2 = task.thenApplyAsync(s -> MultiMCInstancePatch.read(componentID, s)); + patches.add(task2); + dependents.add(task2); } } - - dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(subDirectory), any -> true, config).withStage("hmcl.modpack")); - dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(subDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); } @Override public List> getDependents() { + // Stage #2: Resolve all Json-Patch return dependents; } @Override public void execute() throws Exception { - Version version = repository.readVersionJson(name); + // Stage #3: Build Json-Patch artifact. + MultiMCInstancePatch.ResolvedInstance artifact = MultiMCInstancePatch.resolveArtifact(patches.stream() + .map(value -> Objects.requireNonNull(value.getResult(), "MultiMCInstancePatch")) + .collect(Collectors.toList()), name + ); - try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) { - Path root = MultiMCModpackProvider.getRootPath(fs.getPath("/")); - Path patches = root.resolve("patches"); - - if (Files.exists(patches)) { - try (DirectoryStream directoryStream = Files.newDirectoryStream(patches)) { - for (Path patchJson : directoryStream) { - if (patchJson.toString().endsWith(".json")) { - // If json is malformed, we should stop installing this modpack instead of skipping it. - MultiMCInstancePatch multiMCPatch = JsonUtils.GSON.fromJson(FileUtils.readText(patchJson), MultiMCInstancePatch.class); - - List arguments = new ArrayList<>(); - for (String arg : multiMCPatch.getTweakers()) { - arguments.add("--tweakClass"); - arguments.add(arg); - } - - Version patch = new Version(multiMCPatch.getName(), multiMCPatch.getVersion(), 1, new Arguments().addGameArguments(arguments), multiMCPatch.getMainClass(), multiMCPatch.getLibraries()); - version = version.addPatch(patch); - } - } - } - } + // Stage #4: Copy embedded files. + try (FileSystem fs = openModpack()) { + Path root = getRootPath(fs); Path libraries = root.resolve("libraries"); if (Files.exists(libraries)) FileUtils.copyDirectory(libraries, repository.getVersionRoot(name).toPath().resolve("libraries")); - Path jarmods = root.resolve("jarmods"); - if (Files.exists(jarmods)) - FileUtils.copyDirectory(jarmods, repository.getVersionRoot(name).toPath().resolve("jarmods")); - String iconKey = this.manifest.getIconKey(); if (iconKey != null) { Path iconFile = root.resolve(iconKey + ".png"); @@ -209,6 +182,95 @@ public final class MultiMCModpackInstallTask extends Task { } } - dependencies.add(repository.saveAsync(version)); + // Stage #5: Assemble game files. + { + Version version = artifact.getVersion(); + + dependencies.add(repository.saveAsync(artifact.getVersion())); + dependencies.add(new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)); + dependencies.add(new GameLibrariesTask( + dependencyManager, + // TODO: check integrity of maven-only files when launching games? + version.setLibraries(Lang.merge(version.getLibraries(), artifact.getMavenOnlyFiles())), + true + )); + + Artifact mainJarArtifact = artifact.getMainJar().getArtifact(); + String gameVersion = artifact.getGameVersion(); + if (gameVersion != null && + "com.mojang".equals(mainJarArtifact.getGroup()) && + "minecraft".equals(mainJarArtifact.getName()) && + Objects.equals(gameVersion, mainJarArtifact.getVersion()) && + "client".equals(mainJarArtifact.getClassifier()) + ) { + dependencies.add(new GameDownloadTask(dependencyManager, gameVersion, version)); + } else { + dependencies.add(new GameDownloadTask(dependencyManager, null, version)); + } + } + + setResult(artifact); + } + + @Override + public List> getDependencies() { + // Stage #6: Download game files. + return dependencies; + } + + @Override + public boolean doPostExecute() { + return true; + } + + @Override + public void postExecute() throws Exception { + MultiMCInstancePatch.ResolvedInstance artifact = Objects.requireNonNull(getResult(), "ResolvedInstance"); + + List files = artifact.getJarModFileNames(); + if (!isDependenciesSucceeded() || files.isEmpty()) { + return; + } + + // Stage #7: Apply jar mods. + try (FileSystem fs = openModpack()) { + Path root = getRootPath(fs).resolve("jarmods"); + + try (FileSystem mc = CompressingUtils.writable( + repository.getVersionRoot(name).toPath().resolve(name + ".jar") + ).setAutoDetectEncoding(true).build()) { + for (String fileName : files) { + try (FileSystem jm = CompressingUtils.readonly(root.resolve(fileName)).setAutoDetectEncoding(true).build()) { + FileUtils.copyDirectory(jm.getPath("/"), mc.getPath("/")); + } + } + } + } + } + + private FileSystem openModpack() throws IOException { + return CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).setEncoding(modpack.getEncoding()).build(); + } + + private static boolean testPath(Path root) { + return Files.exists(root.resolve("instance.cfg")); + } + + private static Path getRootPath(FileSystem fs) throws IOException { + Path root = fs.getPath("/"); + + if (testPath(root)) { + return root; + } + + try (DirectoryStream stream = Files.newDirectoryStream(root)) { + for (Path candidate : stream) { + if (testPath(candidate)) { + return candidate; + } + } + } + + throw new IOException("Not a valid MultiMC modpack"); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java index 6e4b22c48..c50c46079 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java @@ -31,9 +31,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.stream.Stream; public final class MultiMCModpackProvider implements ModpackProvider { public static final MultiMCModpackProvider INSTANCE = new MultiMCModpackProvider(); @@ -56,20 +54,6 @@ public final class MultiMCModpackProvider implements ModpackProvider { return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)); } - private static boolean testPath(Path root) { - return Files.exists(root.resolve("instance.cfg")); - } - - public static Path getRootPath(Path root) throws IOException { - if (testPath(root)) return root; - try (Stream stream = Files.list(root)) { - Path candidate = stream.filter(Files::isDirectory).findAny() - .orElseThrow(() -> new IOException("Not a valid MultiMC modpack")); - if (testPath(candidate)) return candidate; - throw new IOException("Not a valid MultiMC modpack"); - } - } - private static String getRootEntryName(ZipArchiveReader file) throws IOException { final String instanceFileName = "instance.cfg";