Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -155,8 +157,75 @@ public Task<GameInstanceManifest> 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 (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(libraryVersion.getLibraryId()) || !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));
Comment on lines +212 to +214

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude the Minecraft patch from the loader shortcut

When rebuilding an existing instance with the same Minecraft version, DefaultGameBuilder also passes the "game" remote version through this method. The default matcher then searches for library group IDs containing "game", normally finds none, and declares the patch intact without checking the Minecraft JAR, vanilla libraries, or assets. Consequently, an update of an instance whose game files are missing or corrupt skips GameInstallTask, which previously repaired those files, and leaves the instance unlaunchable; either limit this shortcut to actual loaders or perform the game-specific completion checks before skipping.

Useful? React with 👍 / 👎.


return needsRepair ? null : matchingPatch;
} catch (Exception ignored) {
return null;
}
}

@Override
public Task<GameInstanceManifest> installLibraryAsync(GameInstanceManifest baseVersion, RemoteVersion libraryVersion) {
GameInstancePatch matchingPatch = getIntactMatchingPatch(baseVersion, libraryVersion);
if (matchingPatch != null) {
return Task.completed(baseVersion.addPatch(matchingPatch));
}

AtomicReference<GameInstanceManifest> removedLibraryVersion = new AtomicReference<>();

return removeLibraryAsync(baseVersion, libraryVersion.getLibraryId())
Expand Down