From 60fa4eb0a153cc598c55542ac6e0eaa6de0460ee Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Mon, 19 Aug 2019 00:16:02 +0800 Subject: [PATCH] Fix LibrariesUniqueTask changing order of classpath --- .../jackhuang/hmcl/game/LauncherHelper.java | 3 +- .../ui/download/AdditionalInstallersPage.java | 2 +- .../hmcl/ui/versions/ModListPage.java | 2 +- .../download/DefaultDependencyManager.java | 35 ++--- .../hmcl/download/LibraryAnalyzer.java | 100 ++++++++++--- .../jackhuang/hmcl/download/MaintainTask.java | 132 ++++++++++++++++-- .../download/game/LibrariesUniqueTask.java | 100 ------------- .../jackhuang/hmcl/game/GameRepository.java | 4 + .../hmcl/game/LibrariesDownloadInfo.java | 2 +- .../java/org/jackhuang/hmcl/game/Version.java | 57 +++++++- .../mod/multimc/MultiMCModpackExportTask.java | 2 +- 11 files changed, 272 insertions(+), 167 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibrariesUniqueTask.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index f23ac4b84..2cb882669 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -26,7 +26,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.CredentialExpiredException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.MaintainTask; -import org.jackhuang.hmcl.download.game.LibrariesUniqueTask; import org.jackhuang.hmcl.download.game.LibraryDownloadException; import org.jackhuang.hmcl.launch.NotDecompressingNativesException; import org.jackhuang.hmcl.launch.PermissionException; @@ -132,7 +131,7 @@ public final class LauncherHelper { private void launch0() { HMCLGameRepository repository = profile.getRepository(); DefaultDependencyManager dependencyManager = profile.getDependency(); - Version version = MaintainTask.maintain(LibrariesUniqueTask.unique(repository.getResolvedVersion(selectedVersion))); + Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)); Optional gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version)); TaskExecutor executor = Task.runAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES)) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java index e084d66ab..35ce88d62 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java @@ -107,7 +107,7 @@ class AdditionalInstallersPage extends StackPane implements WizardPage { public void onNavigate(Map settings) { lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion()); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(provider.getVersion().resolve(provider.getProfile().getRepository())); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(provider.getVersion().resolvePreservingPatches(provider.getProfile().getRepository())); String fabric = analyzer.getVersion(FABRIC).orElse(null); String forge = analyzer.getVersion(FORGE).orElse(null); String liteLoader = analyzer.getVersion(LITELOADER).orElse(null); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 3370ca0b8..79c349ade 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -79,7 +79,7 @@ public final class ModListPage extends ListPageBase removeLibraryWithoutSavingAsync(String versionId, String libraryId) { - Version version = repository.getVersion(versionId); // to ensure version is not resolved + // MaintainTask requires version that does not inherits from any version. + // If we want to remove a library in dependent version, we should keep the dependents not changed + // So resolving this game version to preserve all information in this version.json is necessary. + Version version = repository.getResolvedPreservingPatchesVersion(versionId); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version); - LinkedList newList = new LinkedList<>(version.getLibraries()); - - switch (libraryId) { - case "forge": - analyzer.ifPresent(LibraryAnalyzer.LibraryType.FORGE, (library, libraryVersion) -> newList.remove(library)); - version = version.removePatchById(LibraryAnalyzer.LibraryType.FORGE.getPatchId()); - break; - case "liteloader": - analyzer.ifPresent(LibraryAnalyzer.LibraryType.LITELOADER, (library, libraryVersion) -> newList.remove(library)); - version = version.removePatchById(LibraryAnalyzer.LibraryType.LITELOADER.getPatchId()); - break; - case "optifine": - analyzer.ifPresent(LibraryAnalyzer.LibraryType.OPTIFINE, (library, libraryVersion) -> newList.remove(library)); - version = version.removePatchById(LibraryAnalyzer.LibraryType.OPTIFINE.getPatchId()); - break; - case "fabric": - analyzer.ifPresent(LibraryAnalyzer.LibraryType.FABRIC, (library, libraryVersion) -> newList.remove(library)); - version = version.removePatchById(LibraryAnalyzer.LibraryType.FABRIC.getPatchId()); - break; - } - return new MaintainTask(version.setLibraries(newList)); + return Task.supplyAsync(() -> MaintainTask.maintain(repository, LibraryAnalyzer.analyze(version) + .removeLibrary(libraryId).build())); } /** @@ -173,7 +154,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager { * Will try to remove libraries and patches. * * @param versionId version id - * @param libraryId forge/liteloader/optifine + * @param libraryId forge/liteloader/optifine/fabric * @return task to remove the specified library */ public Task removeLibraryAsync(String versionId, String libraryId) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java index b6927d9a9..746759b19 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java @@ -21,43 +21,99 @@ import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.util.Pair; -import java.util.Arrays; -import java.util.EnumMap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.BiConsumer; import java.util.regex.Pattern; +import java.util.stream.Collectors; public final class LibraryAnalyzer { - private final Map> libraries; + private Version version; + private final Map> libraries; - private LibraryAnalyzer(Map> libraries) { + private LibraryAnalyzer(Version version, Map> libraries) { + this.version = version; this.libraries = libraries; } public Optional getVersion(LibraryType type) { + return getVersion(type.getPatchId()); + } + + public Optional getVersion(String type) { return Optional.ofNullable(libraries.get(type)).map(Pair::getValue); } - public void ifPresent(LibraryType type, BiConsumer consumer) { - if (libraries.containsKey(type)) { - Pair value = libraries.get(type); - consumer.accept(value.getKey(), value.getValue()); - } + public void forEachLibrary(BiConsumer callback) { + for (Map.Entry> entry : libraries.entrySet()) + callback.accept(entry.getKey(), entry.getValue().getValue()); } public boolean has(LibraryType type) { + return has(type.getPatchId()); + } + + public boolean has(String type) { return libraries.containsKey(type); } public boolean hasModLoader() { - return Arrays.stream(LibraryType.values()) - .filter(LibraryType::isModLoader) - .anyMatch(this::has); + return libraries.keySet().stream().map(LibraryType::fromPatchId) + .filter(Objects::nonNull) + .anyMatch(LibraryType::isModLoader); + } + + public boolean hasModLauncher() { + final String modLauncher = "cpw.mods.modlauncher.Launcher"; + return modLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> modLauncher.equals(patch.getMainClass())); + } + + private Version removingMatchedLibrary(Version version, String libraryId) { + LibraryType type = LibraryType.fromPatchId(libraryId); + if (type == null) return version; + + List libraries = new ArrayList<>(); + for (Library library : version.getLibraries()) { + String groupId = library.getGroupId(); + String artifactId = library.getArtifactId(); + + if (type.group.matcher(groupId).matches() && type.artifact.matcher(artifactId).matches()) { + // skip + } else { + libraries.add(library); + } + } + return version.setLibraries(libraries); + } + + /** + * Remove library by library id + * @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric" + * @return this + */ + public LibraryAnalyzer removeLibrary(String libraryId) { + if (!has(libraryId)) return this; + version = removingMatchedLibrary(version, libraryId) + .setPatches(version.getPatches().stream() + .filter(patch -> !libraryId.equals(patch.getId())) + .map(patch -> removingMatchedLibrary(patch, libraryId)) + .collect(Collectors.toList())); + return this; + } + + public Version build() { + return version; } public static LibraryAnalyzer analyze(Version version) { - Map> libraries = new EnumMap<>(LibraryType.class); + if (version.getInheritsFrom() != null) + throw new IllegalArgumentException("LibraryAnalyzer can only analyze independent game version"); + + Map> libraries = new HashMap<>(); for (Library library : version.getLibraries()) { String groupId = library.getGroupId(); @@ -65,22 +121,17 @@ public final class LibraryAnalyzer { for (LibraryType type : LibraryType.values()) { if (type.group.matcher(groupId).matches() && type.artifact.matcher(artifactId).matches()) { - libraries.put(type, Pair.pair(library, library.getVersion())); + libraries.put(type.getPatchId(), Pair.pair(library, library.getVersion())); break; } } } for (Version patch : version.getPatches()) { - for (LibraryType type : LibraryType.values()) { - if (type.patchId.equals(patch.getId())) { - libraries.put(type, Pair.pair(null, patch.getVersion())); - break; - } - } + libraries.put(patch.getId(), Pair.pair(null, patch.getVersion())); } - return new LibraryAnalyzer(libraries); + return new LibraryAnalyzer(version, libraries); } public enum LibraryType { @@ -107,5 +158,12 @@ public final class LibraryAnalyzer { public String getPatchId() { return patchId; } + + public static LibraryType fromPatchId(String patchId) { + for (LibraryType type : values()) + if (type.getPatchId().equals(patchId)) + return type; + return null; + } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java index 94f3b5b1f..d751670ea 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java @@ -17,8 +17,23 @@ */ package org.jackhuang.hmcl.download; -import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.game.CompatibilityRule; +import org.jackhuang.hmcl.game.GameRepository; +import org.jackhuang.hmcl.game.Library; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.VersionLibraryBuilder; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.SimpleMultimap; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.versioning.VersionNumber; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; public class MaintainTask extends Task { @@ -27,27 +42,34 @@ public class MaintainTask extends Task { public MaintainTask(Version version) { this.version = version; + + if (version.getInheritsFrom() != null) + throw new IllegalArgumentException("MaintainTask requires independent game version"); } @Override public void execute() { - setResult(maintain(version)); + setResult(maintain(null, version)); } - public static Version maintain(Version version) { - if (version.getMainClass().contains("launchwrapper")) { - return maintainGameWithLaunchWrapper(version); + public static Version maintain(GameRepository repository, Version version) { + if (version.getInheritsFrom() != null) + throw new IllegalArgumentException("MaintainTask requires independent game version"); + + if (version.resolve(null).getMainClass().contains("launchwrapper")) { + return maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version))); } else { // Vanilla Minecraft does not need maintain // Forge 1.13 support not implemented, not compatible with OptiFine currently. // Fabric does not need maintain, nothing compatible with fabric now. - return version; + return maintainOptiFineLibrary(repository, unique(version)); } } private static Version maintainGameWithLaunchWrapper(Version version) { LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); VersionLibraryBuilder builder = new VersionLibraryBuilder(version); + String mainClass = null; if (!libraryAnalyzer.has(FORGE)) { builder.removeTweakClass("forge"); @@ -56,7 +78,7 @@ public class MaintainTask extends Task { // Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added. builder.removeTweakClass("liteloader"); - if (libraryAnalyzer.has(LITELOADER)) { + if (libraryAnalyzer.has(LITELOADER) && !libraryAnalyzer.hasModLauncher()) { builder.addArgument("--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker"); } @@ -65,11 +87,101 @@ public class MaintainTask extends Task { if (!libraryAnalyzer.has(LITELOADER) && !libraryAnalyzer.has(FORGE)) { builder.addArgument("--tweakClass", "optifine.OptiFineTweaker"); } else { - // If forge or LiteLoader installed, OptiFine Forge Tweaker is needed. - builder.addArgument("--tweakClass", "optifine.OptiFineForgeTweaker"); + if (libraryAnalyzer.hasModLauncher()) { + // If ModLauncher installed, we use ModLauncher in place of LaunchWrapper. + mainClass = "cpw.mods.modlauncher.Launcher"; + } else { + // If forge or LiteLoader installed, OptiFine Forge Tweaker is needed. + builder.addArgument("--tweakClass", "optifine.OptiFineForgeTweaker"); + } } } - return builder.build(); + Version ret = builder.build(); + return mainClass == null ? ret : ret.setMainClass(mainClass); + } + + private static Version maintainOptiFineLibrary(GameRepository repository, Version version) { + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); + List libraries = new ArrayList<>(version.getLibraries()); + + if (libraryAnalyzer.has(OPTIFINE)) { + if (libraryAnalyzer.has(LITELOADER) || libraryAnalyzer.has(FORGE)) { + // If forge or LiteLoader installed, OptiFine Forge Tweaker is needed. + // And we should load the installer jar instead of patch jar. + if (repository != null) + for (int i = 0; i < version.getLibraries().size(); ++i) { + Library library = libraries.get(i); + if (library.is("optifine", "OptiFine")) { + Library newLibrary = new Library("optifine", "OptiFine", library.getVersion(), "installer", null, null); + if (repository.getLibraryFile(version, newLibrary).exists()) { + libraries.set(i, null); + // OptiFine should be loaded after Forge in classpath. + // Although we have altered priority of OptiFine higher than Forge, + // there still exists a situation that Forge is installed without patch. + // Here we manually alter the position of OptiFine library in classpath. + libraries.add(newLibrary); + } + } + } + } + } + + return version.setLibraries(libraries.stream().filter(Objects::nonNull).collect(Collectors.toList())); + } + + public static Version unique(Version version) { + List libraries = new ArrayList<>(); + + SimpleMultimap multimap = new SimpleMultimap(HashMap::new, LinkedList::new); + + for (Library library : version.getLibraries()) { + String id = library.getGroupId() + ":" + library.getArtifactId(); + VersionNumber number = VersionNumber.asVersion(library.getVersion()); + String serialized = JsonUtils.GSON.toJson(library); + + if (multimap.containsKey(id)) { + boolean duplicate = false; + for (int otherLibraryIndex : multimap.get(id)) { + Library otherLibrary = libraries.get(otherLibraryIndex); + VersionNumber otherNumber = VersionNumber.asVersion(otherLibrary.getVersion()); + if (CompatibilityRule.equals(library.getRules(), otherLibrary.getRules())) { // rules equal, ignore older version. + boolean flag = true; + if (number.compareTo(otherNumber) > 0) { // if this library is newer + // replace [otherLibrary] with [library] + libraries.set(otherLibraryIndex, library); + } else if (number.compareTo(otherNumber) == 0) { // same library id. + // prevent from duplicated libraries + if (library.equals(otherLibrary)) { + String otherSerialized = JsonUtils.GSON.toJson(otherLibrary); + // A trick, the library that has more information is better, which can be + // considered whose serialized JSON text will be longer. + if (serialized.length() > otherSerialized.length()) { + libraries.set(otherLibraryIndex, library); + } + } else { + // for text2speech, which have same library id as well as version number, + // but its library and native library does not equal + flag = false; + } + } + if (flag) { + duplicate = true; + break; + } + } + } + + if (!duplicate) { + multimap.put(id, libraries.size()); + libraries.add(library); + } + } else { + multimap.put(id, libraries.size()); + libraries.add(library); + } + } + + return version.setLibraries(libraries); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibrariesUniqueTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibrariesUniqueTask.java deleted file mode 100644 index 7913f5d16..000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/LibrariesUniqueTask.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2019 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.download.game; - -import org.jackhuang.hmcl.game.CompatibilityRule; -import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.SimpleMultimap; -import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jackhuang.hmcl.util.versioning.VersionNumber; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.stream.Collectors; - -public class LibrariesUniqueTask extends Task { - private final Version version; - - public LibrariesUniqueTask(Version version) { - this.version = version; - } - - @Override - public void execute() { - setResult(unique(version)); - } - - public static Version unique(Version version) { - List libraries = new ArrayList<>(version.getLibraries()); - - SimpleMultimap multimap = new SimpleMultimap(HashMap::new, LinkedList::new); - - for (Library library : libraries) { - String id = library.getGroupId() + ":" + library.getArtifactId(); - VersionNumber number = VersionNumber.asVersion(library.getVersion()); - String serialized = JsonUtils.GSON.toJson(library); - - if (multimap.containsKey(id)) { - boolean duplicate = false; - for (Library otherLibrary : multimap.get(id)) { - VersionNumber otherNumber = VersionNumber.asVersion(otherLibrary.getVersion()); - if (CompatibilityRule.equals(library.getRules(), otherLibrary.getRules())) { // rules equal, ignore older version. - boolean flag = true; - if (number.compareTo(otherNumber) > 0) { // if this library is newer - multimap.removeValue(otherLibrary); - multimap.put(id, library); - break; - } else if (number.compareTo(otherNumber) == 0) { // same library id. - // prevent from duplicated libraries - if (library.equals(otherLibrary)) { - String otherSerialized = JsonUtils.GSON.toJson(otherLibrary); - // A trick, the library that has more information is better, which can be - // considered whose serialized JSON text will be longer. - if (serialized.length() > otherSerialized.length()) { - multimap.removeValue(id, otherLibrary); - multimap.put(id, library); - break; - } - } else { - // for text2speech, which have same library id as well as version number, - // but its library and native library does not equal - flag = false; - } - } - if (flag) { - duplicate = true; - break; - } - } - } - - if (!duplicate) { - multimap.put(id, library); - } - } else { - multimap.put(id, library); - } - } - - return version.setLibraries(multimap.values().stream().sorted().collect(Collectors.toList())); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java index 04fb45664..dba7e58eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -56,6 +56,10 @@ public interface GameRepository extends VersionProvider { return getVersion(id).resolve(this); } + default Version getResolvedPreservingPatchesVersion(String id) throws VersionNotFoundException { + return getVersion(id).resolvePreservingPatches(this); + } + /** * How many version are there? */ diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java index 7c54dfae4..a1189c3d2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LibrariesDownloadInfo.java @@ -34,7 +34,7 @@ public final class LibrariesDownloadInfo { private final Map classifiers; public LibrariesDownloadInfo(LibraryDownloadInfo artifact) { - this(artifact, Collections.emptyMap()); + this(artifact, null); } public LibrariesDownloadInfo(LibraryDownloadInfo artifact, Map classifiers) { 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 e0915b95c..b62f76a46 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.gson.Validation; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; @@ -161,6 +162,10 @@ public class Version implements Comparable, Validation { return resolved; } + public boolean isResolvedPreservingPatches() { + return inheritsFrom == null && !resolved; + } + public List getPatches() { return patches == null ? Collections.emptyList() : patches; } @@ -224,7 +229,7 @@ public class Version implements Comparable, Validation { releaseTime, Lang.merge(minimumLauncherVersion, parent.minimumLauncherVersion, Math::max), hidden, - Lang.merge(parent.patches, patches)); + Lang.merge(Lang.merge(parent.patches, Collections.singleton(this.clearPatches().setId("resolved." + getId()))), patches)); } protected Version resolve(VersionProvider provider, Set resolvedSoFar) throws VersionNotFoundException { @@ -256,6 +261,36 @@ public class Version implements Comparable, Validation { return thisVersion.setId(id); } + /** + * Resolve the version preserving all dependencies and patches. + */ + public Version resolvePreservingPatches(VersionProvider provider) throws VersionNotFoundException { + return resolvePreservingPatches(provider, new HashSet<>()); + } + + protected Version mergePreservingPatches(Version parent) { + return parent.addPatch(this.clearPatches().setId("resolved." + getId())).addPatches(patches); + } + + protected Version resolvePreservingPatches(VersionProvider provider, Set resolvedSoFar) throws VersionNotFoundException { + Version thisVersion; + + if (inheritsFrom == null) { + thisVersion = this.jar == null ? this.setJar(id) : this; + } else { + // To maximize the compatibility. + if (!resolvedSoFar.add(id)) { + Logging.LOG.log(Level.WARNING, "Found circular dependency versions: " + resolvedSoFar); + thisVersion = this.jar == null ? this.setJar(id) : this; + } else { + // It is supposed to auto install an version in getVersion. + thisVersion = mergePreservingPatches(provider.getVersion(inheritsFrom).resolvePreservingPatches(provider, resolvedSoFar)); + } + } + + return thisVersion.setId(id); + } + private Version setResolved() { return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches); } @@ -300,8 +335,20 @@ public class Version implements Comparable, Validation { return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches); } - public Version addPatch(Version patch) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, Lang.merge(patches, Collections.singleton(patch))); + public Version setPatches(List patches) { + return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches); + } + + public Version addPatch(Version... additional) { + return addPatches(Arrays.asList(additional)); + } + + public Version addPatches(List additional) { + return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, Lang.merge(patches, additional)); + } + + public Version clearPatches() { + return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, null); } public Version removePatchById(String patchId) { @@ -309,6 +356,10 @@ public class Version implements Comparable, Validation { patches == null ? null : patches.stream().filter(patch -> !patchId.equals(patch.getId())).collect(Collectors.toList())); } + public boolean hasPatch(String patchId) { + return patches != null && patches.stream().anyMatch(patch -> patchId.equals(patch.getId())); + } + @Override public int hashCode() { return id.hashCode(); 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 cafe03519..c2a72dfe7 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 @@ -78,7 +78,7 @@ public class MultiMCModpackExportTask extends Task { return false; }); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedVersion(versionId)); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(versionId)); String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(versionId)) .orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + versionId)); List components = new ArrayList<>();