From 65c9e9749f328ea28e52cfd492534da9c1133064 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 21 Aug 2026 21:47:46 +0800 Subject: [PATCH 01/60] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20GameComponentAnalyze?= =?UTF-8?q?r=20=E4=B8=AD=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20Status=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/game/GameComponentAnalyzer.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java index 2aa2ba16d2..d9d884d643 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java @@ -169,13 +169,6 @@ public Iterator iterator() { return components.values().iterator(); } - /// If a library is provided in `$.patches`, it's structure is so clear that we can do any operation. - /// Otherwise, we must guess how are these libraries mixed. - /// Maybe a guessing implementation will be provided in the future. But by now, we simply set it to JUST\_EXISTED. - public enum Status { - CLEAR, UNSURE, JUST_EXISTED - } - public record Mark( GameComponentType componentType, @Nullable String version, From 58675df8913bf15d350a757e2674063a80030e1d Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 22 Aug 2026 20:58:41 +0800 Subject: [PATCH 02/60] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20GameComponentAnalyze?= =?UTF-8?q?r=20=E4=B8=AD=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20Status=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/InstallerListPage.java | 7 +- .../download/DefaultDependencyManager.java | 94 ++++++++++--------- .../hmcl/game/GameComponentAnalyzer.java | 7 ++ .../hmcl/game/GameInstanceManifest.java | 14 +++ .../java/org/jackhuang/hmcl/game/Library.java | 9 ++ 5 files changed, 82 insertions(+), 49 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java index ff38c42dc6..c72cd90fea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java @@ -35,10 +35,7 @@ import org.jetbrains.annotations.Nullable; import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import java.util.*; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -52,7 +49,7 @@ public class InstallerListPage extends ListPageBase { /// @param instanceContext the parent page's instance property public InstallerListPage(ObservableValue instanceContext) { Objects.requireNonNull(instanceContext, "instanceContext"); - FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "exe").contains(FileUtils.getExtension(it)), mods -> { + FXUtils.applyDragListener(this, it -> Set.of("jar", "exe").contains(FileUtils.getExtension(it)), mods -> { if (!mods.isEmpty()) doInstallOffline(mods.get(0)); }); 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 b9b34c3b62..80a27730c1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -36,9 +36,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.regex.Matcher; -import java.util.regex.Pattern; /// Provides downloads and game-component installation for one game repository. @NotNullByDefault @@ -107,7 +105,7 @@ public Task checkGameCompletionAsync( return Files.notExists(instanceJar) || FileUtils.size(instanceJar) == 0L ? new GameDownloadTask(this, manifest).thenAcceptAsync( - cachedJar -> FileUtils.copyFile(cachedJar, instanceJar)) + cachedJar -> FileUtils.copyFile(cachedJar, instanceJar)) : null; }).thenComposeAsync(checkPatchCompletionAsync(instance, manifest, integrityCheck)), new GameAssetDownloadTask(this, manifest, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck) @@ -136,37 +134,43 @@ public Task checkPatchCompletionAsync( String gameVersion = detectedVersion.toString(); GameInstanceManifest original = instance.getManifest(); - for (GameComponentType type : GameComponentType.values()) { - if (!instance.hasComponent(type)) - continue; - - if (type == GameComponentType.OPTIFINE) { - @Nullable String optifinePatchVersion = Optional.ofNullable(instance.getComponentVersion(type)).map(optifineVersion -> { - Matcher matcher = Pattern.compile("^([0-9.]+)_(?HD_.+)$").matcher(optifineVersion); - return matcher.find() ? matcher.group("optifine") : optifineVersion; - }) - .orElseGet(() -> instance.getResolvedManifest().standaloneManifest().getPatches().stream() - .filter(patch -> "optifine".equals(patch.id())) - .findAny() - .map(GameInstancePatch::version) - .orElse(null)); - - boolean needsReInstallation = manifest.getLibraries().stream() - .anyMatch(library -> !library.hasDownloadURL() - && "optifine".equals(library.groupId()) - && GameLibrariesTask.shouldDownloadLibrary(repository, manifest, library, integrityCheck)); - - if (needsReInstallation) { - Library installer = new Library(new Artifact("optifine", "OptiFine", gameVersion + "_" + optifinePatchVersion, "installer")); - if (GameLibrariesTask.shouldDownloadLibrary(repository, manifest, installer, integrityCheck)) { - tasks.add(installComponentAsync(instance, original, gameVersion, GameComponentType.OPTIFINE, optifinePatchVersion)); - } else { - tasks.add(OptiFineInstallTask.install( - this, - original, - gameVersion, - repository.getLayout().getLibraryFile(manifest.id(), installer))); - } + + + optifine: + { + @Nullable GameComponentAnalyzer.Mark mark = instance.getAnalyzer().getMark(GameComponentType.OPTIFINE); + if (mark == null || mark.version() == null) + break optifine; + + String fullVersion = mark.version(); + String patchVersion; + + Matcher matcher = GameComponentAnalyzer.OPTIFINE_VERSION_PATTERN.matcher(fullVersion); + if (matcher.matches()) { + patchVersion = matcher.group("optifine"); + } else { + @Nullable GameInstancePatch patch = manifest.findPatch(GameComponentType.OPTIFINE.getPatchId()); + if (patch != null && patch.version() != null) + patchVersion = patch.version(); + else + break optifine; + } + + boolean needsReInstallation = manifest.getLibraries().stream() + .anyMatch(library -> !library.hasDownloadURL() + && "optifine".equals(library.groupId()) + && GameLibrariesTask.shouldDownloadLibrary(repository, manifest, library, integrityCheck)); + + if (needsReInstallation) { + Library installer = new Library("optifine", "OptiFine", fullVersion, "installer"); + if (GameLibrariesTask.shouldDownloadLibrary(repository, manifest, installer, integrityCheck)) { + tasks.add(installComponentAsync(instance, original, gameVersion, GameComponentType.OPTIFINE, patchVersion)); + } else { + tasks.add(OptiFineInstallTask.install( + this, + original, + gameVersion, + repository.getLayout().getLibraryFile(manifest.id(), installer))); } } } @@ -206,9 +210,9 @@ public Task installComponentAsync( /// Installs a component into an unpublished new instance without constructing a /// [GameInstance]. /// - /// @param instanceId the unpublished instance id - /// @param baseManifest the working manifest for this step - /// @param gameVersion the Minecraft version used for component analysis + /// @param instanceId the unpublished instance id + /// @param baseManifest the working manifest for this step + /// @param gameVersion the Minecraft version used for component analysis /// @param componentVersion the remote component to install /// @return the task producing the updated manifest (not yet committed) Task installNewInstanceComponentAsync( @@ -284,10 +288,10 @@ private Task removeNewInstanceComponentAsync( /// Resolves a remote component by id/version and installs it into the working manifest. /// - /// @param instance the registered instance being modified - /// @param baseManifest the working manifest for this step - /// @param gameVersion the Minecraft version used to look up the remote list - /// @param componentType the component list id, such as `game` or `forge` + /// @param instance the registered instance being modified + /// @param baseManifest the working manifest for this step + /// @param gameVersion the Minecraft version used to look up the remote list + /// @param componentType the component list id, such as `game` or `forge` /// @param componentVersion the component version id /// @return the installation task public Task installComponentAsync( @@ -308,8 +312,8 @@ public Task installComponentAsync( baseManifest, versionList.getVersion(gameVersion, componentVersion) .orElseThrow(() -> new IOException( - "Remote library " + componentType + " has no version " + componentVersion)))) - .withStage(String.format("hmcl.install.%s:%s", componentType, componentVersion)); + "Remote component " + componentType + " has no version " + componentVersion)))) + .withStage("hmcl.install.%s:%s".formatted(componentType, componentVersion)); } /// Installs a component from a local installer jar into a registered instance. @@ -361,7 +365,9 @@ public Task installComponentAsync( throw new UnsupportedLibraryInstallerException(); }) - .thenApplyAsync(patch -> patch == null ? baseManifest : baseManifest.addPatch(patch)); + .thenApplyAsync(patch -> { + return patch == null ? baseManifest : baseManifest.addPatch(patch); + }); } /// Indicates that a local library installer is not recognized by any supported installer. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java index d9d884d643..0d9c96e981 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java @@ -26,6 +26,7 @@ import org.jetbrains.annotations.Unmodifiable; import java.util.*; +import java.util.regex.Pattern; @NotNullByDefault public final class GameComponentAnalyzer implements Iterable { @@ -148,6 +149,10 @@ public GameInstanceManifest removeLibrary(GameComponentType componentType) { .toList()); } + public @Nullable Mark getMark(GameComponentType type) { + return components.get(type); + } + public @Nullable String getVersion(GameComponentType type) { Mark mark = components.get(type); return mark != null ? mark.version() : null; @@ -213,4 +218,6 @@ public record Mark( "optifine.OptiFineForgeTweaker" ); public static final String LITELOADER_TWEAKER = "com.mumfrey.liteloader.launch.LiteLoaderTweaker"; + + public static final Pattern OPTIFINE_VERSION_PATTERN = Pattern.compile("^([0-9.]+)_(?HD_.+)$"); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 7ad8706099..2f8a68220d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -362,6 +362,20 @@ public List getPatches() { return patches == null ? List.of() : patches; } + /// Finds a patch by its id. + /// + /// @return the patch with the given id, or `null` if no such patch exists. + public @Nullable GameInstancePatch findPatch(String patchId) { + if (patches != null) { + for (GameInstancePatch patch : patches) { + if (patchId.equals(patch.id())) { + return patch; + } + } + } + return null; + } + /// Returns logging metadata. /// /// @return logging metadata 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 e47bc158a2..e5e31c2588 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java @@ -136,6 +136,15 @@ public Library(Artifact artifact, @Nullable String url, @Nullable LibrariesDownl this(artifact, url, downloads, null, null, null, null, null, null); } + public Library(String group, String name, String version) { + this(group, name, version, null); + } + + public Library(String group, String name, String version, @Nullable String classifier) { + this(new Artifact(group, name, version, classifier)); + } + + public String groupId() { return artifact.getGroup(); } From a33a587fb17b42aaef0205dd302ff51a5cc24827 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 22 Aug 2026 21:01:24 +0800 Subject: [PATCH 03/60] =?UTF-8?q?=E7=AE=80=E5=8C=96=20DefaultDependencyMan?= =?UTF-8?q?ager=20=E4=B8=AD=E7=9A=84=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E4=B8=BA=20formatted=20=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/download/DefaultDependencyManager.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 80a27730c1..8307bcbd21 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -204,7 +204,7 @@ public Task installComponentAsync( .thenComposeAsync(manifest -> libraryVersion .getInstallTask(this, manifest, modsDirectory) .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) - .withStage(String.format("hmcl.install.%s:%s", libraryVersion.getComponentType().getPatchId(), libraryVersion.getSelfVersion())); + .withStage("hmcl.install.%s:%s".formatted(libraryVersion.getComponentType().getPatchId(), libraryVersion.getSelfVersion())); } /// Installs a component into an unpublished new instance without constructing a @@ -232,8 +232,7 @@ Task installNewInstanceComponentAsync( .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) - .withStage(String.format( - "hmcl.install.%s:%s", + .withStage("hmcl.install.%s:%s".formatted( componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); } @@ -265,7 +264,7 @@ Task installNewInstanceComponentAsync( versionList.getVersion(gameVersion, componentVersion) .orElseThrow(() -> new IOException( "Remote component " + componentType + " has no version " + componentVersion)))) - .withStage(String.format("hmcl.install.%s:%s", componentType, componentVersion)); + .withStage("hmcl.install.%s:%s".formatted(componentType, componentVersion)); } /// Removes one component from an unpublished new instance manifest. From 3ea1d9c66d78d2b1827f0fa2c9d3e8f0f08142c3 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 22 Aug 2026 21:02:47 +0800 Subject: [PATCH 04/60] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20installComponentAsyn?= =?UTF-8?q?c=20=E6=96=B9=E6=B3=95=E4=B8=AD=E7=9A=84=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=EF=BC=8C=E4=BB=8E=20libraryVersion=20?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=E4=B8=BA=20componentVersion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/download/DefaultDependencyManager.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 8307bcbd21..82fc8f050f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -187,12 +187,12 @@ public Task checkPatchCompletionAsync( /// /// @param instance the registered instance being modified /// @param baseManifest the working standalone-oriented manifest for this step - /// @param libraryVersion the remote component to install + /// @param componentVersion the remote component to install /// @return the task producing the updated manifest (not yet saved) public Task installComponentAsync( GameInstance instance, GameInstanceManifest baseManifest, - RemoteVersion libraryVersion) { + RemoteVersion componentVersion) { validateGameInstance(instance); if (!instance.getId().equals(baseManifest.id())) { throw new IllegalArgumentException("baseManifest id does not match instance"); @@ -200,11 +200,11 @@ public Task installComponentAsync( Path modsDirectory = instance.getModsDirectory(); - return removeComponentAsync(instance, baseManifest, libraryVersion.getComponentType()) - .thenComposeAsync(manifest -> libraryVersion + return removeComponentAsync(instance, baseManifest, componentVersion.getComponentType()) + .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) - .withStage("hmcl.install.%s:%s".formatted(libraryVersion.getComponentType().getPatchId(), libraryVersion.getSelfVersion())); + .withStage("hmcl.install.%s:%s".formatted(componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); } /// Installs a component into an unpublished new instance without constructing a From 81512f79292dc94a44b28bb14ae36ec8f6031074 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 22 Aug 2026 21:23:21 +0800 Subject: [PATCH 05/60] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20DefaultDependencyMan?= =?UTF-8?q?ager=20=E4=B8=AD=E7=9A=84=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E5=B9=B6=E8=B0=83=E6=95=B4=20OptiFineInstallTask=20=E6=9E=84?= =?UTF-8?q?=E9=80=A0=E5=87=BD=E6=95=B0=E7=9A=84=E5=8F=82=E6=95=B0=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/download/DefaultDependencyManager.java | 1 - .../jackhuang/hmcl/download/optifine/OptiFineInstallTask.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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 82fc8f050f..7fa42a7602 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -135,7 +135,6 @@ public Task checkPatchCompletionAsync( GameInstanceManifest original = instance.getManifest(); - optifine: { @Nullable GameComponentAnalyzer.Mark mark = instance.getAnalyzer().getMark(GameComponentType.OPTIFINE); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index 10f1c19448..91d628413e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -86,7 +86,7 @@ public OptiFineInstallTask( /// @param remoteVersion selected OptiFine version /// @param minecraftJar vanilla client JAR for the target Minecraft version /// @param installer local installer JAR, or `null` to download it - public OptiFineInstallTask( + public OptiFineInstallTask( DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, OptiFineRemoteVersion remoteVersion, From 78e76d673a4c219f51bad19cc4692c98878f6406 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 22 Aug 2026 21:56:06 +0800 Subject: [PATCH 06/60] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20installComponentAsyn?= =?UTF-8?q?c=20=E6=96=B9=E6=B3=95=EF=BC=8C=E7=A7=BB=E9=99=A4=20gameVersion?= =?UTF-8?q?=20=E5=8F=82=E6=95=B0=E5=B9=B6=E6=B7=BB=E5=8A=A0=20isModifiable?= =?UTF-8?q?=20=E6=96=B9=E6=B3=95=E4=BB=A5=E5=A2=9E=E5=BC=BA=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7=E5=92=8C=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../download/DefaultDependencyManager.java | 38 ++++++++----------- .../hmcl/download/DefaultGameBuilder.java | 2 +- .../hmcl/game/GameInstanceManifest.java | 10 ++++- 3 files changed, 25 insertions(+), 25 deletions(-) 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 7fa42a7602..1df91b41aa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -184,8 +184,8 @@ public Task checkPatchCompletionAsync( /// supplies repository identity, mods directory, and detected game version; `baseManifest` is /// the draft JSON being edited. /// - /// @param instance the registered instance being modified - /// @param baseManifest the working standalone-oriented manifest for this step + /// @param instance the registered instance being modified + /// @param baseManifest the working standalone-oriented manifest for this step /// @param componentVersion the remote component to install /// @return the task producing the updated manifest (not yet saved) public Task installComponentAsync( @@ -211,13 +211,11 @@ public Task installComponentAsync( /// /// @param instanceId the unpublished instance id /// @param baseManifest the working manifest for this step - /// @param gameVersion the Minecraft version used for component analysis /// @param componentVersion the remote component to install /// @return the task producing the updated manifest (not yet committed) Task installNewInstanceComponentAsync( GameInstanceID instanceId, GameInstanceManifest baseManifest, - String gameVersion, RemoteVersion componentVersion) { if (!instanceId.equals(baseManifest.id())) { throw new IllegalArgumentException("baseManifest id does not match instanceId"); @@ -226,7 +224,6 @@ Task installNewInstanceComponentAsync( Path modsDirectory = repository.getRunDirectoryForInstallation(instanceId).resolve("mods"); return removeNewInstanceComponentAsync( baseManifest, - GameVersionNumber.asGameVersion(gameVersion), componentVersion.getComponentType()) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) @@ -259,7 +256,6 @@ Task installNewInstanceComponentAsync( .thenComposeAsync(() -> installNewInstanceComponentAsync( instanceId, baseManifest, - gameVersion, versionList.getVersion(gameVersion, componentVersion) .orElseThrow(() -> new IOException( "Remote component " + componentType + " has no version " + componentVersion)))) @@ -269,18 +265,21 @@ Task installNewInstanceComponentAsync( /// Removes one component from an unpublished new instance manifest. /// /// @param workingManifest the manifest being edited - /// @param gameVersion the Minecraft version used for component analysis /// @param componentType the component to remove /// @return the task producing the updated standalone manifest private Task removeNewInstanceComponentAsync( GameInstanceManifest workingManifest, - GameVersionNumber gameVersion, GameComponentType componentType) { return Task.supplyAsync(() -> { - GameInstanceManifest standalone = workingManifest.inheritsFrom() == null - ? workingManifest - : repository.resolve(workingManifest).standaloneManifest(); - return GameComponentAnalyzer.analyze(standalone, gameVersion).removeLibrary(componentType); + @Nullable GameInstancePatch patch = workingManifest.findPatch(componentType.getPatchId()); + if (patch != null) { + return workingManifest.withPatches(workingManifest.getPatches() + .stream() + .filter(it -> it != patch) + .toList()); // TODO: need resolve + } else { + return workingManifest; + } }); } @@ -404,18 +403,11 @@ public Task removeComponentAsync( throw new IllegalArgumentException("workingManifest id does not match instance"); } - return Task.supplyAsync(() -> { - GameInstanceManifest standalone; - if (workingManifest.equals(instance.getManifest())) { - standalone = instance.getResolvedManifest().standaloneManifest(); - } else if (workingManifest.inheritsFrom() == null) { - standalone = workingManifest; - } else { - standalone = repository.resolve(workingManifest).standaloneManifest(); - } + if (!workingManifest.isModifiable()) { + throw new IllegalArgumentException("Cannot remove component from a non-modifiable manifest"); + } - return GameComponentAnalyzer.analyze(standalone, instance.getVersion()).removeLibrary(componentType); - }); + return removeNewInstanceComponentAsync(workingManifest, componentType); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index c6289d8352..106da4bdf6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -97,7 +97,7 @@ public Task buildAsync() { if (entry.getValue() instanceof RemoteVersion remoteVersion) { libraryTask = libraryTask.thenComposeAsync(manifest -> dependencyManager.installNewInstanceComponentAsync( - id, manifest, gameVersion, remoteVersion)); + id, manifest, remoteVersion)); } else if (entry.getValue() instanceof String version) { libraryTask = libraryTask.thenComposeAsync(manifest -> dependencyManager.installNewInstanceComponentAsync( diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 2f8a68220d..f2c3a32436 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -355,6 +355,14 @@ public boolean isRoot() { return root != null && root; } + public boolean isResolved() { + return inheritsFrom == null; + } + + public boolean isModifiable() { + return inheritsFrom == null && patches != null && !patches.isEmpty(); + } + /// Returns the pending patches. /// /// @return the pending patches, or an empty list when absent @@ -609,7 +617,7 @@ public GameInstanceManifest addPatches(@Nullable List additio /// Returns whether this manifest has a patch with the given id. public boolean hasPatch(String patchId) { - return patches != null && patches.stream().anyMatch(patch -> patchId.equals(patch.id())); + return findPatch(patchId) != null; } /// Converts this manifest into a hidden patch entry for preserving resolved inheritance. From e8e9cebb8d029cd33d03bc7995657cfea69764e6 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 22 Aug 2026 21:57:07 +0800 Subject: [PATCH 07/60] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20GameComponentAnalyze?= =?UTF-8?q?r=20=E4=B8=AD=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=20removingMat?= =?UTF-8?q?chedLibrary=20=E6=96=B9=E6=B3=95=E4=BB=A5=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/game/GameComponentAnalyzer.java | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java index 0d9c96e981..b7b4e37e36 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java @@ -110,45 +110,6 @@ public boolean hasModLauncher() { ); } - private static GameInstanceManifest removingMatchedLibrary(GameInstanceManifest manifest, GameComponentType type) { - List libraries = new ArrayList<>(); - List rawLibraries = manifest.getLibraries(); - for (Library library : rawLibraries) { - if (type.matchLibrary(library, rawLibraries)) { - // skip - } else { - libraries.add(library); - } - } - return manifest.withLibraries(libraries); - } - - private GameInstancePatch removingMatchedLibrary(GameInstancePatch patch, GameComponentType type) { - List libraries = new ArrayList<>(); - List rawLibraries = patch.getLibraries(); - for (Library library : rawLibraries) { - if (type.matchLibrary(library, rawLibraries)) { - // skip - } else { - libraries.add(library); - } - } - return patch.withLibraries(libraries); - } - - /// Remove library by library id - /// - /// @param componentType the patch identifier, such as `forge`, `optifine`, or `fabric` - /// @return this - public GameInstanceManifest removeLibrary(GameComponentType componentType) { - if (!has(componentType)) return manifest; - GameInstanceManifest manifest = removingMatchedLibrary(this.manifest, componentType); - return manifest.withPatches(this.manifest.getPatches().stream() - .filter(patch -> !componentType.getPatchId().equals(patch.id())) - .map(patch -> removingMatchedLibrary(patch, componentType)) - .toList()); - } - public @Nullable Mark getMark(GameComponentType type) { return components.get(type); } From b041e92275c4a49e497c721d51cfa73c0d2cdf7c Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 23 Aug 2026 20:30:46 +0800 Subject: [PATCH 08/60] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20installComponentAsyn?= =?UTF-8?q?c=20=E6=96=B9=E6=B3=95=EF=BC=8C=E7=A7=BB=E9=99=A4=20gameVersion?= =?UTF-8?q?=20=E5=8F=82=E6=95=B0=E5=B9=B6=E8=B0=83=E6=95=B4=20OptiFineInst?= =?UTF-8?q?allTask=20=E4=B8=AD=E7=9A=84=E5=8F=82=E6=95=B0=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E4=BB=A5=E5=A2=9E=E5=BC=BA=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../download/DefaultDependencyManager.java | 20 +++++++------------ .../optifine/OptiFineInstallTask.java | 8 ++++---- 2 files changed, 11 insertions(+), 17 deletions(-) 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 1df91b41aa..8422620e7b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -320,43 +320,37 @@ public Task installComponentAsync( /// @return the task producing the updated manifest (not yet saved) public Task installComponentAsync(GameInstance instance, Path installer) { validateGameInstance(instance); - return installComponentAsync(instance, instance.getManifest(), installer); + return installComponentAsync(instance.getVersion(), instance.getManifest(), installer); } /// Installs a component from a local installer jar into a working manifest. /// - /// @param instance the registered instance (paths / identity) + /// @param gameVersion the Minecraft version expected by the installation /// @param baseManifest the working manifest for this step /// @param installer the local installer jar /// @return the task producing the updated manifest (not yet saved) public Task installComponentAsync( - GameInstance instance, + GameVersionNumber gameVersion, GameInstanceManifest baseManifest, Path installer) { - validateGameInstance(instance); - if (!instance.getId().equals(baseManifest.id())) { - throw new IllegalArgumentException("baseManifest id does not match instance"); - } - String gameVersion = instance.getVersion().toString(); - return Task.composeAsync(() -> { try { - return CleanroomInstallTask.install(this, baseManifest, gameVersion, installer); + return CleanroomInstallTask.install(this, baseManifest, gameVersion.toString(), installer); } catch (IOException ignore) { } try { - return NeoForgeInstallTask.install(this, baseManifest, gameVersion, installer); + return NeoForgeInstallTask.install(this, baseManifest, gameVersion.toString(), installer); } catch (IOException ignore) { } try { - return ForgeInstallTask.install(this, baseManifest, gameVersion, installer); + return ForgeInstallTask.install(this, baseManifest, gameVersion.toString(), installer); } catch (IOException ignore) { } try { - return OptiFineInstallTask.install(this, baseManifest, gameVersion, installer); + return OptiFineInstallTask.install(this, baseManifest, gameVersion.toString(), installer); } catch (IOException ignore) { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index 91d628413e..1f7d0abde3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -253,7 +253,7 @@ public void execute() throws Exception { /// Creates a task that installs OptiFine from a local installer JAR. /// /// @param dependencyManager repository-scoped download services - /// @param version working manifest receiving the OptiFine patch + /// @param manifest working manifest receiving the OptiFine patch /// @param gameVersion Minecraft version expected by the installation /// @param installer the OptiFine installer JAR /// @return the task producing the OptiFine patch @@ -261,7 +261,7 @@ public void execute() throws Exception { /// @throws VersionMismatchException if the installer targets another Minecraft version public static Task install( DefaultDependencyManager dependencyManager, - GameInstanceManifest version, + GameInstanceManifest manifest, String gameVersion, Path installer) throws IOException, VersionMismatchException { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { @@ -287,10 +287,10 @@ public static Task install( ofEdition + "_" + ofRelease, Collections.singletonList(""), false); - return new GameDownloadTask(dependencyManager, version) + return new GameDownloadTask(dependencyManager, manifest) .thenComposeAsync(minecraftJar -> new OptiFineInstallTask( dependencyManager, - version, + manifest, remoteVersion, minecraftJar, installer)); From bd6b6dc1f29c6aeb76a51dc2747ac2043294e8c0 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 23 Aug 2026 20:40:09 +0800 Subject: [PATCH 09/60] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20DefaultGameBuilder?= =?UTF-8?q?=20=E4=B8=AD=E7=9A=84=E5=AD=97=E7=AC=A6=E4=B8=B2=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E6=96=B9=E5=BC=8F=EF=BC=8C=E5=B9=B6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=20GameInstanceManifest=20=E4=B8=AD=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20isResolved=20=E6=96=B9=E6=B3=95=E4=BB=A5?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/download/DefaultGameBuilder.java | 3 ++- .../org/jackhuang/hmcl/game/GameInstanceManifest.java | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 106da4bdf6..c44d0d3ad2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -70,7 +70,8 @@ public Task buildAsync() { components.forEach((componentType, version) -> { hints.add(new Task.StagesHint( - String.format("hmcl.install.%s:%s", componentType.getPatchId(), + "hmcl.install.%s:%s".formatted( + componentType.getPatchId(), version instanceof RemoteVersion remoteVersion ? remoteVersion.getSelfVersion() : (String) version))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index f2c3a32436..63da36f237 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -355,10 +355,6 @@ public boolean isRoot() { return root != null && root; } - public boolean isResolved() { - return inheritsFrom == null; - } - public boolean isModifiable() { return inheritsFrom == null && patches != null && !patches.isEmpty(); } @@ -716,6 +712,10 @@ public JsonObject toJsonObject() { return json; } + public GameInstanceManifest resolvePatches() { + + } + private static final class Builder { // @formatter:off private @Nullable GameInstanceID id; From 8ca8dea279cb8121c75954f578dc9c5574fb2312 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 24 Aug 2026 21:44:08 +0800 Subject: [PATCH 10/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E6=96=B9=E6=B3=95=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=20installComponentAsync=EF=BC=8C=E6=96=B0=E5=A2=9E=20installCo?= =?UTF-8?q?mponentRemoteAsync=20=E4=BB=A5=E5=A2=9E=E5=BC=BA=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7=E5=92=8C=E5=8F=AF=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/game/HMCLModpackInstallTask.java | 2 +- .../UpdateInstallerWizardProvider.java | 2 +- .../hmcl/ui/instances/InstallerListPage.java | 2 +- .../download/DefaultDependencyManager.java | 129 ++++++++++-------- .../hmcl/game/GameInstanceManifest.java | 55 +++++++- 5 files changed, 126 insertions(+), 64 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 7352b02ab3..a71424df0e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -103,7 +103,7 @@ public void execute() throws Exception { if (componentVersion == null) { continue; } - libraryTask = libraryTask.thenComposeAsync(manifest -> dependency.installComponentAsync( + libraryTask = libraryTask.thenComposeAsync(manifest -> dependency.installComponentRemoteAsync( publishedInstance, manifest, modpack.getGameVersion(), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index 182b5ddf82..fc59409d23 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java @@ -87,7 +87,7 @@ public Object finish(SettingsMap settings) { for (Object value : settings.asStringMap().values()) { if (value instanceof RemoteVersion remoteVersion) { update = update.thenComposeAsync(manifest -> - dependencyManager.installComponentAsync(publishedInstance, manifest, remoteVersion)); + dependencyManager.installComponentRemoteAsync(publishedInstance, manifest, remoteVersion)); } else if (value instanceof RemoveVersionAction removeVersionAction) { update = update.thenComposeAsync(manifest -> dependencyManager.removeComponentAsync( diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java index c72cd90fea..6521e2576f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java @@ -152,7 +152,7 @@ private void doInstallOffline(Path file) { HMCLGameRepository repository = gameInstance.getRepository(); Task task = repository.updateInstanceAsync( gameInstance.getId(), - publishedInstance -> repository.getDependency().installComponentAsync(publishedInstance, file)); + publishedInstance -> repository.getDependency().installComponentLocalAsync(publishedInstance, file)); task.setName(i18n("install.installer.install_offline")); TaskExecutor executor = task.executor(new TaskListener() { @Override 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 8422620e7b..09b4f4495e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -163,7 +163,7 @@ public Task checkPatchCompletionAsync( if (needsReInstallation) { Library installer = new Library("optifine", "OptiFine", fullVersion, "installer"); if (GameLibrariesTask.shouldDownloadLibrary(repository, manifest, installer, integrityCheck)) { - tasks.add(installComponentAsync(instance, original, gameVersion, GameComponentType.OPTIFINE, patchVersion)); + tasks.add(installComponentRemoteAsync(instance, original, gameVersion, GameComponentType.OPTIFINE, patchVersion)); } else { tasks.add(OptiFineInstallTask.install( this, @@ -178,34 +178,6 @@ public Task checkPatchCompletionAsync( }); } - /// Installs a component using a working manifest that may be ahead of the instance's stored state. - /// - /// Used by multi-step install pipelines after a previous in-memory remove/install. `instance` - /// supplies repository identity, mods directory, and detected game version; `baseManifest` is - /// the draft JSON being edited. - /// - /// @param instance the registered instance being modified - /// @param baseManifest the working standalone-oriented manifest for this step - /// @param componentVersion the remote component to install - /// @return the task producing the updated manifest (not yet saved) - public Task installComponentAsync( - GameInstance instance, - GameInstanceManifest baseManifest, - RemoteVersion componentVersion) { - validateGameInstance(instance); - if (!instance.getId().equals(baseManifest.id())) { - throw new IllegalArgumentException("baseManifest id does not match instance"); - } - - Path modsDirectory = instance.getModsDirectory(); - - return removeComponentAsync(instance, baseManifest, componentVersion.getComponentType()) - .thenComposeAsync(manifest -> componentVersion - .getInstallTask(this, manifest, modsDirectory) - .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) - .withStage("hmcl.install.%s:%s".formatted(componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); - } - /// Installs a component into an unpublished new instance without constructing a /// [GameInstance]. /// @@ -222,9 +194,8 @@ Task installNewInstanceComponentAsync( } Path modsDirectory = repository.getRunDirectoryForInstallation(instanceId).resolve("mods"); - return removeNewInstanceComponentAsync( - baseManifest, - componentVersion.getComponentType()) + GameComponentType componentType = componentVersion.getComponentType(); + return Task.supplyAsync(() -> removeNewInstanceComponent(baseManifest, componentType)) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) @@ -262,25 +233,31 @@ Task installNewInstanceComponentAsync( .withStage("hmcl.install.%s:%s".formatted(componentType, componentVersion)); } - /// Removes one component from an unpublished new instance manifest. + /// Installs a component using a working manifest that may be ahead of the instance's stored state. /// - /// @param workingManifest the manifest being edited - /// @param componentType the component to remove - /// @return the task producing the updated standalone manifest - private Task removeNewInstanceComponentAsync( - GameInstanceManifest workingManifest, - GameComponentType componentType) { - return Task.supplyAsync(() -> { - @Nullable GameInstancePatch patch = workingManifest.findPatch(componentType.getPatchId()); - if (patch != null) { - return workingManifest.withPatches(workingManifest.getPatches() - .stream() - .filter(it -> it != patch) - .toList()); // TODO: need resolve - } else { - return workingManifest; - } - }); + /// Used by multi-step install pipelines after a previous in-memory remove/install. `instance` + /// supplies repository identity, mods directory, and detected game version; `baseManifest` is + /// the draft JSON being edited. + /// + /// @param instance the registered instance being modified + /// @param baseManifest the working standalone-oriented manifest for this step + /// @param componentVersion the remote component to install + /// @return the task producing the updated manifest (not yet saved) + public Task installComponentRemoteAsync( + GameInstance instance, + GameInstanceManifest baseManifest, + RemoteVersion componentVersion) { + validateGameInstance(instance); + if (!instance.getId().equals(baseManifest.id())) { + throw new IllegalArgumentException("baseManifest id does not match instance"); + } + + Path modsDirectory = instance.getModsDirectory(); + return removeComponentAsync(instance, baseManifest, componentVersion.getComponentType()) + .thenComposeAsync(manifest -> componentVersion + .getInstallTask(this, manifest, modsDirectory) + .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) + .withStage("hmcl.install.%s:%s".formatted(componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); } /// Resolves a remote component by id/version and installs it into the working manifest. @@ -291,7 +268,7 @@ private Task removeNewInstanceComponentAsync( /// @param componentType the component list id, such as `game` or `forge` /// @param componentVersion the component version id /// @return the installation task - public Task installComponentAsync( + public Task installComponentRemoteAsync( GameInstance instance, GameInstanceManifest baseManifest, String gameVersion, @@ -304,7 +281,7 @@ public Task installComponentAsync( VersionList versionList = getVersionList(componentType); return versionList.loadAsync(gameVersion) - .thenComposeAsync(() -> installComponentAsync( + .thenComposeAsync(() -> installComponentRemoteAsync( instance, baseManifest, versionList.getVersion(gameVersion, componentVersion) @@ -318,9 +295,9 @@ public Task installComponentAsync( /// @param instance the target instance /// @param installer the local installer jar /// @return the task producing the updated manifest (not yet saved) - public Task installComponentAsync(GameInstance instance, Path installer) { + public Task installComponentLocalAsync(GameInstance instance, Path installer) { validateGameInstance(instance); - return installComponentAsync(instance.getVersion(), instance.getManifest(), installer); + return installComponentLocalAsync(instance.getVersion(), instance.getManifest(), installer); } /// Installs a component from a local installer jar into a working manifest. @@ -329,7 +306,7 @@ public Task installComponentAsync(GameInstance instance, P /// @param baseManifest the working manifest for this step /// @param installer the local installer jar /// @return the task producing the updated manifest (not yet saved) - public Task installComponentAsync( + public Task installComponentLocalAsync( GameVersionNumber gameVersion, GameInstanceManifest baseManifest, Path installer) { @@ -369,6 +346,27 @@ public UnsupportedLibraryInstallerException() { } } + /// Removes one component from an unpublished new instance manifest. + /// + /// @param workingManifest the manifest being edited + /// @param componentType the component to remove + /// @return the task producing the updated standalone manifest + private GameInstanceManifest removeNewInstanceComponent( + GameInstanceManifest workingManifest, + GameComponentType componentType) { + @Nullable GameInstancePatch patch = workingManifest.findPatch(componentType.getPatchId()); + if (patch == null) { + return workingManifest; + } + + return reconstructManifest(workingManifest + .withPatches(workingManifest.getPatches() + .stream() + .filter(it -> it != patch) + .toList())); + } + + /// Removes a component from a registered instance using its current stored manifest. /// /// @param instance the target instance @@ -401,7 +399,26 @@ public Task removeComponentAsync( throw new IllegalArgumentException("Cannot remove component from a non-modifiable manifest"); } - return removeNewInstanceComponentAsync(workingManifest, componentType); + return Task.supplyAsync(() -> removeNewInstanceComponent(workingManifest, componentType)); + } + + private static GameInstanceManifest reconstructManifest(GameInstanceManifest manifest) { + if (manifest.inheritsFrom() != null || !manifest.isRoot()) { + throw new IllegalArgumentException("Cannot reconstruct a manifest that inherits from another or is not root"); + } + + if (manifest.patches() == null || manifest.patches().isEmpty()) { + return manifest; + } + + var builder = new GameInstanceManifest.Builder(); + builder.setId(manifest.id()); + builder.setJar(manifest.jar() == null ? manifest.id() : manifest.jar()); + for (GameInstancePatch patch : manifest.patches()) { + builder.merge(patch); + } + builder.setPatches(manifest.patches()); + return builder.toManifest(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 63da36f237..e52e21e0d5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -35,6 +35,7 @@ import java.time.Instant; import java.time.ZoneOffset; import java.util.*; +import java.util.stream.Collectors; @NotNullByDefault @JsonAdapter(GameInstanceManifest.Adapter.class) @@ -712,11 +713,26 @@ public JsonObject toJsonObject() { return json; } - public GameInstanceManifest resolvePatches() { + public GameInstanceManifest applyPatches() { + if (inheritsFrom != null) { + throw new IllegalStateException("Cannot resolve patches on a manifest that inherits from another manifest"); + } + + if (patches == null || patches.isEmpty()) { + return this; + } + GameInstanceManifest manifest = isRoot() + ? new GameInstanceManifest(id) + : this; + for (GameInstancePatch patch : Lang.toIterable(patches().stream() + .sorted(Comparator.comparing(GameInstancePatch::getPriority)))) { + manifest = patch.merge(manifest); + } + return this; } - private static final class Builder { + public static final class Builder { // @formatter:off private @Nullable GameInstanceID id; private @Nullable String minecraftArguments; @@ -742,10 +758,10 @@ private static final class Builder { private @Nullable JsonObject rawJson; // @formatter:on - Builder() { + public Builder() { } - Builder(GameInstanceManifest manifest) { + public Builder(GameInstanceManifest manifest) { this.id = manifest.id; this.minecraftArguments = manifest.minecraftArguments; this.arguments = manifest.arguments; @@ -929,7 +945,36 @@ public void setPatches(@Nullable List patches) { } } - GameInstanceManifest toManifest() { + public void merge(GameInstancePatch patch) { + if (patch.minecraftArguments() != null) + this.minecraftArguments = patch.minecraftArguments(); + this.arguments = Arguments.merge(this.arguments, patch.arguments()); + if (patch.mainClass() != null) + this.mainClass = patch.mainClass(); + if (patch.assetIndex() != null) + this.assetIndex = patch.assetIndex(); + if (patch.assets() != null) + this.assets = patch.assets(); + if (patch.complianceLevel() != null) + this.complianceLevel = patch.complianceLevel(); + if (patch.javaVersion() != null) + this.javaVersion = patch.javaVersion(); + this.libraries = Lang.merge(this.libraries, patch.libraries()); + this.compatibilityRules = Lang.merge(this.compatibilityRules, patch.compatibilityRules()); + if (patch.downloads() != null) + this.downloads = patch.downloads(); + if (patch.logging() != null) + this.logging = patch.logging(); + if (patch.type() != null) + this.type = patch.type(); + if (patch.time() != null) + this.time = patch.time(); + if (patch.releaseTime() != null) + this.releaseTime = patch.releaseTime(); + this.minimumLauncherVersion = Lang.merge(this.minimumLauncherVersion, patch.minimumLauncherVersion(), Math::max); + } + + public GameInstanceManifest toManifest() { if (id == null) { throw new IllegalStateException("id is null"); } From 106ccbab20b8ac1299ddc4e7d71a90aeb6368d32 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 24 Aug 2026 21:58:15 +0800 Subject: [PATCH 11/60] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9=E4=B8=8D=E5=8F=AF=E4=BF=AE=E6=94=B9=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E7=9A=84=E6=A3=80=E6=9F=A5=EF=BC=8C=E5=B9=B6=E7=AE=80=E5=8C=96?= =?UTF-8?q?=20addPatch=20=E6=96=B9=E6=B3=95=E4=BB=A5=E6=8F=90=E9=AB=98?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/InstallerListPage.java | 2 +- .../download/DefaultDependencyManager.java | 12 ++++++++++-- .../hmcl/game/GameInstanceManifest.java | 18 +++--------------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java index 6521e2576f..2233cde3e2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java @@ -145,7 +145,7 @@ public void installOffline() { } private void doInstallOffline(Path file) { - if (gameInstance == null) { + if (gameInstance == null || !gameInstance.getManifest().isModifiable()) { return; } 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 09b4f4495e..717e28fbee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -251,12 +251,15 @@ public Task installComponentRemoteAsync( if (!instance.getId().equals(baseManifest.id())) { throw new IllegalArgumentException("baseManifest id does not match instance"); } + if (!baseManifest.isModifiable()) { + throw new IllegalArgumentException("Cannot install component into a non-modifiable manifest"); + } Path modsDirectory = instance.getModsDirectory(); return removeComponentAsync(instance, baseManifest, componentVersion.getComponentType()) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) - .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) + .thenApplyAsync(patch -> patch == null ? manifest : reconstructManifest(manifest.addPatch(patch)))) .withStage("hmcl.install.%s:%s".formatted(componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); } @@ -310,6 +313,11 @@ public Task installComponentLocalAsync( GameVersionNumber gameVersion, GameInstanceManifest baseManifest, Path installer) { + + if (!baseManifest.isModifiable()) { + throw new IllegalArgumentException("Cannot install component into a non-modifiable manifest"); + } + return Task.composeAsync(() -> { try { return CleanroomInstallTask.install(this, baseManifest, gameVersion.toString(), installer); @@ -334,7 +342,7 @@ public Task installComponentLocalAsync( throw new UnsupportedLibraryInstallerException(); }) .thenApplyAsync(patch -> { - return patch == null ? baseManifest : baseManifest.addPatch(patch); + return patch == null ? baseManifest : reconstructManifest(baseManifest.addPatch(patch)); }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index e52e21e0d5..64274d02e3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -35,7 +35,6 @@ import java.time.Instant; import java.time.ZoneOffset; import java.util.*; -import java.util.stream.Collectors; @NotNullByDefault @JsonAdapter(GameInstanceManifest.Adapter.class) @@ -584,18 +583,9 @@ public GameInstanceManifest withPatches(@Nullable List patche /// Returns a manifest copy with additional patches. public GameInstanceManifest addPatch(GameInstancePatch additional) { - return addPatches(List.of(additional)); - } - - /// Returns a manifest copy with additional patches. - public GameInstanceManifest addPatches(@Nullable List additional) { Set patchIds = new HashSet<>(); - if (additional != null) { - for (GameInstancePatch patch : additional) { - if (patch.id() != null) { - patchIds.add(patch.id()); - } - } + if (additional.id() != null) { + patchIds.add(additional.id()); } List patches = new ArrayList<>(); @@ -606,9 +596,7 @@ public GameInstanceManifest addPatches(@Nullable List additio } } } - if (additional != null) { - patches.addAll(additional); - } + patches.add(additional); return withPatches(patches); } From 8b9f581960e2d4a7fb1cde7644ea2d187a39865e Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:16:21 +0800 Subject: [PATCH 12/60] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9=E4=B8=8D=E5=8F=AF=E4=BF=AE=E6=94=B9=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E7=9A=84=E6=A3=80=E6=9F=A5=EF=BC=8C=E5=B9=B6=E7=AE=80=E5=8C=96?= =?UTF-8?q?=20installComponentLocalAsync=20=E6=96=B9=E6=B3=95=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../download/DefaultDependencyManager.java | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) 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 717e28fbee..33c47eb900 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -300,42 +300,33 @@ public Task installComponentRemoteAsync( /// @return the task producing the updated manifest (not yet saved) public Task installComponentLocalAsync(GameInstance instance, Path installer) { validateGameInstance(instance); - return installComponentLocalAsync(instance.getVersion(), instance.getManifest(), installer); - } - /// Installs a component from a local installer jar into a working manifest. - /// - /// @param gameVersion the Minecraft version expected by the installation - /// @param baseManifest the working manifest for this step - /// @param installer the local installer jar - /// @return the task producing the updated manifest (not yet saved) - public Task installComponentLocalAsync( - GameVersionNumber gameVersion, - GameInstanceManifest baseManifest, - Path installer) { + GameInstanceManifest baseManifest = instance.getManifest(); if (!baseManifest.isModifiable()) { throw new IllegalArgumentException("Cannot install component into a non-modifiable manifest"); } + String gameVersion = instance.getVersion().toString(); + return Task.composeAsync(() -> { try { - return CleanroomInstallTask.install(this, baseManifest, gameVersion.toString(), installer); + return CleanroomInstallTask.install(this, baseManifest, gameVersion, installer); } catch (IOException ignore) { } try { - return NeoForgeInstallTask.install(this, baseManifest, gameVersion.toString(), installer); + return NeoForgeInstallTask.install(this, baseManifest, gameVersion, installer); } catch (IOException ignore) { } try { - return ForgeInstallTask.install(this, baseManifest, gameVersion.toString(), installer); + return ForgeInstallTask.install(this, baseManifest, gameVersion, installer); } catch (IOException ignore) { } try { - return OptiFineInstallTask.install(this, baseManifest, gameVersion.toString(), installer); + return OptiFineInstallTask.install(this, baseManifest, gameVersion, installer); } catch (IOException ignore) { } From 947904482f591b06c4e024f6044d339e48a66481 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:35:22 +0800 Subject: [PATCH 13/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=20RemoveVersionAction=20=E4=B8=BA=20RemoveComponentAction?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E7=AE=80=E5=8C=96=E7=9B=B8=E5=85=B3=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E6=8F=90=E9=AB=98=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/download/AdditionalInstallersPage.java | 4 +- .../UpdateInstallerWizardProvider.java | 17 +++---- .../download/DefaultDependencyManager.java | 45 ++----------------- .../hmcl/game/GameInstanceManifest.java | 31 +++++++++++++ 4 files changed, 42 insertions(+), 55 deletions(-) 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 73a7d64536..cec5186f91 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 @@ -52,7 +52,7 @@ public AdditionalInstallersPage(HMCLGameInstance instance, String gameVersion, W component.setOnRemove(() -> { controller.getSettings().put( component.getComponentType().getPatchId(), - new UpdateInstallerWizardProvider.RemoveVersionAction(component.getComponentType())); + new UpdateInstallerWizardProvider.RemoveComponentAction(component.getComponentType())); reload(); }); } @@ -85,7 +85,7 @@ protected void reload() { GameComponentType componentType = component.getComponentType(); String version = instance.getComponentVersion(component.getComponentType()); String libraryVersion = Lang.requireNonNullElse(getVersion(componentType), version); - boolean alreadyInstalled = version != null && !(controller.getSettings().get(componentType.getPatchId()) instanceof UpdateInstallerWizardProvider.RemoveVersionAction); + boolean alreadyInstalled = version != null && !(controller.getSettings().get(componentType.getPatchId()) instanceof UpdateInstallerWizardProvider.RemoveComponentAction); if (component.getComponentType() != GameComponentType.GAME && gameVersionChanged && getVersion(componentType) == null && alreadyInstalled) { // For third-party libraries, if game version is being changed, and the library is not being reinstalled, // warns the user that we should update the library. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index fc59409d23..9916cc6609 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java @@ -88,12 +88,10 @@ public Object finish(SettingsMap settings) { if (value instanceof RemoteVersion remoteVersion) { update = update.thenComposeAsync(manifest -> dependencyManager.installComponentRemoteAsync(publishedInstance, manifest, remoteVersion)); - } else if (value instanceof RemoveVersionAction removeVersionAction) { - update = update.thenComposeAsync(manifest -> - dependencyManager.removeComponentAsync( - publishedInstance, - manifest, - removeVersionAction.componentType)); + } else if (value instanceof RemoveComponentAction removeComponentAction) { + update = update.thenApplyAsync(manifest -> + manifest.removeComponent(removeComponentAction.componentType)); + } } return update; @@ -183,11 +181,6 @@ public static void alertFailureMessage(Exception exception, Runnable next) { } } - public static class RemoveVersionAction { - private final GameComponentType componentType; - - public RemoveVersionAction(GameComponentType componentType) { - this.componentType = componentType; - } + public record RemoveComponentAction(GameComponentType componentType) { } } 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 33c47eb900..36055a0809 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -194,8 +194,7 @@ Task installNewInstanceComponentAsync( } Path modsDirectory = repository.getRunDirectoryForInstallation(instanceId).resolve("mods"); - GameComponentType componentType = componentVersion.getComponentType(); - return Task.supplyAsync(() -> removeNewInstanceComponent(baseManifest, componentType)) + return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) @@ -256,7 +255,7 @@ public Task installComponentRemoteAsync( } Path modsDirectory = instance.getModsDirectory(); - return removeComponentAsync(instance, baseManifest, componentVersion.getComponentType()) + return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) .thenApplyAsync(patch -> patch == null ? manifest : reconstructManifest(manifest.addPatch(patch)))) @@ -345,27 +344,6 @@ public UnsupportedLibraryInstallerException() { } } - /// Removes one component from an unpublished new instance manifest. - /// - /// @param workingManifest the manifest being edited - /// @param componentType the component to remove - /// @return the task producing the updated standalone manifest - private GameInstanceManifest removeNewInstanceComponent( - GameInstanceManifest workingManifest, - GameComponentType componentType) { - @Nullable GameInstancePatch patch = workingManifest.findPatch(componentType.getPatchId()); - if (patch == null) { - return workingManifest; - } - - return reconstructManifest(workingManifest - .withPatches(workingManifest.getPatches() - .stream() - .filter(it -> it != patch) - .toList())); - } - - /// Removes a component from a registered instance using its current stored manifest. /// /// @param instance the target instance @@ -373,22 +351,7 @@ private GameInstanceManifest removeNewInstanceComponent( /// @return the task producing the updated standalone manifest (not yet saved) public Task removeComponentAsync(GameInstance instance, GameComponentType componentType) { validateGameInstance(instance); - return removeComponentAsync(instance, instance.getManifest(), componentType); - } - - /// Removes a component from a working manifest bound to a registered instance. - /// - /// When `workingManifest` is the instance's stored manifest, edits its resolved standalone view; - /// otherwise edits the independent draft (resolving inheritance if still present). - /// - /// @param instance the registered instance - /// @param workingManifest the draft being edited - /// @param componentType the component to remove - /// @return the task producing the updated standalone manifest (not yet saved) - public Task removeComponentAsync( - GameInstance instance, - GameInstanceManifest workingManifest, - GameComponentType componentType) { + GameInstanceManifest workingManifest = instance.getManifest(); validateGameInstance(instance); if (!instance.getId().equals(workingManifest.id())) { throw new IllegalArgumentException("workingManifest id does not match instance"); @@ -398,7 +361,7 @@ public Task removeComponentAsync( throw new IllegalArgumentException("Cannot remove component from a non-modifiable manifest"); } - return Task.supplyAsync(() -> removeNewInstanceComponent(workingManifest, componentType)); + return Task.completed(workingManifest.removeComponent(componentType)); } private static GameInstanceManifest reconstructManifest(GameInstanceManifest manifest) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 64274d02e3..6aabf6c2e7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -720,6 +720,37 @@ public GameInstanceManifest applyPatches() { return this; } + private static GameInstanceManifest reconstruct(GameInstanceManifest manifest) { + if (manifest.inheritsFrom() != null || !manifest.isRoot()) { + throw new IllegalArgumentException("Cannot reconstruct a manifest that inherits from another or is not root"); + } + + if (manifest.patches() == null || manifest.patches().isEmpty()) { + return manifest; + } + + var builder = new GameInstanceManifest.Builder(); + builder.setId(manifest.id()); + builder.setJar(manifest.jar() == null ? manifest.id() : manifest.jar()); + for (GameInstancePatch patch : manifest.patches()) { + builder.merge(patch); + } + builder.setPatches(manifest.patches()); + return builder.toManifest(); + } + + public GameInstanceManifest removeComponent(GameComponentType type) { + @Nullable GameInstancePatch patch = findPatch(type.getPatchId()); + if (patch == null) { + return this; + } + + return reconstruct(this.withPatches(getPatches() + .stream() + .filter(it -> it != patch) + .toList())); + } + public static final class Builder { // @formatter:off private @Nullable GameInstanceID id; From 176b651f7fb2063f45506469cf02201726cf7e97 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:39:04 +0800 Subject: [PATCH 14/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20updateInstanceAsync?= =?UTF-8?q?=20=E6=96=B9=E6=B3=95=EF=BC=8C=E7=AE=80=E5=8C=96=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=B9=B6=E6=8F=90=E9=AB=98=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/game/DefaultGameRepository.java | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) 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 d24a7655b9..d5c3423ddd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -627,39 +627,31 @@ public Task saveAsync(GameInstanceManifest instanceManifes /// working manifest with the same id. Its result is staged and committed exactly once. Failure /// or cancellation aborts the draft; shared cache files written by the updater are retained. /// - /// @param the checked exception type thrown while creating the update task + /// @param the checked exception type thrown while creating the update task /// @param instanceId the instance to update /// @param updater the asynchronous manifest update /// @return the task that commits the updated manifest public Task updateInstanceAsync( GameInstanceID instanceId, ExceptionalFunction, E> updater) { - var active = new AtomicReference<@Nullable GameRepositoryDraft>(); - return Task.supplyAsync(() -> { - GameRepositoryDraft draft = openDraft(); - active.set(draft); - return draft.getBaseSnapshot().getInstance(instanceId); - }) - .thenComposeAsync(updater) - .thenApplyAsync(manifest -> { - GameRepositoryDraft draft = active.get(); - if (draft == null) { - throw new IllegalStateException("Game repository draft is unavailable"); - } - if (!instanceId.equals(manifest.id())) { - throw new IllegalArgumentException( - "Instance updater changed id from " + instanceId + " to " + manifest.id()); - } - draft.put(manifest); - draft.commit(); - return manifest; - }) - .whenComplete(exception -> { - GameRepositoryDraft draft = active.getAndSet(null); - if (draft != null && draft.isOpen()) { - draft.abort(); - } - }); + return Task.composeAsync(() -> { + @SuppressWarnings("resource") + GameRepositoryDraft draft = openDraft(); + return updater.apply(draft.getBaseSnapshot().getInstance(instanceId)) + .thenApplyAsync(manifest -> { + if (!instanceId.equals(manifest.id())) { + throw new IllegalArgumentException( + "Instance updater changed id from " + instanceId + " to " + manifest.id()); + } + draft.put(manifest); + draft.commit(); + return manifest; + }).whenComplete(exception -> { + if (draft.isOpen()) { + draft.abort(); + } + }); + }); } @Override From 8c6d26b511d55a37b5d54211e18a47688e3e379c Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:41:30 +0800 Subject: [PATCH 15/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20installNewInstanceCo?= =?UTF-8?q?mponentAsync=20=E6=96=B9=E6=B3=95=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=20instanceId=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=BB=A5=E6=8F=90=E9=AB=98=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/download/DefaultDependencyManager.java | 15 +-------------- .../hmcl/download/DefaultGameBuilder.java | 6 +++--- 2 files changed, 4 insertions(+), 17 deletions(-) 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 36055a0809..3001b76081 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -181,19 +181,13 @@ public Task checkPatchCompletionAsync( /// Installs a component into an unpublished new instance without constructing a /// [GameInstance]. /// - /// @param instanceId the unpublished instance id /// @param baseManifest the working manifest for this step /// @param componentVersion the remote component to install /// @return the task producing the updated manifest (not yet committed) Task installNewInstanceComponentAsync( - GameInstanceID instanceId, GameInstanceManifest baseManifest, RemoteVersion componentVersion) { - if (!instanceId.equals(baseManifest.id())) { - throw new IllegalArgumentException("baseManifest id does not match instanceId"); - } - - Path modsDirectory = repository.getRunDirectoryForInstallation(instanceId).resolve("mods"); + Path modsDirectory = repository.getRunDirectoryForInstallation(baseManifest.id()).resolve("mods"); return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) @@ -205,26 +199,19 @@ Task installNewInstanceComponentAsync( /// Resolves and installs a component into an unpublished new instance. /// - /// @param instanceId the unpublished instance id /// @param baseManifest the working manifest for this step /// @param gameVersion the Minecraft version used to look up the remote list /// @param componentType the component list id, such as `game` or `forge` /// @param componentVersion the component version id /// @return the installation task Task installNewInstanceComponentAsync( - GameInstanceID instanceId, GameInstanceManifest baseManifest, String gameVersion, GameComponentType componentType, String componentVersion) { - if (!instanceId.equals(baseManifest.id())) { - throw new IllegalArgumentException("baseManifest id does not match instanceId"); - } - VersionList versionList = getVersionList(componentType); return versionList.loadAsync(gameVersion) .thenComposeAsync(() -> installNewInstanceComponentAsync( - instanceId, baseManifest, versionList.getVersion(gameVersion, componentVersion) .orElseThrow(() -> new IOException( diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index c44d0d3ad2..f57adcec93 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -88,7 +88,7 @@ public Task buildAsync() { DefaultGameRepositoryDraft draft = repository.openDraft(); Task libraryTask = dependencyManager.installNewInstanceComponentAsync( - id, new GameInstanceManifest(id), gameVersion, GameComponentType.GAME, gameVersion); + new GameInstanceManifest(id), gameVersion, GameComponentType.GAME, gameVersion); for (Map.Entry entry : components.entrySet()) { GameComponentType componentType = entry.getKey(); @@ -98,11 +98,11 @@ public Task buildAsync() { if (entry.getValue() instanceof RemoteVersion remoteVersion) { libraryTask = libraryTask.thenComposeAsync(manifest -> dependencyManager.installNewInstanceComponentAsync( - id, manifest, remoteVersion)); + manifest, remoteVersion)); } else if (entry.getValue() instanceof String version) { libraryTask = libraryTask.thenComposeAsync(manifest -> dependencyManager.installNewInstanceComponentAsync( - id, manifest, gameVersion, componentType, version)); + manifest, gameVersion, componentType, version)); } else { throw new AssertionError("Unexpected version type: " + entry.getValue().getClass()); } From 9fab53381dca071aaf33524d5fa1aa6e7c5ecf15 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:47:13 +0800 Subject: [PATCH 16/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20GameInstanceManifest?= =?UTF-8?q?=20=E7=9A=84=E9=87=8D=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E7=AE=80=E5=8C=96=20removeComponent=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=20reconstructByPatches=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E6=8F=90=E9=AB=98=E4=BB=A3=E7=A0=81=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../download/DefaultDependencyManager.java | 29 +++----------- .../hmcl/game/GameInstanceManifest.java | 40 +++++++++---------- 2 files changed, 26 insertions(+), 43 deletions(-) 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 3001b76081..e150608695 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -245,7 +245,9 @@ public Task installComponentRemoteAsync( return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) - .thenApplyAsync(patch -> patch == null ? manifest : reconstructManifest(manifest.addPatch(patch)))) + .thenApplyAsync(patch -> patch == null + ? manifest + : manifest.addPatch(patch).reconstructByPatches())) .withStage("hmcl.install.%s:%s".formatted(componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); } @@ -318,9 +320,9 @@ public Task installComponentLocalAsync(GameInstance instan throw new UnsupportedLibraryInstallerException(); }) - .thenApplyAsync(patch -> { - return patch == null ? baseManifest : reconstructManifest(baseManifest.addPatch(patch)); - }); + .thenApplyAsync(patch -> patch == null + ? baseManifest + : baseManifest.addPatch(patch).reconstructByPatches()); } /// Indicates that a local library installer is not recognized by any supported installer. @@ -351,23 +353,4 @@ public Task removeComponentAsync(GameInstance instance, Ga return Task.completed(workingManifest.removeComponent(componentType)); } - private static GameInstanceManifest reconstructManifest(GameInstanceManifest manifest) { - if (manifest.inheritsFrom() != null || !manifest.isRoot()) { - throw new IllegalArgumentException("Cannot reconstruct a manifest that inherits from another or is not root"); - } - - if (manifest.patches() == null || manifest.patches().isEmpty()) { - return manifest; - } - - var builder = new GameInstanceManifest.Builder(); - builder.setId(manifest.id()); - builder.setJar(manifest.jar() == null ? manifest.id() : manifest.jar()); - for (GameInstancePatch patch : manifest.patches()) { - builder.merge(patch); - } - builder.setPatches(manifest.patches()); - return builder.toManifest(); - } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 6aabf6c2e7..1addf6f1b9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -720,37 +720,37 @@ public GameInstanceManifest applyPatches() { return this; } - private static GameInstanceManifest reconstruct(GameInstanceManifest manifest) { - if (manifest.inheritsFrom() != null || !manifest.isRoot()) { + public GameInstanceManifest removeComponent(GameComponentType type) { + @Nullable GameInstancePatch patch = findPatch(type.getPatchId()); + if (patch == null) { + return this; + } + + return this.withPatches(getPatches() + .stream() + .filter(it -> it != patch) + .toList()).reconstructByPatches(); + } + + public GameInstanceManifest reconstructByPatches() { + if (inheritsFrom() != null || !isRoot()) { throw new IllegalArgumentException("Cannot reconstruct a manifest that inherits from another or is not root"); } - if (manifest.patches() == null || manifest.patches().isEmpty()) { - return manifest; + if (patches() == null || patches().isEmpty()) { + return this; } var builder = new GameInstanceManifest.Builder(); - builder.setId(manifest.id()); - builder.setJar(manifest.jar() == null ? manifest.id() : manifest.jar()); - for (GameInstancePatch patch : manifest.patches()) { + builder.setId(id()); + builder.setJar(jar() == null ? id() : jar()); + for (GameInstancePatch patch : patches()) { builder.merge(patch); } - builder.setPatches(manifest.patches()); + builder.setPatches(patches()); return builder.toManifest(); } - public GameInstanceManifest removeComponent(GameComponentType type) { - @Nullable GameInstancePatch patch = findPatch(type.getPatchId()); - if (patch == null) { - return this; - } - - return reconstruct(this.withPatches(getPatches() - .stream() - .filter(it -> it != patch) - .toList())); - } - public static final class Builder { // @formatter:off private @Nullable GameInstanceID id; From 35115bd8e7658c884a449ab3e2bf61eeaae7de25 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:52:43 +0800 Subject: [PATCH 17/60] =?UTF-8?q?=E4=BC=98=E5=8C=96=20libraryTask=20?= =?UTF-8?q?=E7=9A=84=E5=88=9B=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B0=86?= =?UTF-8?q?=20Task.supplyAsync=20=E6=9B=BF=E6=8D=A2=E4=B8=BA=20Task.comple?= =?UTF-8?q?ted=20=E4=BB=A5=E6=8F=90=E9=AB=98=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index a71424df0e..6479f82bc3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -93,7 +93,7 @@ public void execute() throws Exception { GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(originalManifest, null); dependencies.add(repository.updateInstanceAsync(instanceId, publishedInstance -> { - Task libraryTask = Task.supplyAsync(() -> originalManifest); + Task libraryTask = Task.completed(originalManifest); // Forge and OptiFine libraries must be regenerated by their installers. for (GameComponentAnalyzer.Mark mark : analyzer) { if (mark.componentType() == GameComponentType.GAME) { From 4938eddeb2067211f728b07ec7a8304836ebc971 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:55:59 +0800 Subject: [PATCH 18/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20McbbsModpackLocalIns?= =?UTF-8?q?tallTask=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=A4=9A=E4=BD=99=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E8=A1=8C=E4=BB=A5=E6=8F=90=E9=AB=98=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=95=B4=E6=B4=81=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index c2a801815c..e4ca6207b8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -64,7 +64,6 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, throw new IllegalArgumentException("Instance " + instanceId + " already exists."); this.update = repository.hasInstance(instanceId); - GameBuilder builder = dependencyManager.newGameBuilder().id(instanceId); for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); From a831c6585030a1f07b3e7dcb8d8f61126c2fa725 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 20:59:38 +0800 Subject: [PATCH 19/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20RemoteVersion=20?= =?UTF-8?q?=E7=B1=BB=E7=9A=84=20getInstallTask=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20modsDirectory=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=BB=A5=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=20mods=20=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/download/RemoteVersion.java | 9 +-------- .../hmcl/download/cleanroom/CleanroomRemoteVersion.java | 3 ++- .../hmcl/download/fabric/FabricRemoteVersion.java | 3 ++- .../hmcl/download/forge/ForgeRemoteVersion.java | 3 ++- .../jackhuang/hmcl/download/game/GameRemoteVersion.java | 3 ++- .../download/legacyfabric/LegacyFabricRemoteVersion.java | 3 ++- .../download/liteloader/LiteLoaderRemoteVersion.java | 3 ++- .../hmcl/download/neoforge/NeoForgeRemoteVersion.java | 3 ++- .../hmcl/download/optifine/OptiFineRemoteVersion.java | 3 ++- .../hmcl/download/quilt/QuiltRemoteVersion.java | 3 ++- 10 files changed, 19 insertions(+), 17 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java index 6c71b80862..1e68ddd64d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java @@ -98,16 +98,9 @@ public Type getVersionType() { return type; } - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { - throw new UnsupportedOperationException(this + " cannot be installed yet"); - } - /// Creates an install task with an explicit mods directory for libraries that download into the /// instance run tree (for example Fabric/Quilt API). /// - /// The default implementation ignores `modsDirectory` and delegates to - /// [#getInstallTask(DefaultDependencyManager, GameInstanceManifest)]. - /// /// @param dependencyManager the dependency manager /// @param baseVersion the manifest being installed into /// @param modsDirectory the mods directory of the target instance run directory @@ -116,7 +109,7 @@ public Task getInstallTask( DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return getInstallTask(dependencyManager, baseVersion); + throw new UnsupportedOperationException(this + " cannot be installed yet"); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java index e7a3d07392..cb401108bb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.time.Instant; import java.util.List; @@ -33,7 +34,7 @@ public CleanroomRemoteVersion(String gameVersion, String selfVersion, Instant re } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new CleanroomInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java index a95fdd8ab1..bbd649d05c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.util.List; public class FabricRemoteVersion extends RemoteVersion { @@ -39,7 +40,7 @@ public class FabricRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new FabricInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java index f93607092c..36cc5fb5a2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.time.Instant; import java.util.List; @@ -40,7 +41,7 @@ public ForgeRemoteVersion(String gameVersion, String selfVersion, Instant releas } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new ForgeInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java index 5d3dcfc68d..e6dec17401 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import java.nio.file.Path; import java.time.Instant; import java.util.List; @@ -49,7 +50,7 @@ public ReleaseType getType() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new GameInstallTask(dependencyManager, baseVersion, this); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java index efb24bee0d..ba8ec166b0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.util.List; public class LegacyFabricRemoteVersion extends RemoteVersion { @@ -39,7 +40,7 @@ public class LegacyFabricRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new LegacyFabricInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java index f6a09e79b8..7181ec8937 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java @@ -25,6 +25,7 @@ import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.util.Collection; import java.util.List; @@ -55,7 +56,7 @@ public String getTweakClass() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new LiteLoaderInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java index 4b3483f961..7f6b0415bd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.util.List; public class NeoForgeRemoteVersion extends RemoteVersion { @@ -32,7 +33,7 @@ public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new NeoForgeInstallTask(dependencyManager, baseVersion, this); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java index b7b788b8ea..0c2ef67cf7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java @@ -25,6 +25,7 @@ import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.util.List; public class OptiFineRemoteVersion extends RemoteVersion { @@ -39,7 +40,7 @@ public String getFullVersion() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new GameDownloadTask(dependencyManager, baseVersion) .thenComposeAsync(minecraftJar -> new OptiFineInstallTask( dependencyManager, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java index 4385fb3907..1a624fd4da 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; +import java.nio.file.Path; import java.util.List; public class QuiltRemoteVersion extends RemoteVersion { @@ -39,7 +40,7 @@ public class QuiltRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { return new QuiltInstallTask(dependencyManager, baseVersion, this); } } From b817862c9c9ed82cdd99626f4a59968861b323db Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 21:35:45 +0800 Subject: [PATCH 20/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E4=BB=BB=E5=8A=A1=E7=9A=84=20getInstallTask?= =?UTF-8?q?=20=E6=96=B9=E6=B3=95=EF=BC=8C=E7=BB=9F=E4=B8=80=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=91=BD=E5=90=8D=E4=B8=BA=20baseManifest=20=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/game/HMCLGameRepository.java | 121 ++---------------- .../hmcl/game/HMCLModpackInstallTask.java | 6 +- .../jackhuang/hmcl/game/ModpackHelper.java | 4 - .../hmcl/ui/download/DownloadPage.java | 4 +- .../hmcl/setting/GameDirectoriesTest.java | 16 ++- .../download/DefaultDependencyManager.java | 6 +- .../hmcl/download/DefaultGameBuilder.java | 43 ++++++- .../jackhuang/hmcl/download/GameBuilder.java | 16 +++ .../hmcl/download/RemoteVersion.java | 28 ++-- .../cleanroom/CleanroomRemoteVersion.java | 4 +- .../fabric/FabricAPIRemoteVersion.java | 4 +- .../download/fabric/FabricRemoteVersion.java | 4 +- .../download/forge/ForgeRemoteVersion.java | 4 +- .../hmcl/download/game/GameRemoteVersion.java | 4 +- .../LegacyFabricAPIRemoteVersion.java | 4 +- .../LegacyFabricRemoteVersion.java | 4 +- .../liteloader/LiteLoaderRemoteVersion.java | 4 +- .../neoforge/NeoForgeRemoteVersion.java | 4 +- .../optifine/OptiFineRemoteVersion.java | 6 +- .../download/quilt/QuiltAPIRemoteVersion.java | 4 +- .../download/quilt/QuiltRemoteVersion.java | 4 +- .../hmcl/game/DefaultGameRepository.java | 37 ------ .../hmcl/game/DefaultGameRepositoryDraft.java | 7 +- .../hmcl/modpack/curse/CurseInstallTask.java | 5 +- .../mcbbs/McbbsModpackLocalInstallTask.java | 4 +- .../modpack/modrinth/ModrinthInstallTask.java | 4 +- .../multimc/MultiMCModpackInstallTask.java | 82 ++++++++++-- .../server/ServerModpackLocalInstallTask.java | 4 +- .../ServerModpackRemoteInstallTask.java | 4 +- 29 files changed, 215 insertions(+), 226 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index c495aea12f..3837aff909 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -49,7 +49,6 @@ import java.nio.file.Path; import java.time.Instant; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; import static org.jackhuang.hmcl.setting.SettingsManager.settings; @@ -67,9 +66,6 @@ public final class HMCLGameRepository extends DefaultGameRepository { /// The selected instance resolved from the current repository snapshot. private final ReadOnlyObjectWrapper<@Nullable HMCLGameInstance> selectedInstance; - /// Settings reservations transferred to the next draft that creates the corresponding id. - private final Map preparedInstanceSettings = new ConcurrentHashMap<>(); - /// Creates a repository backed by the given game directory. /// /// @param gameDirectory the persistent game directory represented by this repository @@ -95,45 +91,6 @@ protected HMCLGameRepositorySnapshot createSnapshot(DefaultGameRepositoryLayout return new HMCLGameRepositorySnapshot(this, (HMCLGameRepositoryLayout) layout); } - /// {@inheritDoc} - /// - /// Prepared settings belong to the old layout and are discarded after a successful replacement. - @Override - public void setBaseDirectory(Path baseDirectory) { - super.setBaseDirectory(baseDirectory); - preparedInstanceSettings.clear(); - } - - /// {@inheritDoc} - /// - /// Accepts an existing root only when this repository reserved the id while the root was absent. - @Override - protected boolean mayClaimDraftInstanceRoot(GameInstanceID instanceId, Path instanceRoot) { - PreparedInstanceSettings prepared = preparedInstanceSettings.get(instanceId); - if (prepared != null) { - return prepared.instanceRoot().equals(instanceRoot) && prepared.rootWasAbsent(); - } - return super.mayClaimDraftInstanceRoot(instanceId, instanceRoot); - } - - /// {@inheritDoc} - /// - /// Writes settings prepared by [#ensureIsolatedRunningDirectory(GameInstanceID)] only after the - /// draft owns the instance root. - @Override - protected void initializeDraftInstanceRoot(GameInstanceID instanceId, Path instanceRoot) throws IOException { - PreparedInstanceSettings prepared = preparedInstanceSettings.get(instanceId); - if (prepared == null) { - return; - } - if (!prepared.instanceRoot().equals(instanceRoot) || !prepared.rootWasAbsent()) { - throw new IOException("Prepared instance root cannot be claimed: " + instanceRoot); - } - - writeInstanceGameSettings(instanceId, prepared.settings()); - preparedInstanceSettings.remove(instanceId, prepared); - } - @Override protected HMCLGameInstance createInstance( DefaultGameRepositorySnapshot snapshot, @@ -286,19 +243,6 @@ Path computeRunDirectory( } } - /// {@inheritDoc} - /// - /// Resolves HMCL isolation and modpack rules directly from files and settings so an unpublished - /// installation does not require a [GameInstance]. - @Override - public Path getRunDirectoryForInstallation(GameInstanceID instanceId) { - @Nullable PreparedInstanceSettings prepared = preparedInstanceSettings.get(instanceId); - return computeRunDirectory( - instanceId, - Files.exists(getLayout().getModpackConfigurationFile(instanceId)), - prepared != null ? prepared.settings() : getInstanceGameSettings(instanceId)); - } - private String selectedRunningDirectory( GameSettings.@Nullable Instance localSetting, boolean useInstanceRunningDirectory) { @@ -315,8 +259,8 @@ private String selectedRunningDirectory( /// Reads instance-local settings from disk without requiring a registered snapshot member. /// - /// Used for install-time path resolution and migration before the instance is indexed. Does not - /// publish a snapshot entry. + /// Used while loading or migrating settings before the instance is indexed. Does not publish a + /// snapshot entry. /// /// @param instanceId the instance id /// @return the loaded settings, or `null` when none can be loaded @@ -349,40 +293,21 @@ private void writeInstanceGameSettings(GameInstanceID instanceId, GameSettings.I /// Ensures the instance uses an isolated running directory under its instance root. /// - /// When the instance is already registered, settings are updated through - /// [HMCLGameInstance]. Otherwise the settings are retained in memory and transferred to the - /// draft that creates the instance, so the draft owns every file created for the installation. - /// /// @param instanceId the instance id + /// @throws NoSuchGameInstanceException if the instance is not registered public void ensureIsolatedRunningDirectory(GameInstanceID instanceId) { HMCLGameInstance instance = findInstance(instanceId); - if (instance != null) { - if (instance.isSettingsReadOnly()) { - return; - } - GameSettings.Instance setting = instance.getSettingsOrCreate(); - if (setting != null - && setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) { - instance.saveSettings(); - } + if (instance == null) { + throw new NoSuchGameInstanceException(instanceId); + } + if (instance.isSettingsReadOnly()) { return; } - - Path instanceRoot = getLayout().getInstanceRoot(instanceId).toAbsolutePath().normalize(); - PreparedInstanceSettings prepared = preparedInstanceSettings.get(instanceId); - GameSettings.Instance setting = prepared != null - ? prepared.settings() - : peekInstanceGameSettings(instanceId); - if (setting == null) { - setting = new GameSettings.Instance(); + @Nullable GameSettings.Instance setting = instance.getSettingsOrCreate(); + if (setting != null + && setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) { + instance.saveSettings(); } - setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY); - preparedInstanceSettings.put( - instanceId, - new PreparedInstanceSettings( - setting, - instanceRoot, - prepared != null ? prepared.rootWasAbsent() : Files.notExists(instanceRoot))); } public Stream getDisplayInstances() { @@ -518,6 +443,9 @@ public GameSettings.Effective getEffectiveGameSettings(GameInstanceID instanceId } /// Returns whether a new instance should use an isolated running directory under the default isolation settings. + /// + /// @param modded whether the new instance contains a mod loader + /// @return whether installation-time run-directory content should be placed under the instance root public boolean shouldIsolateNewInstance(boolean modded) { GameSettings.Preset preset = getParentGameSettings(null); DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED); @@ -528,17 +456,6 @@ public boolean shouldIsolateNewInstance(boolean modded) { }; } - /// Applies default isolation to a new instance before its manifest is saved. - /// - /// Writes the isolation flag to the instance settings file so a later - /// [HMCLGameInstance#getRunDirectory] returns the instance root. - public void applyDefaultIsolationSettingForNewInstance(GameInstanceID instanceId, boolean modded) { - if (!shouldIsolateNewInstance(modded)) { - return; - } - ensureIsolatedRunningDirectory(instanceId); - } - /// Loads settings from disk for an unregistered id, running legacy migration when needed. private GameSettings.@Nullable Instance loadOrMigrateInstanceGameSettings(GameInstanceID instanceId) { Path file = getLayout().getInstanceGameSettingsFile(instanceId); @@ -627,14 +544,4 @@ public static long getAutoAllocatedMemory(long available, Platform platform) { : suggested; } - /// Records settings prepared for an instance that has not entered a repository draft yet. - /// - /// @param settings the settings to materialize after the draft claims the root - /// @param instanceRoot the normalized root reserved for the instance - /// @param rootWasAbsent whether the root was absent when the reservation was made - private record PreparedInstanceSettings( - GameSettings.Instance settings, - Path instanceRoot, - boolean rootWasAbsent) { - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 6479f82bc3..8c6b7c19c6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -55,7 +55,11 @@ public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpa if (repository.hasInstance(this.instanceId) && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists"); - dependents.add(dependency.newGameBuilder().id(this.instanceId).component(GameComponentType.GAME, modpack.getGameVersion()).buildAsync()); + dependents.add(dependency.newGameBuilder() + .id(this.instanceId) + .useInstanceRunDirectory() + .component(GameComponentType.GAME, modpack.getGameVersion()) + .buildAsync()); onDone().register(event -> { if (event.isFailed()) repository.removeInstanceFromDisk(this.instanceId); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 7fdad1a09f..6a8beb4858 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -158,8 +158,6 @@ public static ModpackConfiguration readModpackConfiguration(Path file) throws } public static Task getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, Modpack modpack) { - repository.ensureIsolatedRunningDirectory(instanceId); - ExceptionalRunnable success = () -> { repository.refresh(); repository.ensureIsolatedRunningDirectory(instanceId); @@ -198,8 +196,6 @@ public static Task getInstallManuallyCreatedModpackTask(Path zipFile, String } public static Task getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) { - repository.ensureIsolatedRunningDirectory(instanceId); - ExceptionalRunnable success = () -> { repository.refresh(); repository.ensureIsolatedRunningDirectory(instanceId); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 8de3cb718a..defe1cd28b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -313,7 +313,9 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { builder.component(remoteVersion); }); - repository.applyDefaultIsolationSettingForNewInstance(instanceId, settings.isInstallingModdedVersion()); + if (repository.shouldIsolateNewInstance(settings.isInstallingModdedVersion())) { + builder.useInstanceRunDirectory(); + } return builder.buildAsync().whenComplete(any -> { repository.refresh(); repository.getInstance(instanceId).applyDefaultIsolationSetting(); diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java index 95c9aa4f95..fa027f519f 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.game.GameRepositoryDraft; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.game.NoSuchGameInstanceException; import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.PortablePath; import org.jackhuang.hmcl.util.gson.JsonSchema; @@ -439,14 +440,14 @@ public void repositoryDirectoryFollowsGameDirectoryPath() throws ReflectiveOpera } } - /// Tests that an unpublished isolated installation resolves paths without a [HMCLGameInstance]. + /// Tests that default isolation is applied after a new instance is published. @Test - public void newIsolatedInstallationUsesVersionRootBeforePublication(@TempDir Path tempDirectory) + public void newInstanceAppliesDefaultIsolationAfterPublication(@TempDir Path tempDirectory) throws Exception { GameSettingsPresetID defaultPresetId = GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174002"); GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId); - defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.MODDED); + defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS); GameSettingsPresets presets = new GameSettingsPresets(); presets.getPresets().setAll(defaultPreset); @@ -465,18 +466,19 @@ public void newIsolatedInstallationUsesVersionRootBeforePublication(@TempDir Pat GameInstanceID id = new GameInstanceID("1.21.11-fabric"); assertFalse(repository.hasInstance(id)); + assertTrue(repository.shouldIsolateNewInstance(false)); + assertThrows( + NoSuchGameInstanceException.class, + () -> repository.ensureIsolatedRunningDirectory(id)); - repository.applyDefaultIsolationSettingForNewInstance(id, true); try (GameRepositoryDraft draft = repository.openDraft()) { draft.put(new GameInstanceManifest(id)); assertFalse(repository.hasInstance(id)); - assertEquals( - repository.getLayout().getInstanceRoot(id), - repository.getRunDirectoryForInstallation(id)); draft.commit(); } HMCLGameInstance instance = repository.getInstance(id); + instance.applyDefaultIsolationSetting(); assertEquals(repository.getLayout().getInstanceRoot(id), instance.getRunDirectory()); assertEquals(repository.getLayout().getInstanceRoot(id).resolve("mods"), instance.getModsDirectory()); GameSettings.Instance instanceSettings = Objects.requireNonNull(instance.getSettings()); 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 e150608695..6239ddcf09 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -182,12 +182,13 @@ public Task checkPatchCompletionAsync( /// [GameInstance]. /// /// @param baseManifest the working manifest for this step + /// @param modsDirectory the mods directory to use during installation /// @param componentVersion the remote component to install /// @return the task producing the updated manifest (not yet committed) Task installNewInstanceComponentAsync( GameInstanceManifest baseManifest, + Path modsDirectory, RemoteVersion componentVersion) { - Path modsDirectory = repository.getRunDirectoryForInstallation(baseManifest.id()).resolve("mods"); return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) @@ -200,12 +201,14 @@ Task installNewInstanceComponentAsync( /// Resolves and installs a component into an unpublished new instance. /// /// @param baseManifest the working manifest for this step + /// @param modsDirectory the mods directory to use during installation /// @param gameVersion the Minecraft version used to look up the remote list /// @param componentType the component list id, such as `game` or `forge` /// @param componentVersion the component version id /// @return the installation task Task installNewInstanceComponentAsync( GameInstanceManifest baseManifest, + Path modsDirectory, String gameVersion, GameComponentType componentType, String componentVersion) { @@ -213,6 +216,7 @@ Task installNewInstanceComponentAsync( return versionList.loadAsync(gameVersion) .thenComposeAsync(() -> installNewInstanceComponentAsync( baseManifest, + modsDirectory, versionList.getVersion(gameVersion, componentVersion) .orElseThrow(() -> new IOException( "Remote component " + componentType + " has no version " + componentVersion)))) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index f57adcec93..a9c2e5fd31 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -21,7 +21,10 @@ import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.task.Task; import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Map; import java.util.Objects; @@ -54,15 +57,15 @@ public DefaultDependencyManager getDependencyManager() { /// {@inheritDoc} /// /// Retains an unpublished working manifest, installs the configured game and optional loaders, - /// resolves them into launch and standalone views, and commits the standalone manifest once. - /// Failure or cancellation aborts the draft. + /// resolves the launch view, and commits it with the original patches once. Failure or + /// cancellation aborts the draft. /// /// @return the build task /// @throws NullPointerException if [#id] was not set @Override public Task buildAsync() { GameInstanceID id = Objects.requireNonNull(this.id, "GameBuilder.id must be set"); - String gameVersion = (String) components.get(GameComponentType.GAME); + @Nullable String gameVersion = (String) components.get(GameComponentType.GAME); if (gameVersion == null) throw new IllegalStateException("GameBuilder.gameVersion must be set"); @@ -86,9 +89,23 @@ public Task buildAsync() { DefaultGameRepository repository = dependencyManager.getGameRepository(); //noinspection resource DefaultGameRepositoryDraft draft = repository.openDraft(); + GameInstanceManifest initialManifest = new GameInstanceManifest(id); + try { + draft.put(initialManifest); + } catch (IOException | RuntimeException e) { + abortAfterReservationFailure(draft, e); + throw new IllegalStateException("Cannot reserve game instance " + id, e); + } + + Path runDirectory = repository.hasInstance(id) + ? repository.getInstance(id).getRunDirectory() + : useInstanceRunDirectory + ? repository.getLayout().getInstanceRoot(id) + : repository.getBaseDirectory(); + Path modsDirectory = runDirectory.resolve("mods"); Task libraryTask = dependencyManager.installNewInstanceComponentAsync( - new GameInstanceManifest(id), gameVersion, GameComponentType.GAME, gameVersion); + initialManifest, modsDirectory, gameVersion, GameComponentType.GAME, gameVersion); for (Map.Entry entry : components.entrySet()) { GameComponentType componentType = entry.getKey(); @@ -98,11 +115,11 @@ public Task buildAsync() { if (entry.getValue() instanceof RemoteVersion remoteVersion) { libraryTask = libraryTask.thenComposeAsync(manifest -> dependencyManager.installNewInstanceComponentAsync( - manifest, remoteVersion)); + manifest, modsDirectory, remoteVersion)); } else if (entry.getValue() instanceof String version) { libraryTask = libraryTask.thenComposeAsync(manifest -> dependencyManager.installNewInstanceComponentAsync( - manifest, gameVersion, componentType, version)); + manifest, modsDirectory, gameVersion, componentType, version)); } else { throw new AssertionError("Unexpected version type: " + entry.getValue().getClass()); } @@ -125,4 +142,18 @@ public Task buildAsync() { .withStagesHints(hints); } + /// Aborts a draft whose initial instance reservation failed. + /// + /// @param draft the draft to abort + /// @param failure the reservation failure that receives any cleanup failure as suppressed + private static void abortAfterReservationFailure( + DefaultGameRepositoryDraft draft, + Throwable failure) { + try { + draft.abort(); + } catch (IOException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java index 6b35aeb964..47176fdc16 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java @@ -35,6 +35,9 @@ public abstract class GameBuilder { protected @Nullable GameInstanceID id; protected final EnumMap components = new EnumMap<>(GameComponentType.class); + /// Whether new-instance component installers write run-directory content under the instance root. + protected boolean useInstanceRunDirectory; + /// The new game instance id, for `.minecraft/`. /// /// @param id the instance id of new game instance. @@ -43,6 +46,19 @@ public GameBuilder id(GameInstanceID id) { return this; } + /// Uses the instance root as the run directory while installing a new instance. + /// + /// This affects files installed by components, such as loader-provided mods. It does not write + /// instance settings; callers must persist the corresponding isolation setting after the + /// instance is published. + /// + /// @return this builder + @Contract("-> this") + public GameBuilder useInstanceRunDirectory() { + useInstanceRunDirectory = true; + return this; + } + @Contract("_, _ -> this") public GameBuilder component(GameComponentType componentType, String version) { components.put(componentType, version); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java index 1e68ddd64d..750bc6a979 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java @@ -43,24 +43,20 @@ public class RemoteVersion implements Comparable { private final List urls; private final Type type; - /** - * Constructor. - * - * @param gameVersion the Minecraft version that this remote version suits. - * @param selfVersion the version string of the remote version. - * @param urls the installer or universal jar original URL. - */ + /// Constructor. + /// + /// @param gameVersion the Minecraft version that this remote version suits. + /// @param selfVersion the version string of the remote version. + /// @param urls the installer or universal jar original URL. public RemoteVersion(GameComponentType componentType, String gameVersion, String selfVersion, Instant releaseDate, List urls) { this(componentType, gameVersion, selfVersion, releaseDate, Type.UNCATEGORIZED, urls); } - /** - * Constructor. - * - * @param gameVersion the Minecraft version that this remote version suits. - * @param selfVersion the version string of the remote version. - * @param urls the installer or universal jar URL. - */ + /// Constructor. + /// + /// @param gameVersion the Minecraft version that this remote version suits. + /// @param selfVersion the version string of the remote version. + /// @param urls the installer or universal jar URL. public RemoteVersion(GameComponentType componentType, String gameVersion, String selfVersion, Instant releaseDate, Type type, List urls) { this.componentType = Objects.requireNonNull(componentType); this.gameVersion = Objects.requireNonNull(gameVersion); @@ -102,12 +98,12 @@ public Type getVersionType() { /// instance run tree (for example Fabric/Quilt API). /// /// @param dependencyManager the dependency manager - /// @param baseVersion the manifest being installed into + /// @param baseManifest the manifest being installed into /// @param modsDirectory the mods directory of the target instance run directory /// @return the install task public Task getInstallTask( DefaultDependencyManager dependencyManager, - GameInstanceManifest baseVersion, + GameInstanceManifest baseManifest, Path modsDirectory) { throw new UnsupportedOperationException(this + " cannot be installed yet"); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java index cb401108bb..cc68162985 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java @@ -34,7 +34,7 @@ public CleanroomRemoteVersion(String gameVersion, String selfVersion, Instant re } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new CleanroomInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new CleanroomInstallTask(dependencyManager, baseManifest, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java index 03b287370c..d83b147d42 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java @@ -59,9 +59,9 @@ public RemoteAddon.Version getVersion() { @Override public Task getInstallTask( DefaultDependencyManager dependencyManager, - GameInstanceManifest baseVersion, + GameInstanceManifest baseManifest, Path modsDirectory) { - return new FabricAPIInstallTask(dependencyManager, baseVersion, this, modsDirectory); + return new FabricAPIInstallTask(dependencyManager, baseManifest, this, modsDirectory); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java index bbd649d05c..4571c76067 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java @@ -40,7 +40,7 @@ public class FabricRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new FabricInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new FabricInstallTask(dependencyManager, baseManifest, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java index 36cc5fb5a2..edb5ad78d1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java @@ -41,7 +41,7 @@ public ForgeRemoteVersion(String gameVersion, String selfVersion, Instant releas } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new ForgeInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new ForgeInstallTask(dependencyManager, baseManifest, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java index e6dec17401..33ca26d88d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java @@ -50,8 +50,8 @@ public ReleaseType getType() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new GameInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new GameInstallTask(dependencyManager, baseManifest, this); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java index c93ed57817..9d17c8f531 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java @@ -59,9 +59,9 @@ public RemoteAddon.Version getVersion() { @Override public Task getInstallTask( DefaultDependencyManager dependencyManager, - GameInstanceManifest baseVersion, + GameInstanceManifest baseManifest, Path modsDirectory) { - return new LegacyFabricAPIInstallTask(dependencyManager, baseVersion, this, modsDirectory); + return new LegacyFabricAPIInstallTask(dependencyManager, baseManifest, this, modsDirectory); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java index ba8ec166b0..f73fc8bcbb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java @@ -40,7 +40,7 @@ public class LegacyFabricRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new LegacyFabricInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new LegacyFabricInstallTask(dependencyManager, baseManifest, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java index 7181ec8937..fd22f27ec2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java @@ -56,7 +56,7 @@ public String getTweakClass() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new LiteLoaderInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new LiteLoaderInstallTask(dependencyManager, baseManifest, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java index 7f6b0415bd..8b8f1a9873 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java @@ -33,8 +33,8 @@ public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new NeoForgeInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new NeoForgeInstallTask(dependencyManager, baseManifest, this); } private static Type getType(String version) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java index 0c2ef67cf7..049b20f2ac 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java @@ -40,11 +40,11 @@ public String getFullVersion() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new GameDownloadTask(dependencyManager, baseVersion) + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new GameDownloadTask(dependencyManager, baseManifest) .thenComposeAsync(minecraftJar -> new OptiFineInstallTask( dependencyManager, - baseVersion, + baseManifest, this, minecraftJar)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java index 96d46ef9a8..e561e2cbd8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java @@ -59,9 +59,9 @@ public RemoteAddon.Version getVersion() { @Override public Task getInstallTask( DefaultDependencyManager dependencyManager, - GameInstanceManifest baseVersion, + GameInstanceManifest baseManifest, Path modsDirectory) { - return new QuiltAPIInstallTask(dependencyManager, baseVersion, this, modsDirectory); + return new QuiltAPIInstallTask(dependencyManager, baseManifest, this, modsDirectory); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java index 1a624fd4da..f8e9c10cd0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java @@ -40,7 +40,7 @@ public class QuiltRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion, Path modsDirectory) { - return new QuiltInstallTask(dependencyManager, baseVersion, this); + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + return new QuiltInstallTask(dependencyManager, baseManifest, this); } } 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 d5c3423ddd..31c4e512d5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -116,31 +116,6 @@ public DefaultGameRepository(Path baseDirectory) { /// @return the layout used by this repository protected abstract DefaultGameRepositoryLayout createLayout(Path baseDirectory); - /// Returns whether a new draft may claim `instanceRoot` as draft-owned storage. - /// - /// The default implementation permits only a root that does not exist. Subclasses may recognize - /// an explicit pre-install reservation, but must not permit an unrelated pre-existing directory: - /// aborting the draft will recursively remove every claimed root. - /// - /// @param instanceId the instance being created - /// @param instanceRoot the normalized instance root - /// @return whether the draft may own and clean up the root - protected boolean mayClaimDraftInstanceRoot(GameInstanceID instanceId, Path instanceRoot) { - return Files.notExists(instanceRoot); - } - - /// Materializes subclass-specific data for a newly claimed draft instance root. - /// - /// This method is called during commit, after the draft has recorded ownership and created the - /// root, so failure cleanup will remove the root. The default implementation has no additional - /// data to materialize. - /// - /// @param instanceId the instance being created - /// @param instanceRoot the normalized instance root owned by the draft - /// @throws IOException if prepared data cannot be written - protected void initializeDraftInstanceRoot(GameInstanceID instanceId, Path instanceRoot) throws IOException { - } - /// Prepares subclass-managed writes before instance files are moved or removed. /// /// The default implementation does nothing. @@ -194,7 +169,6 @@ public ReadOnlyObjectProperty snapshotP protected void publishSnapshot(DefaultGameRepositorySnapshot newSnapshot) { newSnapshot.seal(); runOnFxThreadAndWait(() -> { - snapshot.set(newSnapshot); }); } @@ -574,17 +548,6 @@ public Path getInstanceJson(GameInstanceID instanceId) { return getLayout().getInstanceJson(instanceId); } - /// Returns the run directory to use while installing an instance before it is published. - /// - /// The default official-layout repository uses its shared base directory. Subclasses may derive - /// an isolated directory from repository-specific settings without creating a [GameInstance]. - /// - /// @param instanceId the instance being installed - /// @return the installation run directory - public Path getRunDirectoryForInstallation(GameInstanceID instanceId) { - return getBaseDirectory(); - } - /// Opens a draft for staging instance index changes and committing them once. /// /// @return a new open draft diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraft.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraft.java index 48ccdf7332..9ef4b22d6d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraft.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraft.java @@ -218,7 +218,7 @@ private void putManifest( Path root = baseSnapshot.getLayout().getInstanceRoot(id) .toAbsolutePath() .normalize(); - if (!repository.mayClaimDraftInstanceRoot(id, root)) { + if (!Files.notExists(root)) { throw new FileAlreadyExistsException(root.toString(), null, "An unregistered instance directory already exists"); } @@ -365,9 +365,9 @@ private DefaultGameRepositorySnapshot buildCommittedSnapshot() { return committedSnapshot; } - /// Creates and initializes roots reserved for instances added by this draft. + /// Creates roots reserved for instances added by this draft. /// - /// @throws IOException if a root or repository-specific initial data cannot be created + /// @throws IOException if a root cannot be created private void materializeCreatedInstanceRoots() throws IOException { for (GameInstanceID id : createdIds) { if (!manifests.containsKey(id)) { @@ -377,7 +377,6 @@ private void materializeCreatedInstanceRoots() throws IOException { .toAbsolutePath() .normalize(); Files.createDirectories(root); - repository.initializeDraftInstanceRoot(id, root); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java index 6d649e4f48..de224276ea 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java @@ -84,7 +84,10 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile if (repository.hasInstance(instanceId) && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder().id(instanceId).component(GameComponentType.GAME, manifest.minecraft().gameVersion()); + GameBuilder builder = dependencyManager.newGameBuilder() + .id(instanceId) + .useInstanceRunDirectory() + .component(GameComponentType.GAME, manifest.minecraft().gameVersion()); for (CurseManifestModLoader modLoader : manifest.minecraft().modLoaders()) { if (modLoader.id().startsWith("forge-")) { builder.component(GameComponentType.FORGE, modLoader.id().substring("forge-".length())); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index e4ca6207b8..190ef28ef3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -64,7 +64,9 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, throw new IllegalArgumentException("Instance " + instanceId + " already exists."); this.update = repository.hasInstance(instanceId); - GameBuilder builder = dependencyManager.newGameBuilder().id(instanceId); + GameBuilder builder = dependencyManager.newGameBuilder() + .id(instanceId) + .useInstanceRunDirectory(); for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java index ab579ff1b9..25bae62aeb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java @@ -69,7 +69,9 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF if (repository.hasInstance(instanceId) && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder().id(instanceId); + GameBuilder builder = dependencyManager.newGameBuilder() + .id(instanceId) + .useInstanceRunDirectory(); builder.component(GameComponentType.GAME, manifest.getGameVersion()); for (Map.Entry modLoader : manifest.getDependencies().entrySet()) { switch (modLoader.getKey()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java index b957a771a5..f6c9bceb22 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java @@ -50,6 +50,8 @@ import java.util.Map; import java.util.Objects; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + /** *

A task transforming MultiMC Modpack Scheme to Official Launcher Scheme. * The transforming process contains 7 stage: @@ -81,6 +83,16 @@ public final class MultiMCModpackInstallTask extends Task> dependencies = new ArrayList<>(); private final DefaultDependencyManager dependencyManager; + /// The repository transaction that owns a newly created instance root until publication. + private @Nullable DefaultGameRepositoryDraft draft; + + /// Creates a MultiMC modpack installation task. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the source modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the MultiMC instance configuration + /// @param instanceId the target instance ID public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) { this.zipFile = zipFile; this.modpack = modpack; @@ -94,6 +106,7 @@ public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, Pat throw new IllegalArgumentException("Instance " + instanceId + " already exists."); onDone().register(event -> { + abortOpenDraft(); if (event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); @@ -104,8 +117,23 @@ public boolean doPreExecute() { return true; } + /// Reserves the instance in a repository draft before preparing tasks that write its root. @Override public void preExecute() throws Exception { + DefaultGameRepositoryDraft openedDraft = repository.openDraft(); + draft = openedDraft; + try { + openedDraft.put(new GameInstanceManifest(instanceId)); + } catch (IOException | RuntimeException e) { + try { + openedDraft.abort(); + } catch (IOException cleanupFailure) { + e.addSuppressed(cleanupFailure); + } + draft = null; + throw e; + } + // Stage #0: General Setup { Path run = repository.getLayout().getInstanceRoot(instanceId); @@ -286,8 +314,8 @@ public void execute() throws Exception { // Stage #5: Assemble game files. { GameInstanceManifest instanceManifest = artifact.getManifest(); + requireDraft().put(instanceManifest); - dependencies.add(repository.saveAsync(artifact.getManifest())); dependencies.add(new GameAssetDownloadTask(dependencyManager, instanceManifest, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)); dependencies.add(new GameLibrariesTask( dependencyManager, @@ -315,29 +343,59 @@ public boolean doPostExecute() { return true; } + /// Applies JAR mods after downloads succeed and then publishes the completed manifest. @Override public void postExecute() throws Exception { MultiMCInstancePatch.ResolvedInstance artifact = Objects.requireNonNull(getResult(), "ResolvedInstance"); List files = artifact.getJarModFileNames(); - if (!isDependenciesSucceeded() || files.isEmpty()) { + if (!isDependenciesSucceeded()) { return; } - // Stage #7: Apply jar mods. - try (FileSystem fs = openModpack()) { - Path root = getRootPath(fs).resolve("jarmods"); - - try (FileSystem mc = CompressingUtils.writable( - repository.getLayout().getInstanceRoot(instanceId).resolve(instanceId + ".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("/")); + if (!files.isEmpty()) { + // Stage #7: Apply jar mods. + try (FileSystem fs = openModpack()) { + Path root = getRootPath(fs).resolve("jarmods"); + + try (FileSystem mc = CompressingUtils.writable( + repository.getLayout().getInstanceRoot(instanceId).resolve(instanceId + ".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("/")); + } } } } } + + requireDraft().commit(); + } + + /// Returns the open draft associated with this task. + /// + /// @return the open draft + /// @throws IllegalStateException if the task has not reserved a draft or already released it + private DefaultGameRepositoryDraft requireDraft() { + @Nullable DefaultGameRepositoryDraft currentDraft = draft; + if (currentDraft == null || !currentDraft.isOpen()) { + throw new IllegalStateException("MultiMC installation draft is not open"); + } + return currentDraft; + } + + /// Aborts the task's draft when execution ends before commit. + private void abortOpenDraft() { + @Nullable DefaultGameRepositoryDraft currentDraft = draft; + if (currentDraft == null || !currentDraft.isOpen()) { + return; + } + try { + currentDraft.abort(); + } catch (IOException e) { + LOG.warning("Failed to abort MultiMC installation draft for " + instanceId, e); + } } private FileSystem openModpack() throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java index 57c3361ebd..0cde3a752f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java @@ -60,7 +60,9 @@ public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, if (repository.hasInstance(instanceId) && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder().id(instanceId); + GameBuilder builder = dependencyManager.newGameBuilder() + .id(instanceId) + .useInstanceRunDirectory(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java index 1f8394dedd..46f1da507c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java @@ -54,7 +54,9 @@ public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager if (repository.hasInstance(instanceId) && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder().id(instanceId); + GameBuilder builder = dependencyManager.newGameBuilder() + .id(instanceId) + .useInstanceRunDirectory(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) From 16b07822ea4ba82e758393fc06e338b1b9c4faca Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 21:38:17 +0800 Subject: [PATCH 21/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20RemoteVersion=20?= =?UTF-8?q?=E7=B1=BB=E4=B8=BA=20ComponentRemoteVersion=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9B=B8=E5=85=B3=E5=BC=95=E7=94=A8=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E4=BB=A3=E7=A0=81=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/download/AdditionalInstallersPage.java | 6 ++-- .../hmcl/ui/download/DownloadPage.java | 6 ++-- .../hmcl/ui/download/InstallersPage.java | 6 ++-- .../UpdateInstallerWizardProvider.java | 8 ++--- .../hmcl/ui/download/VersionsPage.java | 36 +++++++++---------- .../org/jackhuang/hmcl/ui/main/MainPage.java | 2 +- .../org/jackhuang/hmcl/util/i18n/I18n.java | 4 +-- .../hmcl/util/i18n/translator/Translator.java | 4 +-- .../i18n/translator/Translator_en_Qabs.java | 4 +-- .../util/i18n/translator/Translator_lzh.java | 4 +-- ...rsion.java => ComponentRemoteVersion.java} | 28 +++++++-------- .../download/DefaultDependencyManager.java | 4 +-- .../hmcl/download/DefaultGameBuilder.java | 4 +-- .../jackhuang/hmcl/download/GameBuilder.java | 2 +- .../download/MultipleSourceVersionList.java | 2 +- .../jackhuang/hmcl/download/VersionList.java | 2 +- .../cleanroom/CleanroomRemoteVersion.java | 4 +-- .../fabric/FabricAPIRemoteVersion.java | 6 ++-- .../download/fabric/FabricRemoteVersion.java | 4 +-- .../download/forge/ForgeRemoteVersion.java | 4 +-- .../game/GameInstanceJsonDownloadTask.java | 4 +-- .../hmcl/download/game/GameRemoteVersion.java | 6 ++-- .../LegacyFabricAPIRemoteVersion.java | 6 ++-- .../LegacyFabricRemoteVersion.java | 4 +-- .../liteloader/LiteLoaderBMCLVersionList.java | 4 +-- .../liteloader/LiteLoaderRemoteVersion.java | 4 +-- .../liteloader/LiteLoaderVersionList.java | 6 ++-- .../neoforge/NeoForgeRemoteVersion.java | 4 +-- .../optifine/OptiFineRemoteVersion.java | 4 +-- .../download/quilt/QuiltAPIRemoteVersion.java | 6 ++-- .../download/quilt/QuiltRemoteVersion.java | 4 +-- .../org/jackhuang/hmcl/util/SettingsMap.java | 4 +-- .../jackhuang/hmcl/util/SettingsMapTest.java | 17 +++++++-- 33 files changed, 110 insertions(+), 103 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/download/{RemoteVersion.java => ComponentRemoteVersion.java} (81%) 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 cec5186f91..074151eeab 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 @@ -21,7 +21,7 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.ui.InstallerItem; import org.jackhuang.hmcl.ui.wizard.WizardController; @@ -72,8 +72,8 @@ public String getTitle() { private String getVersion(GameComponentType type) { return Optional.ofNullable(controller.getSettings().get(type.getPatchId())) - .flatMap(it -> Lang.tryCast(it, RemoteVersion.class)) - .map(RemoteVersion::getSelfVersion).orElse(null); + .flatMap(it -> Lang.tryCast(it, ComponentRemoteVersion.class)) + .map(ComponentRemoteVersion::getSelfVersion).orElse(null); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index defe1cd28b..b1835f148d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -305,11 +305,11 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID); builder.id(instanceId); - builder.component(GameComponentType.GAME, ((RemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion()); + builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion()); settings.asStringMap().forEach((key, value) -> { if (!GameComponentType.GAME.getPatchId().equals(key) - && value instanceof RemoteVersion remoteVersion) + && value instanceof ComponentRemoteVersion remoteVersion) builder.component(remoteVersion); }); @@ -335,7 +335,7 @@ public Object finish(SettingsMap settings) { public Node createPage(WizardController controller, int step, SettingsMap settings) { switch (step) { case 0: - return new InstallersPage(controller, repository, ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider); + return new InstallersPage(controller, repository, ((ComponentRemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider); default: throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java index 2dcd020127..eff5dfcdfa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.ui.download; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -52,11 +52,11 @@ public InstallersPage(WizardController controller, HMCLGameRepository repository @Override public String getTitle() { - return ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(); + return ((ComponentRemoteVersion) controller.getSettings().get("game")).getGameVersion(); } private String getVersion(String id) { - return I18n.getDisplayVersion((RemoteVersion) controller.getSettings().get(id)); + return I18n.getDisplayVersion((ComponentRemoteVersion) controller.getSettings().get(id)); } protected void reload() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index 9916cc6609..73033ac7b4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java @@ -73,7 +73,7 @@ public Object finish(SettingsMap settings) { var hints = new ArrayList(); for (Object value : settings.asStringMap().values()) { - if (value instanceof RemoteVersion remoteVersion) { + if (value instanceof ComponentRemoteVersion remoteVersion) { hints.add(new Task.StagesHint(String.format("hmcl.install.%s:%s", remoteVersion.getComponentType().getPatchId(), remoteVersion.getSelfVersion()))); if (remoteVersion.getComponentType() == GameComponentType.GAME) { hints.add(new Task.StagesHint("hmcl.install.libraries")); @@ -85,7 +85,7 @@ public Object finish(SettingsMap settings) { return gameInstance.getRepository().updateInstanceAsync(gameInstance.getId(), publishedInstance -> { Task update = Task.supplyAsync(publishedInstance::getManifest); for (Object value : settings.asStringMap().values()) { - if (value instanceof RemoteVersion remoteVersion) { + if (value instanceof ComponentRemoteVersion remoteVersion) { update = update.thenComposeAsync(manifest -> dependencyManager.installComponentRemoteAsync(publishedInstance, manifest, remoteVersion)); } else if (value instanceof RemoveComponentAction removeComponentAction) { @@ -106,10 +106,10 @@ public Node createPage(WizardController controller, int step, SettingsMap settin if (oldLibraryVersion == null) { controller.onFinish(); } else if (componentType == GameComponentType.GAME) { - String newGameVersion = ((RemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion(); + String newGameVersion = ((ComponentRemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion(); controller.onNext(new AdditionalInstallersPage(gameInstance, newGameVersion, controller, downloadProvider)); } else { - Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + componentType), oldLibraryVersion, ((RemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion()), + Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + componentType), oldLibraryVersion, ((ComponentRemoteVersion) settings.get(componentType.getPatchId())).getSelfVersion()), i18n("install.change_version"), controller::onFinish, controller::onCancel); } }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 82218e8dac..ca3d661411 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -31,7 +31,7 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.*; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.VersionList; import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion; import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion; @@ -85,7 +85,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab private final VersionList versionList; private final Runnable callback; - private final ObservableList versions = FXCollections.observableArrayList(); + private final ObservableList versions = FXCollections.observableArrayList(); private final ObjectProperty status = new SimpleObjectProperty<>(Status.LOADING); public VersionsPage(Navigation navigation, @@ -153,7 +153,7 @@ private enum VersionTypeFilter { OLD } - private static class RemoteVersionListCell extends ListCell { + private static class RemoteVersionListCell extends ListCell { private final VersionsPage control; private final TwoLineListItem twoLineListItem = new TwoLineListItem(); @@ -196,7 +196,7 @@ private static class RemoteVersionListCell extends ListCell { } private void onAction() { - RemoteVersion item = getItem(); + ComponentRemoteVersion item = getItem(); if (item == null) return; @@ -205,7 +205,7 @@ private void onAction() { } private void onOpenWiki() { - RemoteVersion item = getItem(); + ComponentRemoteVersion item = getItem(); if (!(item instanceof GameRemoteVersion)) return; @@ -213,8 +213,8 @@ private void onOpenWiki() { } @Override - public void updateItem(RemoteVersion remoteVersion, boolean empty) { - RemoteVersion oldRemoteVersion = getItem(); + public void updateItem(ComponentRemoteVersion remoteVersion, boolean empty) { + ComponentRemoteVersion oldRemoteVersion = getItem(); ripplerContainer.releaseRippleImmediately(); super.updateItem(remoteVersion, empty); @@ -237,7 +237,7 @@ public void updateItem(RemoteVersion remoteVersion, boolean empty) { twoLineListItem.getTags().clear(); if (remoteVersion instanceof GameRemoteVersion) { - RemoteVersion.Type versionType = remoteVersion.getVersionType(); + ComponentRemoteVersion.Type versionType = remoteVersion.getVersionType(); GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()); switch (versionType) { @@ -246,7 +246,7 @@ public void updateItem(RemoteVersion remoteVersion, boolean empty) { imageView.setImage(GameInstanceIconType.GRASS.getIcon()); } case SNAPSHOT, PENDING, UNOBFUSCATED -> { - if (versionType == RemoteVersion.Type.SNAPSHOT + if (versionType == ComponentRemoteVersion.Type.SNAPSHOT && GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) { twoLineListItem.addTag(i18n("instance.game.april_fools")); imageView.setImage(GameInstanceIconType.APRIL_FOOLS.getIcon()); @@ -298,7 +298,7 @@ else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof } private static final class VersionsPageSkin extends SkinBase { - private final JFXListView list; + private final JFXListView list; private final TransitionPane transitionPane; private final JFXSpinner spinner; @@ -456,20 +456,20 @@ else if (status == Status.SUCCESS) } private void updateList() { - Stream versions = getSkinnable().versions.stream(); + Stream versions = getSkinnable().versions.stream(); VersionTypeFilter filter = categoryField.getSelectionModel().getSelectedItem(); if (filter != null) versions = versions.filter(it -> { - RemoteVersion.Type versionType = it.getVersionType(); + ComponentRemoteVersion.Type versionType = it.getVersionType(); return switch (filter) { - case RELEASE -> versionType == RemoteVersion.Type.RELEASE; - case SNAPSHOTS -> versionType == RemoteVersion.Type.SNAPSHOT - || versionType == RemoteVersion.Type.PENDING - || versionType == RemoteVersion.Type.UNOBFUSCATED; - case APRIL_FOOLS -> versionType == RemoteVersion.Type.SNAPSHOT + case RELEASE -> versionType == ComponentRemoteVersion.Type.RELEASE; + case SNAPSHOTS -> versionType == ComponentRemoteVersion.Type.SNAPSHOT + || versionType == ComponentRemoteVersion.Type.PENDING + || versionType == ComponentRemoteVersion.Type.UNOBFUSCATED; + case APRIL_FOOLS -> versionType == ComponentRemoteVersion.Type.SNAPSHOT && GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools(); - case OLD -> versionType == RemoteVersion.Type.OLD; + case OLD -> versionType == ComponentRemoteVersion.Type.OLD; // case ALL, default -> true; }; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index b7e08bb4ac..3474634797 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -81,7 +81,7 @@ import java.util.concurrent.CancellationException; import java.util.function.Consumer; -import static org.jackhuang.hmcl.download.RemoteVersion.Type.RELEASE; +import static org.jackhuang.hmcl.download.ComponentRemoteVersion.Type.RELEASE; import static org.jackhuang.hmcl.setting.SettingsManager.state; import static org.jackhuang.hmcl.ui.FXUtils.SINE; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java index 286910be07..f7ebd93a02 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/I18n.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.util.i18n; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.game.GameRemoteVersion; import org.jackhuang.hmcl.util.i18n.translator.Translator; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -77,7 +77,7 @@ public static String formatSpeed(long bytes) { return getTranslator().formatSpeed(bytes); } - public static String getDisplayVersion(RemoteVersion version) { + public static String getDisplayVersion(ComponentRemoteVersion version) { return getTranslator().getDisplayVersion(version); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java index 7bdcfc8038..223a238f15 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.util.i18n.translator; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.util.i18n.SupportedLocale; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -44,7 +44,7 @@ public final Locale getDisplayLocale() { return displayLocale; } - public String getDisplayVersion(RemoteVersion remoteVersion) { + public String getDisplayVersion(ComponentRemoteVersion remoteVersion) { return remoteVersion.getSelfVersion(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_en_Qabs.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_en_Qabs.java index f593daf5f8..ec140cb867 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_en_Qabs.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_en_Qabs.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.util.i18n.translator; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.util.i18n.SupportedLocale; import java.io.IOException; @@ -73,7 +73,7 @@ public Translator_en_Qabs(SupportedLocale locale) { } @Override - public String getDisplayVersion(RemoteVersion remoteVersion) { + public String getDisplayVersion(ComponentRemoteVersion remoteVersion) { return translate(remoteVersion.getSelfVersion()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java index 25f1966e14..dc561dff0a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/translator/Translator_lzh.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.util.i18n.translator; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.game.GameRemoteVersion; import org.jackhuang.hmcl.util.i18n.SupportedLocale; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -221,7 +221,7 @@ public Translator_lzh(SupportedLocale locale) { } @Override - public String getDisplayVersion(RemoteVersion remoteVersion) { + public String getDisplayVersion(ComponentRemoteVersion remoteVersion) { if (remoteVersion instanceof GameRemoteVersion) return translateGameVersion(GameVersionNumber.asGameVersion(remoteVersion.getSelfVersion())); else diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ComponentRemoteVersion.java similarity index 81% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/ComponentRemoteVersion.java index 750bc6a979..5d9e0769ce 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ComponentRemoteVersion.java @@ -29,12 +29,10 @@ import java.util.List; import java.util.Objects; -/** - * The remote version. - * - * @author huangyuhui - */ -public class RemoteVersion implements Comparable { +/// The remote version. +/// +/// @author huangyuhui +public abstract class ComponentRemoteVersion implements Comparable { private final GameComponentType componentType; private final String gameVersion; @@ -48,7 +46,7 @@ public class RemoteVersion implements Comparable { /// @param gameVersion the Minecraft version that this remote version suits. /// @param selfVersion the version string of the remote version. /// @param urls the installer or universal jar original URL. - public RemoteVersion(GameComponentType componentType, String gameVersion, String selfVersion, Instant releaseDate, List urls) { + public ComponentRemoteVersion(GameComponentType componentType, String gameVersion, String selfVersion, Instant releaseDate, List urls) { this(componentType, gameVersion, selfVersion, releaseDate, Type.UNCATEGORIZED, urls); } @@ -57,7 +55,7 @@ public RemoteVersion(GameComponentType componentType, String gameVersion, String /// @param gameVersion the Minecraft version that this remote version suits. /// @param selfVersion the version string of the remote version. /// @param urls the installer or universal jar URL. - public RemoteVersion(GameComponentType componentType, String gameVersion, String selfVersion, Instant releaseDate, Type type, List urls) { + public ComponentRemoteVersion(GameComponentType componentType, String gameVersion, String selfVersion, Instant releaseDate, Type type, List urls) { this.componentType = Objects.requireNonNull(componentType); this.gameVersion = Objects.requireNonNull(gameVersion); this.selfVersion = Objects.requireNonNull(selfVersion); @@ -94,23 +92,21 @@ public Type getVersionType() { return type; } - /// Creates an install task with an explicit mods directory for libraries that download into the + /// Creates an installation task with an explicit mods directory for libraries that download into the /// instance run tree (for example Fabric/Quilt API). /// /// @param dependencyManager the dependency manager /// @param baseManifest the manifest being installed into /// @param modsDirectory the mods directory of the target instance run directory - /// @return the install task - public Task getInstallTask( + /// @return the installation task + public abstract Task getInstallTask( DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, - Path modsDirectory) { - throw new UnsupportedOperationException(this + " cannot be installed yet"); - } + Path modsDirectory); @Override public boolean equals(Object obj) { - return obj instanceof RemoteVersion && Objects.equals(selfVersion, ((RemoteVersion) obj).selfVersion); + return obj instanceof ComponentRemoteVersion && Objects.equals(selfVersion, ((ComponentRemoteVersion) obj).selfVersion); } @Override @@ -127,7 +123,7 @@ public String toString() { } @Override - public int compareTo(RemoteVersion o) { + public int compareTo(ComponentRemoteVersion o) { // newer versions are smaller than older versions return VersionNumber.asVersion(o.selfVersion).compareTo(VersionNumber.asVersion(selfVersion)); } 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 6239ddcf09..829f8387c4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -188,7 +188,7 @@ public Task checkPatchCompletionAsync( Task installNewInstanceComponentAsync( GameInstanceManifest baseManifest, Path modsDirectory, - RemoteVersion componentVersion) { + ComponentRemoteVersion componentVersion) { return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) @@ -236,7 +236,7 @@ Task installNewInstanceComponentAsync( public Task installComponentRemoteAsync( GameInstance instance, GameInstanceManifest baseManifest, - RemoteVersion componentVersion) { + ComponentRemoteVersion componentVersion) { validateGameInstance(instance); if (!instance.getId().equals(baseManifest.id())) { throw new IllegalArgumentException("baseManifest id does not match instance"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index a9c2e5fd31..efad533138 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -75,7 +75,7 @@ public Task buildAsync() { hints.add(new Task.StagesHint( "hmcl.install.%s:%s".formatted( componentType.getPatchId(), - version instanceof RemoteVersion remoteVersion + version instanceof ComponentRemoteVersion remoteVersion ? remoteVersion.getSelfVersion() : (String) version))); @@ -112,7 +112,7 @@ public Task buildAsync() { if (componentType == GameComponentType.GAME) continue; - if (entry.getValue() instanceof RemoteVersion remoteVersion) { + if (entry.getValue() instanceof ComponentRemoteVersion remoteVersion) { libraryTask = libraryTask.thenComposeAsync(manifest -> dependencyManager.installNewInstanceComponentAsync( manifest, modsDirectory, remoteVersion)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java index 47176fdc16..ed98ec703e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java @@ -66,7 +66,7 @@ public GameBuilder component(GameComponentType componentType, String version) { } @Contract("_ -> this") - public GameBuilder component(RemoteVersion remoteVersion) { + public GameBuilder component(ComponentRemoteVersion remoteVersion) { components.put(remoteVersion.getComponentType(), remoteVersion); return this; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java index 89e3eb5dec..081b57f1e2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java @@ -25,7 +25,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class MultipleSourceVersionList extends VersionList { +public class MultipleSourceVersionList extends VersionList { private final VersionList[] backends; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java index 0ac125384c..14d5ffdf5e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java @@ -29,7 +29,7 @@ * @param The subclass of {@code RemoteVersion}, the type of RemoteVersion. * @author huangyuhui */ -public abstract class VersionList { +public abstract class VersionList { /** * the remote version list. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java index cc68162985..04c0b12ed9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.cleanroom; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -28,7 +28,7 @@ import java.time.Instant; import java.util.List; -public class CleanroomRemoteVersion extends RemoteVersion { +public class CleanroomRemoteVersion extends ComponentRemoteVersion { public CleanroomRemoteVersion(String gameVersion, String selfVersion, Instant releaseDate, List url) { super(GameComponentType.CLEANROOM, gameVersion, selfVersion, releaseDate, url); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java index d83b147d42..2d923f0130 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.fabric; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -29,7 +29,7 @@ import java.time.Instant; import java.util.List; -public class FabricAPIRemoteVersion extends RemoteVersion { +public class FabricAPIRemoteVersion extends ComponentRemoteVersion { private final String fullVersion; private final RemoteAddon.Version version; @@ -65,7 +65,7 @@ public Task getInstallTask( } @Override - public int compareTo(RemoteVersion o) { + public int compareTo(ComponentRemoteVersion o) { if (!(o instanceof FabricAPIRemoteVersion)) return 0; return -this.getReleaseDate().compareTo(o.getReleaseDate()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java index 4571c76067..9b1bfd896b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.fabric; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -27,7 +27,7 @@ import java.nio.file.Path; import java.util.List; -public class FabricRemoteVersion extends RemoteVersion { +public class FabricRemoteVersion extends ComponentRemoteVersion { /** * Constructor. * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java index edb5ad78d1..14f3514075 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.forge; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -28,7 +28,7 @@ import java.time.Instant; import java.util.List; -public class ForgeRemoteVersion extends RemoteVersion { +public class ForgeRemoteVersion extends ComponentRemoteVersion { /** * Constructor. * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java index d3c37a8d66..8cd2e0a8ba 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.game; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.VersionList; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.task.GetTask; @@ -62,7 +62,7 @@ public Collection> getDependents() { @Override public void execute() throws IOException { - RemoteVersion remoteVersion = gameVersionList.getVersion(gameVersion, gameVersion) + ComponentRemoteVersion remoteVersion = gameVersionList.getVersion(gameVersion, gameVersion) .orElseThrow(() -> new IOException("Cannot find specific version " + gameVersion + " in remote repository")); dependencies.add(new GetTask(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls())).storeTo(this::setResult)); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java index 33ca26d88d..aad2e072ea 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.game; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -36,7 +36,7 @@ * @author huangyuhui */ @Immutable -public final class GameRemoteVersion extends RemoteVersion { +public final class GameRemoteVersion extends ComponentRemoteVersion { private final ReleaseType type; @@ -55,7 +55,7 @@ public Task getInstallTask(DefaultDependencyManager dependenc } @Override - public int compareTo(RemoteVersion o) { + public int compareTo(ComponentRemoteVersion o) { if (!(o instanceof GameRemoteVersion)) { return 0; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java index 9d17c8f531..3d11662447 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.legacyfabric; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -29,7 +29,7 @@ import java.time.Instant; import java.util.List; -public class LegacyFabricAPIRemoteVersion extends RemoteVersion { +public class LegacyFabricAPIRemoteVersion extends ComponentRemoteVersion { private final String fullVersion; private final RemoteAddon.Version version; @@ -65,7 +65,7 @@ public Task getInstallTask( } @Override - public int compareTo(RemoteVersion o) { + public int compareTo(ComponentRemoteVersion o) { if (!(o instanceof LegacyFabricAPIRemoteVersion)) return 0; return -this.getReleaseDate().compareTo(o.getReleaseDate()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java index f73fc8bcbb..ae7aabc176 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.legacyfabric; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -27,7 +27,7 @@ import java.nio.file.Path; import java.util.List; -public class LegacyFabricRemoteVersion extends RemoteVersion { +public class LegacyFabricRemoteVersion extends ComponentRemoteVersion { /** * Constructor. * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java index c80aa47a95..ea4708fad4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.liteloader; import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.VersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; @@ -73,7 +73,7 @@ public Task refreshAsync(String gameVersion) { if (v == null) return; versions.put(gameVersion, new LiteLoaderRemoteVersion( - gameVersion, v.version, RemoteVersion.Type.UNCATEGORIZED, + gameVersion, v.version, ComponentRemoteVersion.Type.UNCATEGORIZED, Collections.singletonList(NetworkUtils.withQuery( downloadProvider.getApiRoot() + "/liteloader/download", Collections.singletonMap("version", v.version) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java index fd22f27ec2..b7f624b074 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.liteloader; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -29,7 +29,7 @@ import java.util.Collection; import java.util.List; -public class LiteLoaderRemoteVersion extends RemoteVersion { +public class LiteLoaderRemoteVersion extends ComponentRemoteVersion { private final String tweakClass; private final Collection libraries; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java index dffb8cd2eb..31453a9564 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.liteloader; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.VersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; @@ -99,7 +99,7 @@ private void loadArtifactVersion(String gameVersion, LiteLoaderRepository reposi continue; versions.put(gameVersion, new LiteLoaderRemoteVersion( - gameVersion, v.getVersion(), RemoteVersion.Type.RELEASE, + gameVersion, v.getVersion(), ComponentRemoteVersion.Type.RELEASE, Collections.singletonList(repository.getUrl() + "com/mumfrey/liteloader/" + gameVersion + "/" + v.getFile()), v.getTweakClass(), v.getLibraries() )); @@ -117,7 +117,7 @@ private LiteLoaderRemoteVersion loadSnapshotVersion(String gameVersion, LiteLoad String buildNumber = Objects.requireNonNull(document.select("buildNumber"), "buildNumber").text(); return new LiteLoaderRemoteVersion( - gameVersion, timestamp + "-" + buildNumber, RemoteVersion.Type.SNAPSHOT, + gameVersion, timestamp + "-" + buildNumber, ComponentRemoteVersion.Type.SNAPSHOT, Collections.singletonList(String.format(SNAPSHOT_FILE, gameVersion, gameVersion, timestamp, buildNumber)), v.getTweakClass(), v.getLibraries() ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java index 8b8f1a9873..f30d9b18a6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.neoforge; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -27,7 +27,7 @@ import java.nio.file.Path; import java.util.List; -public class NeoForgeRemoteVersion extends RemoteVersion { +public class NeoForgeRemoteVersion extends ComponentRemoteVersion { public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List urls) { super(GameComponentType.NEO_FORGE, gameVersion, selfVersion, null, getType(selfVersion), urls); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java index 049b20f2ac..7cec29d4f0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.optifine; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; @@ -28,7 +28,7 @@ import java.nio.file.Path; import java.util.List; -public class OptiFineRemoteVersion extends RemoteVersion { +public class OptiFineRemoteVersion extends ComponentRemoteVersion { public OptiFineRemoteVersion(String gameVersion, String selfVersion, List urls, boolean snapshot) { super(GameComponentType.OPTIFINE, gameVersion, selfVersion, null, snapshot ? Type.SNAPSHOT : Type.RELEASE, urls); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java index e561e2cbd8..cf238420c5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.quilt; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -29,7 +29,7 @@ import java.time.Instant; import java.util.List; -public class QuiltAPIRemoteVersion extends RemoteVersion { +public class QuiltAPIRemoteVersion extends ComponentRemoteVersion { private final String fullVersion; private final RemoteAddon.Version version; @@ -65,7 +65,7 @@ public Task getInstallTask( } @Override - public int compareTo(RemoteVersion o) { + public int compareTo(ComponentRemoteVersion o) { if (!(o instanceof QuiltAPIRemoteVersion)) return 0; return -this.getReleaseDate().compareTo(o.getReleaseDate()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java index f8e9c10cd0..2373730bc5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.quilt; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameInstancePatch; @@ -27,7 +27,7 @@ import java.nio.file.Path; import java.util.List; -public class QuiltRemoteVersion extends RemoteVersion { +public class QuiltRemoteVersion extends ComponentRemoteVersion { /** * Constructor. * diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java index cc66267d1b..e2e4ae23cf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.util; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -92,7 +92,7 @@ public void clear() { /// Returns whether the selected installation includes any non-vanilla component. public boolean isInstallingModdedVersion() { for (GameComponentType value : GameComponentType.MOD_LOADERS) { - if (get(value.getPatchId()) instanceof RemoteVersion) { + if (get(value.getPatchId()) instanceof ComponentRemoteVersion) { return true; } } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java index c04c1adebe..11f0193f36 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java @@ -17,11 +17,16 @@ */ package org.jackhuang.hmcl.util; -import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.ComponentRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; +import org.jackhuang.hmcl.task.Task; import org.jetbrains.annotations.NotNullByDefault; import org.junit.jupiter.api.Test; +import java.nio.file.Path; import java.time.Instant; import java.util.List; @@ -51,7 +56,13 @@ public void modLoaderSelectionIsModdedInstallation() { } /// Creates a minimal remote version for installer state tests. - private static RemoteVersion remoteVersion(GameComponentType componentType) { - return new RemoteVersion(componentType, "1.21.11", "test", Instant.EPOCH, List.of()); + private static ComponentRemoteVersion remoteVersion(GameComponentType componentType) { + return new ComponentRemoteVersion(componentType, "1.21.11", "test", Instant.EPOCH, List.of()) { + + @Override + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { + throw new UnsupportedOperationException(); + } + }; } } From df16fa62533fefc93d143254dbe4718eaf556d13 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 21:39:27 +0800 Subject: [PATCH 22/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20VersionList=20?= =?UTF-8?q?=E7=B1=BB=E4=B8=BA=20ComponentVersionList=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=9B=B8=E5=85=B3=E5=BC=95=E7=94=A8=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E4=BB=A3=E7=A0=81=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/download/VersionsPage.java | 4 +-- .../org/jackhuang/hmcl/ui/main/MainPage.java | 4 +-- .../download/AbstractDependencyManager.java | 2 +- .../hmcl/download/AutoDownloadProvider.java | 6 ++-- .../download/BMCLAPIDownloadProvider.java | 2 +- ...ionList.java => ComponentVersionList.java} | 28 +++++++++---------- .../download/DefaultDependencyManager.java | 4 +-- .../hmcl/download/DependencyManager.java | 2 +- .../hmcl/download/DownloadProvider.java | 2 +- .../download/DownloadProviderWrapper.java | 4 +-- .../hmcl/download/MojangDownloadProvider.java | 2 +- .../download/MultipleSourceVersionList.java | 8 +++--- .../cleanroom/CleanroomVersionList.java | 4 +-- .../download/fabric/FabricAPIVersionList.java | 4 +-- .../download/fabric/FabricVersionList.java | 4 +-- .../download/forge/ForgeBMCLVersionList.java | 4 +-- .../hmcl/download/forge/ForgeVersionList.java | 4 +-- .../game/GameInstanceJsonDownloadTask.java | 4 +-- .../hmcl/download/game/GameVersionList.java | 4 +-- .../LegacyFabricAPIVersionList.java | 4 +-- .../legacyfabric/LegacyFabricVersionList.java | 4 +-- .../liteloader/LiteLoaderBMCLVersionList.java | 4 +-- .../liteloader/LiteLoaderVersionList.java | 4 +-- .../neoforge/NeoForgeBMCLVersionList.java | 4 +-- .../neoforge/NeoForgeOfficialVersionList.java | 4 +-- .../optifine/OptiFineBMCLVersionList.java | 4 +-- .../download/quilt/QuiltAPIVersionList.java | 4 +-- .../hmcl/download/quilt/QuiltVersionList.java | 4 +-- 28 files changed, 65 insertions(+), 67 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/download/{VersionList.java => ComponentVersionList.java} (84%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index ca3d661411..62e2c49a94 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -32,7 +32,7 @@ import javafx.scene.layout.*; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.ComponentRemoteVersion; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion; import org.jackhuang.hmcl.download.fabric.FabricAPIRemoteVersion; import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion; @@ -82,7 +82,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab private final String title; private final Navigation navigation; private final DownloadProvider downloadProvider; - private final VersionList versionList; + private final ComponentVersionList versionList; private final Runnable callback; private final ObservableList versions = FXCollections.observableArrayList(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 3474634797..4d21db4a30 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -46,7 +46,7 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -361,7 +361,7 @@ private void launch() { private void launchNoGame() { DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider(); - VersionList versionList = downloadProvider.getVersionList(GameComponentType.GAME); + ComponentVersionList versionList = downloadProvider.getVersionList(GameComponentType.GAME); Holder instanceHolder = new Holder<>(); Task task = versionList.refreshAsync("") diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AbstractDependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AbstractDependencyManager.java index beec622e2b..0d327b1194 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AbstractDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AbstractDependencyManager.java @@ -31,7 +31,7 @@ public abstract class AbstractDependencyManager implements DependencyManager { public abstract DefaultCacheRepository getCacheRepository(); @Override - public VersionList getVersionList(GameComponentType componentType) { + public ComponentVersionList getVersionList(GameComponentType componentType) { return getDownloadProvider().getVersionList(componentType); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java index 45a4ae2a1b..a2c11e01c7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java @@ -30,7 +30,7 @@ public final class AutoDownloadProvider implements DownloadProvider { private final List versionListProviders; private final List fileProviders; - private final ConcurrentMap> versionLists = new ConcurrentHashMap<>(); + private final ConcurrentMap> versionLists = new ConcurrentHashMap<>(); public AutoDownloadProvider( List versionListProviders, @@ -96,9 +96,9 @@ public List injectURLsWithCandidates(List urls) { } @Override - public VersionList getVersionList(GameComponentType componentType) { + public ComponentVersionList getVersionList(GameComponentType componentType) { return versionLists.computeIfAbsent(componentType, value -> { - VersionList[] lists = new VersionList[versionListProviders.size()]; + ComponentVersionList[] lists = new ComponentVersionList[versionListProviders.size()]; for (int i = 0; i < versionListProviders.size(); i++) { lists[i] = versionListProviders.get(i).getVersionList(value); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java index 8db94c8d1b..b301f6f196 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java @@ -121,7 +121,7 @@ public List getAssetObjectCandidates(String assetObjectLocation) { } @Override - public VersionList getVersionList(GameComponentType componentType) { + public ComponentVersionList getVersionList(GameComponentType componentType) { return switch (componentType) { case GAME -> game; case FABRIC -> fabric; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ComponentVersionList.java similarity index 84% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/ComponentVersionList.java index 14d5ffdf5e..6e900c07eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/ComponentVersionList.java @@ -23,20 +23,18 @@ import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; -/** - * The remote version list. - * - * @param The subclass of {@code RemoteVersion}, the type of RemoteVersion. - * @author huangyuhui - */ -public abstract class VersionList { +/// The remote version list. +/// +/// @param the type of ComponentRemoteVersion. +/// @author huangyuhui +public abstract class ComponentVersionList { /** * the remote version list. * key: game version. * values: corresponding remote versions. */ - protected final SimpleMultimap> versions = new SimpleMultimap<>(HashMap::new, TreeSet::new); + protected final SimpleMultimap> versions = new SimpleMultimap<>(HashMap::new, TreeSet::new); /** * True if the version list has been loaded. @@ -82,7 +80,7 @@ public Task loadAsync(String gameVersion) { }); } - protected Collection getVersionsImpl(String gameVersion) { + protected Collection getVersionsImpl(String gameVersion) { return versions.get(gameVersion); } @@ -92,7 +90,7 @@ protected Collection getVersionsImpl(String gameVersion) { * @param gameVersion the Minecraft version that remote versions belong to * @return the collection of specific remote versions */ - public final Collection getVersions(String gameVersion) { + public final Collection getVersions(String gameVersion) { lock.readLock().lock(); try { return Collections.unmodifiableCollection(new ArrayList<>(getVersionsImpl(gameVersion))); @@ -108,16 +106,16 @@ public final Collection getVersions(String gameVersion) { * @param remoteVersion the version of the remote version. * @return the specific remote version, null if it is not found. */ - public Optional getVersion(String gameVersion, String remoteVersion) { + public Optional getVersion(String gameVersion, String remoteVersion) { lock.readLock().lock(); try { - T result = null; - TreeSet remoteVersions = versions.get(gameVersion); - for (T it : remoteVersions) + V result = null; + TreeSet remoteVersions = versions.get(gameVersion); + for (V it : remoteVersions) if (remoteVersion.equals(it.getSelfVersion())) result = it; if (result == null) - for (T it : remoteVersions) + for (V it : remoteVersions) if (remoteVersion.equals(it.getFullVersion())) result = it; return Optional.ofNullable(result); 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 829f8387c4..7b1a2ad85c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -212,7 +212,7 @@ Task installNewInstanceComponentAsync( String gameVersion, GameComponentType componentType, String componentVersion) { - VersionList versionList = getVersionList(componentType); + ComponentVersionList versionList = getVersionList(componentType); return versionList.loadAsync(gameVersion) .thenComposeAsync(() -> installNewInstanceComponentAsync( baseManifest, @@ -274,7 +274,7 @@ public Task installComponentRemoteAsync( throw new IllegalArgumentException("baseManifest id does not match instance"); } - VersionList versionList = getVersionList(componentType); + ComponentVersionList versionList = getVersionList(componentType); return versionList.loadAsync(gameVersion) .thenComposeAsync(() -> installComponentRemoteAsync( instance, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java index c2a4890d28..aff65644aa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java @@ -80,5 +80,5 @@ public interface DependencyManager { /// @param componentType the component type, such as `game`, `forge`, or `optifine` /// @return the registered version list /// @throws IllegalArgumentException if no list is registered for `id` - VersionList getVersionList(GameComponentType componentType); + ComponentVersionList getVersionList(GameComponentType componentType); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java index e29a30f4ae..10a1347d89 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProvider.java @@ -66,7 +66,7 @@ default List injectURLsWithCandidates(List urls) { /// @param componentType the component type of specific version list that this download provider provides. i.e. "fabric", "forge", "liteloader", "game", "optifine" /// @return the version list /// @throws IllegalArgumentException if the version list does not exist - VersionList getVersionList(GameComponentType componentType); + ComponentVersionList getVersionList(GameComponentType componentType); /// The maximum download concurrency that this download provider supports. /// diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProviderWrapper.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProviderWrapper.java index 45a74f4736..5570dbb1ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProviderWrapper.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DownloadProviderWrapper.java @@ -69,8 +69,8 @@ public List injectURLsWithCandidates(List urls) { } @Override - public VersionList getVersionList(GameComponentType componentType) { - return new VersionList<>() { + public ComponentVersionList getVersionList(GameComponentType componentType) { + return new ComponentVersionList<>() { @Override public boolean hasType() { return getProvider().getVersionList(componentType).hasType(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java index a2ecd9955a..5e2ec3cac9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java @@ -82,7 +82,7 @@ public List getAssetObjectCandidates(String assetObjectLocation) { } @Override - public VersionList getVersionList(GameComponentType componentType) { + public ComponentVersionList getVersionList(GameComponentType componentType) { return switch (componentType) { case GAME -> game; case FABRIC -> fabric; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java index 081b57f1e2..03864dc3fa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java @@ -25,11 +25,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class MultipleSourceVersionList extends VersionList { +public class MultipleSourceVersionList extends ComponentVersionList { - private final VersionList[] backends; + private final ComponentVersionList[] backends; - MultipleSourceVersionList(VersionList[] backends) { + MultipleSourceVersionList(ComponentVersionList[] backends) { this.backends = backends; assert (backends.length >= 1); @@ -48,7 +48,7 @@ public Task refreshAsync() { } private Task refreshAsync(String gameVersion, int sourceIndex) { - VersionList versionList = backends[sourceIndex]; + ComponentVersionList versionList = backends[sourceIndex]; Task refreshTask = versionList.refreshAsync(gameVersion); return new Task<>() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomVersionList.java index 6495a130bc..62ec8848b8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomVersionList.java @@ -18,14 +18,14 @@ package org.jackhuang.hmcl.download.cleanroom; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import java.time.Instant; import java.util.Collections; -public final class CleanroomVersionList extends VersionList { +public final class CleanroomVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; private static final String LOADER_LIST_URL = "https://hmcl.glavo.site/metadata/cleanroom/index.json"; private static final String INSTALLER_URL = "https://hmcl.glavo.site/metadata/cleanroom/files/cleanroom-%s-installer.jar"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java index 623240a200..83fc136114 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.fabric; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.task.Task; @@ -26,7 +26,7 @@ import java.util.Collections; -public class FabricAPIVersionList extends VersionList { +public class FabricAPIVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java index ed2e10b3a5..9a3c771430 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.fabric; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonSerializable; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -31,7 +31,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; -public final class FabricVersionList extends VersionList { +public final class FabricVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; public FabricVersionList(DownloadProvider downloadProvider) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java index 14dbcaae3d..6058447ccc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.forge; import com.google.gson.JsonParseException; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Immutable; @@ -41,7 +41,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class ForgeBMCLVersionList extends VersionList { +public final class ForgeBMCLVersionList extends ComponentVersionList { private final String apiRoot; /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java index 9d0941b2b2..be63c85398 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.forge; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.StringUtils; @@ -33,7 +33,7 @@ * * @author huangyuhui */ -public final class ForgeVersionList extends VersionList { +public final class ForgeVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; public ForgeVersionList(DownloadProvider downloadProvider) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java index 8cd2e0a8ba..9d815529a1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java @@ -19,7 +19,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.ComponentRemoteVersion; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; @@ -38,7 +38,7 @@ public final class GameInstanceJsonDownloadTask extends Task { private final DefaultDependencyManager dependencyManager; private final List> dependents = new ArrayList<>(1); private final List> dependencies = new ArrayList<>(1); - private final VersionList gameVersionList; + private final ComponentVersionList gameVersionList; public GameInstanceJsonDownloadTask(String gameVersion, DefaultDependencyManager dependencyManager) { this.gameVersion = gameVersion; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java index cf8428b804..3ef8fe399f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.game; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -34,7 +34,7 @@ * * @author huangyuhui */ -public final class GameVersionList extends VersionList { +public final class GameVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; public GameVersionList(DownloadProvider downloadProvider) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java index 9c5a940817..9f9edb5fcd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.legacyfabric; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.task.Task; @@ -26,7 +26,7 @@ import java.util.Collections; -public class LegacyFabricAPIVersionList extends VersionList { +public class LegacyFabricAPIVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricVersionList.java index 272f82f8cd..a6c336620f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.legacyfabric; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonSerializable; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -31,7 +31,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; -public final class LegacyFabricVersionList extends VersionList { +public final class LegacyFabricVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; public LegacyFabricVersionList(DownloadProvider downloadProvider) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java index ea4708fad4..52d67371dc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderBMCLVersionList.java @@ -19,7 +19,7 @@ import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider; import org.jackhuang.hmcl.download.ComponentRemoteVersion; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -31,7 +31,7 @@ /** * @author huangyuhui */ -public final class LiteLoaderBMCLVersionList extends VersionList { +public final class LiteLoaderBMCLVersionList extends ComponentVersionList { private final BMCLAPIDownloadProvider downloadProvider; public LiteLoaderBMCLVersionList(BMCLAPIDownloadProvider downloadProvider) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java index 31453a9564..a5c36982f9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderVersionList.java @@ -19,7 +19,7 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.ComponentRemoteVersion; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.HttpRequest; @@ -35,7 +35,7 @@ /** * @author huangyuhui */ -public final class LiteLoaderVersionList extends VersionList { +public final class LiteLoaderVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java index d0466fd520..00b5c5a992 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeBMCLVersionList.java @@ -19,7 +19,7 @@ import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Immutable; @@ -30,7 +30,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; -public final class NeoForgeBMCLVersionList extends VersionList { +public final class NeoForgeBMCLVersionList extends ComponentVersionList { private final String apiRoot; /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java index a7cab8ddd2..230851e01e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOfficialVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.neoforge; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonSerializable; @@ -29,7 +29,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class NeoForgeOfficialVersionList extends VersionList { +public final class NeoForgeOfficialVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; public NeoForgeOfficialVersionList(DownloadProvider downloadProvider) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java index 608a1ca047..4ec3b8d193 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineBMCLVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.optifine; import com.google.gson.annotations.SerializedName; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.StringUtils; @@ -33,7 +33,7 @@ /** * @author huangyuhui */ -public final class OptiFineBMCLVersionList extends VersionList { +public final class OptiFineBMCLVersionList extends ComponentVersionList { private final String apiRoot; /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java index 28a2301e73..e521735938 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.quilt; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.task.Task; @@ -26,7 +26,7 @@ import java.util.Collections; -public class QuiltAPIVersionList extends VersionList { +public class QuiltAPIVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java index b007d54372..879f5957e9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltVersionList.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.quilt; import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonSerializable; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -31,7 +31,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf; -public final class QuiltVersionList extends VersionList { +public final class QuiltVersionList extends ComponentVersionList { private final DownloadProvider downloadProvider; public QuiltVersionList(DownloadProvider downloadProvider) { From e1a3b5bb0463736d1c5b572b6fdc2a72327ea200 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 25 Aug 2026 21:39:56 +0800 Subject: [PATCH 23/60] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20MultiMCModpackInstal?= =?UTF-8?q?lTask=20=E7=B1=BB=E7=9A=84=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E7=94=A8=E7=AE=80=E6=B4=81=E7=9A=84=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=A0=BC=E5=BC=8F=E4=BB=A5=E6=8F=90=E9=AB=98=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../multimc/MultiMCModpackInstallTask.java | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java index f6c9bceb22..0b6891c0ba 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java @@ -52,26 +52,23 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -/** - *

A task transforming MultiMC Modpack Scheme to Official Launcher Scheme. - * The transforming process contains 7 stage: - *

    - *
  • General Setup: Compute checksum and copy 'overrides' files.
  • - *
  • Load Components: Parse all local Json-Patch and prepare to fetch others from Internet.
  • - *
  • Resolve Json-Patch: Fetch remote Json-Patch and their dependencies.
  • - *
  • Build Artifact: Transform Json-Patch to Official Scheme lossily, without original structure.
  • - *
  • Copy Embedded Files: Copy embedded libraries and icon.
  • - *
  • Assemble Game: Prepare to download main jar, libraries and assets.
  • - *
  • Download Game: Download files.
  • - *
  • Apply JAR mods: Apply JAR mods into main jar.
  • - *
- * See codes below for detailed implementation. - * - * @implNote To guarantee all features of MultiMC Modpack Scheme is super hard. - * As f*** MMC never provides a detailed API docs, most codes below is guessed from its source code. - * FUNCTIONS OF GAMES MIGHT NOT BE COMPLETELY THE SAME WITH MMC. - *

- */ +/// A task transforming MultiMC Modpack Scheme to Official Launcher Scheme. +/// The transforming process contains 7 stage: +/// +/// - General Setup: Compute checksum and copy 'overrides' files. +/// - Load Components: Parse all local Json-Patch and prepare to fetch others from Internet. +/// - Resolve Json-Patch: Fetch remote Json-Patch and their dependencies. +/// - Build Artifact: Transform Json-Patch to Official Scheme lossily, without original structure. +/// - Copy Embedded Files: Copy embedded libraries and icon. +/// - Assemble Game: Prepare to download main jar, libraries and assets. +/// - Download Game: Download files. +/// - Apply JAR mods: Apply JAR mods into main jar. +/// +/// See codes below for detailed implementation. +/// +/// @implNote To guarantee all features of MultiMC Modpack Scheme is super hard. +/// As f\*\*\* MMC never provides a detailed API docs, most codes below is guessed from its source code. +/// **FUNCTIONS OF GAMES MIGHT NOT BE COMPLETELY THE SAME WITH MMC.** public final class MultiMCModpackInstallTask extends Task { private final Path zipFile; From b5f451d1ecc54efd6b4fe1962034993d96c3d3ff Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 20:21:21 +0800 Subject: [PATCH 24/60] =?UTF-8?q?=E9=87=8D=E6=9E=84=20AutoDownloadProvider?= =?UTF-8?q?=20=E7=B1=BB=EF=BC=8C=E7=A7=BB=E9=99=A4=20MultipleSourceVersion?= =?UTF-8?q?List=20=E7=B1=BB=E5=B9=B6=E5=B0=86=E5=85=B6=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=95=B4=E5=90=88=E5=88=B0=20AutoDownloadProvider=20=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/download/AutoDownloadProvider.java | 91 ++++++++++++++ .../download/MultipleSourceVersionList.java | 113 ------------------ 2 files changed, 91 insertions(+), 113 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java index a2c11e01c7..110dced217 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/AutoDownloadProvider.java @@ -18,14 +18,19 @@ package org.jackhuang.hmcl.download; import org.jackhuang.hmcl.game.GameComponentType; +import org.jackhuang.hmcl.task.Task; import java.net.URI; +import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Function; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + /// @author huangyuhui public final class AutoDownloadProvider implements DownloadProvider { private final List versionListProviders; @@ -115,4 +120,90 @@ public int getConcurrency() { public String toString() { return "AutoDownloadProvider[versionListProviders=%s, fileProviders=%s]".formatted(versionListProviders, fileProviders); } + + private static final class MultipleSourceVersionList extends ComponentVersionList { + private final ComponentVersionList[] backends; + + MultipleSourceVersionList(ComponentVersionList[] backends) { + this.backends = backends; + + assert (backends.length >= 1); + } + + @Override + public boolean hasType() { + boolean hasType = backends[0].hasType(); + assert (Arrays.stream(backends).allMatch(versionList -> versionList.hasType() == hasType)); + return hasType; + } + + @Override + public Task refreshAsync() { + throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list."); + } + + private Task refreshAsync(String gameVersion, int sourceIndex) { + ComponentVersionList versionList = backends[sourceIndex]; + Task refreshTask = versionList.refreshAsync(gameVersion); + + return new Task<>() { + private Task nextTask = null; + + { + setSignificance(TaskSignificance.MODERATE); + setName("MultipleSourceVersionList.refreshAsync(task=%s, index=%d, all=%d)".formatted( + refreshTask.getName(), sourceIndex, backends.length) + ); + } + + @Override + public Collection> getDependents() { + return List.of(refreshTask); + } + + @Override + public Collection> getDependencies() { + return nextTask != null ? List.of(nextTask) : List.of(); + } + + @Override + public boolean isRelyingOnDependents() { + return false; + } + + @Override + public void execute() throws Exception { + if (isDependentsSucceeded()) { + lock.writeLock().lock(); + try { + versions.putAll(gameVersion, versionList.getVersions(gameVersion)); + } finally { + lock.writeLock().unlock(); + } + + setResult(refreshTask.getResult()); + } else { + Exception exception = refreshTask.getException(); + assert exception != null; + + if (sourceIndex == backends.length - 1) { + LOG.warning("Failed to fetch versions list from all sources", exception); + setSignificance(TaskSignificance.MINOR); + throw exception; + } else { + LOG.warning("Failed to fetch versions list and try to fetch from other source", exception); + nextTask = refreshAsync(gameVersion, sourceIndex + 1); + nextTask.storeTo(this::setResult); + } + } + } + }; + } + + @Override + public Task refreshAsync(String gameVersion) { + versions.clear(gameVersion); + return refreshAsync(gameVersion, 0); + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java deleted file mode 100644 index 03864dc3fa..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MultipleSourceVersionList.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 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; - -import org.jackhuang.hmcl.task.Task; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public class MultipleSourceVersionList extends ComponentVersionList { - - private final ComponentVersionList[] backends; - - MultipleSourceVersionList(ComponentVersionList[] backends) { - this.backends = backends; - - assert (backends.length >= 1); - } - - @Override - public boolean hasType() { - boolean hasType = backends[0].hasType(); - assert (Arrays.stream(backends).allMatch(versionList -> versionList.hasType() == hasType)); - return hasType; - } - - @Override - public Task refreshAsync() { - throw new UnsupportedOperationException("MultipleSourceVersionList does not support loading the entire remote version list."); - } - - private Task refreshAsync(String gameVersion, int sourceIndex) { - ComponentVersionList versionList = backends[sourceIndex]; - Task refreshTask = versionList.refreshAsync(gameVersion); - - return new Task<>() { - private Task nextTask = null; - - { - setSignificance(TaskSignificance.MODERATE); - setName("MultipleSourceVersionList.refreshAsync(task=%s, index=%d, all=%d)".formatted( - refreshTask.getName(), sourceIndex, backends.length) - ); - } - - @Override - public Collection> getDependents() { - return List.of(refreshTask); - } - - @Override - public Collection> getDependencies() { - return nextTask != null ? List.of(nextTask) : List.of(); - } - - @Override - public boolean isRelyingOnDependents() { - return false; - } - - @Override - public void execute() throws Exception { - if (isDependentsSucceeded()) { - lock.writeLock().lock(); - try { - versions.putAll(gameVersion, versionList.getVersions(gameVersion)); - } finally { - lock.writeLock().unlock(); - } - - setResult(refreshTask.getResult()); - } else { - Exception exception = refreshTask.getException(); - assert exception != null; - - if (sourceIndex == backends.length - 1) { - LOG.warning("Failed to fetch versions list from all sources", exception); - setSignificance(TaskSignificance.MINOR); - throw exception; - } else { - LOG.warning("Failed to fetch versions list and try to fetch from other source", exception); - nextTask = refreshAsync(gameVersion, sourceIndex + 1); - nextTask.storeTo(this::setResult); - } - } - } - }; - } - - @Override - public Task refreshAsync(String gameVersion) { - versions.clear(gameVersion); - return refreshAsync(gameVersion, 0); - } -} From f87cd5c9156569eb8b9a940ec2f94ca75d58e508 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 20:40:00 +0800 Subject: [PATCH 25/60] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20GameComponentAnalyze?= =?UTF-8?q?r=20=E4=B8=AD=E7=9A=84=20hasModLauncher=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E4=B8=BA=20hasForgeModLauncher=EF=BC=8C=E6=B7=BB=E5=8A=A0=20is?= =?UTF-8?q?Modded=20=E6=96=B9=E6=B3=95=E4=BB=A5=E5=A2=9E=E5=BC=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/HMCLGameInstance.java | 2 +- .../hmcl/game/GameComponentAnalyzer.java | 16 +++++++++++++++- .../org/jackhuang/hmcl/game/GameInstance.java | 4 ++++ .../hmcl/game/LaunchManifestNormalizer.java | 6 +++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java index a94492320d..96686f4254 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java @@ -198,7 +198,7 @@ public void applyDefaultIsolationSetting() { boolean isolated = switch (type) { case NEVER -> false; case ALWAYS -> true; - case MODDED -> getResolvedManifest().isModded(); + case MODDED -> getAnalyzer().isModded(); }; if (isolated) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java index b7b4e37e36..808ac76ca2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java @@ -104,7 +104,7 @@ public boolean has(ModLoaderType type) { return false; } - public boolean hasModLauncher() { + public boolean hasForgeModLauncher() { return GameComponentAnalyzer.MOD_LAUNCHER_MAIN.equals(manifest.mainClass()) || manifest.getPatches().stream().anyMatch( patch -> GameComponentAnalyzer.MOD_LAUNCHER_MAIN.equals(patch.mainClass()) ); @@ -123,6 +123,20 @@ public boolean hasModLauncher() { return bootstrapVersion; } + public boolean isModded() { + String mainClass = manifest.mainClass(); + if (mainClass == null || GameComponentAnalyzer.LAUNCH_WRAPPER_MAIN.equals(mainClass)) { + return false; + } + + for (String packageName : GameComponentAnalyzer.MOD_LOADER_MAIN_CLASSES_PACKAGES) { + if (mainClass.startsWith(packageName)) + return true; + } + + return false; + } + /// If a library is provided in `$.patches`, it's structure is so clear that we can do any operation. /// Otherwise, we must guess how are these libraries mixed. /// Maybe a guessing implementation will be provided in the future. But by now, we simply set it to JUST\_EXISTED. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java index 85258c4b1f..4feb6fa9ed 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java @@ -58,6 +58,10 @@ public interface GameInstance { /// @return the resolved manifest views GameInstanceManifest.Resolved getResolvedManifest(); + default GameInstanceManifest getResolvedManifest2() { + return getResolvedManifest().launchManifest(); + } + /// Returns the manifest used by launch-time consumers. /// /// @return the launch manifest diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java index 0550108765..2d41526566 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java @@ -87,7 +87,7 @@ private static GameInstanceManifest repairLaunchWrapper( @Nullable String mainClass = null; // Re-add LiteLoader tweaker when Forge overwrote the argument list (unless ModLauncher is in use). - if (analyzer.has(GameComponentType.LITELOADER) && !analyzer.hasModLauncher()) { + if (analyzer.has(GameComponentType.LITELOADER) && !analyzer.hasForgeModLauncher()) { builder.replaceTweakClass( GameComponentAnalyzer.LITELOADER_TWEAKER, GameComponentAnalyzer.LITELOADER_TWEAKER, @@ -107,7 +107,7 @@ private static GameInstanceManifest repairLaunchWrapper( !reorderTweakClass, reorderTweakClass); } - } else if (analyzer.hasModLauncher()) { + } else if (analyzer.hasForgeModLauncher()) { // Prefer ModLauncher over LaunchWrapper when both are present. mainClass = GameComponentAnalyzer.MOD_LAUNCHER_MAIN; for (String optiFineTweaker : GameComponentAnalyzer.OPTIFINE_TWEAKERS) { @@ -128,7 +128,7 @@ private static GameInstanceManifest repairLaunchWrapper( } boolean hasForge = analyzer.has(GameComponentType.FORGE); - boolean hasModLauncher = analyzer.hasModLauncher(); + boolean hasModLauncher = analyzer.hasForgeModLauncher(); for (String forgeTweaker : GameComponentAnalyzer.FORGE_TWEAKERS) { if (!hasForge) { builder.removeTweakClass(forgeTweaker); From 72abbf1d0c47da5ff491e7dacea650d7940c7508 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 20:45:35 +0800 Subject: [PATCH 26/60] Remove redundant default isolation re-evaluation Assisted-by: codex:gpt-5.6-sol --- .../jackhuang/hmcl/game/HMCLGameInstance.java | 22 ------------------- .../hmcl/ui/download/DownloadPage.java | 13 +++++++---- .../hmcl/setting/GameDirectoriesTest.java | 11 ++++++---- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java index 96686f4254..0fe11b6546 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java @@ -189,28 +189,6 @@ public GameSettings.Effective getEffectiveSettings() { return GameSettings.resolve(getRepository().getParentGameSettings(setting), setting); } - /// Applies the selected parent preset's default isolation policy to this instance. - public void applyDefaultIsolationSetting() { - @Nullable GameSettings.Instance instanceSetting = getSettings(); - GameSettings.Preset preset = getRepository().getParentGameSettings(instanceSetting); - DefaultIsolationType type = Lang.requireNonNullElse( - preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED); - boolean isolated = switch (type) { - case NEVER -> false; - case ALWAYS -> true; - case MODDED -> getAnalyzer().isModded(); - }; - - if (isolated) { - @Nullable GameSettings.Instance setting = - instanceSetting != null ? instanceSetting : getSettingsOrCreate(); - if (setting != null - && setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) { - saveSettings(); - } - } - } - /// Creates empty instance-local game settings when none are loaded. /// /// @return the settings, or `null` when settings are read-only or already present in a non-creatable state diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index b1835f148d..736737cd3f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -313,12 +313,17 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { builder.component(remoteVersion); }); - if (repository.shouldIsolateNewInstance(settings.isInstallingModdedVersion())) { + boolean isolated = repository.shouldIsolateNewInstance(settings.isInstallingModdedVersion()); + if (isolated) { builder.useInstanceRunDirectory(); } - return builder.buildAsync().whenComplete(any -> { - repository.refresh(); - repository.getInstance(instanceId).applyDefaultIsolationSetting(); + return builder.buildAsync().whenComplete(exception -> { + if (exception == null) { + repository.refresh(); + if (isolated) { + repository.ensureIsolatedRunningDirectory(instanceId); + } + } }).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId))); } diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java index fa027f519f..f825d13952 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java @@ -440,9 +440,9 @@ public void repositoryDirectoryFollowsGameDirectoryPath() throws ReflectiveOpera } } - /// Tests that default isolation is applied after a new instance is published. + /// Tests that a default isolation decision is persisted after a new instance is published. @Test - public void newInstanceAppliesDefaultIsolationAfterPublication(@TempDir Path tempDirectory) + public void newInstancePersistsDefaultIsolationDecisionAfterPublication(@TempDir Path tempDirectory) throws Exception { GameSettingsPresetID defaultPresetId = GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174002"); @@ -466,7 +466,8 @@ public void newInstanceAppliesDefaultIsolationAfterPublication(@TempDir Path tem GameInstanceID id = new GameInstanceID("1.21.11-fabric"); assertFalse(repository.hasInstance(id)); - assertTrue(repository.shouldIsolateNewInstance(false)); + boolean isolated = repository.shouldIsolateNewInstance(false); + assertTrue(isolated); assertThrows( NoSuchGameInstanceException.class, () -> repository.ensureIsolatedRunningDirectory(id)); @@ -478,7 +479,9 @@ public void newInstanceAppliesDefaultIsolationAfterPublication(@TempDir Path tem } HMCLGameInstance instance = repository.getInstance(id); - instance.applyDefaultIsolationSetting(); + if (isolated) { + repository.ensureIsolatedRunningDirectory(id); + } assertEquals(repository.getLayout().getInstanceRoot(id), instance.getRunDirectory()); assertEquals(repository.getLayout().getInstanceRoot(id).resolve("mods"), instance.getModsDirectory()); GameSettings.Instance instanceSettings = Objects.requireNonNull(instance.getSettings()); From 2946ac872feee24b55715089258b4d68f50de879 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 20:50:20 +0800 Subject: [PATCH 27/60] Add validation for instance ID in DownloadPage to prevent null values --- .../java/org/jackhuang/hmcl/ui/download/DownloadPage.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 736737cd3f..4c13f273ae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -304,6 +304,10 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { GameBuilder builder = dependencyManager.newGameBuilder(); GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID); + if (instanceId == null) { + throw new IllegalStateException("Instance ID is not set"); + } + builder.id(instanceId); builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion()); From b59035d8d515ab8b29acfcc7e05dc81b34e99407 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 20:53:54 +0800 Subject: [PATCH 28/60] Refactor GameInstance methods to use getLaunchManifest for consistency --- .../main/java/org/jackhuang/hmcl/game/LauncherHelper.java | 3 +-- .../java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java | 5 ++--- .../main/java/org/jackhuang/hmcl/ui/instances/Instances.java | 2 +- .../java/org/jackhuang/hmcl/game/DefaultGameInstance.java | 2 +- .../src/main/java/org/jackhuang/hmcl/game/GameInstance.java | 4 ---- .../org/jackhuang/hmcl/game/DefaultGameInstanceTest.java | 2 +- 6 files changed, 6 insertions(+), 12 deletions(-) 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 8ed284d6b9..a1d9482a1a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -157,8 +157,7 @@ private void launch0() { HMCLGameRepository repository = repository(); DefaultDependencyManager dependencyManager = repository.getDependency(); // Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch. - var launchManifest = new AtomicReference<>(LaunchManifestNormalizer.repairForLaunch( - gameInstance.getResolvedManifest().launchManifest())); + var launchManifest = new AtomicReference<>(LaunchManifestNormalizer.repairForLaunch(gameInstance.getLaunchManifest())); boolean integrityCheck = gameInstance.unmarkLaunchedAbnormally(); CountDownLatch launchingLatch = new CountDownLatch(1); List javaAgents = new ArrayList<>(0); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index b760183977..e1bc02cf2e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -2820,9 +2820,8 @@ private void initJavaSubtitle() { if (JavaManager.isInitialized()) { GameVersionNumber gameVersionNumber = currentGameVersion(); - GameInstanceManifest manifest = gameInstance != null - ? gameInstance.getResolvedManifest().launchManifest() - : null; + GameInstanceManifest manifest; + manifest = gameInstance != null ? gameInstance.getLaunchManifest() : null; try { JavaRuntime java = effectiveSetting != null diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index 8059594e6f..35e85e5df9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -274,7 +274,7 @@ public static void updateInstance(HMCLGameInstance gameInstance) { public static void updateGameAssets(HMCLGameInstance gameInstance) { TaskExecutor executor = new GameAssetDownloadTask( gameInstance.getRepository().getDependency(), - gameInstance.getResolvedManifest().launchManifest(), + gameInstance.getLaunchManifest(), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true).executor(); Controllers.taskDialog(executor, i18n("instance.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java index 87a3a86742..06ba2011e1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java @@ -261,7 +261,7 @@ public Path getModpackConfigurationFile() { /// instance's own jar is returned from [#getOwnJarFile()]. @Override public Path getInstanceJarFile() { - GameInstanceManifest launchManifest = getResolvedManifest().launchManifest(); + GameInstanceManifest launchManifest = getLaunchManifest(); GameInstanceID jarId = Optional.ofNullable(launchManifest.jar()).orElse(launchManifest.id()); if (!jarId.equals(id)) { DefaultGameInstance other = snapshot.findInstance(jarId); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java index 4feb6fa9ed..85258c4b1f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java @@ -58,10 +58,6 @@ public interface GameInstance { /// @return the resolved manifest views GameInstanceManifest.Resolved getResolvedManifest(); - default GameInstanceManifest getResolvedManifest2() { - return getResolvedManifest().launchManifest(); - } - /// Returns the manifest used by launch-time consumers. /// /// @return the launch manifest diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index ab9df59627..0ff2494698 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -69,7 +69,7 @@ public void testModLauncherLaunchRepairDoesNotWriteBundledLibraries(@TempDir Pat new Library(new Artifact("net.minecraftforge", "forge", "1.0")), new Library(new Artifact("optifine", "OptiFine", "1.0")))); TestGameInstance instance = repository.publish(instanceId, manifest); - GameInstanceManifest launchManifest = instance.getResolvedManifest().launchManifest(); + GameInstanceManifest launchManifest = instance.getLaunchManifest(); assertTrue(launchManifest.getLibraries().stream() .noneMatch(library -> library.is( "org.jackhuang.hmcl", "transformer-discovery-service"))); From 2e174c1339e7dcde3a033af18ba359de979f3377 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 20:58:45 +0800 Subject: [PATCH 29/60] Introduce HMCLDependencyManager and HMCLGameBuilder for enhanced dependency management --- .../hmcl/game/HMCLDependencyManager.java | 41 +++++++++++++++++++ .../jackhuang/hmcl/game/HMCLGameBuilder.java | 30 ++++++++++++++ .../hmcl/game/HMCLGameRepository.java | 2 +- .../download/DefaultDependencyManager.java | 2 +- 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java new file mode 100644 index 0000000000..51c7695820 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java @@ -0,0 +1,41 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.game; + +import org.jackhuang.hmcl.download.DefaultCacheRepository; +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jetbrains.annotations.NotNullByDefault; + +/// @author Glavo +@NotNullByDefault +public class HMCLDependencyManager extends DefaultDependencyManager { + /// Creates a dependency manager for a repository and download context. + /// + /// @param repository the associated game repository + /// @param downloadProvider the remote download provider + /// @param cacheRepository the artifact cache + public HMCLDependencyManager(DefaultGameRepository repository, DownloadProvider downloadProvider, DefaultCacheRepository cacheRepository) { + super(repository, downloadProvider, cacheRepository); + } + + @Override + public HMCLGameBuilder newGameBuilder() { + return new HMCLGameBuilder(this); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java new file mode 100644 index 0000000000..54cd5b958a --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -0,0 +1,30 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 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.game; + +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.DefaultGameBuilder; + +public class HMCLGameBuilder extends DefaultGameBuilder { + /// Creates a builder bound to the given dependency manager. + /// + /// @param dependencyManager the dependency manager for the target repository + public HMCLGameBuilder(DefaultDependencyManager dependencyManager) { + super(dependencyManager); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 3837aff909..a440fc8b63 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -209,7 +209,7 @@ public DefaultDependencyManager getDependency() { /// Returns a dependency manager using the given download provider. public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) { - return new DefaultDependencyManager(this, downloadProvider, HMCLCacheRepository.REPOSITORY); + return new HMCLDependencyManager(this, downloadProvider, HMCLCacheRepository.REPOSITORY); } /// Resolves the run directory from modpack state and local settings. 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 7b1a2ad85c..6858bab4fc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -88,7 +88,7 @@ public DefaultCacheRepository getCacheRepository() { } @Override - public GameBuilder newGameBuilder() { + public DefaultGameBuilder newGameBuilder() { return new DefaultGameBuilder(this); } From 7bb346b52cd3b6a003f2efe27ba2f685cd03ceca Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 21:00:51 +0800 Subject: [PATCH 30/60] Refactor HMCLGameBuilder to use HMCLDependencyManager and enforce non-null parameters --- .../java/org/jackhuang/hmcl/game/HMCLGameBuilder.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index 54cd5b958a..4c811baa70 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -17,14 +17,20 @@ */ package org.jackhuang.hmcl.game; -import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DefaultGameBuilder; +import org.jetbrains.annotations.NotNullByDefault; +@NotNullByDefault public class HMCLGameBuilder extends DefaultGameBuilder { /// Creates a builder bound to the given dependency manager. /// /// @param dependencyManager the dependency manager for the target repository - public HMCLGameBuilder(DefaultDependencyManager dependencyManager) { + public HMCLGameBuilder(HMCLDependencyManager dependencyManager) { super(dependencyManager); } + + @Override + public HMCLDependencyManager getDependencyManager() { + return (HMCLDependencyManager) super.getDependencyManager(); + } } From ef1d43c220aef14472547717ee96e7eafef9c792 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 21:28:38 +0800 Subject: [PATCH 31/60] Refactor DownloadPage to use HMCLDependencyManager and HMCLGameBuilder; remove ensureIsolatedRunningDirectory method --- .../hmcl/game/HMCLDependencyManager.java | 14 +++- .../jackhuang/hmcl/game/HMCLGameBuilder.java | 30 ++++++++ .../jackhuang/hmcl/game/HMCLGameInstance.java | 17 +++++ .../hmcl/game/HMCLGameRepository.java | 29 ++------ .../jackhuang/hmcl/game/ModpackHelper.java | 4 +- .../hmcl/ui/download/DownloadPage.java | 22 +++--- .../hmcl/ui/instances/InstallerListPage.java | 3 +- .../org/jackhuang/hmcl/ui/main/MainPage.java | 4 +- .../hmcl/setting/GameDirectoriesTest.java | 9 ++- .../hmcl/game/GameComponentAnalyzer.java | 2 +- .../jackhuang/hmcl/game/GameException.java | 40 ----------- .../org/jackhuang/hmcl/util/SettingsMap.java | 13 ---- .../jackhuang/hmcl/util/SettingsMapTest.java | 68 ------------------- 13 files changed, 84 insertions(+), 171 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameException.java delete mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java index 51c7695820..54f3db28dd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java @@ -22,7 +22,7 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jetbrains.annotations.NotNullByDefault; -/// @author Glavo +/// Provides HMCL-specific game builders for an HMCL game repository. @NotNullByDefault public class HMCLDependencyManager extends DefaultDependencyManager { /// Creates a dependency manager for a repository and download context. @@ -30,10 +30,20 @@ public class HMCLDependencyManager extends DefaultDependencyManager { /// @param repository the associated game repository /// @param downloadProvider the remote download provider /// @param cacheRepository the artifact cache - public HMCLDependencyManager(DefaultGameRepository repository, DownloadProvider downloadProvider, DefaultCacheRepository cacheRepository) { + public HMCLDependencyManager( + HMCLGameRepository repository, + DownloadProvider downloadProvider, + DefaultCacheRepository cacheRepository) { super(repository, downloadProvider, cacheRepository); } + /// {@inheritDoc} + @Override + public HMCLGameRepository getGameRepository() { + return (HMCLGameRepository) super.getGameRepository(); + } + + /// {@inheritDoc} @Override public HMCLGameBuilder newGameBuilder() { return new HMCLGameBuilder(this); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index 4c811baa70..4aac21a288 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -18,8 +18,12 @@ package org.jackhuang.hmcl.game; import org.jackhuang.hmcl.download.DefaultGameBuilder; +import org.jackhuang.hmcl.task.Task; import org.jetbrains.annotations.NotNullByDefault; +import java.util.Objects; + +/// Builds game instances and applies HMCL-specific post-installation settings. @NotNullByDefault public class HMCLGameBuilder extends DefaultGameBuilder { /// Creates a builder bound to the given dependency manager. @@ -29,8 +33,34 @@ public HMCLGameBuilder(HMCLDependencyManager dependencyManager) { super(dependencyManager); } + /// {@inheritDoc} @Override public HMCLDependencyManager getDependencyManager() { return (HMCLDependencyManager) super.getDependencyManager(); } + + /// {@inheritDoc} + /// + /// For an unregistered instance, applies the repository's default isolation policy to the + /// configured component set. When isolation is selected, component-provided run-directory + /// files are installed under the instance root and the corresponding instance setting is + /// enabled after publication. Existing instances retain their current run-directory settings. + @Override + public Task buildAsync() { + GameInstanceID instanceId = Objects.requireNonNull(id, "GameBuilder.id must be set"); + HMCLGameRepository repository = getDependencyManager().getGameRepository(); + boolean enableIsolation = !repository.hasInstance(instanceId) + && repository.shouldIsolateNewInstance( + components.keySet().stream().anyMatch(GameComponentType::isModLoader)); + if (enableIsolation) { + useInstanceRunDirectory(); + } + + Task buildTask = super.buildAsync(); + if (!enableIsolation) { + return buildTask; + } + + return buildTask.thenRunAsync(() -> repository.getInstance(instanceId).enableIsolation()); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java index 0fe11b6546..115ce60d11 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameInstance.java @@ -211,6 +211,23 @@ public boolean isSettingsReadOnly() { return gameSettingsReadOnly; } + /// Enables instance-local running-directory selection for this instance. + /// + /// A blank local running directory resolves to the instance root. This operation is idempotent + /// and schedules a settings save only when it adds the override. It leaves the instance + /// unchanged when its local settings cannot be written safely. + public void enableIsolation() { + if (isSettingsReadOnly()) { + return; + } + + @Nullable GameSettings.Instance setting = getSettingsOrCreate(); + if (setting != null + && setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) { + saveSettings(); + } + } + /// Backs up and overwrites the settings file when this instance still owns its settings. public void forceOverwriteSettings() { ensureGameSettingsLoaded(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index a440fc8b63..e8a0f6a2e5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -21,7 +21,6 @@ import javafx.beans.binding.ObjectBinding; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; -import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.modpack.ModAdviser; import org.jackhuang.hmcl.modpack.Modpack; @@ -203,12 +202,17 @@ public void refreshSelectedInstance() { } /// Returns a dependency manager using the currently selected download provider. - public DefaultDependencyManager getDependency() { + /// + /// @return a new dependency manager for this repository + public HMCLDependencyManager getDependency() { return getDependency(DownloadProviders.getDownloadProvider()); } /// Returns a dependency manager using the given download provider. - public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) { + /// + /// @param downloadProvider the remote download provider + /// @return a new dependency manager for this repository + public HMCLDependencyManager getDependency(DownloadProvider downloadProvider) { return new HMCLDependencyManager(this, downloadProvider, HMCLCacheRepository.REPOSITORY); } @@ -291,25 +295,6 @@ private void writeInstanceGameSettings(GameInstanceID instanceId, GameSettings.I FileUtils.saveSafely(file, LauncherSettings.SETTINGS_GSON.toJson(setting)); } - /// Ensures the instance uses an isolated running directory under its instance root. - /// - /// @param instanceId the instance id - /// @throws NoSuchGameInstanceException if the instance is not registered - public void ensureIsolatedRunningDirectory(GameInstanceID instanceId) { - HMCLGameInstance instance = findInstance(instanceId); - if (instance == null) { - throw new NoSuchGameInstanceException(instanceId); - } - if (instance.isSettingsReadOnly()) { - return; - } - @Nullable GameSettings.Instance setting = instance.getSettingsOrCreate(); - if (setting != null - && setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) { - instance.saveSettings(); - } - } - public Stream getDisplayInstances() { return getSnapshot().getInstances().stream() .filter(it -> !it.getManifest().isHidden()) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 6a8beb4858..9bc7a8145e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -160,7 +160,7 @@ public static ModpackConfiguration readModpackConfiguration(Path file) throws public static Task getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, Modpack modpack) { ExceptionalRunnable success = () -> { repository.refresh(); - repository.ensureIsolatedRunningDirectory(instanceId); + repository.getInstance(instanceId).enableIsolation(); }; ExceptionalConsumer failure = ex -> { @@ -198,7 +198,7 @@ public static Task getInstallManuallyCreatedModpackTask(Path zipFile, String public static Task getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) { ExceptionalRunnable success = () -> { repository.refresh(); - repository.ensureIsolatedRunningDirectory(instanceId); + repository.getInstance(instanceId).enableIsolation(); }; ExceptionalConsumer failure = ex -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 4c13f273ae..6e334abc90 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -27,6 +27,8 @@ import org.jackhuang.hmcl.download.game.GameRemoteVersion; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.HMCLDependencyManager; +import org.jackhuang.hmcl.game.HMCLGameBuilder; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.DownloadProviders; @@ -282,7 +284,7 @@ public void onGameSelected() { private static class VanillaInstallWizardProvider implements WizardProvider { private final HMCLGameRepository repository; - private final DefaultDependencyManager dependencyManager; + private final HMCLDependencyManager dependencyManager; private final DownloadProvider downloadProvider; private final GameRemoteVersion gameVersion; @@ -301,7 +303,7 @@ public void start(SettingsMap settings) { } private Task finishVersionDownloadingAsync(SettingsMap settings) { - GameBuilder builder = dependencyManager.newGameBuilder(); + HMCLGameBuilder builder = dependencyManager.newGameBuilder(); GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID); if (instanceId == null) { @@ -317,18 +319,10 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { builder.component(remoteVersion); }); - boolean isolated = repository.shouldIsolateNewInstance(settings.isInstallingModdedVersion()); - if (isolated) { - builder.useInstanceRunDirectory(); - } - return builder.buildAsync().whenComplete(exception -> { - if (exception == null) { - repository.refresh(); - if (isolated) { - repository.ensureIsolatedRunningDirectory(instanceId); - } - } - }).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId))); + return builder.buildAsync() + .thenRunAsync( + Schedulers.javafx(), + () -> repository.setSelectedInstance(repository.getInstance(instanceId))); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java index 2233cde3e2..ce98fb95a1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java @@ -67,7 +67,8 @@ protected Skin createDefaultSkin() { } public void loadInstance(HMCLGameInstance.Optional instance) { - this.gameInstance = instance.instance(); + HMCLGameInstance gameInstance = instance.instance(); + this.gameInstance = gameInstance; if (gameInstance == null) { itemsProperty().clear(); return; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 4d21db4a30..3b0e911d04 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -44,7 +44,6 @@ import javafx.scene.text.TextFlow; import javafx.util.Duration; import org.jackhuang.hmcl.Metadata; -import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.ComponentVersionList; import org.jackhuang.hmcl.game.*; @@ -373,7 +372,7 @@ private void launchNoGame() { .orElseThrow(() -> new IOException("No versions found"))) .thenComposeAsync(version -> { HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); - DefaultDependencyManager dependency = repository.getDependency(); + HMCLDependencyManager dependency = repository.getDependency(); String gameVersion = version.getGameVersion(); GameInstanceID instanceId = new GameInstanceID(gameVersion); @@ -385,7 +384,6 @@ private void launchNoGame() { .component(GameComponentType.GAME, gameVersion) .buildAsync(); }) - .whenComplete(any -> GameDirectoryManager.getSelectedRepository().refresh()) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java index f825d13952..75eba993ae 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java @@ -31,7 +31,6 @@ import org.jackhuang.hmcl.game.GameRepositoryDraft; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.NoSuchGameInstanceException; import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.PortablePath; import org.jackhuang.hmcl.util.gson.JsonSchema; @@ -468,9 +467,6 @@ public void newInstancePersistsDefaultIsolationDecisionAfterPublication(@TempDir assertFalse(repository.hasInstance(id)); boolean isolated = repository.shouldIsolateNewInstance(false); assertTrue(isolated); - assertThrows( - NoSuchGameInstanceException.class, - () -> repository.ensureIsolatedRunningDirectory(id)); try (GameRepositoryDraft draft = repository.openDraft()) { draft.put(new GameInstanceManifest(id)); @@ -480,8 +476,11 @@ public void newInstancePersistsDefaultIsolationDecisionAfterPublication(@TempDir HMCLGameInstance instance = repository.getInstance(id); if (isolated) { - repository.ensureIsolatedRunningDirectory(id); + instance.enableIsolation(); } + FileSaver.waitForAllSaves(); + repository.refresh(); + instance = repository.getInstance(id); assertEquals(repository.getLayout().getInstanceRoot(id), instance.getRunDirectory()); assertEquals(repository.getLayout().getInstanceRoot(id).resolve("mods"), instance.getModsDirectory()); GameSettings.Instance instanceSettings = Objects.requireNonNull(instance.getSettings()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java index 808ac76ca2..188450a1e2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java @@ -76,7 +76,7 @@ public static GameComponentAnalyzer analyze(GameInstanceManifest.Resolved resolv public static GameComponentAnalyzer analyze(GameInstanceManifest manifest, @Nullable GameVersionNumber gameVersion) { if (manifest.inheritsFrom() != null) - throw new IllegalArgumentException("LibraryAnalyzer can only analyze independent game version"); + throw new IllegalArgumentException("GameComponentAnalyzer can only analyze independent game version"); return analyze(manifest, manifest, gameVersion); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameException.java deleted file mode 100644 index d45017edaa..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameException.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 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.game; - -/** - * - * @author huangyuhui - */ -public class GameException extends Exception { - - public GameException() { - } - - public GameException(String message) { - super(message); - } - - public GameException(String message, Throwable cause) { - super(message, cause); - } - - public GameException(Throwable cause) { - super(cause); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java index e2e4ae23cf..ea25b1a94b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java @@ -17,8 +17,6 @@ */ package org.jackhuang.hmcl.util; -import org.jackhuang.hmcl.download.ComponentRemoteVersion; -import org.jackhuang.hmcl.game.GameComponentType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -89,17 +87,6 @@ public void clear() { map.clear(); } - /// Returns whether the selected installation includes any non-vanilla component. - public boolean isInstallingModdedVersion() { - for (GameComponentType value : GameComponentType.MOD_LOADERS) { - if (get(value.getPatchId()) instanceof ComponentRemoteVersion) { - return true; - } - } - - return false; - } - @Override public String toString() { return "SettingsMap" + map; diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java deleted file mode 100644 index 11f0193f36..0000000000 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/SettingsMapTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 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.util; - -import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.ComponentRemoteVersion; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceManifest; -import org.jackhuang.hmcl.game.GameInstancePatch; -import org.jackhuang.hmcl.task.Task; -import org.jetbrains.annotations.NotNullByDefault; -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; -import java.time.Instant; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/// Tests for installer state stored in [SettingsMap]. -@NotNullByDefault -public final class SettingsMapTest { - /// Tests that vanilla game selection alone is not treated as a modded installation. - @Test - public void minecraftSelectionIsNotModdedInstallation() { - SettingsMap settings = new SettingsMap(); - settings.put(GameComponentType.GAME.getPatchId(), remoteVersion(GameComponentType.GAME)); - - assertFalse(settings.isInstallingModdedVersion()); - } - - /// Tests that selecting a non-vanilla installer is treated as a modded installation. - @Test - public void modLoaderSelectionIsModdedInstallation() { - SettingsMap settings = new SettingsMap(); - settings.put(GameComponentType.GAME.getPatchId(), remoteVersion(GameComponentType.GAME)); - settings.put(GameComponentType.FABRIC.getPatchId(), remoteVersion(GameComponentType.FABRIC)); - - assertTrue(settings.isInstallingModdedVersion()); - } - - /// Creates a minimal remote version for installer state tests. - private static ComponentRemoteVersion remoteVersion(GameComponentType componentType) { - return new ComponentRemoteVersion(componentType, "1.21.11", "test", Instant.EPOCH, List.of()) { - - @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseManifest, Path modsDirectory) { - throw new UnsupportedOperationException(); - } - }; - } -} From 7e9a01979ec37cf5bf8d40bbc8ad756a8b28f737 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 21:38:55 +0800 Subject: [PATCH 32/60] Enhance HMCLGameBuilder and DefaultGameBuilder with instance commitment handling and isolation settings --- .../jackhuang/hmcl/game/HMCLGameBuilder.java | 18 ++++++++++++------ .../hmcl/ui/download/DownloadPage.java | 17 ++++++++++++----- .../hmcl/download/DefaultGameBuilder.java | 14 +++++++++++++- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index 4aac21a288..55c2d60c2b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -26,6 +26,9 @@ /// Builds game instances and applies HMCL-specific post-installation settings. @NotNullByDefault public class HMCLGameBuilder extends DefaultGameBuilder { + /// Whether the current build must enable instance-local running-directory settings. + private boolean enableIsolation; + /// Creates a builder bound to the given dependency manager. /// /// @param dependencyManager the dependency manager for the target repository @@ -49,18 +52,21 @@ public HMCLDependencyManager getDependencyManager() { public Task buildAsync() { GameInstanceID instanceId = Objects.requireNonNull(id, "GameBuilder.id must be set"); HMCLGameRepository repository = getDependencyManager().getGameRepository(); - boolean enableIsolation = !repository.hasInstance(instanceId) + enableIsolation = !repository.hasInstance(instanceId) && repository.shouldIsolateNewInstance( components.keySet().stream().anyMatch(GameComponentType::isModLoader)); if (enableIsolation) { useInstanceRunDirectory(); } - Task buildTask = super.buildAsync(); - if (!enableIsolation) { - return buildTask; - } + return super.buildAsync(); + } - return buildTask.thenRunAsync(() -> repository.getInstance(instanceId).enableIsolation()); + /// {@inheritDoc} + @Override + protected void onInstanceCommitted(DefaultGameInstance instance) { + if (enableIsolation) { + ((HMCLGameInstance) instance).enableIsolation(); + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 6e334abc90..f5ccddcafc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -302,7 +302,11 @@ public void start(SettingsMap settings) { settings.put(GameComponentType.GAME.getPatchId(), gameVersion); } - private Task finishVersionDownloadingAsync(SettingsMap settings) { + /// Builds the selected instance and selects it after successful completion. + /// + /// @param settings the installer selections and target instance id + /// @return the builder task with its stage hints preserved as the outermost task wrapper + private Task finishVersionDownloadingAsync(SettingsMap settings) { HMCLGameBuilder builder = dependencyManager.newGameBuilder(); GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID); @@ -319,10 +323,13 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { builder.component(remoteVersion); }); - return builder.buildAsync() - .thenRunAsync( - Schedulers.javafx(), - () -> repository.setSelectedInstance(repository.getInstance(instanceId))); + Task buildTask = builder.buildAsync(); + buildTask.onDone().register(event -> { + if (!event.isFailed()) { + runInFX(() -> repository.setSelectedInstance(repository.getInstance(instanceId))); + } + }); + return buildTask; } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index efad533138..4b17567e7b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -131,7 +131,9 @@ public Task buildAsync() { .thenApplyAsync(minecraftJar -> { draft.put(resolved.launchManifest().withPatches(manifest.patches())); draft.putPrimaryJar(id, minecraftJar); - return draft.commit().getInstance(id); + DefaultGameInstance instance = draft.commit().getInstance(id); + onInstanceCommitted(instance); + return instance; }); }) .whenComplete(exception -> { @@ -142,6 +144,16 @@ public Task buildAsync() { .withStagesHints(hints); } + /// Performs repository-specific initialization after an instance is committed. + /// + /// This method is invoked after the repository has published the committed snapshot and before + /// the build task completes. The default implementation does nothing. An unchecked exception + /// prevents the task from completing successfully but does not roll back the committed instance. + /// + /// @param instance the committed instance from the published snapshot + protected void onInstanceCommitted(DefaultGameInstance instance) { + } + /// Aborts a draft whose initial instance reservation failed. /// /// @param draft the draft to abort From 4fca9069790458cdb90d8d060a47456a199ed0d7 Mon Sep 17 00:00:00 2001 From: Glavo Date: Wed, 26 Aug 2026 21:49:49 +0800 Subject: [PATCH 33/60] Decouple HMCLGameBuilder isolation from game repository Assisted-by: codex:gpt-5.6-sol --- .../jackhuang/hmcl/game/HMCLGameBuilder.java | 42 +++++++------------ .../hmcl/ui/download/DownloadPage.java | 7 ++++ 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index 55c2d60c2b..75f7865fc9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -18,16 +18,14 @@ package org.jackhuang.hmcl.game; import org.jackhuang.hmcl.download.DefaultGameBuilder; -import org.jackhuang.hmcl.task.Task; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNullByDefault; -import java.util.Objects; - /// Builds game instances and applies HMCL-specific post-installation settings. @NotNullByDefault public class HMCLGameBuilder extends DefaultGameBuilder { /// Whether the current build must enable instance-local running-directory settings. - private boolean enableIsolation; + private boolean isolationEnabled; /// Creates a builder bound to the given dependency manager. /// @@ -36,36 +34,24 @@ public HMCLGameBuilder(HMCLDependencyManager dependencyManager) { super(dependencyManager); } - /// {@inheritDoc} - @Override - public HMCLDependencyManager getDependencyManager() { - return (HMCLDependencyManager) super.getDependencyManager(); - } - - /// {@inheritDoc} + /// Requests instance-local running-directory settings for the instance built by this builder. /// - /// For an unregistered instance, applies the repository's default isolation policy to the - /// configured component set. When isolation is selected, component-provided run-directory - /// files are installed under the instance root and the corresponding instance setting is - /// enabled after publication. Existing instances retain their current run-directory settings. - @Override - public Task buildAsync() { - GameInstanceID instanceId = Objects.requireNonNull(id, "GameBuilder.id must be set"); - HMCLGameRepository repository = getDependencyManager().getGameRepository(); - enableIsolation = !repository.hasInstance(instanceId) - && repository.shouldIsolateNewInstance( - components.keySet().stream().anyMatch(GameComponentType::isModLoader)); - if (enableIsolation) { - useInstanceRunDirectory(); - } - - return super.buildAsync(); + /// Component-provided run-directory files are installed under the instance root, and the + /// corresponding instance setting is enabled after publication when it can be written safely. + /// A read-only instance retains its existing setting. + /// + /// @return this builder + @Contract("-> this") + public HMCLGameBuilder enableIsolation() { + useInstanceRunDirectory(); + isolationEnabled = true; + return this; } /// {@inheritDoc} @Override protected void onInstanceCommitted(DefaultGameInstance instance) { - if (enableIsolation) { + if (isolationEnabled) { ((HMCLGameInstance) instance).enableIsolation(); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index f5ccddcafc..9e3861c4ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -323,6 +323,13 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { builder.component(remoteVersion); }); + boolean modded = GameComponentType.MOD_LOADERS.stream() + .anyMatch(componentType -> + settings.get(componentType.getPatchId()) instanceof ComponentRemoteVersion); + if (repository.shouldIsolateNewInstance(modded)) { + builder.enableIsolation(); + } + Task buildTask = builder.buildAsync(); buildTask.onDone().register(event -> { if (!event.isFailed()) { From e827a1414e912ace620b6cc955a18a6f30fdbc9a Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 27 Aug 2026 20:26:58 +0800 Subject: [PATCH 34/60] Replace useInstanceRunDirectory with enableIsolation in game builder tasks for improved instance management --- .../jackhuang/hmcl/game/HMCLGameBuilder.java | 21 +++---------------- .../hmcl/game/HMCLModpackInstallTask.java | 2 +- .../hmcl/download/DefaultGameBuilder.java | 21 ++++++++++++++----- .../jackhuang/hmcl/download/GameBuilder.java | 16 +++++--------- .../hmcl/modpack/curse/CurseInstallTask.java | 2 +- .../mcbbs/McbbsModpackLocalInstallTask.java | 2 +- .../modpack/modrinth/ModrinthInstallTask.java | 2 +- .../server/ServerModpackLocalInstallTask.java | 2 +- .../ServerModpackRemoteInstallTask.java | 2 +- 9 files changed, 30 insertions(+), 40 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index 75f7865fc9..d5b74a1bec 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -18,15 +18,11 @@ package org.jackhuang.hmcl.game; import org.jackhuang.hmcl.download.DefaultGameBuilder; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNullByDefault; /// Builds game instances and applies HMCL-specific post-installation settings. @NotNullByDefault public class HMCLGameBuilder extends DefaultGameBuilder { - /// Whether the current build must enable instance-local running-directory settings. - private boolean isolationEnabled; - /// Creates a builder bound to the given dependency manager. /// /// @param dependencyManager the dependency manager for the target repository @@ -34,21 +30,10 @@ public HMCLGameBuilder(HMCLDependencyManager dependencyManager) { super(dependencyManager); } - /// Requests instance-local running-directory settings for the instance built by this builder. - /// - /// Component-provided run-directory files are installed under the instance root, and the - /// corresponding instance setting is enabled after publication when it can be written safely. - /// A read-only instance retains its existing setting. - /// - /// @return this builder - @Contract("-> this") - public HMCLGameBuilder enableIsolation() { - useInstanceRunDirectory(); - isolationEnabled = true; - return this; - } - /// {@inheritDoc} + /// + /// When isolation was enabled, persists the corresponding HMCL instance setting so subsequent + /// launches use the instance root. A read-only instance retains its existing setting. @Override protected void onInstanceCommitted(DefaultGameInstance instance) { if (isolationEnabled) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 8c6b7c19c6..c2712f8bf9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -57,7 +57,7 @@ public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpa dependents.add(dependency.newGameBuilder() .id(this.instanceId) - .useInstanceRunDirectory() + .enableIsolation() .component(GameComponentType.GAME, modpack.getGameVersion()) .buildAsync()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 4b17567e7b..b8d34ad5fb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -20,6 +20,7 @@ import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.task.Task; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; @@ -40,6 +41,9 @@ public class DefaultGameBuilder extends GameBuilder { /// Dependency manager used for component installation and repository access. private final DefaultDependencyManager dependencyManager; + /// Whether instance isolation was requested for this build. + protected boolean isolationEnabled; + /// Creates a builder bound to the given dependency manager. /// /// @param dependencyManager the dependency manager for the target repository @@ -54,6 +58,14 @@ public DefaultDependencyManager getDependencyManager() { return dependencyManager; } + /// {@inheritDoc} + @Override + @Contract("-> this") + public DefaultGameBuilder enableIsolation() { + isolationEnabled = true; + return this; + } + /// {@inheritDoc} /// /// Retains an unpublished working manifest, installs the configured game and optional loaders, @@ -87,7 +99,6 @@ public Task buildAsync() { DefaultGameRepository repository = dependencyManager.getGameRepository(); - //noinspection resource DefaultGameRepositoryDraft draft = repository.openDraft(); GameInstanceManifest initialManifest = new GameInstanceManifest(id); try { @@ -97,10 +108,10 @@ public Task buildAsync() { throw new IllegalStateException("Cannot reserve game instance " + id, e); } - Path runDirectory = repository.hasInstance(id) - ? repository.getInstance(id).getRunDirectory() - : useInstanceRunDirectory - ? repository.getLayout().getInstanceRoot(id) + Path runDirectory = isolationEnabled + ? repository.getLayout().getInstanceRoot(id) + : repository.hasInstance(id) + ? repository.getInstance(id).getRunDirectory() : repository.getBaseDirectory(); Path modsDirectory = runDirectory.resolve("mods"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java index ed98ec703e..1f8897c0e5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java @@ -35,9 +35,6 @@ public abstract class GameBuilder { protected @Nullable GameInstanceID id; protected final EnumMap components = new EnumMap<>(GameComponentType.class); - /// Whether new-instance component installers write run-directory content under the instance root. - protected boolean useInstanceRunDirectory; - /// The new game instance id, for `.minecraft/`. /// /// @param id the instance id of new game instance. @@ -46,18 +43,15 @@ public GameBuilder id(GameInstanceID id) { return this; } - /// Uses the instance root as the run directory while installing a new instance. + /// Enables instance isolation for the built instance. /// - /// This affects files installed by components, such as loader-provided mods. It does not write - /// instance settings; callers must persist the corresponding isolation setting after the - /// instance is published. + /// Component-provided run-directory files, such as loader-provided mods, are installed under + /// the instance root. The concrete builder must also ensure subsequent launches use that same + /// directory. /// /// @return this builder @Contract("-> this") - public GameBuilder useInstanceRunDirectory() { - useInstanceRunDirectory = true; - return this; - } + public abstract GameBuilder enableIsolation(); @Contract("_, _ -> this") public GameBuilder component(GameComponentType componentType, String version) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java index de224276ea..51bbb17e6f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java @@ -86,7 +86,7 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile GameBuilder builder = dependencyManager.newGameBuilder() .id(instanceId) - .useInstanceRunDirectory() + .enableIsolation() .component(GameComponentType.GAME, manifest.minecraft().gameVersion()); for (CurseManifestModLoader modLoader : manifest.minecraft().modLoaders()) { if (modLoader.id().startsWith("forge-")) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index 190ef28ef3..c86cb39093 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -66,7 +66,7 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, GameBuilder builder = dependencyManager.newGameBuilder() .id(instanceId) - .useInstanceRunDirectory(); + .enableIsolation(); for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java index 25bae62aeb..b22653f376 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java @@ -71,7 +71,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF GameBuilder builder = dependencyManager.newGameBuilder() .id(instanceId) - .useInstanceRunDirectory(); + .enableIsolation(); builder.component(GameComponentType.GAME, manifest.getGameVersion()); for (Map.Entry modLoader : manifest.getDependencies().entrySet()) { switch (modLoader.getKey()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java index 0cde3a752f..4472202e15 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java @@ -62,7 +62,7 @@ public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, GameBuilder builder = dependencyManager.newGameBuilder() .id(instanceId) - .useInstanceRunDirectory(); + .enableIsolation(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java index 46f1da507c..929b08cbe7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java @@ -56,7 +56,7 @@ public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager GameBuilder builder = dependencyManager.newGameBuilder() .id(instanceId) - .useInstanceRunDirectory(); + .enableIsolation(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) From 5202ac5bcd43f1f7938b9b208a03d0063d31d973 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 27 Aug 2026 20:40:59 +0800 Subject: [PATCH 35/60] Add support for existing game instances in game builder creation --- .../jackhuang/hmcl/game/HMCLDependencyManager.java | 11 ++++++++++- .../java/org/jackhuang/hmcl/game/HMCLGameBuilder.java | 5 +++-- .../hmcl/download/DefaultDependencyManager.java | 10 +++++++++- .../jackhuang/hmcl/download/DefaultGameBuilder.java | 6 +++++- .../jackhuang/hmcl/download/DependencyManager.java | 5 +++++ 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java index 54f3db28dd..fa043f4e26 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.download.DefaultCacheRepository; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.DefaultGameBuilder; import org.jackhuang.hmcl.download.DownloadProvider; import org.jetbrains.annotations.NotNullByDefault; @@ -46,6 +47,14 @@ public HMCLGameRepository getGameRepository() { /// {@inheritDoc} @Override public HMCLGameBuilder newGameBuilder() { - return new HMCLGameBuilder(this); + return new HMCLGameBuilder(this, null); + } + + @Override + public HMCLGameBuilder newGameBuilder(GameInstance instance) { + if (instance.getRepository() != this.getGameRepository()) + throw new IllegalArgumentException("Game instance and dependency manager belong to different repositories"); + + return new HMCLGameBuilder(this, (HMCLGameInstance) instance); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index d5b74a1bec..4815957023 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.download.DefaultGameBuilder; import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; /// Builds game instances and applies HMCL-specific post-installation settings. @NotNullByDefault @@ -26,8 +27,8 @@ public class HMCLGameBuilder extends DefaultGameBuilder { /// Creates a builder bound to the given dependency manager. /// /// @param dependencyManager the dependency manager for the target repository - public HMCLGameBuilder(HMCLDependencyManager dependencyManager) { - super(dependencyManager); + public HMCLGameBuilder(HMCLDependencyManager dependencyManager, @Nullable HMCLGameInstance instance) { + super(dependencyManager, instance); } /// {@inheritDoc} 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 6858bab4fc..afd2b123d0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -89,7 +89,15 @@ public DefaultCacheRepository getCacheRepository() { @Override public DefaultGameBuilder newGameBuilder() { - return new DefaultGameBuilder(this); + return new DefaultGameBuilder(this, null); + } + + @Override + public DefaultGameBuilder newGameBuilder(GameInstance instance) { + if (instance.getRepository() != this.getGameRepository()) + throw new IllegalArgumentException("Game instance and dependency manager belong to different repositories"); + + return new DefaultGameBuilder(this, (DefaultGameInstance) instance); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index b8d34ad5fb..00e0477f7c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -41,14 +41,18 @@ public class DefaultGameBuilder extends GameBuilder { /// Dependency manager used for component installation and repository access. private final DefaultDependencyManager dependencyManager; + private final @Nullable DefaultGameInstance instance; + /// Whether instance isolation was requested for this build. protected boolean isolationEnabled; /// Creates a builder bound to the given dependency manager. /// /// @param dependencyManager the dependency manager for the target repository - public DefaultGameBuilder(DefaultDependencyManager dependencyManager) { + /// @param instance the existing game instance, or null to create a new one + public DefaultGameBuilder(DefaultDependencyManager dependencyManager, @Nullable DefaultGameInstance instance) { this.dependencyManager = dependencyManager; + this.instance = instance; } /// Returns the dependency manager used by this builder. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java index aff65644aa..d73ca6a447 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java @@ -75,6 +75,11 @@ public interface DependencyManager { /// @return a new game builder GameBuilder newGameBuilder(); + /// Creates a builder for modifying an existing game instance and optional loaders. + /// + /// @param instance the existing game instance to update + GameBuilder newGameBuilder(GameInstance instance); + /// Returns a registered remote-version list. /// /// @param componentType the component type, such as `game`, `forge`, or `optifine` From c89e0fcccd617e33888b8b217fbcf4db5d680d81 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 27 Aug 2026 21:45:21 +0800 Subject: [PATCH 36/60] Refactor game instance handling in installation tasks to support existing instances --- .../hmcl/game/HMCLDependencyManager.java | 9 +- .../jackhuang/hmcl/game/HMCLGameBuilder.java | 14 ++- .../hmcl/game/HMCLModpackInstallTask.java | 17 +++- .../hmcl/ui/download/DownloadPage.java | 4 +- .../org/jackhuang/hmcl/ui/main/MainPage.java | 3 +- .../download/DefaultDependencyManager.java | 17 ++-- .../hmcl/download/DefaultGameBuilder.java | 95 ++++++++++++++----- .../hmcl/download/DependencyManager.java | 17 +++- .../jackhuang/hmcl/download/GameBuilder.java | 35 +++---- .../hmcl/modpack/curse/CurseInstallTask.java | 80 ++++++++++------ .../mcbbs/McbbsModpackLocalInstallTask.java | 16 ++-- .../modpack/modrinth/ModrinthInstallTask.java | 60 +++++++++--- .../server/ServerModpackCompletionTask.java | 2 +- .../server/ServerModpackLocalInstallTask.java | 15 +-- .../ServerModpackRemoteInstallTask.java | 13 ++- .../hmcl/game/DefaultGameInstanceTest.java | 54 +++++++++++ 16 files changed, 316 insertions(+), 135 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java index fa043f4e26..44f896b9c4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java @@ -19,7 +19,6 @@ import org.jackhuang.hmcl.download.DefaultCacheRepository; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.DefaultGameBuilder; import org.jackhuang.hmcl.download.DownloadProvider; import org.jetbrains.annotations.NotNullByDefault; @@ -46,14 +45,14 @@ public HMCLGameRepository getGameRepository() { /// {@inheritDoc} @Override - public HMCLGameBuilder newGameBuilder() { - return new HMCLGameBuilder(this, null); + public HMCLGameBuilder newGameBuilder(GameInstanceID instanceId) { + return new HMCLGameBuilder(this, instanceId); } + /// {@inheritDoc} @Override public HMCLGameBuilder newGameBuilder(GameInstance instance) { - if (instance.getRepository() != this.getGameRepository()) - throw new IllegalArgumentException("Game instance and dependency manager belong to different repositories"); + validateGameInstance(instance); return new HMCLGameBuilder(this, (HMCLGameInstance) instance); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index 4815957023..6b97790407 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -19,15 +19,23 @@ import org.jackhuang.hmcl.download.DefaultGameBuilder; import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; /// Builds game instances and applies HMCL-specific post-installation settings. @NotNullByDefault public class HMCLGameBuilder extends DefaultGameBuilder { - /// Creates a builder bound to the given dependency manager. + /// Creates a builder for installing a new HMCL instance. /// /// @param dependencyManager the dependency manager for the target repository - public HMCLGameBuilder(HMCLDependencyManager dependencyManager, @Nullable HMCLGameInstance instance) { + /// @param instanceId the id of the new instance + public HMCLGameBuilder(HMCLDependencyManager dependencyManager, GameInstanceID instanceId) { + super(dependencyManager, instanceId); + } + + /// Creates a builder for replacing the components of an existing HMCL instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param instance the existing instance selecting update mode and the target id + public HMCLGameBuilder(HMCLDependencyManager dependencyManager, HMCLGameInstance instance) { super(dependencyManager, instance); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index c2712f8bf9..3e7b8d3928 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -19,6 +19,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.modpack.MinecraftInstanceTask; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackConfiguration; @@ -26,6 +27,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; @@ -52,20 +54,25 @@ public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpa Path run = repository.getLayout().getInstanceRoot(this.instanceId); Path json = repository.getLayout().getModpackConfigurationFile(this.instanceId); - if (repository.hasInstance(this.instanceId) && Files.notExists(json)) + @Nullable HMCLGameInstance existingInstance = repository.findInstance(this.instanceId); + if (existingInstance != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists"); - dependents.add(dependency.newGameBuilder() - .id(this.instanceId) + GameBuilder builder = existingInstance == null + ? dependency.newGameBuilder(this.instanceId) + : dependency.newGameBuilder(existingInstance); + dependents.add(builder .enableIsolation() .component(GameComponentType.GAME, modpack.getGameVersion()) .buildAsync()); onDone().register(event -> { - if (event.isFailed()) repository.removeInstanceFromDisk(this.instanceId); + if (existingInstance == null && event.isFailed()) { + repository.removeInstanceFromDisk(this.instanceId); + } }); - ModpackConfiguration config = null; + @Nullable ModpackConfiguration config = null; try { if (Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 9e3861c4ac..ce7f1f1d7a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -307,14 +307,12 @@ public void start(SettingsMap settings) { /// @param settings the installer selections and target instance id /// @return the builder task with its stage hints preserved as the outermost task wrapper private Task finishVersionDownloadingAsync(SettingsMap settings) { - HMCLGameBuilder builder = dependencyManager.newGameBuilder(); - GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID); if (instanceId == null) { throw new IllegalStateException("Instance ID is not set"); } - builder.id(instanceId); + HMCLGameBuilder builder = dependencyManager.newGameBuilder(instanceId); builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion()); settings.asStringMap().forEach((key, value) -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 3b0e911d04..8113886f90 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -379,8 +379,7 @@ private void launchNoGame() { instanceHolder.value = instanceId; - return dependency.newGameBuilder() - .id(instanceId) + return dependency.newGameBuilder(instanceId) .component(GameComponentType.GAME, gameVersion) .buildAsync(); }) 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 afd2b123d0..6f8165f12a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -88,14 +88,13 @@ public DefaultCacheRepository getCacheRepository() { } @Override - public DefaultGameBuilder newGameBuilder() { - return new DefaultGameBuilder(this, null); + public DefaultGameBuilder newGameBuilder(GameInstanceID instanceId) { + return new DefaultGameBuilder(this, instanceId); } @Override public DefaultGameBuilder newGameBuilder(GameInstance instance) { - if (instance.getRepository() != this.getGameRepository()) - throw new IllegalArgumentException("Game instance and dependency manager belong to different repositories"); + validateGameInstance(instance); return new DefaultGameBuilder(this, (DefaultGameInstance) instance); } @@ -186,14 +185,14 @@ public Task checkPatchCompletionAsync( }); } - /// Installs a component into an unpublished new instance without constructing a + /// Installs a component into an unpublished working manifest without constructing a /// [GameInstance]. /// /// @param baseManifest the working manifest for this step /// @param modsDirectory the mods directory to use during installation /// @param componentVersion the remote component to install /// @return the task producing the updated manifest (not yet committed) - Task installNewInstanceComponentAsync( + Task installUnpublishedComponentAsync( GameInstanceManifest baseManifest, Path modsDirectory, ComponentRemoteVersion componentVersion) { @@ -206,7 +205,7 @@ Task installNewInstanceComponentAsync( componentVersion.getSelfVersion())); } - /// Resolves and installs a component into an unpublished new instance. + /// Resolves and installs a component into an unpublished working manifest. /// /// @param baseManifest the working manifest for this step /// @param modsDirectory the mods directory to use during installation @@ -214,7 +213,7 @@ Task installNewInstanceComponentAsync( /// @param componentType the component list id, such as `game` or `forge` /// @param componentVersion the component version id /// @return the installation task - Task installNewInstanceComponentAsync( + Task installUnpublishedComponentAsync( GameInstanceManifest baseManifest, Path modsDirectory, String gameVersion, @@ -222,7 +221,7 @@ Task installNewInstanceComponentAsync( String componentVersion) { ComponentVersionList versionList = getVersionList(componentType); return versionList.loadAsync(gameVersion) - .thenComposeAsync(() -> installNewInstanceComponentAsync( + .thenComposeAsync(() -> installUnpublishedComponentAsync( baseManifest, modsDirectory, versionList.getVersion(gameVersion, componentVersion) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 00e0477f7c..0dffa53397 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -28,10 +28,12 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Map; -import java.util.Objects; -/// Builds a new game instance in an exclusive [GameRepositoryDraft], installs its components, and -/// publishes the completed instance once. +/// Installs a new game instance or replaces an existing instance in an exclusive +/// [GameRepositoryDraft], then publishes the completed instance once. +/// +/// Replacement starts from an empty manifest with the target id and installs only the configured +/// components. It retains the existing run directory unless isolation is explicitly enabled. /// /// Shared libraries, assets, and download caches may remain after failure. The instance manifest /// and primary JAR enter the instance tree only when the draft commits. @@ -41,18 +43,35 @@ public class DefaultGameBuilder extends GameBuilder { /// Dependency manager used for component installation and repository access. private final DefaultDependencyManager dependencyManager; - private final @Nullable DefaultGameInstance instance; + /// Id of the instance to create or replace. + private final GameInstanceID instanceId; + + /// Whether this builder must replace a currently published instance. + private final boolean updating; /// Whether instance isolation was requested for this build. protected boolean isolationEnabled; - /// Creates a builder bound to the given dependency manager. + /// Creates a builder for installing a new instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param instanceId the id of the new instance + public DefaultGameBuilder(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + this.dependencyManager = dependencyManager; + this.instanceId = instanceId; + this.updating = false; + } + + /// Creates a builder for replacing the components of an existing instance. /// /// @param dependencyManager the dependency manager for the target repository - /// @param instance the existing game instance, or null to create a new one - public DefaultGameBuilder(DefaultDependencyManager dependencyManager, @Nullable DefaultGameInstance instance) { + /// @param instance the existing instance selecting update mode and the target id + /// @throws IllegalArgumentException if `instance` belongs to another repository + public DefaultGameBuilder(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) { + dependencyManager.validateGameInstance(instance); this.dependencyManager = dependencyManager; - this.instance = instance; + this.instanceId = instance.getId(); + this.updating = true; } /// Returns the dependency manager used by this builder. @@ -77,10 +96,11 @@ public DefaultGameBuilder enableIsolation() { /// cancellation aborts the draft. /// /// @return the build task - /// @throws NullPointerException if [#id] was not set + /// @throws IllegalStateException if the configured game version is absent, the target of a new + /// installation already exists, or the target of an update no + /// longer exists @Override public Task buildAsync() { - GameInstanceID id = Objects.requireNonNull(this.id, "GameBuilder.id must be set"); @Nullable String gameVersion = (String) components.get(GameComponentType.GAME); if (gameVersion == null) throw new IllegalStateException("GameBuilder.gameVersion must be set"); @@ -104,22 +124,27 @@ public Task buildAsync() { DefaultGameRepository repository = dependencyManager.getGameRepository(); DefaultGameRepositoryDraft draft = repository.openDraft(); - GameInstanceManifest initialManifest = new GameInstanceManifest(id); + @Nullable DefaultGameInstance currentInstance; + GameInstanceManifest initialManifest = new GameInstanceManifest(instanceId); try { + currentInstance = resolveCurrentInstance(draft.getBaseSnapshot()); draft.put(initialManifest); - } catch (IOException | RuntimeException e) { - abortAfterReservationFailure(draft, e); - throw new IllegalStateException("Cannot reserve game instance " + id, e); + } catch (IOException e) { + abortAfterSetupFailure(draft, e); + throw new IllegalStateException("Cannot reserve game instance " + instanceId, e); + } catch (RuntimeException e) { + abortAfterSetupFailure(draft, e); + throw e; } Path runDirectory = isolationEnabled - ? repository.getLayout().getInstanceRoot(id) - : repository.hasInstance(id) - ? repository.getInstance(id).getRunDirectory() + ? repository.getLayout().getInstanceRoot(instanceId) + : currentInstance != null + ? currentInstance.getRunDirectory() : repository.getBaseDirectory(); Path modsDirectory = runDirectory.resolve("mods"); - Task libraryTask = dependencyManager.installNewInstanceComponentAsync( + Task libraryTask = dependencyManager.installUnpublishedComponentAsync( initialManifest, modsDirectory, gameVersion, GameComponentType.GAME, gameVersion); for (Map.Entry entry : components.entrySet()) { @@ -129,11 +154,11 @@ public Task buildAsync() { if (entry.getValue() instanceof ComponentRemoteVersion remoteVersion) { libraryTask = libraryTask.thenComposeAsync(manifest -> - dependencyManager.installNewInstanceComponentAsync( + dependencyManager.installUnpublishedComponentAsync( manifest, modsDirectory, remoteVersion)); } else if (entry.getValue() instanceof String version) { libraryTask = libraryTask.thenComposeAsync(manifest -> - dependencyManager.installNewInstanceComponentAsync( + dependencyManager.installUnpublishedComponentAsync( manifest, modsDirectory, gameVersion, componentType, version)); } else { throw new AssertionError("Unexpected version type: " + entry.getValue().getClass()); @@ -145,8 +170,8 @@ public Task buildAsync() { return new GameDownloadTask(dependencyManager, resolved.launchManifest()) .thenApplyAsync(minecraftJar -> { draft.put(resolved.launchManifest().withPatches(manifest.patches())); - draft.putPrimaryJar(id, minecraftJar); - DefaultGameInstance instance = draft.commit().getInstance(id); + draft.putPrimaryJar(instanceId, minecraftJar); + DefaultGameInstance instance = draft.commit().getInstance(instanceId); onInstanceCommitted(instance); return instance; }); @@ -159,6 +184,26 @@ public Task buildAsync() { .withStagesHints(hints); } + /// Resolves and validates the declared installation mode against the draft base snapshot. + /// + /// @param snapshot the immutable repository state captured by the draft + /// @return the current update target, or `null` for a valid new installation + /// @throws IllegalStateException if the declared target state does not match the snapshot + private @Nullable DefaultGameInstance resolveCurrentInstance(DefaultGameRepositorySnapshot snapshot) { + @Nullable DefaultGameInstance currentInstance = snapshot.findInstance(instanceId); + if (!updating) { + if (currentInstance != null) { + throw new IllegalStateException("Game instance already exists: " + instanceId); + } + return null; + } + + if (currentInstance == null) { + throw new IllegalStateException("Game instance no longer exists: " + instanceId); + } + return currentInstance; + } + /// Performs repository-specific initialization after an instance is committed. /// /// This method is invoked after the repository has published the committed snapshot and before @@ -169,11 +214,11 @@ public Task buildAsync() { protected void onInstanceCommitted(DefaultGameInstance instance) { } - /// Aborts a draft whose initial instance reservation failed. + /// Aborts a draft after target validation or initial reservation fails. /// /// @param draft the draft to abort - /// @param failure the reservation failure that receives any cleanup failure as suppressed - private static void abortAfterReservationFailure( + /// @param failure the setup failure that receives any cleanup failure as suppressed + private static void abortAfterSetupFailure( DefaultGameRepositoryDraft draft, Throwable failure) { try { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java index d73ca6a447..c0b2393206 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstance; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.task.Task; @@ -70,14 +71,24 @@ public interface DependencyManager { /// @throws IllegalArgumentException if `instance` belongs to another repository Task checkPatchCompletionAsync(GameInstance instance, GameInstanceManifest manifest, boolean integrityCheck); - /// Creates a builder for installing a new game instance and optional loaders. + /// Creates a builder for installing a new game instance and optional components. /// + /// The target id must be absent when the builder starts the installation. + /// + /// @param instanceId the id of the new instance /// @return a new game builder - GameBuilder newGameBuilder(); + GameBuilder newGameBuilder(GameInstanceID instanceId); - /// Creates a builder for modifying an existing game instance and optional loaders. + /// Creates a builder for replacing the components of an existing game instance. + /// + /// The instance selects update mode and fixes the target repository and id. The currently + /// published instance with that id is used when the builder starts the update, so the supplied + /// snapshot-bound object need not remain current. The resulting manifest is rebuilt from the + /// components configured on the builder; components that are not configured are not retained. /// /// @param instance the existing game instance to update + /// @return a game builder targeting the existing instance + /// @throws IllegalArgumentException if `instance` belongs to another repository GameBuilder newGameBuilder(GameInstance instance); /// Returns a registered remote-version list. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java index 1f8897c0e5..9e8077d971 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java @@ -18,31 +18,21 @@ package org.jackhuang.hmcl.download; import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.task.Task; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNullByDefault; -import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.EnumMap; -/// The builder which provide a task to build Minecraft environment. +/// Configures the components used to install or update a game instance. /// /// @author huangyuhui @NotNullByDefault public abstract class GameBuilder { - protected @Nullable GameInstanceID id; + /// Components to install, keyed by their manifest patch type. protected final EnumMap components = new EnumMap<>(GameComponentType.class); - /// The new game instance id, for `.minecraft/`. - /// - /// @param id the instance id of new game instance. - public GameBuilder id(GameInstanceID id) { - this.id = Objects.requireNonNull(id); - return this; - } - /// Enables instance isolation for the built instance. /// /// Component-provided run-directory files, such as loader-provided mods, are installed under @@ -53,20 +43,33 @@ public GameBuilder id(GameInstanceID id) { @Contract("-> this") public abstract GameBuilder enableIsolation(); + /// Configures a component by its remote version id. + /// + /// Reconfiguring the same component type replaces its previous value. + /// + /// @param componentType the component type + /// @param version the remote version id + /// @return this builder @Contract("_, _ -> this") public GameBuilder component(GameComponentType componentType, String version) { components.put(componentType, version); return this; } + /// Configures a component using an already resolved remote version. + /// + /// Reconfiguring the same component type replaces its previous value. + /// + /// @param remoteVersion the remote component version + /// @return this builder @Contract("_ -> this") public GameBuilder component(ComponentRemoteVersion remoteVersion) { components.put(remoteVersion.getComponentType(), remoteVersion); return this; } - /** - * @return the task that can build the whole Minecraft environment - */ + /// Creates the task that installs the configured components and publishes the target instance. + /// + /// @return the instance build task public abstract Task buildAsync(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java index 51bbb17e6f..d71221984b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.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.game.DefaultGameInstance; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceID; @@ -31,6 +32,7 @@ import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.net.URI; @@ -40,11 +42,9 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -/** - * Install a downloaded CurseForge modpack. - * - * @author huangyuhui - */ +/// Install a downloaded CurseForge modpack. +/// +/// @author huangyuhui public final class CurseInstallTask extends Task { private final DefaultDependencyManager dependencyManager; @@ -53,23 +53,37 @@ public final class CurseInstallTask extends Task { private final Modpack modpack; private final CurseManifest manifest; private final GameInstanceID instanceId; - private final String iconUrl; + + /// Optional remote icon URL supplied by the install source. + private final @Nullable String iconUrl; private final Path run; - private final ModpackConfiguration config; - private String iconExt; - private Task downloadIconTask; + + /// Previous modpack configuration when updating, or `null` for a new installation. + private final @Nullable ModpackConfiguration config; + + /// Validated extension of the scheduled icon download, or `null` when no icon is scheduled. + private @Nullable String iconExt; + + /// Scheduled icon download corresponding to [#iconExt], or `null` when absent. + private @Nullable Task downloadIconTask; private final List> dependents = new ArrayList<>(4); private final List> dependencies = new ArrayList<>(1); - /** - * Constructor. - * - * @param dependencyManager the dependency manager. - * @param zipFile the CurseForge modpack file. - * @param manifest The manifest content of given CurseForge modpack. - * @param instanceId the new instance ID - */ - public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, GameInstanceID instanceId, String iconUrl) { + /// Creates a task that installs or updates a CurseForge modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the CurseForge modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the CurseForge manifest + /// @param instanceId the target instance id; an existing modpack instance is updated + /// @param iconUrl the optional icon URL, or `null` + public CurseInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + CurseManifest manifest, + GameInstanceID instanceId, + @Nullable String iconUrl) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; @@ -81,12 +95,14 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - if (repository.hasInstance(instanceId) && Files.notExists(json)) + @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); + if (existingInstance != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder() - .id(instanceId) - .enableIsolation() + GameBuilder builder = existingInstance == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(existingInstance); + builder.enableIsolation() .component(GameComponentType.GAME, manifest.minecraft().gameVersion()); for (CurseManifestModLoader modLoader : manifest.minecraft().modLoaders()) { if (modLoader.id().startsWith("forge-")) { @@ -100,15 +116,15 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile dependents.add(builder.buildAsync()); onDone().register(event -> { - Exception ex = event.getTask().getException(); - if (event.isFailed()) { + @Nullable Exception ex = event.getTask().getException(); + if (existingInstance == null && event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { repository.removeInstanceFromDisk(instanceId); } } }); - ModpackConfiguration config = null; + @Nullable ModpackConfiguration config = null; try { if (Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(CurseManifest.class)); @@ -122,7 +138,7 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), any -> true, config).withStage("hmcl.modpack")); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), manifest, CurseModpackProvider.INSTANCE, manifest.name(), manifest.version(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack")); - URI iconUri = NetworkUtils.toURIOrNull(iconUrl); + @Nullable URI iconUri = NetworkUtils.toURIOrNull(iconUrl); if (iconUri != null) { String ext = FileUtils.getExtension(StringUtils.substringAfter(iconUri.getPath(), '/')).toLowerCase(Locale.ROOT); if (Modpack.SUPPORTED_ICON_EXTS.contains(ext)) { @@ -177,10 +193,10 @@ public void execute() throws Exception { // resolves those file names and writes the enriched manifest to // manifest.json, so read from there when available. Path oldManifestFile = repository.getLayout().getInstanceRoot(instanceId).resolve("manifest.json"); - List oldFiles = config.getManifest().files(); + @Nullable List oldFiles = config.getManifest().files(); if (Files.exists(oldManifestFile)) { try { - CurseManifest oldManifest = JsonUtils.fromJsonFile(oldManifestFile, CurseManifest.class); + @Nullable CurseManifest oldManifest = JsonUtils.fromJsonFile(oldManifestFile, CurseManifest.class); if (oldManifest != null) { oldFiles = oldManifest.files(); } @@ -204,9 +220,13 @@ public void execute() throws Exception { Files.createDirectories(root); JsonUtils.writeToJsonFile(root.resolve("manifest.json"), manifest); - if (iconExt != null && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) { + @Nullable String iconExtension = iconExt; + @Nullable Task iconTask = downloadIconTask; + if (iconExtension != null + && iconTask != null + && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) { try { - Files.copy(downloadIconTask.getResult(), root.resolve("icon." + iconExt)); + Files.copy(iconTask.getResult(), root.resolve("icon." + iconExtension)); } catch (Exception e) { LOG.warning("Failed to copy modpack icon", e); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index c86cb39093..59254acdcf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -60,13 +60,15 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - if (repository.hasInstance(instanceId) && Files.notExists(json)) + @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); + if (existingInstance != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - this.update = repository.hasInstance(instanceId); + this.update = existingInstance != null; - GameBuilder builder = dependencyManager.newGameBuilder() - .id(instanceId) - .enableIsolation(); + GameBuilder builder = existingInstance == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(existingInstance); + builder.enableIsolation(); for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) @@ -75,11 +77,11 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, dependents.add(builder.buildAsync()); onDone().register(event -> { - if (event.isFailed()) + if (!update && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); - ModpackConfiguration config = null; + @Nullable ModpackConfiguration config = null; try { if (Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(McbbsModpackManifest.class)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java index b22653f376..dcb9d0e5c9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.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.game.DefaultGameInstance; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceID; @@ -30,6 +31,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.net.URI; @@ -47,15 +49,37 @@ public class ModrinthInstallTask extends Task { private final Modpack modpack; private final ModrinthManifest manifest; private final GameInstanceID instanceId; - private final String iconUrl; + + /// Optional remote icon URL supplied by the install source. + private final @Nullable String iconUrl; private final Path run; - private final ModpackConfiguration config; - private String iconExt; - private Task downloadIconTask; + + /// Previous modpack configuration when updating, or `null` for a new installation. + private final @Nullable ModpackConfiguration config; + + /// Validated extension of the scheduled icon download, or `null` when no icon is scheduled. + private @Nullable String iconExt; + + /// Scheduled icon download corresponding to [#iconExt], or `null` when absent. + private @Nullable Task downloadIconTask; private final List> dependents = new ArrayList<>(4); private final List> dependencies = new ArrayList<>(1); - public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ModrinthManifest manifest, GameInstanceID instanceId, String iconUrl) { + /// Creates a task that installs or updates a Modrinth modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the Modrinth modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the Modrinth index + /// @param instanceId the target instance id; an existing modpack instance is updated + /// @param iconUrl the optional icon URL, or `null` + public ModrinthInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + ModrinthManifest manifest, + GameInstanceID instanceId, + @Nullable String iconUrl) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; @@ -66,12 +90,14 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - if (repository.hasInstance(instanceId) && Files.notExists(json)) + @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); + if (existingInstance != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder() - .id(instanceId) - .enableIsolation(); + GameBuilder builder = existingInstance == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(existingInstance); + builder.enableIsolation(); builder.component(GameComponentType.GAME, manifest.getGameVersion()); for (Map.Entry modLoader : manifest.getDependencies().entrySet()) { switch (modLoader.getKey()) { @@ -98,15 +124,15 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF dependents.add(builder.buildAsync()); onDone().register(event -> { - Exception ex = event.getTask().getException(); - if (event.isFailed()) { + @Nullable Exception ex = event.getTask().getException(); + if (existingInstance == null && event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { repository.removeInstanceFromDisk(instanceId); } } }); - ModpackConfiguration config = null; + @Nullable ModpackConfiguration config = null; try { if (Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ModrinthManifest.class)); @@ -122,7 +148,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), subDirectories, any -> true, config).withStage("hmcl.modpack")); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack")); - URI iconUri = NetworkUtils.toURIOrNull(iconUrl); + @Nullable URI iconUri = NetworkUtils.toURIOrNull(iconUrl); if (iconUri != null) { String ext = FileUtils.getExtension(StringUtils.substringAfter(iconUri.getPath(), '/')).toLowerCase(Locale.ROOT); if (Modpack.SUPPORTED_ICON_EXTS.contains(ext)) { @@ -160,9 +186,13 @@ public void execute() throws Exception { Files.createDirectories(root); JsonUtils.writeToJsonFile(root.resolve("modrinth.index.json"), manifest); - if (iconExt != null && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) { + @Nullable String iconExtension = iconExt; + @Nullable Task iconTask = downloadIconTask; + if (iconExtension != null + && iconTask != null + && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) { try { - Files.copy(downloadIconTask.getResult(), root.resolve("icon." + iconExt)); + Files.copy(iconTask.getResult(), root.resolve("icon." + iconExtension)); } catch (Exception e) { LOG.warning("Failed to copy modpack icon", e); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java index 84a50b826f..68cfcc79ca 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java @@ -142,7 +142,7 @@ public void execute() throws Exception { Map oldAddons = toMap(manifest.getManifest().getAddons()); Map newAddons = toMap(remoteManifest.getAddons()); if (!Objects.equals(oldAddons, newAddons)) { - GameBuilder builder = dependencyManager.newGameBuilder().id(instance.getId()); + GameBuilder builder = dependencyManager.newGameBuilder(instance); for (ServerModpackManifest.Addon addon : remoteManifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java index 4472202e15..bf6d5b403d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.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.game.DefaultGameInstance; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceID; @@ -57,12 +58,14 @@ public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - if (repository.hasInstance(instanceId) && Files.notExists(json)) + @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); + if (existingInstance != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder() - .id(instanceId) - .enableIsolation(); + GameBuilder builder = existingInstance == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(existingInstance); + builder.enableIsolation(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) @@ -71,11 +74,11 @@ public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, dependents.add(builder.buildAsync()); onDone().register(event -> { - if (event.isFailed()) + if (existingInstance == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); - ModpackConfiguration config = null; + @Nullable ModpackConfiguration config = null; try { if (Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java index 929b08cbe7..62183ac1da 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.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.game.DefaultGameInstance; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.GameComponentType; import org.jackhuang.hmcl.game.GameInstanceID; @@ -51,12 +52,14 @@ public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager this.manifest = manifest; Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - if (repository.hasInstance(instanceId) && Files.notExists(json)) + @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); + if (existingInstance != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.newGameBuilder() - .id(instanceId) - .enableIsolation(); + GameBuilder builder = existingInstance == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(existingInstance); + builder.enableIsolation(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); if (componentType != null) @@ -65,7 +68,7 @@ public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager dependents.add(builder.buildAsync()); onDone().register(event -> { - if (event.isFailed()) + if (existingInstance == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index 0ff2494698..19150ccfab 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.download.DefaultCacheRepository; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.DefaultGameBuilder; import org.jackhuang.hmcl.download.MojangDownloadProvider; import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask; import org.jackhuang.hmcl.download.game.GameDownloadTask; @@ -377,12 +378,60 @@ public void testDependencyManagerValidatesInstanceRepository(@TempDir Path tempD new DefaultCacheRepository(tempDirectory.resolve("cache"))); assertThrows(IllegalArgumentException.class, () -> dependencyManager.validateGameInstance(instance)); + assertThrows(IllegalArgumentException.class, () -> dependencyManager.newGameBuilder(instance)); assertThrows(IllegalArgumentException.class, () -> new CurseCompletionTask(dependencyManager, instance)); assertThrows(IllegalArgumentException.class, () -> new McbbsModpackCompletionTask(dependencyManager, instance)); assertThrows(IllegalArgumentException.class, () -> new ModrinthCompletionTask(dependencyManager, instance)); assertThrows(IllegalArgumentException.class, () -> new ServerModpackCompletionTask(dependencyManager, instance)); } + /// A new-install builder rejects an id that is already present when its draft opens. + @Test + public void testGameBuilderRejectsNewInstallationOverExistingInstance(@TempDir Path tempDirectory) + throws IOException { + TestRepository repository = new TestRepository(tempDirectory.resolve("game")); + GameInstanceID instanceId = new GameInstanceID("instance"); + repository.publish(instanceId, new GameInstanceManifest(instanceId)); + DefaultDependencyManager dependencyManager = new DefaultDependencyManager( + repository, + new MojangDownloadProvider(), + new DefaultCacheRepository(tempDirectory.resolve("cache"))); + DefaultGameBuilder builder = dependencyManager.newGameBuilder(instanceId); + builder.component(GameComponentType.GAME, "1.21.1"); + + IllegalStateException exception = assertThrows(IllegalStateException.class, builder::buildAsync); + + assertEquals("Game instance already exists: instance", exception.getMessage()); + try (DefaultGameRepositoryDraft draft = repository.openDraft()) { + assertTrue(draft.isOpen()); + } + } + + /// An update builder rejects a target that disappeared before its draft opened. + @Test + public void testGameBuilderRejectsMissingUpdateTarget(@TempDir Path tempDirectory) + throws IOException { + TestRepository repository = new TestRepository(tempDirectory.resolve("game")); + GameInstanceID instanceId = new GameInstanceID("instance"); + TestGameInstance instance = repository.publish( + instanceId, + new GameInstanceManifest(instanceId)); + DefaultDependencyManager dependencyManager = new DefaultDependencyManager( + repository, + new MojangDownloadProvider(), + new DefaultCacheRepository(tempDirectory.resolve("cache"))); + DefaultGameBuilder builder = dependencyManager.newGameBuilder(instance); + builder.component(GameComponentType.GAME, "1.21.1"); + repository.publishEmpty(); + + IllegalStateException exception = assertThrows(IllegalStateException.class, builder::buildAsync); + + assertEquals("Game instance no longer exists: instance", exception.getMessage()); + try (DefaultGameRepositoryDraft draft = repository.openDraft()) { + assertTrue(draft.isOpen()); + } + } + /// Non-conventional JSON/jar basenames are kept on disk and recorded on the instance. @Test public void testRefreshRecordsNonConventionalStoragePaths(@TempDir Path tempDirectory) throws IOException { @@ -538,6 +587,11 @@ private TestGameInstance publish( return instance; } + /// Publishes an empty snapshot to simulate removal during a builder's lifetime. + private void publishEmpty() { + publishSnapshot(newSnapshot()); + } + /// Creates an empty mutable snapshot using the current layout. /// /// @return the new snapshot From f5351d3eae69fe3a9d77be718d3f48ae565e85cf Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 28 Aug 2026 20:15:23 +0800 Subject: [PATCH 37/60] Add support for updating existing CurseForge modpack instances in installation tasks --- .../hmcl/game/HMCLModpackInstallTask.java | 86 ++++++++++++---- .../hmcl/game/HMCLModpackProvider.java | 2 +- .../jackhuang/hmcl/game/ModpackHelper.java | 8 +- .../hmcl/modpack/curse/CurseInstallTask.java | 86 ++++++++++++---- .../modpack/curse/CurseModpackProvider.java | 2 +- .../mcbbs/McbbsModpackLocalInstallTask.java | 87 ++++++++++++---- .../modpack/mcbbs/McbbsModpackProvider.java | 2 +- .../modpack/modrinth/ModrinthInstallTask.java | 87 ++++++++++++---- .../modrinth/ModrinthModpackProvider.java | 2 +- .../multimc/MultiMCModpackInstallTask.java | 99 +++++++++++++++---- .../multimc/MultiMCModpackProvider.java | 2 +- .../server/ServerModpackLocalInstallTask.java | 92 +++++++++++++---- .../modpack/server/ServerModpackProvider.java | 2 +- .../ServerModpackRemoteInstallTask.java | 80 +++++++++++---- .../hmcl/game/DefaultGameInstanceTest.java | 92 +++++++++++++++++ 15 files changed, 589 insertions(+), 140 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 3e7b8d3928..69980fe43f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -36,52 +36,102 @@ import java.util.Collections; import java.util.List; +/// Installs or updates an HMCL modpack using the mode selected at construction. public final class HMCLModpackInstallTask extends Task { private final Path zipFile; private final GameInstanceID instanceId; + + /// Whether this task updates the instance supplied at construction. + private final boolean updating; + private final HMCLGameRepository repository; private final DefaultDependencyManager dependency; private final Modpack modpack; private final List> dependencies = new ArrayList<>(1); private final List> dependents = new ArrayList<>(4); - public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpack modpack, GameInstanceID instanceId) { + /// Creates a task that installs a new HMCL modpack instance. + /// + /// @param repository the target HMCL game repository + /// @param zipFile the HMCL modpack archive + /// @param modpack the parsed modpack metadata + /// @param instanceId the id of the new instance + /// @throws IllegalStateException if `instanceId` is already registered + public HMCLModpackInstallTask( + HMCLGameRepository repository, + Path zipFile, + Modpack modpack, + GameInstanceID instanceId) { + this(repository, zipFile, modpack, instanceId, null); + } + + /// Creates a task that updates an existing HMCL modpack instance. + /// + /// @param repository the target HMCL game repository + /// @param zipFile the HMCL modpack archive + /// @param modpack the parsed modpack metadata + /// @param instance the existing instance to update + /// @throws IllegalArgumentException if `instance` belongs to another repository, has no + /// modpack configuration, or records another provider type + /// @throws IllegalStateException if `instance` is no longer registered + public HMCLModpackInstallTask( + HMCLGameRepository repository, + Path zipFile, + Modpack modpack, + DefaultGameInstance instance) { + this(repository, zipFile, modpack, instance.getId(), instance); + } + + /// Creates an HMCL modpack task in the mode selected by `updateTarget`. + /// + /// @param repository the target HMCL game repository + /// @param zipFile the HMCL modpack archive + /// @param modpack the parsed modpack metadata + /// @param instanceId the target instance id + /// @param updateTarget the existing instance selecting update mode, or `null` for install + private HMCLModpackInstallTask( + HMCLGameRepository repository, + Path zipFile, + Modpack modpack, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget) { this.repository = repository; this.dependency = repository.getDependency(); this.zipFile = zipFile; this.instanceId = instanceId; + this.updating = updateTarget != null; this.modpack = modpack; Path run = repository.getLayout().getInstanceRoot(this.instanceId); Path json = repository.getLayout().getModpackConfigurationFile(this.instanceId); - @Nullable HMCLGameInstance existingInstance = repository.findInstance(this.instanceId); - if (existingInstance != null && Files.notExists(json)) - throw new IllegalArgumentException("Instance " + instanceId + " already exists"); - - GameBuilder builder = existingInstance == null + GameBuilder builder = updateTarget == null ? dependency.newGameBuilder(this.instanceId) - : dependency.newGameBuilder(existingInstance); + : dependency.newGameBuilder(updateTarget); + if (updating && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance."); + + @Nullable ModpackConfiguration config = null; + try { + if (updating && Files.exists(json)) { + config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class)); + + if (config == null || !HMCLModpackProvider.INSTANCE.getName().equals(config.getType())) + throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance."); + } + } catch (JsonParseException | IOException ignore) { + } + dependents.add(builder .enableIsolation() .component(GameComponentType.GAME, modpack.getGameVersion()) .buildAsync()); onDone().register(event -> { - if (existingInstance == null && event.isFailed()) { + if (!updating && event.isFailed()) { repository.removeInstanceFromDisk(this.instanceId); } }); - @Nullable ModpackConfiguration config = null; - try { - if (Files.exists(json)) { - config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class)); - - if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance."); - } - } catch (JsonParseException | IOException ignore) { - } dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config)); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack")); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java index 00a1233ba5..1170688d65 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java @@ -56,7 +56,7 @@ public Task createUpdateTask(DefaultDependencyManager dependencyManager, Defa throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository"); } - return new ModpackUpdateTask(instance, new HMCLModpackInstallTask(repository, zipFile, modpack, instance.getId())); + return new ModpackUpdateTask(instance, new HMCLModpackInstallTask(repository, zipFile, modpack, instance)); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 9bc7a8145e..65bef100c9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -226,12 +226,14 @@ else if (modpack.getManifest() instanceof McbbsModpackManifest) public static Task getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, GameInstanceID instanceId, ModpackConfiguration configuration) throws UnsupportedModpackException { switch (configuration.getType()) { - case ServerModpackRemoteInstallTask.MODPACK_TYPE: + case ServerModpackRemoteInstallTask.MODPACK_TYPE: { + HMCLGameInstance instance = repository.getInstance(instanceId); return new ModpackUpdateTask( - repository.getInstance(instanceId), - new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId)) + instance, + new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instance)) .thenComposeAsync(repository.refreshAsync()) .withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries"))); + } default: throw new UnsupportedModpackException(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java index d71221984b..2579ed748a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java @@ -54,6 +54,9 @@ public final class CurseInstallTask extends Task { private final CurseManifest manifest; private final GameInstanceID instanceId; + /// Whether this task updates the instance supplied at construction. + private final boolean updating; + /// Optional remote icon URL supplied by the install source. private final @Nullable String iconUrl; private final Path run; @@ -69,14 +72,15 @@ public final class CurseInstallTask extends Task { private final List> dependents = new ArrayList<>(4); private final List> dependencies = new ArrayList<>(1); - /// Creates a task that installs or updates a CurseForge modpack instance. + /// Creates a task that installs a new CurseForge modpack instance. /// /// @param dependencyManager the dependency manager for the target repository /// @param zipFile the CurseForge modpack archive /// @param modpack the parsed modpack metadata /// @param manifest the CurseForge manifest - /// @param instanceId the target instance id; an existing modpack instance is updated + /// @param instanceId the id of the new instance /// @param iconUrl the optional icon URL, or `null` + /// @throws IllegalStateException if `instanceId` is already registered public CurseInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -84,24 +88,77 @@ public CurseInstallTask( CurseManifest manifest, GameInstanceID instanceId, @Nullable String iconUrl) { + this(dependencyManager, zipFile, modpack, manifest, instanceId, null, iconUrl); + } + + /// Creates a task that updates an existing CurseForge modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the CurseForge modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the CurseForge manifest + /// @param instance the existing instance to update + /// @param iconUrl the optional icon URL, or `null` + /// @throws IllegalArgumentException if `instance` belongs to another repository, has no + /// modpack configuration, or records another provider type + /// @throws IllegalStateException if `instance` is no longer registered + public CurseInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + CurseManifest manifest, + DefaultGameInstance instance, + @Nullable String iconUrl) { + this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance, iconUrl); + } + + /// Creates a CurseForge installation task in the mode selected by `updateTarget`. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the CurseForge modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the CurseForge manifest + /// @param instanceId the target instance id + /// @param updateTarget the existing instance selecting update mode, or `null` for install + /// @param iconUrl the optional icon URL, or `null` + private CurseInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + CurseManifest manifest, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget, + @Nullable String iconUrl) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; + this.updating = updateTarget != null; this.iconUrl = iconUrl; this.repository = dependencyManager.getGameRepository(); this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); - if (existingInstance != null && Files.notExists(json)) - throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - - GameBuilder builder = existingInstance == null + GameBuilder builder = updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(existingInstance); + : dependencyManager.newGameBuilder(updateTarget); + if (updating && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Curse modpack. Cannot update this instance."); + + @Nullable ModpackConfiguration config = null; + try { + if (updating && Files.exists(json)) { + config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(CurseManifest.class)); + + if (config == null || !CurseModpackProvider.INSTANCE.getName().equals(config.getType())) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Curse modpack. Cannot update this instance."); + } + } catch (JsonParseException | IOException ignore) { + } + this.config = config; + builder.enableIsolation() .component(GameComponentType.GAME, manifest.minecraft().gameVersion()); for (CurseManifestModLoader modLoader : manifest.minecraft().modLoaders()) { @@ -117,24 +174,13 @@ public CurseInstallTask( onDone().register(event -> { @Nullable Exception ex = event.getTask().getException(); - if (existingInstance == null && event.isFailed()) { + if (!updating && event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { repository.removeInstanceFromDisk(instanceId); } } }); - @Nullable ModpackConfiguration config = null; - try { - if (Files.exists(json)) { - config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(CurseManifest.class)); - - if (!CurseModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Instance " + instanceId + " is not a Curse modpack. Cannot update this instance."); - } - } catch (JsonParseException | IOException ignore) { - } - this.config = config; dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), any -> true, config).withStage("hmcl.modpack")); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), manifest, CurseModpackProvider.INSTANCE, manifest.name(), manifest.version(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java index 9cc2fc59aa..32dcd04a72 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java @@ -54,7 +54,7 @@ public Task createUpdateTask(DefaultDependencyManager dependencyManager, Defa if (!(modpack.getManifest() instanceof CurseManifest curseManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(instance, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, instance.getId(), null)); + return new ModpackUpdateTask(instance, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, instance, null)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index 59254acdcf..d3c9b128fb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.Optional; +/// Installs or updates a local MCBBS modpack using the mode selected at construction. public final class McbbsModpackLocalInstallTask extends Task { private final DefaultDependencyManager dependencyManager; @@ -50,24 +51,84 @@ public final class McbbsModpackLocalInstallTask extends Task { private final List> dependencies = new ArrayList<>(2); private final List> dependents = new ArrayList<>(4); - public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, McbbsModpackManifest manifest, GameInstanceID instanceId) { + /// Creates a task that installs a new MCBBS modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the MCBBS modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the MCBBS manifest + /// @param instanceId the id of the new instance + /// @throws IllegalStateException if `instanceId` is already registered + public McbbsModpackLocalInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + McbbsModpackManifest manifest, + GameInstanceID instanceId) { + this(dependencyManager, zipFile, modpack, manifest, instanceId, null); + } + + /// Creates a task that updates an existing MCBBS modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the MCBBS modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the MCBBS manifest + /// @param instance the existing instance to update + /// @throws IllegalArgumentException if `instance` belongs to another repository, has no + /// modpack configuration, or records another provider type + /// @throws IllegalStateException if `instance` is no longer registered + public McbbsModpackLocalInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + McbbsModpackManifest manifest, + DefaultGameInstance instance) { + this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance); + } + + /// Creates an MCBBS installation task in the mode selected by `updateTarget`. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the MCBBS modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the MCBBS manifest + /// @param instanceId the target instance id + /// @param updateTarget the existing instance selecting update mode, or `null` for install + private McbbsModpackLocalInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + McbbsModpackManifest manifest, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; + this.update = updateTarget != null; this.repository = dependencyManager.getGameRepository(); Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); - if (existingInstance != null && Files.notExists(json)) - throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - this.update = existingInstance != null; - - GameBuilder builder = existingInstance == null + GameBuilder builder = updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(existingInstance); + : dependencyManager.newGameBuilder(updateTarget); + if (update && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Mcbbs modpack. Cannot update this instance."); + + @Nullable ModpackConfiguration config = null; + try { + if (update && Files.exists(json)) { + config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(McbbsModpackManifest.class)); + + if (config == null || !McbbsModpackProvider.INSTANCE.getName().equals(config.getType())) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Mcbbs modpack. Cannot update this instance."); + } + } catch (JsonParseException | IOException ignore) { + } + builder.enableIsolation(); for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); @@ -81,16 +142,6 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, repository.removeInstanceFromDisk(instanceId); }); - @Nullable ModpackConfiguration config = null; - try { - if (Files.exists(json)) { - config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(McbbsModpackManifest.class)); - - if (!McbbsModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Instance " + instanceId + " is not a Mcbbs modpack. Cannot update this instance."); - } - } catch (JsonParseException | IOException ignore) { - } dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack")); instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, McbbsModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId)); dependents.add(instanceTask.withStage("hmcl.modpack")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java index d4993662b4..e19eccbdcf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java @@ -50,7 +50,7 @@ public Task createUpdateTask(DefaultDependencyManager dependencyManager, Defa if (!(modpack.getManifest() instanceof McbbsModpackManifest mcbbsModpackManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(instance, new McbbsModpackLocalInstallTask(dependencyManager, zipFile, modpack, mcbbsModpackManifest, instance.getId())); + return new ModpackUpdateTask(instance, new McbbsModpackLocalInstallTask(dependencyManager, zipFile, modpack, mcbbsModpackManifest, instance)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java index dcb9d0e5c9..279a27b387 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java @@ -50,6 +50,9 @@ public class ModrinthInstallTask extends Task { private final ModrinthManifest manifest; private final GameInstanceID instanceId; + /// Whether this task updates the instance supplied at construction. + private final boolean updating; + /// Optional remote icon URL supplied by the install source. private final @Nullable String iconUrl; private final Path run; @@ -65,14 +68,15 @@ public class ModrinthInstallTask extends Task { private final List> dependents = new ArrayList<>(4); private final List> dependencies = new ArrayList<>(1); - /// Creates a task that installs or updates a Modrinth modpack instance. + /// Creates a task that installs a new Modrinth modpack instance. /// /// @param dependencyManager the dependency manager for the target repository /// @param zipFile the Modrinth modpack archive /// @param modpack the parsed modpack metadata /// @param manifest the Modrinth index - /// @param instanceId the target instance id; an existing modpack instance is updated + /// @param instanceId the id of the new instance /// @param iconUrl the optional icon URL, or `null` + /// @throws IllegalStateException if `instanceId` is already registered public ModrinthInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -80,23 +84,76 @@ public ModrinthInstallTask( ModrinthManifest manifest, GameInstanceID instanceId, @Nullable String iconUrl) { + this(dependencyManager, zipFile, modpack, manifest, instanceId, null, iconUrl); + } + + /// Creates a task that updates an existing Modrinth modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the Modrinth modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the Modrinth index + /// @param instance the existing instance to update + /// @param iconUrl the optional icon URL, or `null` + /// @throws IllegalArgumentException if `instance` belongs to another repository, has no + /// modpack configuration, or records another provider type + /// @throws IllegalStateException if `instance` is no longer registered + public ModrinthInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + ModrinthManifest manifest, + DefaultGameInstance instance, + @Nullable String iconUrl) { + this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance, iconUrl); + } + + /// Creates a Modrinth installation task in the mode selected by `updateTarget`. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the Modrinth modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the Modrinth index + /// @param instanceId the target instance id + /// @param updateTarget the existing instance selecting update mode, or `null` for install + /// @param iconUrl the optional icon URL, or `null` + private ModrinthInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + ModrinthManifest manifest, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget, + @Nullable String iconUrl) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; + this.updating = updateTarget != null; this.iconUrl = iconUrl; this.repository = dependencyManager.getGameRepository(); this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); - if (existingInstance != null && Files.notExists(json)) - throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - - GameBuilder builder = existingInstance == null + GameBuilder builder = updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(existingInstance); + : dependencyManager.newGameBuilder(updateTarget); + if (updating && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Modrinth modpack. Cannot update this instance."); + + @Nullable ModpackConfiguration config = null; + try { + if (updating && Files.exists(json)) { + config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ModrinthManifest.class)); + + if (config == null || !ModrinthModpackProvider.INSTANCE.getName().equals(config.getType())) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Modrinth modpack. Cannot update this instance."); + } + } catch (JsonParseException | IOException ignore) { + } + this.config = config; + builder.enableIsolation(); builder.component(GameComponentType.GAME, manifest.getGameVersion()); for (Map.Entry modLoader : manifest.getDependencies().entrySet()) { @@ -125,25 +182,13 @@ public ModrinthInstallTask( onDone().register(event -> { @Nullable Exception ex = event.getTask().getException(); - if (existingInstance == null && event.isFailed()) { + if (!updating && event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { repository.removeInstanceFromDisk(instanceId); } } }); - @Nullable ModpackConfiguration config = null; - try { - if (Files.exists(json)) { - config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ModrinthManifest.class)); - - if (!ModrinthModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Instance " + instanceId + " is not a Modrinth modpack. Cannot update this instance."); - } - } catch (JsonParseException | IOException ignore) { - } - - this.config = config; List subDirectories = Arrays.asList("/client-overrides", "/overrides"); dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), subDirectories, any -> true, config).withStage("hmcl.modpack")); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java index f1eb340bba..219402aef3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java @@ -52,7 +52,7 @@ public Task createUpdateTask(DefaultDependencyManager dependencyManager, Defa if (!(modpack.getManifest() instanceof ModrinthManifest modrinthManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(instance, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, instance.getId(), null)); + return new ModpackUpdateTask(instance, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, instance, null)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java index 0b6891c0ba..46b41ac6cf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java @@ -75,36 +75,103 @@ public final class MultiMCModpackInstallTask extends Task> dependents = new ArrayList<>(); private final List> dependencies = new ArrayList<>(); private final DefaultDependencyManager dependencyManager; + /// Previous modpack configuration when updating, or `null` for a new installation. + private final @Nullable ModpackConfiguration config; + /// The repository transaction that owns a newly created instance root until publication. private @Nullable DefaultGameRepositoryDraft draft; + /// Whether this task successfully reserved a previously absent instance in its draft. + private boolean newInstallationReserved; + /// Creates a MultiMC modpack installation task. /// /// @param dependencyManager the dependency manager for the target repository /// @param zipFile the source modpack archive /// @param modpack the parsed modpack metadata /// @param manifest the MultiMC instance configuration - /// @param instanceId the target instance ID - public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) { + /// @param instanceId the id of the new instance + public MultiMCModpackInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + MultiMCInstanceConfiguration manifest, + GameInstanceID instanceId) { + this(dependencyManager, zipFile, modpack, manifest, instanceId, null); + } + + /// Creates a MultiMC modpack update task. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the source modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the MultiMC instance configuration + /// @param instance the existing instance to update + /// @throws IllegalArgumentException if `instance` belongs to another repository, has no + /// modpack configuration, or records another provider type + public MultiMCModpackInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + MultiMCInstanceConfiguration manifest, + DefaultGameInstance instance) { + this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance); + } + + /// Creates a MultiMC modpack task in the mode selected by `updateTarget`. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the source modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the MultiMC instance configuration + /// @param instanceId the target instance id + /// @param updateTarget the existing instance selecting update mode, or `null` for install + private MultiMCModpackInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + MultiMCInstanceConfiguration manifest, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget) { this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; + this.updating = updateTarget != null; this.dependencyManager = dependencyManager; this.repository = dependencyManager.getGameRepository(); + if (updateTarget != null) { + dependencyManager.validateGameInstance(updateTarget); + } Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - if (repository.hasInstance(instanceId) && Files.notExists(json)) - throw new IllegalArgumentException("Instance " + instanceId + " already exists."); + if (updating && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " is not a MultiMC modpack. Cannot update this instance."); + + @Nullable ModpackConfiguration config = null; + try { + if (updating && Files.exists(json)) { + config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class)); + + if (config == null || !MultiMCModpackProvider.INSTANCE.getName().equals(config.getType())) + throw new IllegalArgumentException("Instance " + instanceId + " is not a MultiMC modpack. Cannot update this instance."); + } + } catch (JsonParseException | IOException ignore) { + } + this.config = config; onDone().register(event -> { abortOpenDraft(); - if (event.isFailed()) + if (event.isFailed() && newInstallationReserved) repository.removeInstanceFromDisk(instanceId); }); } @@ -120,7 +187,17 @@ public void preExecute() throws Exception { DefaultGameRepositoryDraft openedDraft = repository.openDraft(); draft = openedDraft; try { + // Construction fixes the mode; the captured snapshot only verifies that it is still valid. + boolean targetExists = openedDraft.getBaseSnapshot().hasInstance(instanceId); + if (!updating && targetExists) { + throw new IllegalStateException("Game instance already exists: " + instanceId); + } + if (updating && !targetExists) { + throw new IllegalStateException("Game instance no longer exists: " + instanceId); + } + openedDraft.put(new GameInstanceManifest(instanceId)); + newInstallationReserved = !updating; } catch (IOException | RuntimeException e) { try { openedDraft.abort(); @@ -134,18 +211,6 @@ public void preExecute() throws Exception { // Stage #0: General Setup { Path run = repository.getLayout().getInstanceRoot(instanceId); - Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - - ModpackConfiguration config = null; - try { - if (Files.exists(json)) { - config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class)); - - if (!MultiMCModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Instance " + instanceId + " is not a MultiMC modpack. Cannot update this instance."); - } - } catch (JsonParseException | IOException ignore) { - } String mcDirectory; try (FileSystem fs = openModpack()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java index 966dd0ef37..9243dc7f33 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java @@ -53,7 +53,7 @@ public Task createUpdateTask(DefaultDependencyManager dependencyManager, Defa if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration multiMCInstanceConfiguration)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(instance, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, multiMCInstanceConfiguration, instance.getId())); + return new ModpackUpdateTask(instance, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, multiMCInstanceConfiguration, instance)); } private static String getRootEntryName(ZipArchiveReader file) throws IOException { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java index bf6d5b403d..119095f8ff 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java @@ -39,32 +39,98 @@ import java.util.Collections; import java.util.List; +/// Installs or updates a local server modpack using the mode selected at construction. public class ServerModpackLocalInstallTask extends Task { private final Path zipFile; private final Modpack modpack; private final ServerModpackManifest manifest; private final GameInstanceID instanceId; + + /// Whether this task updates the instance supplied at construction. + private final boolean updating; + private final DefaultGameRepository repository; private final List> dependencies = new ArrayList<>(); private final List> dependents = new ArrayList<>(4); - public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ServerModpackManifest manifest, GameInstanceID instanceId) { + /// Creates a task that installs a new local server modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the server modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the server modpack manifest + /// @param instanceId the id of the new instance + /// @throws IllegalStateException if `instanceId` is already registered + public ServerModpackLocalInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + ServerModpackManifest manifest, + GameInstanceID instanceId) { + this(dependencyManager, zipFile, modpack, manifest, instanceId, null); + } + + /// Creates a task that updates an existing local server modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the server modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the server modpack manifest + /// @param instance the existing instance to update + /// @throws IllegalArgumentException if `instance` belongs to another repository, has no + /// modpack configuration, or records another provider type + /// @throws IllegalStateException if `instance` is no longer registered + public ServerModpackLocalInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + ServerModpackManifest manifest, + DefaultGameInstance instance) { + this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance); + } + + /// Creates a local server modpack task in the mode selected by `updateTarget`. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param zipFile the server modpack archive + /// @param modpack the parsed modpack metadata + /// @param manifest the server modpack manifest + /// @param instanceId the target instance id + /// @param updateTarget the existing instance selecting update mode, or `null` for install + private ServerModpackLocalInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + Modpack modpack, + ServerModpackManifest manifest, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget) { this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; + this.updating = updateTarget != null; this.repository = dependencyManager.getGameRepository(); Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); - if (existingInstance != null && Files.notExists(json)) - throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - - GameBuilder builder = existingInstance == null + GameBuilder builder = updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(existingInstance); + : dependencyManager.newGameBuilder(updateTarget); + if (updating && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); + + @Nullable ModpackConfiguration config = null; + try { + if (updating && Files.exists(json)) { + config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); + + if (config == null || !ServerModpackProvider.INSTANCE.getName().equals(config.getType())) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); + } + } catch (JsonParseException | IOException ignore) { + } + builder.enableIsolation(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); @@ -74,20 +140,10 @@ public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, dependents.add(builder.buildAsync()); onDone().register(event -> { - if (existingInstance == null && event.isFailed()) + if (!updating && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); - @Nullable ModpackConfiguration config = null; - try { - if (Files.exists(json)) { - config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); - - if (!ServerModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); - } - } catch (JsonParseException | IOException ignore) { - } dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack")); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, ServerModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack")); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java index 90e82f0392..d081bcc889 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java @@ -51,7 +51,7 @@ public Task createUpdateTask(DefaultDependencyManager dependencyManager, Defa if (!(modpack.getManifest() instanceof ServerModpackManifest serverModpackManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(instance, new ServerModpackLocalInstallTask(dependencyManager, zipFile, modpack, serverModpackManifest, instance.getId())); + return new ModpackUpdateTask(instance, new ServerModpackLocalInstallTask(dependencyManager, zipFile, modpack, serverModpackManifest, instance)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java index 62183ac1da..7990e2a45a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java @@ -36,29 +36,82 @@ import java.util.Collections; import java.util.List; +/// Installs or updates a remote server modpack using the mode selected at construction. public class ServerModpackRemoteInstallTask extends Task { private final GameInstanceID instanceId; + + /// Whether this task updates the instance supplied at construction. + private final boolean updating; + private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; private final List> dependencies = new ArrayList<>(1); private final List> dependents = new ArrayList<>(1); private final ServerModpackManifest manifest; - public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, GameInstanceID instanceId) { + /// Creates a task that installs a new remote server modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param manifest the remote server modpack manifest + /// @param instanceId the id of the new instance + /// @throws IllegalStateException if `instanceId` is already registered + public ServerModpackRemoteInstallTask( + DefaultDependencyManager dependencyManager, + ServerModpackManifest manifest, + GameInstanceID instanceId) { + this(dependencyManager, manifest, instanceId, null); + } + + /// Creates a task that updates an existing remote server modpack instance. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param manifest the remote server modpack manifest + /// @param instance the existing instance to update + /// @throws IllegalArgumentException if `instance` belongs to another repository, has no + /// modpack configuration, or records another provider type + /// @throws IllegalStateException if `instance` is no longer registered + public ServerModpackRemoteInstallTask( + DefaultDependencyManager dependencyManager, + ServerModpackManifest manifest, + DefaultGameInstance instance) { + this(dependencyManager, manifest, instance.getId(), instance); + } + + /// Creates a remote server modpack task in the mode selected by `updateTarget`. + /// + /// @param dependencyManager the dependency manager for the target repository + /// @param manifest the remote server modpack manifest + /// @param instanceId the target instance id + /// @param updateTarget the existing instance selecting update mode, or `null` for install + private ServerModpackRemoteInstallTask( + DefaultDependencyManager dependencyManager, + ServerModpackManifest manifest, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget) { this.instanceId = instanceId; + this.updating = updateTarget != null; this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); this.manifest = manifest; Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - @Nullable DefaultGameInstance existingInstance = repository.getSnapshot().findInstance(instanceId); - if (existingInstance != null && Files.notExists(json)) - throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - - GameBuilder builder = existingInstance == null + GameBuilder builder = updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(existingInstance); + : dependencyManager.newGameBuilder(updateTarget); + if (updating && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); + + try { + if (updating && Files.exists(json)) { + @Nullable ModpackConfiguration config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); + + if (config == null || !MODPACK_TYPE.equals(config.getType())) + throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); + } + } catch (JsonParseException | IOException ignore) { + } + builder.enableIsolation(); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); @@ -68,20 +121,9 @@ public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager dependents.add(builder.buildAsync()); onDone().register(event -> { - if (existingInstance == null && event.isFailed()) + if (!updating && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); - - ModpackConfiguration config; - try { - if (Files.exists(json)) { - config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); - - if (!MODPACK_TYPE.equals(config.getType())) - throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); - } - } catch (JsonParseException | IOException ignore) { - } } @Override diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index 19150ccfab..c5d4596bdd 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -24,10 +24,14 @@ import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask; import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.download.game.GameVerificationFixTask; +import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.curse.CurseCompletionTask; import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackCompletionTask; import org.jackhuang.hmcl.modpack.modrinth.ModrinthCompletionTask; +import org.jackhuang.hmcl.modpack.multimc.MultiMCInstanceConfiguration; +import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackInstallTask; import org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -42,6 +46,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -432,6 +437,43 @@ public void testGameBuilderRejectsMissingUpdateTarget(@TempDir Path tempDirector } } + /// A MultiMC task declared as a new installation rejects an existing id without deleting it. + @Test + public void testMultiMCInstallTaskDoesNotInferUpdateFromExistingInstance( + @TempDir Path tempDirectory) throws IOException { + TestRepository repository = new TestRepository(tempDirectory.resolve("game")); + GameInstanceID instanceId = new GameInstanceID("instance"); + TestGameInstance instance = repository.publish( + instanceId, + new GameInstanceManifest(instanceId)); + Path marker = instance.getInstanceRoot().resolve("marker.txt"); + Files.createDirectories(marker.getParent()); + Files.writeString(marker, "existing"); + DefaultDependencyManager dependencyManager = new DefaultDependencyManager( + repository, + new MojangDownloadProvider(), + new DefaultCacheRepository(tempDirectory.resolve("cache"))); + MultiMCInstanceConfiguration manifest = createMultiMCConfiguration(); + Modpack modpack = createMultiMCModpack(manifest); + MultiMCModpackInstallTask task = new MultiMCModpackInstallTask( + dependencyManager, + tempDirectory.resolve("modpack.zip"), + modpack, + manifest, + instanceId); + + assertFalse(task.executor().test()); + + assertEquals( + "Game instance already exists: instance", + Objects.requireNonNull(task.getException()).getMessage()); + assertSame(instance, repository.getInstance(instanceId)); + assertEquals("existing", Files.readString(marker)); + try (DefaultGameRepositoryDraft draft = repository.openDraft()) { + assertTrue(draft.isOpen()); + } + } + /// Non-conventional JSON/jar basenames are kept on disk and recorded on the instance. @Test public void testRefreshRecordsNonConventionalStoragePaths(@TempDir Path tempDirectory) throws IOException { @@ -534,6 +576,56 @@ private static boolean hasZipEntry(Path zipFile, String entryName) throws IOExce } } + /// Creates minimal MultiMC settings for testing mode validation before archive access. + /// + /// @return the test configuration + private static MultiMCInstanceConfiguration createMultiMCConfiguration() { + return new MultiMCInstanceConfiguration( + "OneSix", + "Test", + "1.21.1", + 0, + "", + "", + "", + "", + "", + "", + false, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + false, + false, + false, + false, + false, + ""); + } + + /// Creates modpack metadata backed by the given MultiMC configuration. + /// + /// @param manifest the MultiMC configuration + /// @return the test modpack metadata + private static Modpack createMultiMCModpack(MultiMCInstanceConfiguration manifest) { + return new Modpack("Test", "", "", manifest.getGameVersion(), "", StandardCharsets.UTF_8, manifest) { + /// This test invokes the concrete installation task directly. + @Override + public Task getInstallTask( + DefaultDependencyManager dependencyManager, + Path zipFile, + GameInstanceID instanceId, + String iconUrl) { + throw new UnsupportedOperationException(); + } + }; + } + /// Minimal repository implementation for snapshot-bound instance tests. @NotNullByDefault private static final class TestRepository extends DefaultGameRepository { From 16822886b2afefd82b3bb2329e85895dbc34f21e Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 28 Aug 2026 21:29:35 +0800 Subject: [PATCH 38/60] Refactor installation tasks to use updateTarget for existing instance handling --- .../hmcl/game/HMCLModpackInstallTask.java | 16 +++++++------- .../hmcl/download/DefaultGameBuilder.java | 10 ++++----- .../optifine/OptiFineInstallTask.java | 2 +- .../java/org/jackhuang/hmcl/game/Library.java | 1 - .../hmcl/modpack/curse/CurseInstallTask.java | 21 ++++++++----------- .../mcbbs/McbbsModpackLocalInstallTask.java | 17 ++++++++------- .../modpack/modrinth/ModrinthInstallTask.java | 21 ++++++++----------- .../multimc/MultiMCModpackInstallTask.java | 20 +++++++++--------- .../server/ServerModpackLocalInstallTask.java | 21 ++++++++----------- .../ServerModpackRemoteInstallTask.java | 21 ++++++++----------- 10 files changed, 69 insertions(+), 81 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 69980fe43f..41b27705e6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -41,8 +41,8 @@ public final class HMCLModpackInstallTask extends Task { private final Path zipFile; private final GameInstanceID instanceId; - /// Whether this task updates the instance supplied at construction. - private final boolean updating; + /// Existing instance selecting update mode, or `null` for a new installation. + private final @Nullable DefaultGameInstance updateTarget; private final HMCLGameRepository repository; private final DefaultDependencyManager dependency; @@ -99,20 +99,20 @@ private HMCLModpackInstallTask( this.dependency = repository.getDependency(); this.zipFile = zipFile; this.instanceId = instanceId; - this.updating = updateTarget != null; + this.updateTarget = updateTarget; this.modpack = modpack; Path run = repository.getLayout().getInstanceRoot(this.instanceId); Path json = repository.getLayout().getModpackConfigurationFile(this.instanceId); - GameBuilder builder = updateTarget == null + GameBuilder builder = this.updateTarget == null ? dependency.newGameBuilder(this.instanceId) - : dependency.newGameBuilder(updateTarget); - if (updating && Files.notExists(json)) + : dependency.newGameBuilder(this.updateTarget); + if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance."); @Nullable ModpackConfiguration config = null; try { - if (updating && Files.exists(json)) { + if (this.updateTarget != null && Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class)); if (config == null || !HMCLModpackProvider.INSTANCE.getName().equals(config.getType())) @@ -127,7 +127,7 @@ private HMCLModpackInstallTask( .buildAsync()); onDone().register(event -> { - if (!updating && event.isFailed()) { + if (this.updateTarget == null && event.isFailed()) { repository.removeInstanceFromDisk(this.instanceId); } }); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 0dffa53397..3f5dda2998 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -46,8 +46,8 @@ public class DefaultGameBuilder extends GameBuilder { /// Id of the instance to create or replace. private final GameInstanceID instanceId; - /// Whether this builder must replace a currently published instance. - private final boolean updating; + /// Existing instance selecting update mode, or `null` for a new installation. + private final @Nullable GameInstance updateTarget; /// Whether instance isolation was requested for this build. protected boolean isolationEnabled; @@ -59,7 +59,7 @@ public class DefaultGameBuilder extends GameBuilder { public DefaultGameBuilder(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { this.dependencyManager = dependencyManager; this.instanceId = instanceId; - this.updating = false; + this.updateTarget = null; } /// Creates a builder for replacing the components of an existing instance. @@ -71,7 +71,7 @@ public DefaultGameBuilder(DefaultDependencyManager dependencyManager, DefaultGam dependencyManager.validateGameInstance(instance); this.dependencyManager = dependencyManager; this.instanceId = instance.getId(); - this.updating = true; + this.updateTarget = instance; } /// Returns the dependency manager used by this builder. @@ -191,7 +191,7 @@ public Task buildAsync() { /// @throws IllegalStateException if the declared target state does not match the snapshot private @Nullable DefaultGameInstance resolveCurrentInstance(DefaultGameRepositorySnapshot snapshot) { @Nullable DefaultGameInstance currentInstance = snapshot.findInstance(instanceId); - if (!updating) { + if (updateTarget == null) { if (currentInstance != null) { throw new IllegalStateException("Game instance already exists: " + instanceId); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index 1f7d0abde3..f8258bb29f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -86,7 +86,7 @@ public OptiFineInstallTask( /// @param remoteVersion selected OptiFine version /// @param minecraftJar vanilla client JAR for the target Minecraft version /// @param installer local installer JAR, or `null` to download it - public OptiFineInstallTask( + public OptiFineInstallTask( DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, OptiFineRemoteVersion remoteVersion, 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 e5e31c2588..fb0911a9e6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java @@ -144,7 +144,6 @@ public Library(String group, String name, String version, @Nullable String class this(new Artifact(group, name, version, classifier)); } - public String groupId() { return artifact.getGroup(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java index 2579ed748a..8d01df4254 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java @@ -20,10 +20,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; -import org.jackhuang.hmcl.game.DefaultGameInstance; -import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.modpack.*; import org.jackhuang.hmcl.task.CacheFileTask; import org.jackhuang.hmcl.task.Task; @@ -54,8 +51,8 @@ public final class CurseInstallTask extends Task { private final CurseManifest manifest; private final GameInstanceID instanceId; - /// Whether this task updates the instance supplied at construction. - private final boolean updating; + /// Existing instance selecting update mode, or `null` for a new installation. + private final @Nullable DefaultGameInstance updateTarget; /// Optional remote icon URL supplied by the install source. private final @Nullable String iconUrl; @@ -134,22 +131,22 @@ private CurseInstallTask( this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; - this.updating = updateTarget != null; + this.updateTarget = updateTarget; this.iconUrl = iconUrl; this.repository = dependencyManager.getGameRepository(); this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = updateTarget == null + GameBuilder builder = this.updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(updateTarget); - if (updating && Files.notExists(json)) + : dependencyManager.newGameBuilder(this.updateTarget); + if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Curse modpack. Cannot update this instance."); @Nullable ModpackConfiguration config = null; try { - if (updating && Files.exists(json)) { + if (this.updateTarget != null && Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(CurseManifest.class)); if (config == null || !CurseModpackProvider.INSTANCE.getName().equals(config.getType())) @@ -174,7 +171,7 @@ private CurseInstallTask( onDone().register(event -> { @Nullable Exception ex = event.getTask().getException(); - if (!updating && event.isFailed()) { + if (this.updateTarget == null && event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { repository.removeInstanceFromDisk(instanceId); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index d3c9b128fb..732cc36442 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -45,7 +45,8 @@ public final class McbbsModpackLocalInstallTask extends Task { private final Modpack modpack; private final McbbsModpackManifest manifest; private final GameInstanceID instanceId; - private final boolean update; + /// Existing instance selecting update mode, or `null` for a new installation. + private final @Nullable DefaultGameInstance updateTarget; private final DefaultGameRepository repository; private final MinecraftInstanceTask instanceTask; private final List> dependencies = new ArrayList<>(2); @@ -107,20 +108,20 @@ private McbbsModpackLocalInstallTask( this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; - this.update = updateTarget != null; + this.updateTarget = updateTarget; this.repository = dependencyManager.getGameRepository(); Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = updateTarget == null + GameBuilder builder = this.updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(updateTarget); - if (update && Files.notExists(json)) + : dependencyManager.newGameBuilder(this.updateTarget); + if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Mcbbs modpack. Cannot update this instance."); @Nullable ModpackConfiguration config = null; try { - if (update && Files.exists(json)) { + if (this.updateTarget != null && Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(McbbsModpackManifest.class)); if (config == null || !McbbsModpackProvider.INSTANCE.getName().equals(config.getType())) @@ -138,7 +139,7 @@ private McbbsModpackLocalInstallTask( dependents.add(builder.buildAsync()); onDone().register(event -> { - if (!update && event.isFailed()) + if (this.updateTarget == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); @@ -161,7 +162,7 @@ public List> getDependencies() { public void execute() throws Exception { GameInstanceManifest instanceManifest = repository.getInstanceManifest(instanceId); Optional mcbbsPatch = instanceManifest.getPatches().stream().filter(patch -> PATCH_NAME.equals(patch.id())).findFirst(); - if (!update) { + if (this.updateTarget == null) { GameInstancePatch patch = new GameInstancePatch(PATCH_NAME).withLibraries(manifest.getLibraries()); dependencies.add(repository.saveAsync(instanceManifest.addPatch(patch))); } else if (mcbbsPatch.isPresent()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java index 279a27b387..89376cfa52 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java @@ -20,10 +20,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; -import org.jackhuang.hmcl.game.DefaultGameInstance; -import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.modpack.*; import org.jackhuang.hmcl.task.CacheFileTask; import org.jackhuang.hmcl.task.Task; @@ -50,8 +47,8 @@ public class ModrinthInstallTask extends Task { private final ModrinthManifest manifest; private final GameInstanceID instanceId; - /// Whether this task updates the instance supplied at construction. - private final boolean updating; + /// Existing instance selecting update mode, or `null` for a new installation. + private final @Nullable DefaultGameInstance updateTarget; /// Optional remote icon URL supplied by the install source. private final @Nullable String iconUrl; @@ -130,21 +127,21 @@ private ModrinthInstallTask( this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; - this.updating = updateTarget != null; + this.updateTarget = updateTarget; this.iconUrl = iconUrl; this.repository = dependencyManager.getGameRepository(); this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = updateTarget == null + GameBuilder builder = this.updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(updateTarget); - if (updating && Files.notExists(json)) + : dependencyManager.newGameBuilder(this.updateTarget); + if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Modrinth modpack. Cannot update this instance."); @Nullable ModpackConfiguration config = null; try { - if (updating && Files.exists(json)) { + if (this.updateTarget != null && Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ModrinthManifest.class)); if (config == null || !ModrinthModpackProvider.INSTANCE.getName().equals(config.getType())) @@ -182,7 +179,7 @@ private ModrinthInstallTask( onDone().register(event -> { @Nullable Exception ex = event.getTask().getException(); - if (!updating && event.isFailed()) { + if (this.updateTarget == null && event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { repository.removeInstanceFromDisk(instanceId); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java index 46b41ac6cf..36ba0eb85d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java @@ -76,8 +76,8 @@ public final class MultiMCModpackInstallTask extends Task> dependents = new ArrayList<>(); @@ -146,20 +146,20 @@ private MultiMCModpackInstallTask( this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; - this.updating = updateTarget != null; + this.updateTarget = updateTarget; this.dependencyManager = dependencyManager; this.repository = dependencyManager.getGameRepository(); - if (updateTarget != null) { - dependencyManager.validateGameInstance(updateTarget); + if (this.updateTarget != null) { + dependencyManager.validateGameInstance(this.updateTarget); } Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - if (updating && Files.notExists(json)) + if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a MultiMC modpack. Cannot update this instance."); @Nullable ModpackConfiguration config = null; try { - if (updating && Files.exists(json)) { + if (this.updateTarget != null && Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class)); if (config == null || !MultiMCModpackProvider.INSTANCE.getName().equals(config.getType())) @@ -189,15 +189,15 @@ public void preExecute() throws Exception { try { // Construction fixes the mode; the captured snapshot only verifies that it is still valid. boolean targetExists = openedDraft.getBaseSnapshot().hasInstance(instanceId); - if (!updating && targetExists) { + if (this.updateTarget == null && targetExists) { throw new IllegalStateException("Game instance already exists: " + instanceId); } - if (updating && !targetExists) { + if (this.updateTarget != null && !targetExists) { throw new IllegalStateException("Game instance no longer exists: " + instanceId); } openedDraft.put(new GameInstanceManifest(instanceId)); - newInstallationReserved = !updating; + newInstallationReserved = this.updateTarget == null; } catch (IOException | RuntimeException e) { try { openedDraft.abort(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java index 119095f8ff..816ddc5ddd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java @@ -20,10 +20,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; -import org.jackhuang.hmcl.game.DefaultGameInstance; -import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.modpack.MinecraftInstanceTask; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackConfiguration; @@ -47,8 +44,8 @@ public class ServerModpackLocalInstallTask extends Task { private final ServerModpackManifest manifest; private final GameInstanceID instanceId; - /// Whether this task updates the instance supplied at construction. - private final boolean updating; + /// Existing instance selecting update mode, or `null` for a new installation. + private final @Nullable DefaultGameInstance updateTarget; private final DefaultGameRepository repository; private final List> dependencies = new ArrayList<>(); @@ -109,20 +106,20 @@ private ServerModpackLocalInstallTask( this.modpack = modpack; this.manifest = manifest; this.instanceId = instanceId; - this.updating = updateTarget != null; + this.updateTarget = updateTarget; this.repository = dependencyManager.getGameRepository(); Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = updateTarget == null + GameBuilder builder = this.updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(updateTarget); - if (updating && Files.notExists(json)) + : dependencyManager.newGameBuilder(this.updateTarget); + if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); @Nullable ModpackConfiguration config = null; try { - if (updating && Files.exists(json)) { + if (this.updateTarget != null && Files.exists(json)) { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); if (config == null || !ServerModpackProvider.INSTANCE.getName().equals(config.getType())) @@ -140,7 +137,7 @@ private ServerModpackLocalInstallTask( dependents.add(builder.buildAsync()); onDone().register(event -> { - if (!updating && event.isFailed()) + if (this.updateTarget == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java index 7990e2a45a..19a18a4ae6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java @@ -20,10 +20,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; -import org.jackhuang.hmcl.game.DefaultGameInstance; -import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.GameComponentType; -import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -41,8 +38,8 @@ public class ServerModpackRemoteInstallTask extends Task { private final GameInstanceID instanceId; - /// Whether this task updates the instance supplied at construction. - private final boolean updating; + /// Existing instance selecting update mode, or `null` for a new installation. + private final @Nullable DefaultGameInstance updateTarget; private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; @@ -90,20 +87,20 @@ private ServerModpackRemoteInstallTask( GameInstanceID instanceId, @Nullable DefaultGameInstance updateTarget) { this.instanceId = instanceId; - this.updating = updateTarget != null; + this.updateTarget = updateTarget; this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); this.manifest = manifest; Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = updateTarget == null + GameBuilder builder = this.updateTarget == null ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(updateTarget); - if (updating && Files.notExists(json)) + : dependencyManager.newGameBuilder(this.updateTarget); + if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); try { - if (updating && Files.exists(json)) { + if (this.updateTarget != null && Files.exists(json)) { @Nullable ModpackConfiguration config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); if (config == null || !MODPACK_TYPE.equals(config.getType())) @@ -121,7 +118,7 @@ private ServerModpackRemoteInstallTask( dependents.add(builder.buildAsync()); onDone().register(event -> { - if (!updating && event.isFailed()) + if (this.updateTarget == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); } From f8373f297856a7785cc9d4f7ef491daef95398ea Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 28 Aug 2026 21:37:16 +0800 Subject: [PATCH 39/60] refactor: simplify GameComponentAnalyzer usage in installation tasks --- .../hmcl/download/cleanroom/CleanroomInstallTask.java | 4 +--- .../hmcl/download/forge/ForgeInstallTask.java | 10 +++------- .../org/jackhuang/hmcl/game/DefaultGameInstance.java | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java index c305024375..209809d93c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java @@ -180,9 +180,7 @@ public static Task install( String installProfileText = Files.readString(fs.getPath("install_profile.json")); Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); if (GameComponentType.CLEANROOM.getPatchId().equals(installProfile.get("profile"))) { - GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze( - dependencyManager.getGameRepository().resolve(manifest), - GameVersionNumber.asGameVersion(gameVersion)); + GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, GameVersionNumber.asGameVersion(gameVersion)); if (analyzer.has(GameComponentType.FORGE)) { throw new UnsupportedInstallationException(CLEANROOM_NOT_COMPATIBLE_WITH_FORGE); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index 8b6e089659..a5ed6d41a0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -173,7 +173,7 @@ public static Task install( String installProfileText = Files.readString(fs.getPath("install_profile.json")); Map installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class); if (installProfile.containsKey("spec")) { - checkCleanroomCompatibility(dependencyManager, manifest, gameVersion); + checkCleanroomCompatibility(manifest, gameVersion); ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); if (!gameVersion.equals(profile.getMinecraft())) throw new VersionMismatchException(profile.getMinecraft(), gameVersion); @@ -185,7 +185,7 @@ public static Task install( modifyVersion(gameVersion, profile.getVersion()), installer)); } else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) { - checkCleanroomCompatibility(dependencyManager, manifest, gameVersion); + checkCleanroomCompatibility(manifest, gameVersion); ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class); if (!gameVersion.equals(profile.install().getMinecraft())) throw new VersionMismatchException(profile.install().getMinecraft(), gameVersion); @@ -198,17 +198,13 @@ public static Task install( /// Rejects Forge installation when the manifest already contains Cleanroom. /// - /// @param dependencyManager repository-scoped download services /// @param manifest working manifest receiving the Forge patch /// @param gameVersion Minecraft version used for component analysis /// @throws UnsupportedInstallationException if the manifest already contains Cleanroom private static void checkCleanroomCompatibility( - DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, String gameVersion) throws UnsupportedInstallationException { - GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze( - dependencyManager.getGameRepository().resolve(manifest), - GameVersionNumber.asGameVersion(gameVersion)); + GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, GameVersionNumber.asGameVersion(gameVersion)); if (analyzer.has(GameComponentType.CLEANROOM)) { throw new UnsupportedInstallationException(CLEANROOM_NOT_COMPATIBLE_WITH_FORGE); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java index 06ba2011e1..a5252d29e3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java @@ -168,7 +168,7 @@ public GameInstanceManifest.Resolved getResolvedManifest() { @Override public GameComponentAnalyzer getAnalyzer() { if (analyzer == null) { - analyzer = GameComponentAnalyzer.analyze(getResolvedManifest(), getVersion()); + analyzer = GameComponentAnalyzer.analyze(manifest.isModifiable() ? manifest : getLaunchManifest(), getVersion()); } return analyzer; } From f6ea241082ae1461f0b830977ad43ea2b92bf775 Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 28 Aug 2026 21:51:07 +0800 Subject: [PATCH 40/60] refactor: update DefaultGameInstance type for updateTarget in DefaultGameBuilder --- .../hmcl/download/DefaultGameBuilder.java | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 3f5dda2998..338ea70a07 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -47,7 +47,7 @@ public class DefaultGameBuilder extends GameBuilder { private final GameInstanceID instanceId; /// Existing instance selecting update mode, or `null` for a new installation. - private final @Nullable GameInstance updateTarget; + private final @Nullable DefaultGameInstance updateTarget; /// Whether instance isolation was requested for this build. protected boolean isolationEnabled; @@ -121,13 +121,19 @@ public Task buildAsync() { } }); - DefaultGameRepository repository = dependencyManager.getGameRepository(); DefaultGameRepositoryDraft draft = repository.openDraft(); @Nullable DefaultGameInstance currentInstance; GameInstanceManifest initialManifest = new GameInstanceManifest(instanceId); try { - currentInstance = resolveCurrentInstance(draft.getBaseSnapshot()); + currentInstance = draft.getBaseSnapshot().findInstance(instanceId); + if (updateTarget == null) { + if (currentInstance != null) { + throw new IllegalStateException("Game instance already exists: " + instanceId); + } + } else if (currentInstance == null) { + throw new IllegalStateException("Game instance no longer exists: " + instanceId); + } draft.put(initialManifest); } catch (IOException e) { abortAfterSetupFailure(draft, e); @@ -184,26 +190,6 @@ public Task buildAsync() { .withStagesHints(hints); } - /// Resolves and validates the declared installation mode against the draft base snapshot. - /// - /// @param snapshot the immutable repository state captured by the draft - /// @return the current update target, or `null` for a valid new installation - /// @throws IllegalStateException if the declared target state does not match the snapshot - private @Nullable DefaultGameInstance resolveCurrentInstance(DefaultGameRepositorySnapshot snapshot) { - @Nullable DefaultGameInstance currentInstance = snapshot.findInstance(instanceId); - if (updateTarget == null) { - if (currentInstance != null) { - throw new IllegalStateException("Game instance already exists: " + instanceId); - } - return null; - } - - if (currentInstance == null) { - throw new IllegalStateException("Game instance no longer exists: " + instanceId); - } - return currentInstance; - } - /// Performs repository-specific initialization after an instance is committed. /// /// This method is invoked after the repository has published the committed snapshot and before From 842cfe5cfd997afb91989a734819d6bbd2023b82 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 20:32:13 +0800 Subject: [PATCH 41/60] Refactor GameBuilder to reserve repository drafts at construction Assisted-by: codex:gpt-5.6-sol --- .../hmcl/game/HMCLModpackInstallTask.java | 26 +- .../hmcl/ui/download/DownloadPage.java | 45 ++-- .../org/jackhuang/hmcl/ui/main/MainPage.java | 8 +- .../hmcl/download/DefaultGameBuilder.java | 242 ++++++++++++------ .../hmcl/download/DependencyManager.java | 18 +- .../jackhuang/hmcl/download/GameBuilder.java | 28 +- .../hmcl/modpack/curse/CurseInstallTask.java | 42 +-- .../mcbbs/McbbsModpackLocalInstallTask.java | 32 ++- .../modpack/modrinth/ModrinthInstallTask.java | 70 ++--- .../server/ServerModpackCompletionTask.java | 15 +- .../server/ServerModpackLocalInstallTask.java | 32 ++- .../ServerModpackRemoteInstallTask.java | 32 ++- .../hmcl/game/DefaultGameInstanceTest.java | 84 +++++- 13 files changed, 445 insertions(+), 229 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 41b27705e6..e998f5587f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -56,7 +56,8 @@ public final class HMCLModpackInstallTask extends Task { /// @param zipFile the HMCL modpack archive /// @param modpack the parsed modpack metadata /// @param instanceId the id of the new instance - /// @throws IllegalStateException if `instanceId` is already registered + /// @throws IllegalStateException if the target cannot be reserved or another repository draft + /// is open public HMCLModpackInstallTask( HMCLGameRepository repository, Path zipFile, @@ -73,7 +74,8 @@ public HMCLModpackInstallTask( /// @param instance the existing instance to update /// @throws IllegalArgumentException if `instance` belongs to another repository, has no /// modpack configuration, or records another provider type - /// @throws IllegalStateException if `instance` is no longer registered + /// @throws IllegalStateException if `instance` is not the exact currently published object + /// or another repository draft is open public HMCLModpackInstallTask( HMCLGameRepository repository, Path zipFile, @@ -89,6 +91,9 @@ public HMCLModpackInstallTask( /// @param modpack the parsed modpack metadata /// @param instanceId the target instance id /// @param updateTarget the existing instance selecting update mode, or `null` for install + /// @throws IllegalArgumentException if an update target has no compatible configuration + /// @throws IllegalStateException if the target cannot be reserved, an update target is not + /// the exact published object, or another draft is open private HMCLModpackInstallTask( HMCLGameRepository repository, Path zipFile, @@ -104,9 +109,6 @@ private HMCLModpackInstallTask( Path run = repository.getLayout().getInstanceRoot(this.instanceId); Path json = repository.getLayout().getModpackConfigurationFile(this.instanceId); - GameBuilder builder = this.updateTarget == null - ? dependency.newGameBuilder(this.instanceId) - : dependency.newGameBuilder(this.updateTarget); if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a HMCL modpack. Cannot update this instance."); @@ -121,11 +123,6 @@ private HMCLModpackInstallTask( } catch (JsonParseException | IOException ignore) { } - dependents.add(builder - .enableIsolation() - .component(GameComponentType.GAME, modpack.getGameVersion()) - .buildAsync()); - onDone().register(event -> { if (this.updateTarget == null && event.isFailed()) { repository.removeInstanceFromDisk(this.instanceId); @@ -134,6 +131,15 @@ private HMCLModpackInstallTask( dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config)); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack")); + + try (GameBuilder builder = this.updateTarget == null + ? dependency.newGameBuilder(this.instanceId) + : dependency.newGameBuilder(this.updateTarget)) { + dependents.add(0, builder + .enableIsolation() + .component(GameComponentType.GAME, modpack.getGameVersion()) + .buildAsync()); + } } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index ce7f1f1d7a..cf5c19a9a7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -312,29 +312,30 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { throw new IllegalStateException("Instance ID is not set"); } - HMCLGameBuilder builder = dependencyManager.newGameBuilder(instanceId); - builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion()); - - settings.asStringMap().forEach((key, value) -> { - if (!GameComponentType.GAME.getPatchId().equals(key) - && value instanceof ComponentRemoteVersion remoteVersion) - builder.component(remoteVersion); - }); - - boolean modded = GameComponentType.MOD_LOADERS.stream() - .anyMatch(componentType -> - settings.get(componentType.getPatchId()) instanceof ComponentRemoteVersion); - if (repository.shouldIsolateNewInstance(modded)) { - builder.enableIsolation(); - } - - Task buildTask = builder.buildAsync(); - buildTask.onDone().register(event -> { - if (!event.isFailed()) { - runInFX(() -> repository.setSelectedInstance(repository.getInstance(instanceId))); + try (HMCLGameBuilder builder = dependencyManager.newGameBuilder(instanceId)) { + builder.component(GameComponentType.GAME, ((ComponentRemoteVersion) settings.get(GameComponentType.GAME.getPatchId())).getGameVersion()); + + settings.asStringMap().forEach((key, value) -> { + if (!GameComponentType.GAME.getPatchId().equals(key) + && value instanceof ComponentRemoteVersion remoteVersion) + builder.component(remoteVersion); + }); + + boolean modded = GameComponentType.MOD_LOADERS.stream() + .anyMatch(componentType -> + settings.get(componentType.getPatchId()) instanceof ComponentRemoteVersion); + if (repository.shouldIsolateNewInstance(modded)) { + builder.enableIsolation(); } - }); - return buildTask; + + Task buildTask = builder.buildAsync(); + buildTask.onDone().register(event -> { + if (!event.isFailed()) { + runInFX(() -> repository.setSelectedInstance(repository.getInstance(instanceId))); + } + }); + return buildTask; + } } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 8113886f90..52c32536af 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -379,9 +379,11 @@ private void launchNoGame() { instanceHolder.value = instanceId; - return dependency.newGameBuilder(instanceId) - .component(GameComponentType.GAME, gameVersion) - .buildAsync(); + try (HMCLGameBuilder builder = dependency.newGameBuilder(instanceId)) { + return builder + .component(GameComponentType.GAME, gameVersion) + .buildAsync(); + } }) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 338ea70a07..97e76edc1c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Map; @@ -32,6 +33,9 @@ /// Installs a new game instance or replaces an existing instance in an exclusive /// [GameRepositoryDraft], then publishes the completed instance once. /// +/// Construction immediately reserves the target in an exclusive draft. An update requires the +/// supplied snapshot-bound instance to remain the exact currently published object. +/// /// Replacement starts from an empty manifest with the target id and installs only the configured /// components. It retains the existing run directory unless isolation is explicitly enabled. /// @@ -49,6 +53,15 @@ public class DefaultGameBuilder extends GameBuilder { /// Existing instance selecting update mode, or `null` for a new installation. private final @Nullable DefaultGameInstance updateTarget; + /// Exclusive repository draft retained until the build task takes ownership or this builder closes. + private final DefaultGameRepositoryDraft draft; + + /// Empty manifest reserved in the draft for the target instance. + private final GameInstanceManifest initialManifest; + + /// Whether ownership of [#draft] has been transferred to the task returned by [#buildAsync()]. + private boolean draftTransferred; + /// Whether instance isolation was requested for this build. protected boolean isolationEnabled; @@ -56,10 +69,14 @@ public class DefaultGameBuilder extends GameBuilder { /// /// @param dependencyManager the dependency manager for the target repository /// @param instanceId the id of the new instance + /// @throws IllegalStateException if the id is already registered, its root cannot be reserved, + /// or another repository draft is open public DefaultGameBuilder(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { this.dependencyManager = dependencyManager; this.instanceId = instanceId; this.updateTarget = null; + this.initialManifest = new GameInstanceManifest(instanceId); + this.draft = openDraft(dependencyManager, instanceId, null, initialManifest); } /// Creates a builder for replacing the components of an existing instance. @@ -67,11 +84,15 @@ public DefaultGameBuilder(DefaultDependencyManager dependencyManager, GameInstan /// @param dependencyManager the dependency manager for the target repository /// @param instance the existing instance selecting update mode and the target id /// @throws IllegalArgumentException if `instance` belongs to another repository + /// @throws IllegalStateException if `instance` is no longer the exact published instance or + /// another repository draft is open public DefaultGameBuilder(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) { dependencyManager.validateGameInstance(instance); this.dependencyManager = dependencyManager; this.instanceId = instance.getId(); this.updateTarget = instance; + this.initialManifest = new GameInstanceManifest(instanceId); + this.draft = openDraft(dependencyManager, instanceId, instance, initialManifest); } /// Returns the dependency manager used by this builder. @@ -85,109 +106,112 @@ public DefaultDependencyManager getDependencyManager() { @Override @Contract("-> this") public DefaultGameBuilder enableIsolation() { + checkOpen(); isolationEnabled = true; return this; } /// {@inheritDoc} /// - /// Retains an unpublished working manifest, installs the configured game and optional loaders, + /// Installs the configured game and optional loaders into the draft reserved by this builder, /// resolves the launch view, and commits it with the original patches once. Failure or - /// cancellation aborts the draft. + /// cancellation aborts the transferred draft. /// /// @return the build task - /// @throws IllegalStateException if the configured game version is absent, the target of a new - /// installation already exists, or the target of an update no - /// longer exists + /// @throws IllegalStateException if the configured game version is absent, the builder is + /// closed, or this method has already returned a task @Override public Task buildAsync() { - @Nullable String gameVersion = (String) components.get(GameComponentType.GAME); - if (gameVersion == null) - throw new IllegalStateException("GameBuilder.gameVersion must be set"); - - var hints = new ArrayList(); - - components.forEach((componentType, version) -> { - hints.add(new Task.StagesHint( - "hmcl.install.%s:%s".formatted( - componentType.getPatchId(), - version instanceof ComponentRemoteVersion remoteVersion - ? remoteVersion.getSelfVersion() - : (String) version))); - - if (componentType == GameComponentType.GAME) { - hints.add(new Task.StagesHint("hmcl.install.libraries")); - hints.add(new Task.StagesHint("hmcl.install.assets")); - } - }); - - DefaultGameRepository repository = dependencyManager.getGameRepository(); - DefaultGameRepositoryDraft draft = repository.openDraft(); - @Nullable DefaultGameInstance currentInstance; - GameInstanceManifest initialManifest = new GameInstanceManifest(instanceId); + checkOpen(); try { - currentInstance = draft.getBaseSnapshot().findInstance(instanceId); - if (updateTarget == null) { - if (currentInstance != null) { - throw new IllegalStateException("Game instance already exists: " + instanceId); + @Nullable String gameVersion = (String) components.get(GameComponentType.GAME); + if (gameVersion == null) + throw new IllegalStateException("GameBuilder.gameVersion must be set"); + + var hints = new ArrayList(); + + components.forEach((componentType, version) -> { + hints.add(new Task.StagesHint( + "hmcl.install.%s:%s".formatted( + componentType.getPatchId(), + version instanceof ComponentRemoteVersion remoteVersion + ? remoteVersion.getSelfVersion() + : (String) version))); + + if (componentType == GameComponentType.GAME) { + hints.add(new Task.StagesHint("hmcl.install.libraries")); + hints.add(new Task.StagesHint("hmcl.install.assets")); + } + }); + + DefaultGameRepository repository = dependencyManager.getGameRepository(); + Path runDirectory = isolationEnabled + ? repository.getLayout().getInstanceRoot(instanceId) + : updateTarget != null + ? updateTarget.getRunDirectory() + : repository.getBaseDirectory(); + Path modsDirectory = runDirectory.resolve("mods"); + + Task libraryTask = dependencyManager.installUnpublishedComponentAsync( + initialManifest, modsDirectory, gameVersion, GameComponentType.GAME, gameVersion); + + for (Map.Entry entry : components.entrySet()) { + GameComponentType componentType = entry.getKey(); + if (componentType == GameComponentType.GAME) + continue; + + if (entry.getValue() instanceof ComponentRemoteVersion remoteVersion) { + libraryTask = libraryTask.thenComposeAsync(manifest -> + dependencyManager.installUnpublishedComponentAsync( + manifest, modsDirectory, remoteVersion)); + } else if (entry.getValue() instanceof String version) { + libraryTask = libraryTask.thenComposeAsync(manifest -> + dependencyManager.installUnpublishedComponentAsync( + manifest, modsDirectory, gameVersion, componentType, version)); + } else { + throw new AssertionError("Unexpected version type: " + entry.getValue().getClass()); } - } else if (currentInstance == null) { - throw new IllegalStateException("Game instance no longer exists: " + instanceId); } - draft.put(initialManifest); - } catch (IOException e) { - abortAfterSetupFailure(draft, e); - throw new IllegalStateException("Cannot reserve game instance " + instanceId, e); - } catch (RuntimeException e) { - abortAfterSetupFailure(draft, e); - throw e; + + Task buildTask = libraryTask.thenComposeAsync(manifest -> { + GameInstanceManifest.Resolved resolved = draft.getBaseSnapshot().resolve(manifest); + return new GameDownloadTask(dependencyManager, resolved.launchManifest()) + .thenApplyAsync(minecraftJar -> { + draft.put(resolved.launchManifest().withPatches(manifest.patches())); + draft.putPrimaryJar(instanceId, minecraftJar); + DefaultGameInstance instance = draft.commit().getInstance(instanceId); + onInstanceCommitted(instance); + return instance; + }); + }) + .whenComplete(exception -> { + if (draft.isOpen()) { + draft.abort(); + } + }) + .withStagesHints(hints); + draftTransferred = true; + return buildTask; + } catch (RuntimeException | Error failure) { + abortAfterSetupFailure(draft, failure); + throw failure; } + } - Path runDirectory = isolationEnabled - ? repository.getLayout().getInstanceRoot(instanceId) - : currentInstance != null - ? currentInstance.getRunDirectory() - : repository.getBaseDirectory(); - Path modsDirectory = runDirectory.resolve("mods"); - - Task libraryTask = dependencyManager.installUnpublishedComponentAsync( - initialManifest, modsDirectory, gameVersion, GameComponentType.GAME, gameVersion); - - for (Map.Entry entry : components.entrySet()) { - GameComponentType componentType = entry.getKey(); - if (componentType == GameComponentType.GAME) - continue; - - if (entry.getValue() instanceof ComponentRemoteVersion remoteVersion) { - libraryTask = libraryTask.thenComposeAsync(manifest -> - dependencyManager.installUnpublishedComponentAsync( - manifest, modsDirectory, remoteVersion)); - } else if (entry.getValue() instanceof String version) { - libraryTask = libraryTask.thenComposeAsync(manifest -> - dependencyManager.installUnpublishedComponentAsync( - manifest, modsDirectory, gameVersion, componentType, version)); - } else { - throw new AssertionError("Unexpected version type: " + entry.getValue().getClass()); - } + /// {@inheritDoc} + /// + /// @throws UncheckedIOException if reserved instance files cannot be cleaned up + @Override + public void close() { + if (draftTransferred || !draft.isOpen()) { + return; } - return libraryTask.thenComposeAsync(manifest -> { - GameInstanceManifest.Resolved resolved = draft.getBaseSnapshot().resolve(manifest); - return new GameDownloadTask(dependencyManager, resolved.launchManifest()) - .thenApplyAsync(minecraftJar -> { - draft.put(resolved.launchManifest().withPatches(manifest.patches())); - draft.putPrimaryJar(instanceId, minecraftJar); - DefaultGameInstance instance = draft.commit().getInstance(instanceId); - onInstanceCommitted(instance); - return instance; - }); - }) - .whenComplete(exception -> { - if (draft.isOpen()) { - draft.abort(); - } - }) - .withStagesHints(hints); + try { + draft.abort(); + } catch (IOException e) { + throw new UncheckedIOException("Cannot abort game builder draft", e); + } } /// Performs repository-specific initialization after an instance is committed. @@ -200,6 +224,54 @@ public Task buildAsync() { protected void onInstanceCommitted(DefaultGameInstance instance) { } + /// {@inheritDoc} + @Override + protected final void checkOpen() { + if (draftTransferred) { + throw new IllegalStateException("GameBuilder has already created its build task"); + } + if (!draft.isOpen()) { + throw new IllegalStateException("GameBuilder is closed"); + } + } + + /// Opens an exclusive draft, validates the exact update snapshot, and reserves the target. + /// + /// @param dependencyManager the dependency manager whose repository owns the draft + /// @param instanceId the target instance id + /// @param updateTarget the exact published instance required for an update, or `null` + /// @param initialManifest the empty target manifest to retain in the draft + /// @return the open draft containing `initialManifest` + /// @throws IllegalStateException if the target conflicts with the selected operation or cannot + /// be reserved + private static DefaultGameRepositoryDraft openDraft( + DefaultDependencyManager dependencyManager, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget, + GameInstanceManifest initialManifest) { + DefaultGameRepositoryDraft draft = dependencyManager.getGameRepository().openDraft(); + try { + @Nullable DefaultGameInstance currentInstance = draft.getBaseSnapshot().findInstance(instanceId); + if (updateTarget == null) { + if (currentInstance != null) { + throw new IllegalStateException("Game instance already exists: " + instanceId); + } + } else if (currentInstance == null) { + throw new IllegalStateException("Game instance no longer exists: " + instanceId); + } else if (currentInstance != updateTarget) { + throw new IllegalStateException("Game instance has changed: " + instanceId); + } + draft.put(initialManifest); + return draft; + } catch (IOException e) { + abortAfterSetupFailure(draft, e); + throw new IllegalStateException("Cannot reserve game instance " + instanceId, e); + } catch (RuntimeException | Error failure) { + abortAfterSetupFailure(draft, failure); + throw failure; + } + } + /// Aborts a draft after target validation or initial reservation fails. /// /// @param draft the draft to abort diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java index c0b2393206..67f5e2175b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java @@ -73,22 +73,30 @@ public interface DependencyManager { /// Creates a builder for installing a new game instance and optional components. /// - /// The target id must be absent when the builder starts the installation. + /// This operation opens an exclusive repository draft and reserves the target id immediately. + /// The returned builder must be closed if it is abandoned before [GameBuilder#buildAsync()]. A + /// synchronous failure from that method aborts the draft itself. /// /// @param instanceId the id of the new instance /// @return a new game builder + /// @throws IllegalStateException if the id is already registered, its root cannot be reserved, + /// or another repository draft is open GameBuilder newGameBuilder(GameInstanceID instanceId); /// Creates a builder for replacing the components of an existing game instance. /// - /// The instance selects update mode and fixes the target repository and id. The currently - /// published instance with that id is used when the builder starts the update, so the supplied - /// snapshot-bound object need not remain current. The resulting manifest is rebuilt from the - /// components configured on the builder; components that are not configured are not retained. + /// This operation opens an exclusive repository draft immediately. `instance` must be the exact + /// object in the current published snapshot; an instance invalidated by any intervening + /// repository publication is rejected. The returned builder must be closed if it is abandoned + /// before [GameBuilder#buildAsync()]; a synchronous failure from that method aborts the draft + /// itself. The resulting manifest is rebuilt from the configured components; components that + /// are not configured are not retained. /// /// @param instance the existing game instance to update /// @return a game builder targeting the existing instance /// @throws IllegalArgumentException if `instance` belongs to another repository + /// @throws IllegalStateException if `instance` is no longer the exact published instance or + /// another repository draft is open GameBuilder newGameBuilder(GameInstance instance); /// Returns a registered remote-version list. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java index 9e8077d971..da0642c93e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java @@ -26,9 +26,13 @@ /// Configures the components used to install or update a game instance. /// +/// A builder owns an exclusive repository draft from construction until it is closed or transfers +/// that draft to the task returned by [#buildAsync()]. Builders are single-use and are not +/// thread-safe. +/// /// @author huangyuhui @NotNullByDefault -public abstract class GameBuilder { +public abstract class GameBuilder implements AutoCloseable { /// Components to install, keyed by their manifest patch type. protected final EnumMap components = new EnumMap<>(GameComponentType.class); @@ -40,6 +44,7 @@ public abstract class GameBuilder { /// directory. /// /// @return this builder + /// @throws IllegalStateException if this builder is closed or has already created its build task @Contract("-> this") public abstract GameBuilder enableIsolation(); @@ -50,8 +55,10 @@ public abstract class GameBuilder { /// @param componentType the component type /// @param version the remote version id /// @return this builder + /// @throws IllegalStateException if this builder is closed or has already created its build task @Contract("_, _ -> this") public GameBuilder component(GameComponentType componentType, String version) { + checkOpen(); components.put(componentType, version); return this; } @@ -62,14 +69,33 @@ public GameBuilder component(GameComponentType componentType, String version) { /// /// @param remoteVersion the remote component version /// @return this builder + /// @throws IllegalStateException if this builder is closed or has already created its build task @Contract("_ -> this") public GameBuilder component(ComponentRemoteVersion remoteVersion) { + checkOpen(); components.put(remoteVersion.getComponentType(), remoteVersion); return this; } /// Creates the task that installs the configured components and publishes the target instance. /// + /// This operation may be invoked once. On success, ownership of the builder's exclusive + /// repository draft is transferred to the returned task. Closing the builder after that + /// transfer has no effect. If this method fails before returning a task, the draft is aborted. + /// /// @return the instance build task public abstract Task buildAsync(); + + /// Abandons this builder and aborts its exclusive repository draft unless ownership has already + /// been transferred to a build task. + /// + /// This operation has no effect after a successful [#buildAsync()] call or after the builder has + /// already been closed. + @Override + public abstract void close(); + + /// Ensures this builder still accepts configuration or task creation. + /// + /// @throws IllegalStateException if this builder is closed or has already created its build task + protected abstract void checkOpen(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java index 8d01df4254..0c68d91f48 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java @@ -77,7 +77,8 @@ public final class CurseInstallTask extends Task { /// @param manifest the CurseForge manifest /// @param instanceId the id of the new instance /// @param iconUrl the optional icon URL, or `null` - /// @throws IllegalStateException if `instanceId` is already registered + /// @throws IllegalStateException if the target cannot be reserved or another repository draft + /// is open public CurseInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -98,7 +99,8 @@ public CurseInstallTask( /// @param iconUrl the optional icon URL, or `null` /// @throws IllegalArgumentException if `instance` belongs to another repository, has no /// modpack configuration, or records another provider type - /// @throws IllegalStateException if `instance` is no longer registered + /// @throws IllegalStateException if `instance` is not the exact currently published object + /// or another repository draft is open public CurseInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -118,6 +120,9 @@ public CurseInstallTask( /// @param instanceId the target instance id /// @param updateTarget the existing instance selecting update mode, or `null` for install /// @param iconUrl the optional icon URL, or `null` + /// @throws IllegalArgumentException if an update target has no compatible configuration + /// @throws IllegalStateException if the target cannot be reserved, an update target is not + /// the exact published object, or another draft is open private CurseInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -138,9 +143,6 @@ private CurseInstallTask( this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = this.updateTarget == null - ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(this.updateTarget); if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Curse modpack. Cannot update this instance."); @@ -156,19 +158,6 @@ private CurseInstallTask( } this.config = config; - builder.enableIsolation() - .component(GameComponentType.GAME, manifest.minecraft().gameVersion()); - for (CurseManifestModLoader modLoader : manifest.minecraft().modLoaders()) { - if (modLoader.id().startsWith("forge-")) { - builder.component(GameComponentType.FORGE, modLoader.id().substring("forge-".length())); - } else if (modLoader.id().startsWith("fabric-")) { - builder.component(GameComponentType.FABRIC, modLoader.id().substring("fabric-".length())); - } else if (modLoader.id().startsWith("neoforge-")) { - builder.component(GameComponentType.NEO_FORGE, modLoader.id().substring("neoforge-".length())); - } - } - dependents.add(builder.buildAsync()); - onDone().register(event -> { @Nullable Exception ex = event.getTask().getException(); if (this.updateTarget == null && event.isFailed()) { @@ -189,6 +178,23 @@ private CurseInstallTask( dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl))); } } + + try (GameBuilder builder = this.updateTarget == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(this.updateTarget)) { + builder.enableIsolation() + .component(GameComponentType.GAME, manifest.minecraft().gameVersion()); + for (CurseManifestModLoader modLoader : manifest.minecraft().modLoaders()) { + if (modLoader.id().startsWith("forge-")) { + builder.component(GameComponentType.FORGE, modLoader.id().substring("forge-".length())); + } else if (modLoader.id().startsWith("fabric-")) { + builder.component(GameComponentType.FABRIC, modLoader.id().substring("fabric-".length())); + } else if (modLoader.id().startsWith("neoforge-")) { + builder.component(GameComponentType.NEO_FORGE, modLoader.id().substring("neoforge-".length())); + } + } + dependents.add(0, builder.buildAsync()); + } } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index 732cc36442..3af6a05c46 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -59,7 +59,8 @@ public final class McbbsModpackLocalInstallTask extends Task { /// @param modpack the parsed modpack metadata /// @param manifest the MCBBS manifest /// @param instanceId the id of the new instance - /// @throws IllegalStateException if `instanceId` is already registered + /// @throws IllegalStateException if the target cannot be reserved or another repository draft + /// is open public McbbsModpackLocalInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -78,7 +79,8 @@ public McbbsModpackLocalInstallTask( /// @param instance the existing instance to update /// @throws IllegalArgumentException if `instance` belongs to another repository, has no /// modpack configuration, or records another provider type - /// @throws IllegalStateException if `instance` is no longer registered + /// @throws IllegalStateException if `instance` is not the exact currently published object + /// or another repository draft is open public McbbsModpackLocalInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -96,6 +98,9 @@ public McbbsModpackLocalInstallTask( /// @param manifest the MCBBS manifest /// @param instanceId the target instance id /// @param updateTarget the existing instance selecting update mode, or `null` for install + /// @throws IllegalArgumentException if an update target has no compatible configuration + /// @throws IllegalStateException if the target cannot be reserved, an update target is not + /// the exact published object, or another draft is open private McbbsModpackLocalInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -113,9 +118,6 @@ private McbbsModpackLocalInstallTask( Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = this.updateTarget == null - ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(this.updateTarget); if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Mcbbs modpack. Cannot update this instance."); @@ -130,14 +132,6 @@ private McbbsModpackLocalInstallTask( } catch (JsonParseException | IOException ignore) { } - builder.enableIsolation(); - for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { - @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); - if (componentType != null) - builder.component(componentType, addon.getVersion()); - } - - dependents.add(builder.buildAsync()); onDone().register(event -> { if (this.updateTarget == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); @@ -146,6 +140,18 @@ private McbbsModpackLocalInstallTask( dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack")); instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, McbbsModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId)); dependents.add(instanceTask.withStage("hmcl.modpack")); + + try (GameBuilder builder = this.updateTarget == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(this.updateTarget)) { + builder.enableIsolation(); + for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { + @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); + if (componentType != null) + builder.component(componentType, addon.getVersion()); + } + dependents.add(0, builder.buildAsync()); + } } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java index 89376cfa52..dbcb979fee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java @@ -73,7 +73,8 @@ public class ModrinthInstallTask extends Task { /// @param manifest the Modrinth index /// @param instanceId the id of the new instance /// @param iconUrl the optional icon URL, or `null` - /// @throws IllegalStateException if `instanceId` is already registered + /// @throws IllegalStateException if the manifest declares an unsupported mod loader, the target + /// cannot be reserved, or another repository draft is open public ModrinthInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -94,7 +95,9 @@ public ModrinthInstallTask( /// @param iconUrl the optional icon URL, or `null` /// @throws IllegalArgumentException if `instance` belongs to another repository, has no /// modpack configuration, or records another provider type - /// @throws IllegalStateException if `instance` is no longer registered + /// @throws IllegalStateException if the manifest declares an unsupported mod loader, + /// `instance` is not the exact currently published object, or + /// another repository draft is open public ModrinthInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -114,6 +117,10 @@ public ModrinthInstallTask( /// @param instanceId the target instance id /// @param updateTarget the existing instance selecting update mode, or `null` for install /// @param iconUrl the optional icon URL, or `null` + /// @throws IllegalArgumentException if an update target has no compatible configuration + /// @throws IllegalStateException if the manifest declares an unsupported mod loader, the + /// target cannot be reserved, an update target is not the exact + /// published object, or another draft is open private ModrinthInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -133,9 +140,6 @@ private ModrinthInstallTask( this.run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = this.updateTarget == null - ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(this.updateTarget); if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Modrinth modpack. Cannot update this instance."); @@ -151,32 +155,6 @@ private ModrinthInstallTask( } this.config = config; - builder.enableIsolation(); - builder.component(GameComponentType.GAME, manifest.getGameVersion()); - for (Map.Entry modLoader : manifest.getDependencies().entrySet()) { - switch (modLoader.getKey()) { - case "minecraft": - break; - case "forge": - builder.component(GameComponentType.FORGE, modLoader.getValue()); - break; - case "neoforge": - // https://github.com/HMCL-dev/HMCL/pull/5170 - case "neo-forge": - builder.component(GameComponentType.NEO_FORGE, modLoader.getValue()); - break; - case "fabric-loader": - builder.component(GameComponentType.FABRIC, modLoader.getValue()); - break; - case "quilt-loader": - builder.component(GameComponentType.QUILT, modLoader.getValue()); - break; - default: - throw new IllegalStateException("Unsupported mod loader " + modLoader.getKey()); - } - } - dependents.add(builder.buildAsync()); - onDone().register(event -> { @Nullable Exception ex = event.getTask().getException(); if (this.updateTarget == null && event.isFailed()) { @@ -199,6 +177,36 @@ private ModrinthInstallTask( dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl))); } } + + try (GameBuilder builder = this.updateTarget == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(this.updateTarget)) { + builder.enableIsolation(); + builder.component(GameComponentType.GAME, manifest.getGameVersion()); + for (Map.Entry modLoader : manifest.getDependencies().entrySet()) { + switch (modLoader.getKey()) { + case "minecraft": + break; + case "forge": + builder.component(GameComponentType.FORGE, modLoader.getValue()); + break; + case "neoforge": + // https://github.com/HMCL-dev/HMCL/pull/5170 + case "neo-forge": + builder.component(GameComponentType.NEO_FORGE, modLoader.getValue()); + break; + case "fabric-loader": + builder.component(GameComponentType.FABRIC, modLoader.getValue()); + break; + case "quilt-loader": + builder.component(GameComponentType.QUILT, modLoader.getValue()); + break; + default: + throw new IllegalStateException("Unsupported mod loader " + modLoader.getKey()); + } + } + dependents.add(0, builder.buildAsync()); + } } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java index 68cfcc79ca..7604663a73 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java @@ -142,14 +142,15 @@ public void execute() throws Exception { Map oldAddons = toMap(manifest.getManifest().getAddons()); Map newAddons = toMap(remoteManifest.getAddons()); if (!Objects.equals(oldAddons, newAddons)) { - GameBuilder builder = dependencyManager.newGameBuilder(instance); - for (ServerModpackManifest.Addon addon : remoteManifest.getAddons()) { - @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); - if (componentType != null) - builder.component(componentType, addon.getVersion()); - } + try (GameBuilder builder = dependencyManager.newGameBuilder(instance)) { + for (ServerModpackManifest.Addon addon : remoteManifest.getAddons()) { + @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); + if (componentType != null) + builder.component(componentType, addon.getVersion()); + } - dependencies.add(builder.buildAsync()); + dependencies.add(builder.buildAsync()); + } } Path rootPath = instance.getInstanceRoot().toAbsolutePath().normalize(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java index 816ddc5ddd..44ee61997a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java @@ -58,7 +58,8 @@ public class ServerModpackLocalInstallTask extends Task { /// @param modpack the parsed modpack metadata /// @param manifest the server modpack manifest /// @param instanceId the id of the new instance - /// @throws IllegalStateException if `instanceId` is already registered + /// @throws IllegalStateException if the target cannot be reserved or another repository draft + /// is open public ServerModpackLocalInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -77,7 +78,8 @@ public ServerModpackLocalInstallTask( /// @param instance the existing instance to update /// @throws IllegalArgumentException if `instance` belongs to another repository, has no /// modpack configuration, or records another provider type - /// @throws IllegalStateException if `instance` is no longer registered + /// @throws IllegalStateException if `instance` is not the exact currently published object + /// or another repository draft is open public ServerModpackLocalInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -95,6 +97,9 @@ public ServerModpackLocalInstallTask( /// @param manifest the server modpack manifest /// @param instanceId the target instance id /// @param updateTarget the existing instance selecting update mode, or `null` for install + /// @throws IllegalArgumentException if an update target has no compatible configuration + /// @throws IllegalStateException if the target cannot be reserved, an update target is not + /// the exact published object, or another draft is open private ServerModpackLocalInstallTask( DefaultDependencyManager dependencyManager, Path zipFile, @@ -111,9 +116,6 @@ private ServerModpackLocalInstallTask( Path run = repository.getLayout().getInstanceRoot(instanceId); Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = this.updateTarget == null - ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(this.updateTarget); if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); @@ -128,14 +130,6 @@ private ServerModpackLocalInstallTask( } catch (JsonParseException | IOException ignore) { } - builder.enableIsolation(); - for (ServerModpackManifest.Addon addon : manifest.getAddons()) { - @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); - if (componentType != null) - builder.component(componentType, addon.getVersion()); - } - - dependents.add(builder.buildAsync()); onDone().register(event -> { if (this.updateTarget == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); @@ -143,6 +137,18 @@ private ServerModpackLocalInstallTask( dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack")); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, ServerModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(instanceId)).withStage("hmcl.modpack")); + + try (GameBuilder builder = this.updateTarget == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(this.updateTarget)) { + builder.enableIsolation(); + for (ServerModpackManifest.Addon addon : manifest.getAddons()) { + @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); + if (componentType != null) + builder.component(componentType, addon.getVersion()); + } + dependents.add(0, builder.buildAsync()); + } } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java index 19a18a4ae6..50d9a16d44 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java @@ -52,7 +52,8 @@ public class ServerModpackRemoteInstallTask extends Task { /// @param dependencyManager the dependency manager for the target repository /// @param manifest the remote server modpack manifest /// @param instanceId the id of the new instance - /// @throws IllegalStateException if `instanceId` is already registered + /// @throws IllegalStateException if the target cannot be reserved or another repository draft + /// is open public ServerModpackRemoteInstallTask( DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, @@ -67,7 +68,8 @@ public ServerModpackRemoteInstallTask( /// @param instance the existing instance to update /// @throws IllegalArgumentException if `instance` belongs to another repository, has no /// modpack configuration, or records another provider type - /// @throws IllegalStateException if `instance` is no longer registered + /// @throws IllegalStateException if `instance` is not the exact currently published object + /// or another repository draft is open public ServerModpackRemoteInstallTask( DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, @@ -81,6 +83,9 @@ public ServerModpackRemoteInstallTask( /// @param manifest the remote server modpack manifest /// @param instanceId the target instance id /// @param updateTarget the existing instance selecting update mode, or `null` for install + /// @throws IllegalArgumentException if an update target has no compatible configuration + /// @throws IllegalStateException if the target cannot be reserved, an update target is not + /// the exact published object, or another draft is open private ServerModpackRemoteInstallTask( DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, @@ -93,9 +98,6 @@ private ServerModpackRemoteInstallTask( this.manifest = manifest; Path json = repository.getLayout().getModpackConfigurationFile(instanceId); - GameBuilder builder = this.updateTarget == null - ? dependencyManager.newGameBuilder(instanceId) - : dependencyManager.newGameBuilder(this.updateTarget); if (this.updateTarget != null && Files.notExists(json)) throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); @@ -109,18 +111,22 @@ private ServerModpackRemoteInstallTask( } catch (JsonParseException | IOException ignore) { } - builder.enableIsolation(); - for (ServerModpackManifest.Addon addon : manifest.getAddons()) { - @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); - if (componentType != null) - builder.component(componentType, addon.getVersion()); - } - - dependents.add(builder.buildAsync()); onDone().register(event -> { if (this.updateTarget == null && event.isFailed()) repository.removeInstanceFromDisk(instanceId); }); + + try (GameBuilder builder = this.updateTarget == null + ? dependencyManager.newGameBuilder(instanceId) + : dependencyManager.newGameBuilder(this.updateTarget)) { + builder.enableIsolation(); + for (ServerModpackManifest.Addon addon : manifest.getAddons()) { + @Nullable GameComponentType componentType = GameComponentType.fromPatchId(addon.getId()); + if (componentType != null) + builder.component(componentType, addon.getVersion()); + } + dependents.add(builder.buildAsync()); + } } @Override diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index c5d4596bdd..b972f0e3c1 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -401,10 +401,10 @@ public void testGameBuilderRejectsNewInstallationOverExistingInstance(@TempDir P repository, new MojangDownloadProvider(), new DefaultCacheRepository(tempDirectory.resolve("cache"))); - DefaultGameBuilder builder = dependencyManager.newGameBuilder(instanceId); - builder.component(GameComponentType.GAME, "1.21.1"); - IllegalStateException exception = assertThrows(IllegalStateException.class, builder::buildAsync); + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> dependencyManager.newGameBuilder(instanceId)); assertEquals("Game instance already exists: instance", exception.getMessage()); try (DefaultGameRepositoryDraft draft = repository.openDraft()) { @@ -412,7 +412,7 @@ public void testGameBuilderRejectsNewInstallationOverExistingInstance(@TempDir P } } - /// An update builder rejects a target that disappeared before its draft opened. + /// An update builder rejects a target that disappeared before construction. @Test public void testGameBuilderRejectsMissingUpdateTarget(@TempDir Path tempDirectory) throws IOException { @@ -425,11 +425,11 @@ public void testGameBuilderRejectsMissingUpdateTarget(@TempDir Path tempDirector repository, new MojangDownloadProvider(), new DefaultCacheRepository(tempDirectory.resolve("cache"))); - DefaultGameBuilder builder = dependencyManager.newGameBuilder(instance); - builder.component(GameComponentType.GAME, "1.21.1"); repository.publishEmpty(); - IllegalStateException exception = assertThrows(IllegalStateException.class, builder::buildAsync); + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> dependencyManager.newGameBuilder(instance)); assertEquals("Game instance no longer exists: instance", exception.getMessage()); try (DefaultGameRepositoryDraft draft = repository.openDraft()) { @@ -437,6 +437,74 @@ public void testGameBuilderRejectsMissingUpdateTarget(@TempDir Path tempDirector } } + /// An update builder rejects a snapshot-bound instance replaced under the same id. + @Test + public void testGameBuilderRejectsChangedUpdateTarget(@TempDir Path tempDirectory) + throws IOException { + TestRepository repository = new TestRepository(tempDirectory.resolve("game")); + GameInstanceID instanceId = new GameInstanceID("instance"); + TestGameInstance instance = repository.publish( + instanceId, + new GameInstanceManifest(instanceId)); + repository.publish(instanceId, new GameInstanceManifest(instanceId)); + DefaultDependencyManager dependencyManager = new DefaultDependencyManager( + repository, + new MojangDownloadProvider(), + new DefaultCacheRepository(tempDirectory.resolve("cache"))); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> dependencyManager.newGameBuilder(instance)); + + assertEquals("Game instance has changed: instance", exception.getMessage()); + try (DefaultGameRepositoryDraft draft = repository.openDraft()) { + assertTrue(draft.isOpen()); + } + } + + /// Closing an unused builder releases its exclusive draft and prevents further configuration. + @Test + public void testGameBuilderCloseAbortsReservedDraft(@TempDir Path tempDirectory) + throws IOException { + TestRepository repository = new TestRepository(tempDirectory.resolve("game")); + DefaultDependencyManager dependencyManager = new DefaultDependencyManager( + repository, + new MojangDownloadProvider(), + new DefaultCacheRepository(tempDirectory.resolve("cache"))); + DefaultGameBuilder builder = dependencyManager.newGameBuilder(new GameInstanceID("instance")); + + assertThrows(IllegalStateException.class, repository::openDraft); + builder.close(); + builder.close(); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> builder.component(GameComponentType.GAME, "1.21.1")); + assertEquals("GameBuilder is closed", exception.getMessage()); + try (DefaultGameRepositoryDraft draft = repository.openDraft()) { + assertTrue(draft.isOpen()); + } + } + + /// A synchronous build-configuration failure aborts the builder's reserved draft. + @Test + public void testGameBuilderBuildFailureAbortsReservedDraft(@TempDir Path tempDirectory) + throws IOException { + TestRepository repository = new TestRepository(tempDirectory.resolve("game")); + DefaultDependencyManager dependencyManager = new DefaultDependencyManager( + repository, + new MojangDownloadProvider(), + new DefaultCacheRepository(tempDirectory.resolve("cache"))); + DefaultGameBuilder builder = dependencyManager.newGameBuilder(new GameInstanceID("instance")); + + IllegalStateException exception = assertThrows(IllegalStateException.class, builder::buildAsync); + + assertEquals("GameBuilder.gameVersion must be set", exception.getMessage()); + try (DefaultGameRepositoryDraft draft = repository.openDraft()) { + assertTrue(draft.isOpen()); + } + } + /// A MultiMC task declared as a new installation rejects an existing id without deleting it. @Test public void testMultiMCInstallTaskDoesNotInferUpdateFromExistingInstance( @@ -679,7 +747,7 @@ private TestGameInstance publish( return instance; } - /// Publishes an empty snapshot to simulate removal during a builder's lifetime. + /// Publishes an empty snapshot to simulate removal before builder construction. private void publishEmpty() { publishSnapshot(newSnapshot()); } From a011e91a8bc47161c0b5b1029f9828351e7aa4f9 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 20:51:41 +0800 Subject: [PATCH 42/60] Move game builder draft acquisition into dependency managers Assisted-by: codex:gpt-5.6-sol --- .../hmcl/game/HMCLDependencyManager.java | 10 +- .../jackhuang/hmcl/game/HMCLGameBuilder.java | 25 ++--- .../download/DefaultDependencyManager.java | 73 +++++++++++++- .../hmcl/download/DefaultGameBuilder.java | 98 ++++--------------- 4 files changed, 111 insertions(+), 95 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java index 44f896b9c4..dea1c0b83a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLDependencyManager.java @@ -46,14 +46,20 @@ public HMCLGameRepository getGameRepository() { /// {@inheritDoc} @Override public HMCLGameBuilder newGameBuilder(GameInstanceID instanceId) { - return new HMCLGameBuilder(this, instanceId); + GameInstanceManifest initialManifest = new GameInstanceManifest(instanceId); + DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, null); + return new HMCLGameBuilder(this, instanceId, null, draft, initialManifest); } /// {@inheritDoc} @Override public HMCLGameBuilder newGameBuilder(GameInstance instance) { validateGameInstance(instance); + HMCLGameInstance updateTarget = (HMCLGameInstance) instance; - return new HMCLGameBuilder(this, (HMCLGameInstance) instance); + GameInstanceManifest initialManifest = new GameInstanceManifest(updateTarget.getId()); + DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, updateTarget); + return new HMCLGameBuilder( + this, updateTarget.getId(), updateTarget, draft, initialManifest); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java index 6b97790407..7e259e4f4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameBuilder.java @@ -19,24 +19,25 @@ import org.jackhuang.hmcl.download.DefaultGameBuilder; import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; /// Builds game instances and applies HMCL-specific post-installation settings. @NotNullByDefault public class HMCLGameBuilder extends DefaultGameBuilder { - /// Creates a builder for installing a new HMCL instance. + /// Creates a builder around an HMCL target already reserved by its dependency manager. /// /// @param dependencyManager the dependency manager for the target repository - /// @param instanceId the id of the new instance - public HMCLGameBuilder(HMCLDependencyManager dependencyManager, GameInstanceID instanceId) { - super(dependencyManager, instanceId); - } - - /// Creates a builder for replacing the components of an existing HMCL instance. - /// - /// @param dependencyManager the dependency manager for the target repository - /// @param instance the existing instance selecting update mode and the target id - public HMCLGameBuilder(HMCLDependencyManager dependencyManager, HMCLGameInstance instance) { - super(dependencyManager, instance); + /// @param instanceId the id of the reserved instance + /// @param updateTarget the exact update target retained by the draft, or `null` for install + /// @param draft the open draft containing `initialManifest` + /// @param initialManifest the empty target manifest retained by `draft` + HMCLGameBuilder( + HMCLDependencyManager dependencyManager, + GameInstanceID instanceId, + @Nullable HMCLGameInstance updateTarget, + DefaultGameRepositoryDraft draft, + GameInstanceManifest initialManifest) { + super(dependencyManager, instanceId, updateTarget, draft, initialManifest); } /// {@inheritDoc} 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 6f8165f12a..a81d94e829 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -89,14 +89,78 @@ public DefaultCacheRepository getCacheRepository() { @Override public DefaultGameBuilder newGameBuilder(GameInstanceID instanceId) { - return new DefaultGameBuilder(this, instanceId); + GameInstanceManifest initialManifest = new GameInstanceManifest(instanceId); + DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, null); + return new DefaultGameBuilder(this, instanceId, null, draft, initialManifest); } @Override public DefaultGameBuilder newGameBuilder(GameInstance instance) { validateGameInstance(instance); + DefaultGameInstance updateTarget = (DefaultGameInstance) instance; - return new DefaultGameBuilder(this, (DefaultGameInstance) instance); + GameInstanceManifest initialManifest = new GameInstanceManifest(updateTarget.getId()); + DefaultGameRepositoryDraft draft = openGameBuilderDraft(initialManifest, updateTarget); + return new DefaultGameBuilder( + this, updateTarget.getId(), updateTarget, draft, initialManifest); + } + + /// Opens an exclusive draft and reserves a builder target in it. + /// + /// The exact `updateTarget` object must be present in the draft's base snapshot. An empty target + /// manifest is retained before this method returns. Any validation or reservation failure aborts + /// the draft before it is propagated. + /// + /// @param initialManifest the empty target manifest to reserve + /// @param updateTarget the exact published instance with the same id required for an update, + /// or `null` + /// @return the open draft containing the reserved target + /// @throws IllegalArgumentException if `updateTarget` belongs to another repository + /// @throws IllegalStateException if the target conflicts with the selected operation, cannot + /// be reserved, or another repository draft is open + protected final DefaultGameRepositoryDraft openGameBuilderDraft( + GameInstanceManifest initialManifest, + @Nullable DefaultGameInstance updateTarget) { + if (updateTarget != null) { + validateGameInstance(updateTarget); + } + + GameInstanceID instanceId = initialManifest.id(); + DefaultGameRepositoryDraft draft = repository.openDraft(); + try { + @Nullable DefaultGameInstance currentInstance = draft.getBaseSnapshot().findInstance(instanceId); + if (updateTarget == null) { + if (currentInstance != null) { + throw new IllegalStateException("Game instance already exists: " + instanceId); + } + } else if (currentInstance == null) { + throw new IllegalStateException("Game instance no longer exists: " + instanceId); + } else if (currentInstance != updateTarget) { + throw new IllegalStateException("Game instance has changed: " + instanceId); + } + draft.put(initialManifest); + return draft; + } catch (IOException e) { + abortDraftAfterFailure(draft, e); + throw new IllegalStateException("Cannot reserve game instance " + instanceId, e); + } catch (RuntimeException | Error failure) { + abortDraftAfterFailure(draft, failure); + throw failure; + } + } + + /// Aborts a draft and attaches a cleanup failure to the triggering failure. + /// + /// @param draft the draft to abort + /// @param failure the triggering failure that receives any cleanup failure as suppressed + static void abortDraftAfterFailure( + DefaultGameRepositoryDraft draft, + Throwable failure) { + try { + draft.abort(); + } catch (IOException cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } } @Override @@ -199,7 +263,10 @@ Task installUnpublishedComponentAsync( return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) .thenComposeAsync(manifest -> componentVersion .getInstallTask(this, manifest, modsDirectory) - .thenApplyAsync(patch -> patch == null ? manifest : manifest.addPatch(patch))) + .thenApplyAsync(patch -> patch == null + ? manifest + : manifest.addPatch(patch).reconstructByPatches() + )) .withStage("hmcl.install.%s:%s".formatted( componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index 97e76edc1c..a78322b7af 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -33,8 +33,9 @@ /// Installs a new game instance or replaces an existing instance in an exclusive /// [GameRepositoryDraft], then publishes the completed instance once. /// -/// Construction immediately reserves the target in an exclusive draft. An update requires the -/// supplied snapshot-bound instance to remain the exact currently published object. +/// Each builder receives a target already reserved by its dependency manager in an exclusive +/// draft. An update requires the supplied snapshot-bound instance to remain the exact currently +/// published object when the manager creates the builder. /// /// Replacement starts from an empty manifest with the target id and installs only the configured /// components. It retains the existing run directory unless isolation is explicitly enabled. @@ -65,34 +66,26 @@ public class DefaultGameBuilder extends GameBuilder { /// Whether instance isolation was requested for this build. protected boolean isolationEnabled; - /// Creates a builder for installing a new instance. + /// Creates a builder around a target already reserved by its dependency manager. /// /// @param dependencyManager the dependency manager for the target repository - /// @param instanceId the id of the new instance - /// @throws IllegalStateException if the id is already registered, its root cannot be reserved, - /// or another repository draft is open - public DefaultGameBuilder(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + /// @param instanceId the id of the reserved instance and `initialManifest` + /// @param updateTarget the exact update target retained by the draft, or `null` for install + /// @param draft the open draft containing `initialManifest`; it must belong to + /// `dependencyManager` + /// @param initialManifest the empty target manifest retained by `draft`; its id must equal + /// `instanceId` + protected DefaultGameBuilder( + DefaultDependencyManager dependencyManager, + GameInstanceID instanceId, + @Nullable DefaultGameInstance updateTarget, + DefaultGameRepositoryDraft draft, + GameInstanceManifest initialManifest) { this.dependencyManager = dependencyManager; this.instanceId = instanceId; - this.updateTarget = null; - this.initialManifest = new GameInstanceManifest(instanceId); - this.draft = openDraft(dependencyManager, instanceId, null, initialManifest); - } - - /// Creates a builder for replacing the components of an existing instance. - /// - /// @param dependencyManager the dependency manager for the target repository - /// @param instance the existing instance selecting update mode and the target id - /// @throws IllegalArgumentException if `instance` belongs to another repository - /// @throws IllegalStateException if `instance` is no longer the exact published instance or - /// another repository draft is open - public DefaultGameBuilder(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) { - dependencyManager.validateGameInstance(instance); - this.dependencyManager = dependencyManager; - this.instanceId = instance.getId(); - this.updateTarget = instance; - this.initialManifest = new GameInstanceManifest(instanceId); - this.draft = openDraft(dependencyManager, instanceId, instance, initialManifest); + this.updateTarget = updateTarget; + this.draft = draft; + this.initialManifest = initialManifest; } /// Returns the dependency manager used by this builder. @@ -193,7 +186,7 @@ public Task buildAsync() { draftTransferred = true; return buildTask; } catch (RuntimeException | Error failure) { - abortAfterSetupFailure(draft, failure); + DefaultDependencyManager.abortDraftAfterFailure(draft, failure); throw failure; } } @@ -235,55 +228,4 @@ protected final void checkOpen() { } } - /// Opens an exclusive draft, validates the exact update snapshot, and reserves the target. - /// - /// @param dependencyManager the dependency manager whose repository owns the draft - /// @param instanceId the target instance id - /// @param updateTarget the exact published instance required for an update, or `null` - /// @param initialManifest the empty target manifest to retain in the draft - /// @return the open draft containing `initialManifest` - /// @throws IllegalStateException if the target conflicts with the selected operation or cannot - /// be reserved - private static DefaultGameRepositoryDraft openDraft( - DefaultDependencyManager dependencyManager, - GameInstanceID instanceId, - @Nullable DefaultGameInstance updateTarget, - GameInstanceManifest initialManifest) { - DefaultGameRepositoryDraft draft = dependencyManager.getGameRepository().openDraft(); - try { - @Nullable DefaultGameInstance currentInstance = draft.getBaseSnapshot().findInstance(instanceId); - if (updateTarget == null) { - if (currentInstance != null) { - throw new IllegalStateException("Game instance already exists: " + instanceId); - } - } else if (currentInstance == null) { - throw new IllegalStateException("Game instance no longer exists: " + instanceId); - } else if (currentInstance != updateTarget) { - throw new IllegalStateException("Game instance has changed: " + instanceId); - } - draft.put(initialManifest); - return draft; - } catch (IOException e) { - abortAfterSetupFailure(draft, e); - throw new IllegalStateException("Cannot reserve game instance " + instanceId, e); - } catch (RuntimeException | Error failure) { - abortAfterSetupFailure(draft, failure); - throw failure; - } - } - - /// Aborts a draft after target validation or initial reservation fails. - /// - /// @param draft the draft to abort - /// @param failure the setup failure that receives any cleanup failure as suppressed - private static void abortAfterSetupFailure( - DefaultGameRepositoryDraft draft, - Throwable failure) { - try { - draft.abort(); - } catch (IOException cleanupFailure) { - failure.addSuppressed(cleanupFailure); - } - } - } From 1700515b17267870c2b682fcf8f101438538d9ca Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 20:58:12 +0800 Subject: [PATCH 43/60] refactor: simplify openGameBuilderDraft and improve async task composition --- .../hmcl/download/DefaultDependencyManager.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 a81d94e829..31cc508130 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -118,7 +118,7 @@ public DefaultGameBuilder newGameBuilder(GameInstance instance) { /// @throws IllegalArgumentException if `updateTarget` belongs to another repository /// @throws IllegalStateException if the target conflicts with the selected operation, cannot /// be reserved, or another repository draft is open - protected final DefaultGameRepositoryDraft openGameBuilderDraft( + protected DefaultGameRepositoryDraft openGameBuilderDraft( GameInstanceManifest initialManifest, @Nullable DefaultGameInstance updateTarget) { if (updateTarget != null) { @@ -320,12 +320,14 @@ public Task installComponentRemoteAsync( } Path modsDirectory = instance.getModsDirectory(); - return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) - .thenComposeAsync(manifest -> componentVersion - .getInstallTask(this, manifest, modsDirectory) - .thenApplyAsync(patch -> patch == null - ? manifest - : manifest.addPatch(patch).reconstructByPatches())) + return Task.composeAsync(() -> { + GameInstanceManifest manifest = baseManifest.removeComponent(componentVersion.getComponentType()); + return componentVersion + .getInstallTask(this, manifest, modsDirectory) + .thenApplyAsync(patch -> patch == null + ? manifest + : manifest.addPatch(patch).reconstructByPatches()); + }) .withStage("hmcl.install.%s:%s".formatted(componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); } From 7f69a52db50599c31f7c2fbb71f1ed0e79eddfa6 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 21:18:36 +0800 Subject: [PATCH 44/60] refactor: streamline async component installation and remove unused applyPatches method --- .../download/DefaultDependencyManager.java | 26 +++++++------------ .../hmcl/game/GameInstanceManifest.java | 19 -------------- 2 files changed, 10 insertions(+), 35 deletions(-) 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 31cc508130..dd29f221cc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -260,13 +260,15 @@ Task installUnpublishedComponentAsync( GameInstanceManifest baseManifest, Path modsDirectory, ComponentRemoteVersion componentVersion) { - return Task.supplyAsync(() -> baseManifest.removeComponent(componentVersion.getComponentType())) - .thenComposeAsync(manifest -> componentVersion - .getInstallTask(this, manifest, modsDirectory) - .thenApplyAsync(patch -> patch == null - ? manifest - : manifest.addPatch(patch).reconstructByPatches() - )) + return Task.composeAsync(() -> { + GameInstanceManifest manifest = baseManifest.removeComponent(componentVersion.getComponentType()); + return componentVersion + .getInstallTask(this, manifest, modsDirectory) + .thenApplyAsync(patch -> patch == null + ? manifest + : manifest.addPatch(patch).reconstructByPatches() + ); + }) .withStage("hmcl.install.%s:%s".formatted( componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); @@ -320,15 +322,7 @@ public Task installComponentRemoteAsync( } Path modsDirectory = instance.getModsDirectory(); - return Task.composeAsync(() -> { - GameInstanceManifest manifest = baseManifest.removeComponent(componentVersion.getComponentType()); - return componentVersion - .getInstallTask(this, manifest, modsDirectory) - .thenApplyAsync(patch -> patch == null - ? manifest - : manifest.addPatch(patch).reconstructByPatches()); - }) - .withStage("hmcl.install.%s:%s".formatted(componentVersion.getComponentType().getPatchId(), componentVersion.getSelfVersion())); + return installUnpublishedComponentAsync(baseManifest, modsDirectory, componentVersion); } /// Resolves a remote component by id/version and installs it into the working manifest. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 1addf6f1b9..8524966f05 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -701,25 +701,6 @@ public JsonObject toJsonObject() { return json; } - public GameInstanceManifest applyPatches() { - if (inheritsFrom != null) { - throw new IllegalStateException("Cannot resolve patches on a manifest that inherits from another manifest"); - } - - if (patches == null || patches.isEmpty()) { - return this; - } - - GameInstanceManifest manifest = isRoot() - ? new GameInstanceManifest(id) - : this; - for (GameInstancePatch patch : Lang.toIterable(patches().stream() - .sorted(Comparator.comparing(GameInstancePatch::getPriority)))) { - manifest = patch.merge(manifest); - } - return this; - } - public GameInstanceManifest removeComponent(GameComponentType type) { @Nullable GameInstancePatch patch = findPatch(type.getPatchId()); if (patch == null) { From 1d19b5903da0758c836f3812c553f69076ccff96 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 21:43:08 +0800 Subject: [PATCH 45/60] refactor: validate modifiable manifest and simplify manifest usage in installation tasks --- .../jackhuang/hmcl/download/fabric/FabricInstallTask.java | 6 +++++- .../org/jackhuang/hmcl/download/forge/ForgeInstallTask.java | 6 +++++- .../jackhuang/hmcl/download/game/GameAssetDownloadTask.java | 2 +- .../org/jackhuang/hmcl/download/game/GameDownloadTask.java | 2 +- .../org/jackhuang/hmcl/download/game/GameInstallTask.java | 2 +- .../org/jackhuang/hmcl/download/game/GameLibrariesTask.java | 2 +- .../hmcl/download/optifine/OptiFineInstallTask.java | 2 +- .../org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java | 2 +- .../java/org/jackhuang/hmcl/game/GameInstanceManifest.java | 2 +- 9 files changed, 17 insertions(+), 9 deletions(-) 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 090596ede1..6a89cab808 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 @@ -47,6 +47,10 @@ public final class FabricInstallTask extends Task { private final List> dependencies = new ArrayList<>(1); public FabricInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, FabricRemoteVersion remoteVersion) { + if (!manifest.isModifiable()) { + throw new IllegalArgumentException("Manifest is not modifiable"); + } + this.dependencyManager = dependencyManager; this.manifest = manifest; this.remote = remoteVersion; @@ -62,7 +66,7 @@ public boolean doPreExecute() { @Override public void preExecute() throws Exception { - if (!Objects.equals(GameComponentAnalyzer.VANILLA_MAIN, dependencyManager.getGameRepository().resolve(manifest).launchManifest().mainClass())) + if (!Objects.equals(GameComponentAnalyzer.VANILLA_MAIN, manifest.mainClass())) throw new UnsupportedInstallationException(FABRIC_NOT_COMPATIBLE_WITH_FORGE); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index a5ed6d41a0..35e6b129d2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -56,6 +56,10 @@ public final class ForgeInstallTask extends Task { private Task dependency; public ForgeInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, ForgeRemoteVersion remoteVersion) { + if (!manifest.isModifiable()) { + throw new IllegalArgumentException("Manifest is not modifiable"); + } + this.dependencyManager = dependencyManager; this.manifest = manifest; this.remote = remoteVersion; @@ -102,7 +106,7 @@ public Collection> getDependencies() { @Override public void execute() throws IOException, VersionMismatchException, UnsupportedInstallationException { - String originalMainClass = dependencyManager.getGameRepository().resolve(manifest).launchManifest().mainClass(); + String originalMainClass = manifest.mainClass(); if (GameVersionNumber.compare("1.13", remote.getGameVersion()) <= 0) { // Forge 1.13 is not compatible with fabric. if (!GameComponentAnalyzer.FORGE_OPTIFINE_MAIN.contains(originalMainClass)) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java index 01be3d3635..fc84997843 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java @@ -55,7 +55,7 @@ public final class GameAssetDownloadTask extends Task { /// @param manifest the game version public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, GameInstanceManifest manifest, boolean forceDownloadingIndex, boolean integrityCheck) { this.dependencyManager = dependencyManager; - this.manifest = dependencyManager.getGameRepository().resolve(manifest).launchManifest(); + this.manifest = manifest; this.assetIndexInfo = this.manifest.getAssetIndex(); GameRepository gameRepository = dependencyManager.getGameRepository(); String assetId = assetIndexInfo.getId(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java index 6709cb5c3b..44dde35624 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java @@ -53,7 +53,7 @@ public GameDownloadTask( DefaultDependencyManager dependencyManager, GameInstanceManifest manifest) { this.dependencyManager = dependencyManager; - this.manifest = dependencyManager.getGameRepository().resolve(manifest).launchManifest(); + this.manifest = manifest; setSignificance(TaskSignificance.MODERATE); } 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 0ca46588df..527371fd28 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 @@ -93,7 +93,7 @@ public void execute() throws Exception { GameInstancePatch.PRIORITY_MC).withJar(null); setResult(patch); - GameInstanceManifest newManifest = new GameInstanceManifest(this.manifest.id()).addPatch(patch); + GameInstanceManifest newManifest = new GameInstanceManifest(this.manifest.id()).addPatch(patch).reconstructByPatches(); dependencies.add(Task.allOf( new GameDownloadTask(dependencyManager, newManifest), Task.allOf( diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java index 6d817040d7..fcd6e3ea71 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java @@ -62,7 +62,7 @@ public final class GameLibrariesTask extends Task { * @param manifest the game version */ public GameLibrariesTask(AbstractDependencyManager dependencyManager, GameInstanceManifest manifest, boolean integrityCheck) { - this(dependencyManager, manifest, integrityCheck, dependencyManager.getGameRepository().resolve(manifest).launchManifest().getLibraries()); + this(dependencyManager, manifest, integrityCheck, manifest.getLibraries()); } /** diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index f8258bb29f..39f3d6d2eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -154,7 +154,7 @@ public void execute() throws Exception { throw new IOException("Minecraft client JAR not found: " + minecraftJar); } Path installerFile = Objects.requireNonNull(dest); - String originalMainClass = dependencyManager.getGameRepository().resolve(manifest).launchManifest().mainClass(); + String originalMainClass = manifest.mainClass(); if (!GameComponentAnalyzer.FORGE_OPTIFINE_MAIN.contains(originalMainClass)) throw new UnsupportedInstallationException(UnsupportedInstallationException.UNSUPPORTED_LAUNCH_WRAPPER); 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 e747533e09..bc94a6cb79 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 @@ -61,7 +61,7 @@ public boolean doPreExecute() { @Override public void preExecute() throws Exception { - if (!Objects.equals("net.minecraft.client.main.Main", dependencyManager.getGameRepository().resolve(manifest).launchManifest().mainClass())) + if (!Objects.equals(GameComponentAnalyzer.VANILLA_MAIN, manifest.mainClass())) throw new UnsupportedInstallationException(FABRIC_NOT_COMPATIBLE_WITH_FORGE); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 8524966f05..19e70e518f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -356,7 +356,7 @@ public boolean isRoot() { } public boolean isModifiable() { - return inheritsFrom == null && patches != null && !patches.isEmpty(); + return inheritsFrom == null && patches != null && hasPatch(GameComponentType.GAME.getPatchId()); } /// Returns the pending patches. From a2c9096817ab418fe1e488a6ef5334b0dd3434e7 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 21:48:26 +0800 Subject: [PATCH 46/60] refactor: update instance jar retrieval to simplify path handling in DefaultLauncher --- .../org/jackhuang/hmcl/launch/DefaultLauncher.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index dbaeb43056..82f16243a2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -148,7 +148,9 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { if (!options.isNoGeneratedJVMArgs()) { appendJvmArgs(res); - res.addDefault("-Dminecraft.client.jar=", FileUtils.getAbsolutePath(instance.getRepository().getInstanceJar(manifest))); + Path clientJar = instance.getInstanceJarFile(); + + res.addDefault("-Dminecraft.client.jar=", FileUtils.getAbsolutePath(clientJar)); if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { res.addDefault("-Xdock:name=", "Minecraft " + manifest.id()); @@ -278,11 +280,11 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { libraryClasspath.removeIf(c -> c.contains("2.9.4-nightly-20150209")); } - Path jar = instance.getRepository().getInstanceJar(manifest); + Path jar = instance.getInstanceJarFile(); if (!Files.isRegularFile(jar)) throw new IOException("Minecraft jar does not exist"); Set classpath = new LinkedHashSet<>(libraryClasspath); - classpath.add(FileUtils.getAbsolutePath(jar.toAbsolutePath())); + classpath.add(FileUtils.getAbsolutePath(jar)); // Provided Minecraft arguments Path gameAssets = instance.getActualAssetDirectory(manifest.getAssetIndex().getId()); @@ -633,7 +635,7 @@ protected Map getConfigurations() { pair("${resolution_height}", options.getHeight().toString()), pair("${library_directory}", FileUtils.getAbsolutePath(instance.getLayout().getLibrariesDirectory())), pair("${classpath_separator}", File.pathSeparator), - pair("${primary_jar}", FileUtils.getAbsolutePath(instance.getRepository().getInstanceJar(manifest))), + pair("${primary_jar}", FileUtils.getAbsolutePath(instance.getInstanceJarFile())), pair("${language}", Locale.getDefault().toLanguageTag()), // defined by HMCL @@ -643,7 +645,7 @@ protected Map getConfigurations() { pair("${libraries_directory}", FileUtils.getAbsolutePath(instance.getLayout().getLibrariesDirectory())), // file_separator is used in -DignoreList pair("${file_separator}", File.separator), - pair("${primary_jar_name}", FileUtils.getName(instance.getRepository().getInstanceJar(manifest))) + pair("${primary_jar_name}", FileUtils.getName(instance.getInstanceJarFile())) ); } From 8cb84e895224be00894b37667df27248efb8a4c6 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 21:50:38 +0800 Subject: [PATCH 47/60] refactor: remove unused getGameVersion method and related test for game version lookup --- .../hmcl/game/DefaultGameRepository.java | 27 ------------------- .../jackhuang/hmcl/game/GameRepository.java | 7 ----- .../hmcl/game/DefaultGameInstanceTest.java | 18 ------------- 3 files changed, 52 deletions(-) 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 31c4e512d5..f90e3746bf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -27,7 +27,6 @@ import org.jackhuang.hmcl.util.function.ExceptionalFunction; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; @@ -507,32 +506,6 @@ public boolean removeInstanceFromDisk(GameInstanceID id) { } } - @Override - public Optional getGameVersion(GameInstanceManifest manifest) { - DefaultGameInstance instance = findSnapshotInstance(manifest.id()); - if (instance != null && manifest.equals(instance.getManifest())) { - GameVersionNumber version = instance.getVersion(); - if (version == GameVersionNumber.unknown()) { - return Optional.empty(); - } - return Optional.of(version.toString()); - } - - try { - GameInstanceManifest resolved = resolve(manifest).launchManifest(); - Path instanceJar = getInstanceJar(resolved); - Optional gameVersion = GameVersion.minecraftVersion(instanceJar); - if (gameVersion.isEmpty()) { - LOG.warning("Cannot find out game version of " + manifest.id() - + ", primary jar: " + instanceJar - + ", jar exists: " + Files.exists(instanceJar)); - } - return gameVersion; - } catch (NoSuchGameInstanceException e) { - return Optional.empty(); - } - } - /// Returns the stored instance manifest file for an instance. /// /// When the instance is loaded with a non-conventional path, that path is returned; otherwise 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 e1741129ff..a47822e8af 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -21,7 +21,6 @@ import org.jetbrains.annotations.NotNullByDefault; import java.nio.file.Path; -import java.util.Optional; /// Provides indexed access to local game instances and the filesystem layout used by those instances. /// @@ -132,12 +131,6 @@ default Path getInstanceRoot(GameInstanceID instanceId) { /// @return the primary client jar path Path getInstanceJar(GameInstanceManifest manifest); - /// Detects the Minecraft game version associated with a manifest. - /// - /// @param manifest the manifest to inspect - /// @return the detected Minecraft game version, or empty if it cannot be determined - Optional getGameVersion(GameInstanceManifest manifest); - /// Renames an instance and updates repository-managed references. /// /// @param from the current instance id diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index b972f0e3c1..86bbd79caf 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -189,24 +189,6 @@ public void testSnapshotCopyDoesNotShareAddonManagers(@TempDir Path tempDirector assertEquals(GameVersionNumber.asGameVersion("1.21.1"), updated.getVersion()); } - /// Version lookup for an explicit manifest does not reuse a same-id instance with different content. - @Test - public void testExplicitManifestDoesNotReuseDifferentCachedManifest(@TempDir Path tempDirectory) throws IOException { - TestRepository repository = new TestRepository(tempDirectory); - GameInstanceID instanceId = new GameInstanceID("instance"); - GameInstanceID cachedJarId = new GameInstanceID("cached-jar"); - GameInstanceID requestedJarId = new GameInstanceID("requested-jar"); - writeVersionJar(repository.getLayout().getInstanceJarFile(cachedJarId), "1.20.1"); - writeVersionJar(repository.getLayout().getInstanceJarFile(requestedJarId), "1.21.1"); - - GameInstanceManifest cachedManifest = new GameInstanceManifest(instanceId).withJar(cachedJarId); - TestGameInstance cachedInstance = repository.publish(instanceId, cachedManifest); - assertEquals(GameVersionNumber.asGameVersion("1.20.1"), cachedInstance.getVersion()); - - GameInstanceManifest requestedManifest = cachedManifest.withJar(requestedJarId); - assertEquals(Optional.of("1.21.1"), repository.getGameVersion(requestedManifest)); - } - /// A cached game download can be materialized at an explicit destination. @Test public void testGameDownloadMaterializesExplicitDestination(@TempDir Path tempDirectory) From adf982f6dd0f7a1ef88055b8f44cef20b6fb9fd8 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 21:53:54 +0800 Subject: [PATCH 48/60] refactor: remove unused instance jar retrieval methods and simplify game instance handling --- .../hmcl/game/DefaultGameRepository.java | 34 ------------------- .../jackhuang/hmcl/game/GameRepository.java | 6 ---- .../multimc/MultiMCModpackInstallTask.java | 2 +- .../hmcl/game/DefaultGameInstanceTest.java | 1 - 4 files changed, 1 insertion(+), 42 deletions(-) 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 f90e3746bf..759c055207 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -415,25 +415,6 @@ public DefaultGameInstance getInstance(GameInstanceID id) throws NoSuchGameInsta return getSnapshot().getRegistered(id); } - /// Returns the instance recorded in the current snapshot for the given id. - /// - /// @param id the instance id - /// @return the instance, or `null` when absent from the current snapshot - protected @Nullable DefaultGameInstance findSnapshotInstance(GameInstanceID id) { - return getSnapshot().get(id); - } - - @Override - public Path getInstanceJar(GameInstanceManifest manifest) { - GameInstanceManifest resolved = this.resolve(manifest).launchManifest(); - GameInstanceID id = Optional.ofNullable(resolved.jar()).orElse(resolved.id()); - DefaultGameInstance instance = findSnapshotInstance(id); - if (instance != null) { - return instance.getOwnJarFile(); - } - return getLayout().getInstanceJarFile(id); - } - @Override public boolean renameInstance(GameInstanceID from, GameInstanceID to) { try (DefaultGameRepositoryDraft draft = openDraft()) { @@ -506,21 +487,6 @@ public boolean removeInstanceFromDisk(GameInstanceID id) { } } - /// Returns the stored instance manifest file for an instance. - /// - /// When the instance is loaded with a non-conventional path, that path is returned; otherwise - /// the layout default `versions//.json` is used. - /// - /// @param instanceId the instance id - /// @return the manifest JSON path - public Path getInstanceJson(GameInstanceID instanceId) { - DefaultGameInstance instance = findSnapshotInstance(instanceId); - if (instance != null) { - return instance.getManifestFile(); - } - return getLayout().getInstanceJson(instanceId); - } - /// Opens a draft for staging instance index changes and committing them once. /// /// @return a new open draft 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 a47822e8af..b01c590953 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -125,12 +125,6 @@ default Path getInstanceRoot(GameInstanceID instanceId) { return getLayout().getInstanceRoot(instanceId); } - /// Returns the primary client jar path for a manifest. - /// - /// @param manifest the manifest whose jar should be located - /// @return the primary client jar path - Path getInstanceJar(GameInstanceManifest manifest); - /// Renames an instance and updates repository-managed references. /// /// @param from the current instance id diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java index 024796acf4..d4e57587dd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java @@ -376,7 +376,7 @@ public void execute() throws Exception { true )); - Path instanceJar = repository.getInstanceJar(instanceManifest); + Path instanceJar = requireDraft().getBaseSnapshot().getLayout().getInstanceJarFile(instanceId); dependencies.add(new GameDownloadTask(dependencyManager, instanceManifest) .thenAcceptAsync(cachedJar -> FileUtils.copyFile(cachedJar, instanceJar))); } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index 86bbd79caf..f88c2e8e83 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -545,7 +545,6 @@ public void testRefreshRecordsNonConventionalStoragePaths(@TempDir Path tempDire assertEquals(GameVersionNumber.asGameVersion("1.20.1"), instance.getVersion()); assertEquals(folderId, instance.getId()); assertEquals(folderId, instance.getManifest().id()); - assertEquals(json, repository.getInstanceJson(folderId)); } /// Writes a minimal jar containing the version metadata consumed by [GameVersion]. From afdc877fe7e5eb459ba81c090f13af894c5172a2 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 29 Aug 2026 22:01:29 +0800 Subject: [PATCH 49/60] refactor: simplify GameInstanceManifest handling and remove resolved variant --- .../hmcl/download/DefaultGameBuilder.java | 6 +- .../hmcl/game/DefaultGameInstance.java | 4 +- .../hmcl/game/DefaultGameRepository.java | 5 -- .../game/DefaultGameRepositorySnapshot.java | 42 ++----------- .../org/jackhuang/hmcl/game/GameInstance.java | 9 +-- .../jackhuang/hmcl/game/GameRepository.java | 6 -- .../hmcl/game/GameInstanceManifestTest.java | 60 ------------------- 7 files changed, 12 insertions(+), 120 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index a78322b7af..404a43a73a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -167,10 +167,10 @@ public Task buildAsync() { } Task buildTask = libraryTask.thenComposeAsync(manifest -> { - GameInstanceManifest.Resolved resolved = draft.getBaseSnapshot().resolve(manifest); - return new GameDownloadTask(dependencyManager, resolved.launchManifest()) + GameInstanceManifest resolved = draft.getBaseSnapshot().resolve(manifest); + return new GameDownloadTask(dependencyManager, resolved) .thenApplyAsync(minecraftJar -> { - draft.put(resolved.launchManifest().withPatches(manifest.patches())); + draft.put(resolved.withPatches(manifest.patches())); draft.putPrimaryJar(instanceId, minecraftJar); DefaultGameInstance instance = draft.commit().getInstance(instanceId); onInstanceCommitted(instance); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java index a5252d29e3..6e28bcb655 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java @@ -57,7 +57,7 @@ public abstract class DefaultGameInstance implements GameInstance { /// `.jar` extension. protected final @Nullable Path manifestFile; - protected GameInstanceManifest.@Nullable Resolved resolvedManifest; + protected @Nullable GameInstanceManifest resolvedManifest; private @Nullable GameComponentAnalyzer analyzer; @@ -158,7 +158,7 @@ public GameInstanceManifest getManifest() { } @Override - public GameInstanceManifest.Resolved getResolvedManifest() { + public GameInstanceManifest getLaunchManifest() { if (resolvedManifest == null) { resolvedManifest = snapshot.resolve(manifest); } 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 759c055207..8216ead2d0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -556,11 +556,6 @@ public Task updateInstanceAsync( }); } - @Override - public GameInstanceManifest.Resolved resolve(GameInstanceManifest manifest) throws NoSuchGameInstanceException { - return getSnapshot().resolve(manifest); - } - /// Creates an empty unsealed snapshot for the given layout. /// /// @param layout the layout for the new snapshot diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java index 93cfe166b3..5220bae499 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java @@ -176,24 +176,8 @@ DefaultGameRepositorySnapshot mutableCopy() { return newSnapshot; } - /// Resolves official-layout inheritance and patches, then deduplicates launch libraries. - /// - /// Loader-specific argument repairs are applied later for a concrete launch attempt (for example - /// by [LaunchManifestNormalizer#repairForLaunch(GameInstanceManifest)]). - /// - /// @param manifest the manifest to resolve - /// @return the resolved manifest views - /// @throws NoSuchGameInstanceException if an inherited parent is missing from this snapshot - public GameInstanceManifest.Resolved resolve(GameInstanceManifest manifest) throws NoSuchGameInstanceException { - GameInstanceManifest.Resolved resolved = resolve(manifest, new HashSet<>()); - GameInstanceManifest launchManifest = uniqueLibraries(resolved.launchManifest()); - if (launchManifest != resolved.launchManifest()) { - resolved = new GameInstanceManifest.Resolved( - resolved.unresolved(), - launchManifest, - resolved.standaloneManifest()); - } - return resolved; + public GameInstanceManifest resolve(GameInstanceManifest manifest) throws NoSuchGameInstanceException { + return resolve(manifest, new HashSet<>()); } /// Resolves official-layout inheritance and patches without launch-library deduplication. @@ -202,15 +186,10 @@ public GameInstanceManifest.Resolved resolve(GameInstanceManifest manifest) thro /// @param resolvedSoFar instance ids already visited in the inheritance chain /// @return the resolved manifest views /// @throws NoSuchGameInstanceException if an inherited parent is missing from this snapshot - private GameInstanceManifest.Resolved resolve( + private GameInstanceManifest resolve( GameInstanceManifest manifest, Set resolvedSoFar) throws NoSuchGameInstanceException { GameInstanceManifest launchManifest; - GameInstanceManifest standaloneManifest = manifest.isRoot() - ? manifest - : addPatches( - addPatches(new GameInstanceManifest(manifest.id()), List.of(manifest.toPatch())), - manifest.patches()); if (manifest.inheritsFrom() == null) { if (manifest.isRoot()) { @@ -235,12 +214,8 @@ private GameInstanceManifest.Resolved resolve( } // It is supposed to auto-install a version in getVersion. - GameInstanceManifest.Resolved parentResolved = - resolve(parentInstance.getManifest(), resolvedSoFar); - launchManifest = manifest.merge(parentResolved.launchManifest()); - standaloneManifest = addPatches( - addPatches(parentResolved.standaloneManifest(), List.of(manifest.toPatch())), - manifest.patches()); + GameInstanceManifest parentResolved = resolve(parentInstance.getManifest(), resolvedSoFar); + launchManifest = manifest.merge(parentResolved); } } @@ -255,12 +230,7 @@ private GameInstanceManifest.Resolved resolve( } launchManifest = launchManifest.withId(manifest.id()).withPatches(null); - standaloneManifest = standaloneManifest.withId(manifest.id()); - if (launchManifest.jar() != null) { - standaloneManifest = standaloneManifest.withJar(launchManifest.jar()); - } - - return new GameInstanceManifest.Resolved(manifest, launchManifest, standaloneManifest); + return launchManifest; } /// Removes redundant library declarations while retaining rule-distinct variants. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java index 85258c4b1f..49385530b9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java @@ -53,17 +53,10 @@ public interface GameInstance { /// @return the unresolved stored manifest GameInstanceManifest getManifest(); - /// Returns the eagerly resolved manifest views captured with this instance. - /// - /// @return the resolved manifest views - GameInstanceManifest.Resolved getResolvedManifest(); - /// Returns the manifest used by launch-time consumers. /// /// @return the launch manifest - default GameInstanceManifest getLaunchManifest() { - return getResolvedManifest().launchManifest(); - } + GameInstanceManifest getLaunchManifest(); GameComponentAnalyzer getAnalyzer(); 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 b01c590953..cb07a96aaa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -66,12 +66,6 @@ default Path getBaseDirectory() { /// @see GameRepositoryDraft GameRepositoryDraft openDraft(); - /// Resolves inheritance into a normalized launch view and a patch-preserving standalone view. - /// - /// @param manifest the manifest to resolve - /// @return the resolved manifest view - GameInstanceManifest.Resolved resolve(GameInstanceManifest manifest) throws NoSuchGameInstanceException; - /// Returns whether the instance exists in the current repository index. /// /// @param instanceId the instance id diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/GameInstanceManifestTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/GameInstanceManifestTest.java index 48c46b9c5a..56c5ab63b6 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/GameInstanceManifestTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/GameInstanceManifestTest.java @@ -22,7 +22,6 @@ import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; -import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Objects; @@ -44,65 +43,6 @@ public void testNullableBooleanFieldsAreNotMaterialized() { assertFalse(json.has("hidden")); } - /// Root manifests with patch lists resolve from the patch view instead of their own body fields. - @Test - public void testRootManifestWithPatchesUsesPatchView() throws NoSuchGameInstanceException { - GameInstanceManifest manifest = manifest( - "example", - "example.Main", - true, - false, - List.of(patch("patch", null))); - - GameInstanceManifest.Resolved resolved = new DefaultGameRepository(Path.of(".")) { - @Override - protected DefaultGameRepositoryLayout createLayout(Path baseDirectory) { - return new DefaultGameRepositoryLayout(baseDirectory); - } - - @Override - protected DefaultGameInstance createInstance( - DefaultGameRepositorySnapshot snapshot, - GameInstanceID id, - GameInstanceManifest manifest, - @Nullable Path manifestFile) { - final class MyGameInstance extends DefaultGameInstance { - MyGameInstance( - DefaultGameRepositorySnapshot snapshot, - GameInstanceID id, - GameInstanceManifest manifest, - @Nullable Path manifestFile) { - super(snapshot, id, manifest, manifestFile); - } - - MyGameInstance( - DefaultGameRepositorySnapshot snapshot, - GameInstanceID id, - GameInstanceManifest manifest, - DefaultGameInstance shareSession) { - super(snapshot, id, manifest, shareSession); - } - - @Override - protected DefaultGameInstance withNewSnapshot(DefaultGameRepositorySnapshot newSnapshot) { - return new MyGameInstance(newSnapshot, id, manifest, this); - } - - @Override - protected DefaultGameInstance withManifest(DefaultGameRepositorySnapshot newSnapshot, GameInstanceManifest manifest) { - return new MyGameInstance(newSnapshot, id, manifest, this); - } - } - - return new MyGameInstance(snapshot, id, manifest, manifestFile); - } - }.resolve(manifest); - - assertNull(resolved.launchManifest().mainClass()); - assertNull(resolved.launchManifest().patches()); - assertEquals(List.of(patch("patch", null)), resolved.standaloneManifest().getPatches()); - } - /// Manifest edits update known raw JSON fields without discarding unknown fields. @Test public void testRawJsonIsPreservedWhenUpdatingKnownFields() { From 4b9f884937665188a9221faa5854f5f08a512101 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 30 Aug 2026 20:17:16 +0800 Subject: [PATCH 50/60] refactor: streamline file reading and simplify manifest initialization in game settings --- .../java/org/jackhuang/hmcl/game/HMCLGameRepository.java | 5 ++--- .../java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 0f4fe51b85..8b4255d2f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -273,9 +273,8 @@ private String selectedRunningDirectory( if (!Files.isRegularFile(file)) { return null; } - try { - return LauncherSettings.SETTINGS_GSON - .fromJson(Files.readString(file), GameSettings.Instance.class); + try (var reader = Files.newBufferedReader(file)) { + return LauncherSettings.SETTINGS_GSON.fromJson(reader, GameSettings.Instance.class); } catch (Exception e) { LOG.warning("Failed to peek instance game settings: " + file, e); return null; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index fb092ab1dd..7adfde9539 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -2818,8 +2818,7 @@ private void initJavaSubtitle() { if (JavaManager.isInitialized()) { GameVersionNumber gameVersionNumber = currentGameVersion(); - GameInstanceManifest manifest; - manifest = gameInstance != null ? gameInstance.getLaunchManifest() : null; + GameInstanceManifest manifest = gameInstance != null ? gameInstance.getLaunchManifest() : null; try { JavaRuntime java = effectiveSetting != null From c204cde6130e1691a9064632853a0fb329e14b41 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 30 Aug 2026 20:24:29 +0800 Subject: [PATCH 51/60] refactor: simplify GameInstanceManifest resolution and update patch merging logic --- .../game/DefaultGameRepositorySnapshot.java | 22 +++++++++------- .../hmcl/game/GameInstancePatch.java | 26 ------------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java index 5220bae499..7727a35625 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java @@ -189,23 +189,22 @@ public GameInstanceManifest resolve(GameInstanceManifest manifest) throws NoSuch private GameInstanceManifest resolve( GameInstanceManifest manifest, Set resolvedSoFar) throws NoSuchGameInstanceException { - GameInstanceManifest launchManifest; + GameInstanceManifest resolvedManifest; if (manifest.inheritsFrom() == null) { if (manifest.isRoot()) { - // TODO: Breaking change, require much testing on versions installed with external installer, other launchers, and all kinds of versions. - launchManifest = manifest.patches() != null + resolvedManifest = manifest.patches() != null ? new GameInstanceManifest(manifest.id()).withPatches(manifest.patches()) : manifest; } else { - launchManifest = manifest; + resolvedManifest = manifest; } - launchManifest = launchManifest.withJar(manifest.jar() == null ? manifest.id() : manifest.jar()); + resolvedManifest = resolvedManifest.withJar(manifest.jar() == null ? manifest.id() : manifest.jar()); } else { // To maximize the compatibility. if (!resolvedSoFar.add(manifest.id())) { LOG.warning("Found circular dependency instances: " + resolvedSoFar); - launchManifest = (manifest.jar() == null ? manifest.withJar(manifest.id()) : manifest) + resolvedManifest = (manifest.jar() == null ? manifest.withJar(manifest.id()) : manifest) .withInheritsFrom(null); } else { DefaultGameInstance parentInstance = instances.get(manifest.inheritsFrom()); @@ -215,22 +214,25 @@ private GameInstanceManifest resolve( // It is supposed to auto-install a version in getVersion. GameInstanceManifest parentResolved = resolve(parentInstance.getManifest(), resolvedSoFar); - launchManifest = manifest.merge(parentResolved); + resolvedManifest = manifest.merge(parentResolved); } } + var builder = new GameInstanceManifest.Builder(resolvedManifest); + if (manifest.patches() != null && !manifest.patches().isEmpty()) { // Assume patches themselves do not have patches recursively. List sortedPatches = manifest.patches().stream() .sorted(Comparator.comparing(GameInstancePatch::getPriority)) .toList(); for (GameInstancePatch patch : sortedPatches) { - launchManifest = patch.merge(launchManifest); + builder.merge(patch); } } - launchManifest = launchManifest.withId(manifest.id()).withPatches(null); - return launchManifest; + builder.setId(manifest.id()); + builder.setPatches(null); + return builder.toManifest(); } /// Removes redundant library declarations while retaining rule-distinct variants. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java index 06fd1c6985..cba5cab879 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java @@ -23,7 +23,6 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import org.jackhuang.hmcl.util.ImmutableSequencedMap; -import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.gson.InstantTypeAdapter; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.LowerCaseEnumTypeAdapter; @@ -867,29 +866,4 @@ public JsonObject toJsonObject() { return json; } - GameInstanceManifest merge(GameInstanceManifest parent) { - return new GameInstanceManifest( - parent.id(), - minecraftArguments == null ? parent.minecraftArguments() : minecraftArguments, - Arguments.merge(parent.arguments(), arguments), - mainClass == null ? parent.mainClass() : mainClass, - null, // inheritsFrom - parent.jar(), - assetIndex == null ? parent.assetIndex() : assetIndex, - assets == null ? parent.assets() : assets, - complianceLevel, - javaVersion == null ? parent.javaVersion() : javaVersion, - Lang.merge(this.libraries, parent.libraries()), - Lang.merge(parent.compatibilityRules(), this.compatibilityRules), - downloads == null ? parent.downloads() : downloads, - logging == null ? parent.logging() : logging, - type == null ? parent.type() : type, - time == null ? parent.time() : time, - releaseTime == null ? parent.releaseTime() : releaseTime, - Lang.merge(minimumLauncherVersion, parent.minimumLauncherVersion(), Math::max), - true, - hidden, - parent.patches(), - null); - } } From 465e29931d163c8dd6f86e9b3be7c44204316878 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 30 Aug 2026 20:38:29 +0800 Subject: [PATCH 52/60] refactor: enhance resolve method to handle null resolvedSoFar and remove unused addPatches method --- .../game/DefaultGameRepositorySnapshot.java | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java index 7727a35625..b091bb4b6e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java @@ -188,7 +188,7 @@ public GameInstanceManifest resolve(GameInstanceManifest manifest) throws NoSuch /// @throws NoSuchGameInstanceException if an inherited parent is missing from this snapshot private GameInstanceManifest resolve( GameInstanceManifest manifest, - Set resolvedSoFar) throws NoSuchGameInstanceException { + @Nullable Set resolvedSoFar) throws NoSuchGameInstanceException { GameInstanceManifest resolvedManifest; if (manifest.inheritsFrom() == null) { @@ -201,6 +201,10 @@ private GameInstanceManifest resolve( } resolvedManifest = resolvedManifest.withJar(manifest.jar() == null ? manifest.id() : manifest.jar()); } else { + if (resolvedSoFar == null) { + resolvedSoFar = new HashSet<>(); + } + // To maximize the compatibility. if (!resolvedSoFar.add(manifest.id())) { LOG.warning("Found circular dependency instances: " + resolvedSoFar); @@ -297,27 +301,4 @@ private static GameInstanceManifest uniqueLibraries(GameInstanceManifest manifes : manifest.withLibraries(libraries); } - private static GameInstanceManifest addPatches(GameInstanceManifest manifest, @Nullable List additional) { - if (additional == null || additional.isEmpty()) { - return manifest; - } - - Set patchIds = new HashSet<>(); - for (GameInstancePatch patch : additional) { - if (patch.id() != null) { - patchIds.add(patch.id()); - } - } - - List patches = new ArrayList<>(); - if (manifest.patches() != null) { - for (GameInstancePatch patch : manifest.patches()) { - if (patch.id() == null || !patchIds.contains(patch.id())) { - patches.add(patch); - } - } - } - patches.addAll(additional); - return manifest.withPatches(patches); - } } From ad91fe9034e168d1c5b204c51c5b7dcca516eae6 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 30 Aug 2026 21:12:54 +0800 Subject: [PATCH 53/60] refactor: implement uniqueLibraries method to optimize library declarations in GameInstanceManifest --- .../game/DefaultGameRepositorySnapshot.java | 65 ----------------- .../hmcl/game/LaunchManifestNormalizer.java | 70 ++++++++++++++++++- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java index b091bb4b6e..2a7cfea471 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepositorySnapshot.java @@ -17,9 +17,6 @@ */ package org.jackhuang.hmcl.game; -import org.jackhuang.hmcl.util.SimpleMultimap; -import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; @@ -239,66 +236,4 @@ private GameInstanceManifest resolve( return builder.toManifest(); } - /// Removes redundant library declarations while retaining rule-distinct variants. - /// - /// When two libraries share the same `groupId:artifactId` and equal compatibility rules, the - /// newer version wins. When versions are equal and the coordinate objects compare equal, the - /// declaration with the longer serialized JSON is kept (more metadata is treated as richer). - /// Equal id and version with unequal coordinate payloads (for example distinct `text2speech` - /// library vs native entries) are both retained. - private static GameInstanceManifest uniqueLibraries(GameInstanceManifest manifest) { - List libraries = new ArrayList<>(); - SimpleMultimap> indexes = - new SimpleMultimap<>(HashMap::new, ArrayList::new); - - for (Library library : manifest.getLibraries()) { - String id = library.groupId() + ":" + library.artifactId(); - - if (!indexes.containsKey(id)) { - indexes.put(id, libraries.size()); - libraries.add(library); - continue; - } - - boolean duplicate = false; - for (int otherIndex : indexes.get(id)) { - Library other = libraries.get(otherIndex); - // Rules differ: keep both (platform-specific variants). - if (Objects.hashCode(library.rules()) != Objects.hashCode(other.rules())) { - continue; - } - - // Rules equal: drop the older version. - int comparison = VersionNumber.compare(library.version(), other.version()); - if (comparison > 0) { - libraries.set(otherIndex, library); - } else if (comparison == 0) { - // Same library id and version: collapse true duplicates. - if (library.equals(other)) { - String otherSerialized = JsonUtils.GSON.toJson(other); - String serialized = JsonUtils.GSON.toJson(library); - // Prefer the entry with more serialized metadata when coordinates equal. - if (serialized.length() > otherSerialized.length()) { - libraries.set(otherIndex, library); - } - } else { - // Same id/version but not equal (e.g. text2speech jar vs natives): keep both. - continue; - } - } - duplicate = true; - break; - } - - if (!duplicate) { - indexes.put(id, libraries.size()); - libraries.add(library); - } - } - - return libraries.size() == manifest.getLibraries().size() - ? manifest - : manifest.withLibraries(libraries); - } - } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java index 2d41526566..49d4036e45 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchManifestNormalizer.java @@ -17,12 +17,13 @@ */ package org.jackhuang.hmcl.game; +import org.jackhuang.hmcl.util.SimpleMultimap; +import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.Optional; +import java.util.*; /// Launch-manifest library and argument adjustments used at resolve time and launch time. /// @@ -50,7 +51,7 @@ public static GameInstanceManifest repairForLaunch(GameInstanceManifest manifest } GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, null); - GameInstanceManifest repaired = manifest; + GameInstanceManifest repaired = uniqueLibraries(manifest); @Nullable String mainClass = repaired.mainClass(); if (GameComponentAnalyzer.LAUNCH_WRAPPER_MAIN.equals(mainClass)) { @@ -244,4 +245,67 @@ private static GameInstanceManifest removeLegacyLog4jPatch(GameInstanceManifest } return manifest; } + + /// Removes redundant library declarations while retaining rule-distinct variants. + /// + /// When two libraries share the same `groupId:artifactId` and equal compatibility rules, the + /// newer version wins. When versions are equal and the coordinate objects compare equal, the + /// declaration with the longer serialized JSON is kept (more metadata is treated as richer). + /// Equal id and version with unequal coordinate payloads (for example distinct `text2speech` + /// library vs native entries) are both retained. + private static GameInstanceManifest uniqueLibraries(GameInstanceManifest manifest) { + List libraries = new ArrayList<>(); + SimpleMultimap> indexes = + new SimpleMultimap<>(HashMap::new, ArrayList::new); + + for (Library library : manifest.getLibraries()) { + String id = library.groupId() + ":" + library.artifactId(); + + if (!indexes.containsKey(id)) { + indexes.put(id, libraries.size()); + libraries.add(library); + continue; + } + + boolean duplicate = false; + for (int otherIndex : indexes.get(id)) { + Library other = libraries.get(otherIndex); + // Rules differ: keep both (platform-specific variants). + if (Objects.hashCode(library.rules()) != Objects.hashCode(other.rules())) { + continue; + } + + // Rules equal: drop the older version. + int comparison = VersionNumber.compare(library.version(), other.version()); + if (comparison > 0) { + libraries.set(otherIndex, library); + } else if (comparison == 0) { + // Same library id and version: collapse true duplicates. + if (library.equals(other)) { + String otherSerialized = JsonUtils.GSON.toJson(other); + String serialized = JsonUtils.GSON.toJson(library); + // Prefer the entry with more serialized metadata when coordinates equal. + if (serialized.length() > otherSerialized.length()) { + libraries.set(otherIndex, library); + } + } else { + // Same id/version but not equal (e.g. text2speech jar vs natives): keep both. + continue; + } + } + duplicate = true; + break; + } + + if (!duplicate) { + indexes.put(id, libraries.size()); + libraries.add(library); + } + } + + return libraries.size() == manifest.getLibraries().size() + ? manifest + : manifest.withLibraries(libraries); + } + } From 353168e9e60227bf732e4e7c930e86c733abb39e Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 30 Aug 2026 21:14:24 +0800 Subject: [PATCH 54/60] refactor: remove Resolved record from GameInstanceManifest and simplify analyze method in GameComponentAnalyzer --- .../hmcl/game/GameComponentAnalyzer.java | 4 -- .../hmcl/game/GameInstanceManifest.java | 45 ------------------- 2 files changed, 49 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java index 188450a1e2..c6d7fd8eed 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java @@ -70,10 +70,6 @@ private static GameComponentAnalyzer analyze( return new GameComponentAnalyzer(standaloneManifest, components, bootstrapVersion); } - public static GameComponentAnalyzer analyze(GameInstanceManifest.Resolved resolved, @Nullable GameVersionNumber gameVersion) { - return analyze(resolved.standaloneManifest(), resolved.launchManifest(), gameVersion); - } - public static GameComponentAnalyzer analyze(GameInstanceManifest manifest, @Nullable GameVersionNumber gameVersion) { if (manifest.inheritsFrom() != null) throw new IllegalArgumentException("GameComponentAnalyzer can only analyze independent game version"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 19e70e518f..84cd9d1e4b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -63,51 +63,6 @@ public record GameInstanceManifest( @Nullable @Unmodifiable JsonObject rawJson ) { - /// Resolved manifest views with inheritance folded. - /// - /// @param unresolved the stored manifest supplied to resolution - /// @param launchManifest the normalized final manifest data used by launch-time consumers - /// @param standaloneManifest the structural standalone manifest with pending patches preserved - @NotNullByDefault - public record Resolved(GameInstanceManifest unresolved, - GameInstanceManifest launchManifest, - GameInstanceManifest standaloneManifest) { - - /// Creates a resolved manifest view. - public Resolved { - Objects.requireNonNull(launchManifest); - Objects.requireNonNull(standaloneManifest); - - if (!launchManifest.id().equals(standaloneManifest.id())) { - throw new IllegalArgumentException("Resolved manifest views must have the same id"); - } - - if (launchManifest.inheritsFrom() != null) { - throw new IllegalArgumentException("Launch manifest cannot inherit from another manifest"); - } - if (launchManifest.patches() != null && !launchManifest.patches().isEmpty()) { - throw new IllegalArgumentException("Launch manifest cannot contain pending patches"); - } - if (standaloneManifest.inheritsFrom() != null) { - throw new IllegalArgumentException("Standalone manifest cannot inherit from another manifest"); - } - } - - public boolean isModded() { - String mainClass = launchManifest().mainClass(); - if (mainClass == null || GameComponentAnalyzer.LAUNCH_WRAPPER_MAIN.equals(mainClass)) { - return false; - } - - for (String packageName : GameComponentAnalyzer.MOD_LOADER_MAIN_CLASSES_PACKAGES) { - if (mainClass.startsWith(packageName)) - return true; - } - - return false; - } - } - GameInstanceManifest merge(GameInstanceManifest parent) { return new GameInstanceManifest( id, From ad4261e8c8e3f787494d70ba30357d11c1862726 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 30 Aug 2026 21:22:49 +0800 Subject: [PATCH 55/60] refactor: rename getLaunchManifest to getResolvedManifest and update references across the codebase --- .../java/org/jackhuang/hmcl/game/HMCLGameRepository.java | 2 +- .../main/java/org/jackhuang/hmcl/game/LauncherHelper.java | 2 +- .../java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java | 2 +- .../java/org/jackhuang/hmcl/ui/instances/Instances.java | 2 +- .../java/org/jackhuang/hmcl/game/DefaultGameInstance.java | 6 +++--- .../src/main/java/org/jackhuang/hmcl/game/GameInstance.java | 2 +- .../src/main/java/org/jackhuang/hmcl/launch/Launcher.java | 2 +- .../org/jackhuang/hmcl/game/DefaultGameInstanceTest.java | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 8b4255d2f6..da8cf99b2d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -298,7 +298,7 @@ public Stream getDisplayInstances() { return getSnapshot().getInstances().stream() .filter(it -> !it.getManifest().isHidden()) .sorted(Comparator.comparing(DefaultGameInstance::getVersion) - .thenComparing(instance -> Lang.requireNonNullElse(instance.getLaunchManifest().releaseTime(), Instant.EPOCH)) + .thenComparing(instance -> Lang.requireNonNullElse(instance.getResolvedManifest().releaseTime(), Instant.EPOCH)) .thenComparing(instance -> VersionNumber.asVersion(instance.getId().id()))); } 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 df9c3dd62a..aaef6607bc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -157,7 +157,7 @@ private void launch0() { HMCLGameRepository repository = repository(); DefaultDependencyManager dependencyManager = repository.getDependency(); // Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch. - var launchManifest = new AtomicReference<>(LaunchManifestNormalizer.repairForLaunch(gameInstance.getLaunchManifest())); + var launchManifest = new AtomicReference<>(LaunchManifestNormalizer.repairForLaunch(gameInstance.getResolvedManifest())); boolean integrityCheck = gameInstance.unmarkLaunchedAbnormally(); CountDownLatch launchingLatch = new CountDownLatch(1); List javaAgents = new ArrayList<>(0); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index 9035f1204f..79e51780d2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -2818,7 +2818,7 @@ private void initJavaSubtitle() { if (JavaManager.isInitialized()) { GameVersionNumber gameVersionNumber = currentGameVersion(); - GameInstanceManifest manifest = gameInstance != null ? gameInstance.getLaunchManifest() : null; + GameInstanceManifest manifest = gameInstance != null ? gameInstance.getResolvedManifest() : null; try { JavaRuntime java = effectiveSetting != null diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index 35e85e5df9..b61d96b014 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -274,7 +274,7 @@ public static void updateInstance(HMCLGameInstance gameInstance) { public static void updateGameAssets(HMCLGameInstance gameInstance) { TaskExecutor executor = new GameAssetDownloadTask( gameInstance.getRepository().getDependency(), - gameInstance.getLaunchManifest(), + gameInstance.getResolvedManifest(), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true).executor(); Controllers.taskDialog(executor, i18n("instance.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java index 6e28bcb655..041467d669 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameInstance.java @@ -158,7 +158,7 @@ public GameInstanceManifest getManifest() { } @Override - public GameInstanceManifest getLaunchManifest() { + public GameInstanceManifest getResolvedManifest() { if (resolvedManifest == null) { resolvedManifest = snapshot.resolve(manifest); } @@ -168,7 +168,7 @@ public GameInstanceManifest getLaunchManifest() { @Override public GameComponentAnalyzer getAnalyzer() { if (analyzer == null) { - analyzer = GameComponentAnalyzer.analyze(manifest.isModifiable() ? manifest : getLaunchManifest(), getVersion()); + analyzer = GameComponentAnalyzer.analyze(manifest.isModifiable() ? manifest : getResolvedManifest(), getVersion()); } return analyzer; } @@ -261,7 +261,7 @@ public Path getModpackConfigurationFile() { /// instance's own jar is returned from [#getOwnJarFile()]. @Override public Path getInstanceJarFile() { - GameInstanceManifest launchManifest = getLaunchManifest(); + GameInstanceManifest launchManifest = getResolvedManifest(); GameInstanceID jarId = Optional.ofNullable(launchManifest.jar()).orElse(launchManifest.id()); if (!jarId.equals(id)) { DefaultGameInstance other = snapshot.findInstance(jarId); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java index 49385530b9..a558a8792b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java @@ -56,7 +56,7 @@ public interface GameInstance { /// Returns the manifest used by launch-time consumers. /// /// @return the launch manifest - GameInstanceManifest getLaunchManifest(); + GameInstanceManifest getResolvedManifest(); GameComponentAnalyzer getAnalyzer(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java index 0e5beda342..51a4f53ea6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java @@ -31,7 +31,7 @@ /// The [GameInstance] identifies the instance being launched (paths, repository layout, version /// cache). [#manifest] is the effective launch-time manifest after maintenance and native /// patching; it must not be assumed equal to [GameInstance#getManifest()] or -/// [GameInstance#getLaunchManifest()]. +/// [GameInstance#getResolvedManifest()]. public abstract class Launcher { /// The instance being launched. diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index f88c2e8e83..7321a8da83 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -75,7 +75,7 @@ public void testModLauncherLaunchRepairDoesNotWriteBundledLibraries(@TempDir Pat new Library(new Artifact("net.minecraftforge", "forge", "1.0")), new Library(new Artifact("optifine", "OptiFine", "1.0")))); TestGameInstance instance = repository.publish(instanceId, manifest); - GameInstanceManifest launchManifest = instance.getLaunchManifest(); + GameInstanceManifest launchManifest = instance.getResolvedManifest(); assertTrue(launchManifest.getLibraries().stream() .noneMatch(library -> library.is( "org.jackhuang.hmcl", "transformer-discovery-service"))); From a965a5f1ac0f45771efad54eea6b7c21a1ba83bc Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 30 Aug 2026 21:52:36 +0800 Subject: [PATCH 56/60] refactor: update GameInstanceManifest builder to set root flag and reorder method calls --- .../java/org/jackhuang/hmcl/game/GameInstanceManifest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 84cd9d1e4b..792a841ac4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -678,11 +678,12 @@ public GameInstanceManifest reconstructByPatches() { } var builder = new GameInstanceManifest.Builder(); - builder.setId(id()); - builder.setJar(jar() == null ? id() : jar()); for (GameInstancePatch patch : patches()) { builder.merge(patch); } + builder.setId(id()); + builder.setJar(jar() == null ? id() : jar()); + builder.setRoot(true); builder.setPatches(patches()); return builder.toManifest(); } From 8c80274109c8b10af35984d5a2c7ad3beb7cf898 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 31 Aug 2026 20:20:23 +0800 Subject: [PATCH 57/60] Fix draft cleanup and component repair paths Assisted-by: codex:gpt-5.6-sol --- .../download/DefaultDependencyManager.java | 20 +++-- .../hmcl/game/DefaultGameRepository.java | 50 +++++++---- .../hmcl/game/GameComponentAnalyzer.java | 2 +- .../hmcl/game/GameInstanceManifest.java | 12 +-- .../multimc/MultiMCModpackInstallTask.java | 19 ++++- .../hmcl/game/DefaultGameInstanceTest.java | 82 +++++++++++++++++++ .../game/DefaultGameRepositoryDraftTest.java | 32 ++++++++ 7 files changed, 183 insertions(+), 34 deletions(-) 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 dd29f221cc..93f029b803 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -212,18 +212,20 @@ public Task checkPatchCompletionAsync( if (mark == null || mark.version() == null) break optifine; - String fullVersion = mark.version(); + @Nullable GameInstancePatch patch = original.findPatch(GameComponentType.OPTIFINE); + String fullVersion; String patchVersion; - - Matcher matcher = GameComponentAnalyzer.OPTIFINE_VERSION_PATTERN.matcher(fullVersion); - if (matcher.matches()) { - patchVersion = matcher.group("optifine"); + if (patch != null && patch.version() != null) { + patchVersion = patch.version(); + fullVersion = gameVersion + "_" + patchVersion; } else { - @Nullable GameInstancePatch patch = manifest.findPatch(GameComponentType.OPTIFINE.getPatchId()); - if (patch != null && patch.version() != null) - patchVersion = patch.version(); - else + fullVersion = mark.version(); + Matcher matcher = GameComponentAnalyzer.OPTIFINE_VERSION_PATTERN.matcher(fullVersion); + if (matcher.matches()) { + patchVersion = matcher.group("optifine"); + } else { break optifine; + } } boolean needsReInstallation = manifest.getLibraries().stream() 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 8216ead2d0..fddbe95684 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -537,25 +537,43 @@ public Task updateInstanceAsync( GameInstanceID instanceId, ExceptionalFunction, E> updater) { return Task.composeAsync(() -> { - @SuppressWarnings("resource") - GameRepositoryDraft draft = openDraft(); - return updater.apply(draft.getBaseSnapshot().getInstance(instanceId)) - .thenApplyAsync(manifest -> { - if (!instanceId.equals(manifest.id())) { - throw new IllegalArgumentException( - "Instance updater changed id from " + instanceId + " to " + manifest.id()); - } - draft.put(manifest); - draft.commit(); - return manifest; - }).whenComplete(exception -> { - if (draft.isOpen()) { - draft.abort(); - } - }); + DefaultGameRepositoryDraft draft = openDraft(); + try { + return Objects.requireNonNull( + updater.apply(draft.getBaseSnapshot().getInstance(instanceId)), + "Instance updater returned null") + .thenApplyAsync(manifest -> { + if (!instanceId.equals(manifest.id())) { + throw new IllegalArgumentException( + "Instance updater changed id from " + instanceId + " to " + manifest.id()); + } + draft.put(manifest); + draft.commit(); + return manifest; + }).whenComplete(exception -> { + if (draft.isOpen()) { + draft.abort(); + } + }); + } catch (Throwable exception) { + abortDraftAfterFailure(draft, exception); + throw exception; + } }); } + /// Aborts a draft after task construction fails and preserves any cleanup failure. + /// + /// @param draft the draft to abort + /// @param failure the failure that prevented task construction + private static void abortDraftAfterFailure(DefaultGameRepositoryDraft draft, Throwable failure) { + try { + draft.abort(); + } catch (Exception cleanupFailure) { + failure.addSuppressed(cleanupFailure); + } + } + /// Creates an empty unsealed snapshot for the given layout. /// /// @param layout the layout for the new snapshot diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java index c6d7fd8eed..ea91f49e03 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameComponentAnalyzer.java @@ -137,7 +137,7 @@ public boolean isModded() { /// Otherwise, we must guess how are these libraries mixed. /// Maybe a guessing implementation will be provided in the future. But by now, we simply set it to JUST\_EXISTED. public boolean isClear(GameComponentType type) { - return manifest.hasPatch(type.getPatchId()); + return manifest.hasPatch(type); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java index 792a841ac4..67d0237dd8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -311,7 +311,7 @@ public boolean isRoot() { } public boolean isModifiable() { - return inheritsFrom == null && patches != null && hasPatch(GameComponentType.GAME.getPatchId()); + return inheritsFrom == null && patches != null && hasPatch(GameComponentType.GAME); } /// Returns the pending patches. @@ -324,10 +324,10 @@ public List getPatches() { /// Finds a patch by its id. /// /// @return the patch with the given id, or `null` if no such patch exists. - public @Nullable GameInstancePatch findPatch(String patchId) { + public @Nullable GameInstancePatch findPatch(GameComponentType type) { if (patches != null) { for (GameInstancePatch patch : patches) { - if (patchId.equals(patch.id())) { + if (type.getPatchId().equals(patch.id())) { return patch; } } @@ -556,8 +556,8 @@ public GameInstanceManifest addPatch(GameInstancePatch additional) { } /// Returns whether this manifest has a patch with the given id. - public boolean hasPatch(String patchId) { - return findPatch(patchId) != null; + public boolean hasPatch(GameComponentType type) { + return findPatch(type) != null; } /// Converts this manifest into a hidden patch entry for preserving resolved inheritance. @@ -657,7 +657,7 @@ public JsonObject toJsonObject() { } public GameInstanceManifest removeComponent(GameComponentType type) { - @Nullable GameInstancePatch patch = findPatch(type.getPatchId()); + @Nullable GameInstancePatch patch = findPatch(type); if (patch == null) { return this; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java index d4e57587dd..0d1085fdf6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java @@ -376,7 +376,7 @@ public void execute() throws Exception { true )); - Path instanceJar = requireDraft().getBaseSnapshot().getLayout().getInstanceJarFile(instanceId); + Path instanceJar = getPrimaryJarFile(); dependencies.add(new GameDownloadTask(dependencyManager, instanceManifest) .thenAcceptAsync(cachedJar -> FileUtils.copyFile(cachedJar, instanceJar))); } @@ -411,7 +411,7 @@ public void postExecute() throws Exception { Path root = getRootPath(fs).resolve("jarmods"); try (FileSystem mc = CompressingUtils.writable( - repository.getLayout().getInstanceRoot(instanceId).resolve(instanceId + ".jar") + getPrimaryJarFile() ).setAutoDetectEncoding(true).build()) { for (String fileName : files) { try (FileSystem jm = CompressingUtils.readonly(root.resolve(fileName)).setAutoDetectEncoding(true).build()) { @@ -425,6 +425,21 @@ public void postExecute() throws Exception { requireDraft().commit(); } + /// Returns the primary JAR path retained by the current draft. + /// + /// Updates use the existing manifest's sibling JAR, while new installations use the layout's + /// conventional path. + /// + /// @return the primary JAR destination + private Path getPrimaryJarFile() { + if (updateTarget == null) { + return repository.getLayout().getInstanceJarFile(instanceId); + } + + Path manifestFile = updateTarget.getManifestFile(); + return manifestFile.resolveSibling(FileUtils.getNameWithoutExtension(manifestFile) + ".jar"); + } + /// Returns the open draft associated with this task. /// /// @return the open draft diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java index 7321a8da83..71856a047d 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameInstanceTest.java @@ -92,6 +92,46 @@ public void testModLauncherLaunchRepairDoesNotWriteBundledLibraries(@TempDir Pat assertEquals(repaired, LaunchManifestNormalizer.repairForLaunch(repaired)); } + /// Repairs a missing patch-installed OptiFine library using the patch's self version. + @Test + public void testPatchCompletionUsesStoredOptiFineVersion(@TempDir Path tempDirectory) throws IOException { + TestRepository repository = new TestRepository(tempDirectory.resolve("game")); + DefaultCacheRepository cacheRepository = new DefaultCacheRepository(tempDirectory.resolve("cache")); + CapturingDependencyManager dependencyManager = new CapturingDependencyManager(repository, cacheRepository); + GameInstanceID instanceId = new GameInstanceID("instance"); + String gameVersion = "1.21.1"; + String optiFineVersion = "HD_U_I9"; + GameInstancePatch gamePatch = new GameInstancePatch( + GameComponentType.GAME.getPatchId(), + gameVersion, + GameInstancePatch.PRIORITY_MC, + null, + GameComponentAnalyzer.VANILLA_MAIN, + List.of()); + GameInstancePatch optiFinePatch = new GameInstancePatch( + GameComponentType.OPTIFINE.getPatchId(), + optiFineVersion, + GameInstancePatch.PRIORITY_LOADER, + null, + null, + List.of(new Library("optifine", "OptiFine", gameVersion + "_" + optiFineVersion))); + GameInstanceManifest storedManifest = new GameInstanceManifest(instanceId) + .withRoot(true) + .withPatches(List.of(gamePatch, optiFinePatch)) + .reconstructByPatches(); + writeVersionJar(repository.getLayout().getInstanceJarFile(instanceId), gameVersion); + TestGameInstance instance = repository.publish(instanceId, storedManifest); + + Task repair = dependencyManager.checkPatchCompletionAsync( + instance, + LaunchManifestNormalizer.repairForLaunch(instance.getResolvedManifest()), + true); + + assertTrue(repair.executor().test()); + assertEquals(gameVersion, dependencyManager.requestedGameVersion); + assertEquals(optiFineVersion, dependencyManager.requestedComponentVersion); + } + /// Saving a manifest preserves its root flag and pending patches without baking in normalization. @Test public void testSavePreservesManifestPatchStructure(@TempDir Path tempDirectory) throws Exception { @@ -741,6 +781,48 @@ private DefaultGameRepositorySnapshot newSnapshot() { } } + /// Dependency manager that records remote component repair requests. + @NotNullByDefault + private static final class CapturingDependencyManager extends DefaultDependencyManager { + + /// Minecraft version passed to the captured repair request. + private @Nullable String requestedGameVersion; + + /// Component version passed to the captured repair request. + private @Nullable String requestedComponentVersion; + + /// Creates a capturing manager for the test repository. + /// + /// @param repository the target repository + /// @param cacheRepository the test download cache + private CapturingDependencyManager( + DefaultGameRepository repository, + DefaultCacheRepository cacheRepository) { + super(repository, new MojangDownloadProvider(), cacheRepository); + } + + /// Records the requested remote version without performing a network lookup. + /// + /// @param instance the registered instance being modified + /// @param baseManifest the working manifest for this step + /// @param gameVersion the Minecraft version used for the lookup + /// @param componentType the component list id + /// @param componentVersion the component version id + /// @return a completed task containing `baseManifest` + @Override + public Task installComponentRemoteAsync( + GameInstance instance, + GameInstanceManifest baseManifest, + String gameVersion, + GameComponentType componentType, + String componentVersion) { + assertEquals(GameComponentType.OPTIFINE, componentType); + requestedGameVersion = gameVersion; + requestedComponentVersion = componentVersion; + return Task.completed(baseManifest); + } + } + /// Minimal concrete game instance that exposes cache state to tests. @NotNullByDefault private static final class TestGameInstance extends DefaultGameInstance { diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraftTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraftTest.java index 5ca62408a5..1b156b2ccc 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraftTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/DefaultGameRepositoryDraftTest.java @@ -310,6 +310,38 @@ public void testUpdateInstanceAsyncRejectsChangedId(@TempDir Path tempDirectory) } } + /// Releases the draft when the target disappears before the updater is invoked. + @Test + public void testUpdateInstanceAsyncAbortsDraftWhenTargetIsMissing(@TempDir Path tempDirectory) throws Exception { + TestRepository repository = new TestRepository(tempDirectory); + GameInstanceID id = new GameInstanceID("missing"); + + Task update = repository.updateInstanceAsync(id, workingInstance -> + Task.completed(workingInstance.getManifest())); + assertFalse(update.executor().test()); + + try (DefaultGameRepositoryDraft ignored = repository.openDraft()) { + assertTrue(ignored.isOpen()); + } + } + + /// Releases the draft when the updater throws before returning its task. + @Test + public void testUpdateInstanceAsyncAbortsDraftWhenUpdaterThrows(@TempDir Path tempDirectory) throws Exception { + TestRepository repository = new TestRepository(tempDirectory); + GameInstanceID id = new GameInstanceID("instance"); + repository.save(new GameInstanceManifest(id)); + + Task update = repository.updateInstanceAsync(id, workingInstance -> { + throw new IOException("Simulated updater failure"); + }); + assertFalse(update.executor().test()); + + try (DefaultGameRepositoryDraft ignored = repository.openDraft()) { + assertTrue(ignored.isOpen()); + } + } + /// Minimal repository implementation for draft tests. @NotNullByDefault private static class TestRepository extends DefaultGameRepository { From 1dc1fcd75985e4aaa263dab364f38f69591f5cac Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 1 Sep 2026 20:49:21 +0800 Subject: [PATCH 58/60] refactor: replace Collections.singletonList with List.of for improved readability --- .../java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index e998f5587f..ec4e4e4a9d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -33,7 +33,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /// Installs or updates an HMCL modpack using the mode selected at construction. @@ -129,8 +128,8 @@ private HMCLModpackInstallTask( } }); - dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config)); - dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack")); + dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), List.of("/minecraft"), it -> !"pack.json".equals(it), config)); + dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack")); try (GameBuilder builder = this.updateTarget == null ? dependency.newGameBuilder(this.instanceId) From 4dab809540c983aaa6f633b44279d442874cec95 Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 1 Sep 2026 21:10:42 +0800 Subject: [PATCH 59/60] Rebuild HMCL modpack components with a single game builder Assisted-by: codex:gpt-5.6-sol --- .../hmcl/game/HMCLModpackInstallTask.java | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index ec4e4e4a9d..6b5efbfd76 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; +import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -34,8 +35,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; -/// Installs or updates an HMCL modpack using the mode selected at construction. +/// Installs or updates an HMCL modpack by rebuilding its recognized game components. +@NotNullByDefault public final class HMCLModpackInstallTask extends Task { private final Path zipFile; private final GameInstanceID instanceId; @@ -55,8 +58,6 @@ public final class HMCLModpackInstallTask extends Task { /// @param zipFile the HMCL modpack archive /// @param modpack the parsed modpack metadata /// @param instanceId the id of the new instance - /// @throws IllegalStateException if the target cannot be reserved or another repository draft - /// is open public HMCLModpackInstallTask( HMCLGameRepository repository, Path zipFile, @@ -73,8 +74,6 @@ public HMCLModpackInstallTask( /// @param instance the existing instance to update /// @throws IllegalArgumentException if `instance` belongs to another repository, has no /// modpack configuration, or records another provider type - /// @throws IllegalStateException if `instance` is not the exact currently published object - /// or another repository draft is open public HMCLModpackInstallTask( HMCLGameRepository repository, Path zipFile, @@ -90,9 +89,8 @@ public HMCLModpackInstallTask( /// @param modpack the parsed modpack metadata /// @param instanceId the target instance id /// @param updateTarget the existing instance selecting update mode, or `null` for install - /// @throws IllegalArgumentException if an update target has no compatible configuration - /// @throws IllegalStateException if the target cannot be reserved, an update target is not - /// the exact published object, or another draft is open + /// @throws IllegalArgumentException if an update target belongs to another repository or has + /// no compatible configuration private HMCLModpackInstallTask( HMCLGameRepository repository, Path zipFile, @@ -105,6 +103,9 @@ private HMCLModpackInstallTask( this.instanceId = instanceId; this.updateTarget = updateTarget; this.modpack = modpack; + if (this.updateTarget != null) { + dependency.validateGameInstance(this.updateTarget); + } Path run = repository.getLayout().getInstanceRoot(this.instanceId); Path json = repository.getLayout().getModpackConfigurationFile(this.instanceId); @@ -130,15 +131,6 @@ private HMCLModpackInstallTask( dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), List.of("/minecraft"), it -> !"pack.json".equals(it), config)); dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack")); - - try (GameBuilder builder = this.updateTarget == null - ? dependency.newGameBuilder(this.instanceId) - : dependency.newGameBuilder(this.updateTarget)) { - dependents.add(0, builder - .enableIsolation() - .component(GameComponentType.GAME, modpack.getGameVersion()) - .buildAsync()); - } } @Override @@ -151,32 +143,43 @@ public List> getDependents() { return dependents; } - /// {@inheritDoc} + /// Reads the legacy pack manifest and schedules one builder for all recognized components. + /// + /// The pack manifest is used only for component detection. The builder creates a new + /// patch-structured manifest instead of attempting to mutate the legacy flattened manifest. @Override public void execute() throws Exception { String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json"); - GameInstanceManifest originalManifest = JsonUtils.GSON.fromJson(json, GameInstanceManifest.class).withId(instanceId).withJar(null); + GameInstanceManifest originalManifest = JsonUtils.fromNonNullJson(json, GameInstanceManifest.class) + .withId(instanceId) + .withJar(null); GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(originalManifest, null); - dependencies.add(repository.updateInstanceAsync(instanceId, publishedInstance -> { - Task libraryTask = Task.completed(originalManifest); - // Forge and OptiFine libraries must be regenerated by their installers. + try (GameBuilder builder = updateTarget == null + ? dependency.newGameBuilder(instanceId) + : dependency.newGameBuilder(updateTarget)) { + builder.enableIsolation(); + + String gameVersion = modpack.getGameVersion(); + builder.component(GameComponentType.GAME, gameVersion); for (GameComponentAnalyzer.Mark mark : analyzer) { if (mark.componentType() == GameComponentType.GAME) { continue; } - String componentVersion = mark.version(); - if (componentVersion == null) { - continue; + + @Nullable String componentVersion = mark.version(); + if (componentVersion != null) { + if (mark.componentType() == GameComponentType.OPTIFINE) { + Matcher matcher = GameComponentAnalyzer.OPTIFINE_VERSION_PATTERN.matcher(componentVersion); + if (matcher.matches()) { + componentVersion = matcher.group("optifine"); + } + } + builder.component(mark.componentType(), componentVersion); } - libraryTask = libraryTask.thenComposeAsync(manifest -> dependency.installComponentRemoteAsync( - publishedInstance, - manifest, - modpack.getGameVersion(), - mark.componentType(), - componentVersion)); } - return libraryTask; - })); + dependencies.add(builder.buildAsync()); + } } + } From e070f291c5f337cd65a62cee70c4372ec99189fb Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 1 Sep 2026 21:13:23 +0800 Subject: [PATCH 60/60] fix: improve error handling for game manifest loading --- .../java/org/jackhuang/hmcl/ui/instances/Instances.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index b61d96b014..665983ba4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -175,10 +175,12 @@ public static void installFromJson(HMCLGameRepository repository, Path file) { GameInstanceManifest manifest; try { manifest = JsonUtils.fromJsonFile(file, GameInstanceManifest.class); - if (manifest == null) { + if (manifest == null) throw new IllegalArgumentException("Missing game manifest"); - } + if (manifest.inheritsFrom() != null) + throw new IllegalArgumentException("Game manifest inherits from another manifest"); } catch (Exception e) { + LOG.warning("Failed to read game manifest from " + file, e); Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); return; }