diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java index 3373165ff9f..757f65c8c39 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -27,9 +27,11 @@ import org.jackhuang.hmcl.game.Artifact; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; @@ -155,8 +157,75 @@ public Task installLibraryAsync(String gameVersion, GameIn .withStage(String.format("hmcl.install.%s:%s", libraryId, libraryVersion)); } + /// Checks whether a resolved [Library] belongs to the given loader type. + /// + /// The mapping must stay consistent with [LibraryAnalyzer.LibraryType]'s + /// group-id patterns so that the file-integrity check covers the correct jars. + /// + /// @param library a library entry from the resolved launch manifest + /// @param libraryId the patch id of the loader (e.g. `"forge"`, `"fabric"`) + /// @return `true` if the library's groupId matches the loader + private static boolean isLoaderLibrary(Library library, String libraryId) { + if (library == null || library.groupId() == null) return false; + String groupId = library.groupId().toLowerCase(java.util.Locale.ROOT); + String targetId = libraryId.toLowerCase(java.util.Locale.ROOT); + return switch (targetId) { + case "forge" -> groupId.startsWith("net.minecraftforge"); + case "neoforge" -> groupId.startsWith("net.neoforged"); + case "optifine" -> "optifine".equals(groupId) || "net.optifine".equals(groupId); + case "fabric" -> groupId.startsWith("net.fabricmc"); + case "quilt" -> groupId.startsWith("org.quiltmc"); + case "legacyfabric" -> groupId.startsWith("net.legacyfabric") || groupId.startsWith("net.fabricmc"); + case "liteloader" -> "com.mumfrey".equals(groupId); + case "cleanroom" -> groupId.startsWith("com.cleanroommc") || groupId.startsWith("zone.rong"); + default -> groupId.contains(targetId); + }; + } + + /// Checks if a matching library patch is already installed for the target instance on disk and intact. + /// + /// @param baseVersion the base game instance manifest + /// @param libraryVersion the remote library version to install + /// @return the existing intact matching patch, or `null` if the library is not installed, version mismatches, or files need repair + private @Nullable GameInstancePatch getIntactMatchingPatch(GameInstanceManifest baseVersion, RemoteVersion libraryVersion) { + if (!repository.hasInstance(baseVersion.id())) { + return null; + } + + try { + GameInstanceManifest existingManifest = repository.getInstanceManifest(baseVersion.id()); + String currentGameVersion = repository.getGameVersion(existingManifest).orElse(null); + if (!java.util.Objects.equals(currentGameVersion, libraryVersion.getGameVersion())) { + return null; + } + + GameInstancePatch matchingPatch = existingManifest.getPatches().stream() + .filter(patch -> libraryVersion.getLibraryId().equals(patch.id()) + && java.util.Objects.equals(patch.version(), libraryVersion.getSelfVersion())) + .findFirst() + .orElse(null); + if (matchingPatch == null) { + return null; + } + + GameInstanceManifest.Resolved resolved = repository.getResolvedInstanceManifest(baseVersion.id()); + boolean needsRepair = resolved.launchManifest().getLibraries().stream() + .filter(lib -> isLoaderLibrary(lib, libraryVersion.getLibraryId())) + .anyMatch(lib -> GameLibrariesTask.shouldDownloadLibrary(repository, existingManifest, lib, true)); + + return needsRepair ? null : matchingPatch; + } catch (Exception ignored) { + return null; + } + } + @Override public Task installLibraryAsync(GameInstanceManifest baseVersion, RemoteVersion libraryVersion) { + GameInstancePatch matchingPatch = getIntactMatchingPatch(baseVersion, libraryVersion); + if (matchingPatch != null) { + return Task.completed(baseVersion.addPatch(matchingPatch)); + } + AtomicReference removedLibraryVersion = new AtomicReference<>(); return removeLibraryAsync(baseVersion, libraryVersion.getLibraryId())