diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java index ec00801e9f..49b20ab184 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java @@ -285,6 +285,23 @@ public static JavaRuntime findSuitableJava(Collection javaRuntimes, GameJavaVersion suggestedJavaVersion = (version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null; + if (suggestedJavaVersion != null) { + for (JavaRuntime java : javaRuntimes) { + if (forceX86) { + if (!java.getArchitecture().isX86()) + continue; + } else { + if (java.getArchitecture() != Architecture.SYSTEM_ARCH) + continue; + } + + // 100% matched. + if (java.getParsedVersion() == suggestedJavaVersion.getMajorVersion()) { + return java; + } + } + } + JavaRuntime mandatory = null; JavaRuntime suggested = null; for (JavaRuntime java : javaRuntimes) { 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 96d7d38380..458d8dd9b4 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 @@ private Version getPatch(FabricInfo fabricInfo, String gameVersion, String loade 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 b2b626e0fa..91823b2658 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 @@ -411,7 +411,7 @@ public void execute() throws Exception { 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 a1abeba252..df3b919ad9 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 void execute() throws Exception { } 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 2fd3777c64..aea0e04542 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 boolean isRelyingOnDependencies() { @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 3569a6e11f..55ad6f8fae 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 @@ -407,7 +407,7 @@ public void execute() throws Exception { 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 81f3a84f02..c0bd512907 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 @@ private Version getPatch(QuiltInfo quiltInfo, String gameVersion, String loaderV 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/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index 91c7e0adbf..886a6d84db 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 File getLibrariesDirectory(Version version) { @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 4325eac472..976d1e5af2 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.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,13 +38,47 @@ */ @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.getCheckedName()); + 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; private final String url; private final LibrariesDownloadInfo downloads; private final ExtractRules extract; - private final Map natives; + private final Map natives; private final List rules; private final List checksums; @@ -64,7 +96,7 @@ public Library(Artifact artifact, String url, LibrariesDownloadInfo downloads) { this(artifact, url, downloads, null, null, null, null, null, null); } - public Library(Artifact artifact, String url, LibrariesDownloadInfo downloads, List checksums, ExtractRules extract, Map natives, List rules, String hint, String filename) { + public Library(Artifact artifact, String url, LibrariesDownloadInfo downloads, List checksums, ExtractRules extract, Map natives, List rules, String hint, String filename) { this.artifact = artifact; this.url = url; this.downloads = downloads; @@ -93,13 +125,27 @@ public String getVersion() { } public String getClassifier() { - if (artifact.getClassifier() == null) - if (natives != null && natives.containsKey(OperatingSystem.CURRENT_OS)) - return natives.get(OperatingSystem.CURRENT_OS).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 boolean appliesToCurrentEnvironment() { } 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() { + private LibraryDownloadInfo getRawDownloadInfo() { if (downloads != null) { if (isNative()) return downloads.getClassifiers().get(getClassifier()); @@ -125,6 +178,10 @@ protected LibraryDownloadInfo getRawDownloadInfo() { } } + public Artifact getArtifact() { + return artifact; + } + public String getPath() { LibraryDownloadInfo temp = getRawDownloadInfo(); if (temp != null && temp.getPath() != null) @@ -137,12 +194,28 @@ public LibraryDownloadInfo getDownload() { 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 List getRules() { /** * 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 String getHint() { /** * 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 f2a5ae399b..7643ce70ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/OSRestriction.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/OSRestriction.java @@ -17,16 +17,22 @@ */ package org.jackhuang.hmcl.game; +import com.google.gson.*; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import java.io.IOException; import java.util.regex.Pattern; /** - * * @author huangyuhui */ +@JsonAdapter(OSRestriction.JsonAdapterImpl.class) public final class OSRestriction { private final OperatingSystem name; @@ -78,4 +84,49 @@ public boolean allow() { return true; } + public static final class JsonAdapterImpl implements TypeAdapterFactory { + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != OSRestriction.class) { + return null; + } + + TypeAdapter thisDelegate = (TypeAdapter) gson.getDelegateAdapter(this, type); + + return (TypeAdapter) new TypeAdapter() { + @Override + public void write(JsonWriter writer, OSRestriction restriction) throws IOException { + thisDelegate.write(writer, restriction); + } + + @Override + public OSRestriction read(JsonReader reader) { + JsonObject element = gson.fromJson(reader, JsonObject.class); + + OSRestriction restriction = thisDelegate.fromJsonTree(element); + if (restriction.getName() != null) { + return restriction; + } + + JsonElement name = element.getAsJsonObject().get("name"); + if (name != null && name.isJsonPrimitive()) { + JsonPrimitive jp = name.getAsJsonPrimitive(); + if (jp.isString()) { + String[] parts = jp.getAsString().split("-", 2); + if (parts.length == 2) { + OperatingSystem os = gson.fromJson(new JsonPrimitive(parts[0]), OperatingSystem.class); + Architecture arch = gson.fromJson(new JsonPrimitive(parts[1]), Architecture.class); + if (os != null && arch != null) { + return new OSRestriction(os, restriction.version, arch.getCheckedName()); + } + } + } + } + + return restriction; + } + }; + } + } } 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 e7e808be8a..3d309a3847 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java @@ -25,6 +25,7 @@ import java.time.Instant; import java.util.*; +import java.util.function.BiFunction; import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -36,6 +37,11 @@ @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; @@ -269,7 +275,18 @@ public Version resolve(VersionProvider provider) throws VersionNotFoundException return resolve(provider, new HashSet<>()).markAsResolved(); } - protected Version merge(Version parent, boolean isPatch) { + /** + *

Custom Library Merge Strategies.

+ * + *

THIS_FIRST: Default implementation. For Version::resolve

+ *

THAT_FIRST, ONLY_THIS: MultiMC implementation. For MultiMCModpackInstallTask

+ */ + public static final BiFunction, List, List> + THIS_FIRST = Lang::merge, + THAT_FIRST = (self, parent) -> Lang.merge(parent, self), + ONLY_THIS = (self, parent) -> self; + + public Version merge(Version parent, boolean isPatch, BiFunction, List, List> libMerge) { return new Version( true, id, @@ -284,7 +301,7 @@ protected Version merge(Version parent, boolean isPatch) { assets == null ? parent.assets : assets, complianceLevel, javaVersion == null ? parent.javaVersion : javaVersion, - Lang.merge(this.libraries, parent.libraries), + libMerge.apply(this.libraries, parent.libraries), Lang.merge(parent.compatibilityRules, this.compatibilityRules), downloads == null ? parent.downloads : downloads, logging == null ? parent.logging : logging, @@ -314,7 +331,7 @@ protected Version resolve(VersionProvider provider, Set resolvedSoFar) t thisVersion = this.jar == null ? this.setJar(id) : this; } else { // It is supposed to auto install an version in getVersion. - thisVersion = merge(provider.getVersion(inheritsFrom).resolve(provider, resolvedSoFar), false); + thisVersion = merge(provider.getVersion(inheritsFrom).resolve(provider, resolvedSoFar), false, THIS_FIRST); } } @@ -327,7 +344,7 @@ protected Version resolve(VersionProvider provider, Set resolvedSoFar) t .sorted(Comparator.comparing(Version::getPriority)) .collect(Collectors.toList()); for (Version patch : sortedPatches) { - thisVersion = patch.setJar(null).merge(thisVersion, true); + thisVersion = patch.setJar(null).merge(thisVersion, true, THIS_FIRST); } } @@ -380,6 +397,10 @@ private Version setHidden(Boolean hidden) { 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 +417,10 @@ public Version setMinecraftArguments(String minecraftArguments) { 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,6 +437,10 @@ public Version setJar(String jar) { 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); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java index 647183e743..5bbffdc7cc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java @@ -20,10 +20,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.platform.CommandBuilder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; /** @@ -54,7 +51,12 @@ public Version build() { // The official launcher will not parse the "arguments" property when it detects the presence of "mcArgs". // The "arguments" property with the "rule" is simply ignored here. this.mcArgs.addAll(this.game.stream().map(arg -> arg.toString(new HashMap<>(), new HashMap<>())).flatMap(Collection::stream).collect(Collectors.toList())); - ret = ret.setArguments(null); + + // For compatibility with MultiMC launcher, we only ignore the game arguments but keep the presence of vm arguments. + Arguments arguments = ret.getArguments().orElse(null); + if (arguments != null) { + ret.setArguments(arguments.withGame(Collections.emptyList())); + } // Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping, // so we regenerate the minecraftArgument without escaping. 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 665f7ea2b4..41ad87d11b 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 @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Immutable public class TLauncherLibrary { @@ -58,7 +59,9 @@ public Library toLibrary() { new LibrariesDownloadInfo(artifact, classifiers), checksums, extract, - natives, + natives.entrySet().stream().collect(Collectors.toMap( + entry -> entry.getKey().getCheckedName(), Map.Entry::getValue + )), rules, null, null 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 0000000000..1112a607fb --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCComponents.java @@ -0,0 +1,50 @@ +package org.jackhuang.hmcl.mod.multimc; + +import org.jackhuang.hmcl.download.LibraryAnalyzer; + +import java.util.*; +import java.util.stream.Collectors; + +public final class MultiMCComponents { + public static final String[] META = { + "https://meta.multimc.org/v1/%s/%s.json", + "https://meta.prismlauncher.org/v1/%s/%s.json", + }; + + 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; + } +} 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 7a9f555ebb..579c6d69d2 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 @@ -18,16 +18,17 @@ package org.jackhuang.hmcl.mod.multimc; import com.google.gson.annotations.SerializedName; -import org.jackhuang.hmcl.game.Library; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Lang; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; /** - * * @author huangyuhui */ @Immutable @@ -35,34 +36,50 @@ public final class MultiMCInstancePatch { private final String name; private final String version; + private final int order; + private final AssetIndexInfo assetIndex; + + private final String minecraftArguments; @SerializedName("mcVersion") private final String gameVersion; private final String mainClass; - private final String fileId; + @SerializedName("compatibleJavaMajors") + private final int[] javaMajors; @SerializedName("+tweakers") + @Nullable private final List tweakers; + @SerializedName("+jvmArgs") + @Nullable + private final List jvmArgs; + @SerializedName("+libraries") + @Nullable private final List _libraries; @SerializedName("libraries") + @Nullable private final List libraries; - public MultiMCInstancePatch() { - this("", "", "", "", "", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - } + @Nullable + private final List jarMods; - public MultiMCInstancePatch(String name, String version, String gameVersion, String mainClass, String fileId, List tweakers, List _libraries, List libraries) { + public MultiMCInstancePatch(String name, String version, int order, AssetIndexInfo assetIndex, String minecraftArguments, String gameVersion, String mainClass, int[] javaMajors, @Nullable List tweakers, @Nullable List jvmArgs, @Nullable List _libraries, @Nullable List libraries, @Nullable List jarMods) { this.name = name; this.version = version; + this.order = order; + this.assetIndex = assetIndex; + this.minecraftArguments = minecraftArguments; this.gameVersion = gameVersion; 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.tweakers = tweakers; + this.jvmArgs = jvmArgs; + this._libraries = _libraries; + this.libraries = libraries; + this.jarMods = jarMods; } public String getName() { @@ -73,6 +90,22 @@ public String getVersion() { return version; } + public AssetIndexInfo getAssetIndex() { + return assetIndex; + } + + public String getMinecraftArguments() { + return minecraftArguments; + } + + public int getOrder() { + return order; + } + + public int[] getJavaMajors() { + return javaMajors; + } + public String getGameVersion() { return gameVersion; } @@ -81,16 +114,53 @@ public String getMainClass() { return mainClass; } - public String getFileId() { - return fileId; + public List getTweakers() { + return tweakers != null ? Collections.unmodifiableList(tweakers) : Collections.emptyList(); } - public List getTweakers() { - return Collections.unmodifiableList(tweakers); + public List getJvmArgs() { + return jvmArgs != null ? Collections.unmodifiableList(jvmArgs) : Collections.emptyList(); } public List getLibraries() { return Lang.merge(_libraries, libraries); } + public List getJarMods() { + return jarMods != null ? Collections.unmodifiableList(jarMods) : Collections.emptyList(); + } + + public Version asVersion(String patchID) { + List arguments = new ArrayList<>(); + for (String arg : getTweakers()) { + arguments.add("--tweakClass"); + arguments.add(arg); + } + + Version version = new Version(patchID) + .setVersion(getVersion()) + .setArguments(new Arguments().addGameArguments(arguments).addJVMArguments(getJvmArgs())) + .setMainClass(getMainClass()) + .setMinecraftArguments(getMinecraftArguments()) + .setLibraries(getLibraries()) + .setAssetIndex(getAssetIndex()); + + /* TODO: Official Version Json can only store one GameJavaVersion, not a array of all suitable java versions. + For compatibility with official launcher and any other launchers, a transform is made between int[] and GameJavaVersion. */ + int[] majors = getJavaMajors(); + if (majors != null) { + majors = majors.clone(); + Arrays.sort(majors); + + for (int i = majors.length - 1; i >= 0; i--) { + GameJavaVersion jv = GameJavaVersion.get(majors[i]); + if (jv != null) { + version = version.setJavaVersion(jv); + break; + } + } + } + + return version; + } } 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 99ff9ab358..93670a4d56 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.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 void execute() throws Exception { .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 900f3a01a9..998e2bc2aa 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 @@ -20,6 +20,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; +import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.Version; @@ -27,24 +28,25 @@ 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.Pair; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.NetworkUtils; import java.io.File; import java.io.IOException; +import java.net.URL; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; /** - * * @author huangyuhui */ public final class MultiMCModpackInstallTask extends Task { @@ -56,6 +58,7 @@ public final class MultiMCModpackInstallTask extends Task { private final DefaultGameRepository repository; private final List> dependencies = new ArrayList<>(1); private final List> dependents = new ArrayList<>(4); + private final Map componentOriginalPatch = new HashMap<>(); public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, String name) { this.zipFile = zipFile; @@ -71,35 +74,51 @@ public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, Fil 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()); - }); + for (MultiMCManifest.MultiMCManifestComponent component : manifest.getMmcPack().getComponents()) { + String componentID = component.getUid(); + + String version = component.getVersion(); + if (version == null) { + // https://github.com/MultiMC/Launcher/blob/develop/launcher/minecraft/ComponentUpdateTask.cpp#L586-L602 + 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": { + for (MultiMCManifest.MultiMCManifestComponent c : manifest.getMmcPack().getComponents()) { + if (MultiMCComponents.getComponent(c.getUid()) == LibraryAnalyzer.LibraryType.MINECRAFT) { + version = Objects.requireNonNull(c.getVersion(), "Version of Minecraft must be specific."); + break; + } + } + break; + } + } + } + + if (version != null) { + List urls = new ArrayList<>(MultiMCComponents.META.length); + for (String s : MultiMCComponents.META) { + urls.add(NetworkUtils.toURL(String.format(s, componentID, version))); + } + + GetTask task = new GetTask(urls); + + componentOriginalPatch.put(componentID, task); + dependents.add(task); + + LibraryAnalyzer.LibraryType type = MultiMCComponents.getComponent(componentID); + if (type != null) { + builder.version(type.getPatchId(), version); + } + } + } } dependents.add(builder.buildAsync()); @@ -141,13 +160,13 @@ public void preExecute() throws Exception { // /.minecraft if (Files.exists(fs.getPath("/.minecraft"))) { subDirectory = "/.minecraft"; - // /minecraft + // /minecraft } else if (Files.exists(fs.getPath("/minecraft"))) { subDirectory = "/minecraft"; - // /[name]/.minecraft + // /[name]/.minecraft } else if (Files.exists(fs.getPath("/" + manifest.getName() + "/.minecraft"))) { subDirectory = "/" + manifest.getName() + "/.minecraft"; - // /[name]/minecraft + // /[name]/minecraft } else if (Files.exists(fs.getPath("/" + manifest.getName() + "/minecraft"))) { subDirectory = "/" + manifest.getName() + "/minecraft"; } else { @@ -166,7 +185,19 @@ public List> getDependents() { @Override public void execute() throws Exception { - Version version = repository.readVersionJson(name); + // componentID -> + Map> components = new HashMap<>(); + + for (Map.Entry entry : componentOriginalPatch.entrySet()) { + String componentID = entry.getKey(); + String patchJson = Objects.requireNonNull(entry.getValue().getResult()); + + if (components.put(componentID, Pair.pair(readPatch(patchJson).asVersion( + componentID), null + )) != null) { + throw new IllegalArgumentException("Duplicate libraries: " + componentID); + } + } try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) { Path root = MultiMCModpackProvider.getRootPath(fs.getPath("/")); @@ -176,17 +207,12 @@ public void execute() throws Exception { 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); + String patchID = FileUtils.getNameWithoutExtension(patchJson); - List arguments = new ArrayList<>(); - for (String arg : multiMCPatch.getTweakers()) { - arguments.add("--tweakClass"); - arguments.add(arg); + Pair pair = components.computeIfAbsent(patchID, p -> Pair.pair(null, null)); + if (pair.setValue(readPatch(FileUtils.readText(patchJson))) != null) { + throw new IllegalArgumentException("Duplicate user patch: " + patchID); } - - Version patch = new Version(multiMCPatch.getName(), multiMCPatch.getVersion(), 1, new Arguments().addGameArguments(arguments), multiMCPatch.getMainClass(), multiMCPatch.getLibraries()); - version = version.addPatch(patch); } } } @@ -209,6 +235,73 @@ public void execute() throws Exception { } } - dependencies.add(repository.saveAsync(version)); + // If $.minecraftArguments exist, write default VM arguments into $.patches[name=game].arguments.jvm for compatibility. + // See org.jackhuang.hmcl.game.VersionLibraryBuilder::build + + { + Pair pair = components.get(MultiMCComponents.getComponent(LibraryAnalyzer.LibraryType.MINECRAFT)); + + Version mc = pair.getKey(); + if (mc.getArguments().map(Arguments::getJvm).map(List::isEmpty).orElse(true)) { + pair.setKey(mc.setArguments(new Arguments(null, Arguments.DEFAULT_JVM_ARGUMENTS))); + } + } + + // Rearrange all patches. + + Version artifact = null; + try (FileSystem mc = CompressingUtils.writable( + repository.getVersionRoot(name).toPath().resolve(name + ".jar") + ).setAutoDetectEncoding(true).build()) { + for (MultiMCManifest.MultiMCManifestComponent component : manifest.getMmcPack().getComponents()) { + String componentID = component.getUid(); + + Pair pair = components.get(componentID); + if (pair == null) { + throw new IllegalArgumentException("No such component: " + componentID); + } + + Version original = pair.getKey(); + MultiMCInstancePatch jp = pair.getValue(); + if (jp != null && !jp.getJarMods().isEmpty()) { + // JarMod. Merge it into minecraft.jar + if (original != null || !componentID.startsWith("org.multimc.jarmod.")) { + throw new IllegalArgumentException("Illegal jar mod: " + componentID); + } + + try (FileSystem jm = CompressingUtils.readonly(repository.getVersionRoot(name).toPath().resolve( + "jarmods/" + StringUtils.removePrefix(componentID, "org.multimc.jarmod.") + ".jar" + )).setAutoDetectEncoding(true).build()) { + FileUtils.copyDirectory(jm.getPath("/"), mc.getPath("/")); + } + } else { + Version tc, pp = jp == null ? null : jp.asVersion(componentID); + + if (original == null) { + tc = Objects.requireNonNull(pp, "Original and Json-Patch shouldn't be empty at the same time."); + } else if (jp != null) { + tc = pp.merge(original, true, Version.ONLY_THIS); + } else { + tc = original; + } + + artifact = artifact == null ? tc : tc.merge(artifact, true, Version.THAT_FIRST); + } + } + } + + // Erase all patches info to reject any modification to MultiMC mod packs. + artifact = Objects.requireNonNull(artifact, "There should be at least one component.") + .setPatches(null).setId(name).setJar(name).setRoot(null); + + dependencies.add(repository.saveAsync(artifact)); + } + + private MultiMCInstancePatch readPatch(String patchJson) { + try { + return JsonUtils.GSON.fromJson(patchJson, MultiMCInstancePatch.class); + } catch (JsonParseException e) { + throw new IllegalArgumentException("Cannot parse MultiMC patch json: " + patchJson, e); + } } }