Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.modrinth.ModrinthCheckServerPackUpdateTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthManifest;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
Expand All @@ -43,6 +46,7 @@
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
Expand All @@ -56,6 +60,7 @@
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
Expand Down Expand Up @@ -157,6 +162,43 @@ private void launch0() {
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
version.set(NativePatcher.patchNative(repository, version.get(), gameVersion.orElse(null), java, setting, javaArguments));
return null;
})
.thenComposeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if (ModrinthModpackProvider.INSTANCE.getName().equals(configuration.getType())) {
Path manifestFile = repository.getVersionRoot(selectedVersion).resolve("modrinth.index.json");
if (Files.exists(manifestFile)) {
ModrinthManifest manifest = JsonUtils.fromJsonFile(manifestFile, ModrinthManifest.class);
if (StringUtils.isNotBlank(manifest.getFileApi())) {
return new ModrinthCheckServerPackUpdateTask(manifest.getVersionId(), manifest.getFileApi());
}
}
}
} catch (IOException ignored) {
}
return null;
})
.thenComposeAsync(hasUpdate -> {
if (hasUpdate == null || !hasUpdate) return null;

CompletableFuture<Void> future = new CompletableFuture<>();
runInFX(() -> {
Controllers.confirm(i18n("modpack.update.found"), i18n("modpack.update"), MessageType.QUESTION, () -> {
try {
ModpackConfiguration<?> config = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
Task<?> updateTask = ModpackHelper.getUpdateTask(profile, ModrinthCheckServerPackUpdateTask.getUpdateFile(), java.nio.charset.StandardCharsets.UTF_8, selectedVersion, config);
Controllers.taskDialog(updateTask, i18n("modpack.update"), TaskCancellationAction.NORMAL);
} catch (Exception e) {
Controllers.dialog(StringUtils.getStackTrace(e), i18n("message.error"), MessageType.ERROR);
}
future.completeExceptionally(new CancellationException());
}, () -> future.complete(null));
});
return Task.fromCompletableFuture(future);
})
.thenComposeAsync(() -> {
if (setting.isNotCheckGame())
return null;
return Task.allOf(
Expand All @@ -177,7 +219,7 @@ private void launch0() {
|| renderer.mesaDriverName() == null)
return null;

Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
Library lib = NativePatcher.getWindowsMesaLoader(javaVersionRef.get(), renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ modpack.type.server.export=Allows server owner to remotely update the game insta
modpack.type.server.malformed=Invalid modpack manifest. Please contact the modpack maker to resolve this problem.
modpack.unsupported=Unsupported modpack format
modpack.update=Updating modpack
modpack.update.found=A new version of the modpack is available. Update now?
modpack.wizard=Modpack Export Guide
modpack.wizard.step.1=Basic Settings
modpack.wizard.step.1.title=Some basic informations for the modpack.
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ modpack.type.server.export=允許伺服器管理員遠端更新遊戲用戶端
modpack.type.server.malformed=伺服器模組包配置格式錯誤,請聯絡伺服器管理員解決此問題
modpack.unsupported=Hello Minecraft! Launcher 不支援該模組包格式
modpack.update=正在升級模組包
modpack.update.found=發現模組包新版本,是否更新?
modpack.wizard=匯出模組包引導
modpack.wizard.step.1=基本設定
modpack.wizard.step.1.title=設定模組包的主要訊息
Expand Down
1 change: 1 addition & 0 deletions HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,7 @@ modpack.type.server.export=允许服务器管理员远程更新游戏客户端
modpack.type.server.malformed=服务器整合包配置格式错误,请联系服务器管理员解决此问题。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
modpack.unsupported=Hello Minecraft! Launcher 不支持该整合包格式
modpack.update=正在升级整合包
modpack.update.found=发现整合包新版本,是否更新?
modpack.wizard=导出整合包向导
modpack.wizard.step.1=基本设置
modpack.wizard.step.1.title=设置整合包的主要信息
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modrinth;

import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;

/**
* Checks whether a Modrinth server modpack has a newer version available on the remote server.
*
* The task downloads {@code server.mrpack} from the remote API, extracts the manifest, and compares
* the remote version ID with the local one using {@link VersionNumber}. The result is {@code true} if
* an update is available, {@code false} otherwise.
*
* When an update is found, the downloaded {@code server.mrpack} path is stored in a static field
* and can be retrieved via {@link #getUpdateFile()}. If no update is available or an error occurs,
* the temporary file is deleted.
*
* @see ModrinthManifest
* @see FileDownloadTask
*/
public class ModrinthCheckServerPackUpdateTask extends Task<Boolean> {

/**
* The path to the downloaded {@code server.mrpack} file, or {@code null} if no update file is available.
*/
private static @Nullable Path updateFilePath = null;

/**
* The current local version ID of the modpack.
*/
private final String versionId;

/**
* The task that downloads {@code server.mrpack} from the remote API, or {@code null} if no download URL is available.
*/
private final @Nullable FileDownloadTask downloadTask;

/**
* Creates a new update check task.
*
* @param versionId the current local version ID of the modpack.
* @param fileApi the base URL for downloading the server modpack; if blank, no download will be attempted.
*/
public ModrinthCheckServerPackUpdateTask(String versionId, String fileApi) {
this.versionId = versionId;

if (StringUtils.isNotBlank(fileApi)) {
try {
updateFilePath = Files.createTempFile("hmcl-server-auto-update", ".mrpack");
downloadTask = new FileDownloadTask(fileApi + "/server.mrpack", updateFilePath);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
downloadTask = null;
}
}

@Override
public Collection<Task<?>> getDependents() {
return downloadTask != null ? Collections.singleton(downloadTask) : Collections.emptySet();
}

@Override
public boolean isRelyingOnDependents() {
return false;
}

/**
* Returns the path to the downloaded {@code server.mrpack} file, or {@code null} if no update file is available.
*
* @return the path to the downloaded update file, or {@code null}
*/
public static @Nullable Path getUpdateFile() {
return updateFilePath;
}

@Override
public void execute() throws Exception {
if (updateFilePath == null || downloadTask == null || downloadTask.getException() != null) {
setResult(false);
return;
}

try {
ModrinthManifest remoteManifest;
try (var zip = CompressingUtils.openZipFile(updateFilePath)) {
remoteManifest = JsonUtils.fromNonNullJson(
CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"),
ModrinthManifest.class);
}

if (VersionNumber.compare(remoteManifest.getVersionId(), versionId) > 0) {
setResult(true);
} else {
setResult(false);
}
} catch (Exception e) {
LOG.warning("Failed to check for modpack updates", e);
setResult(false);
} finally {
if (!getResult()) {
try {
Files.deleteIfExists(updateFilePath);
} catch (IOException ignored) {
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,6 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF
throw new IllegalStateException("Unsupported mod loader " + modLoader.getKey());
}
}
dependents.add(builder.buildAsync());

onDone().register(event -> {
Exception ex = event.getTask().getException();
if (event.isFailed()) {
if (!(ex instanceof ModpackCompletionException)) {
repository.removeVersionFromDisk(name);
}
}
});

ModpackConfiguration<ModrinthManifest> config = null;
try {
Expand All @@ -111,8 +101,21 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF
}
} catch (JsonParseException | IOException ignore) {
}

this.config = config;

if (config == null || !config.getManifest().getDependencies().equals(manifest.getDependencies())) {
dependents.add(builder.buildAsync());
}

onDone().register(event -> {
Exception ex = event.getTask().getException();
if (event.isFailed()) {
if (!(ex instanceof ModpackCompletionException)) {
repository.removeVersionFromDisk(name);
}
}
});

List<String> 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.getModpackConfiguration(name)).withStage("hmcl.modpack"));
Expand Down Expand Up @@ -150,6 +153,15 @@ public void execute() throws Exception {
Files.deleteIfExists(oldFile);
}
}
// For local files which is already in new manifest, remove them
for (ModrinthManifest.File newManifestFile : manifest.getFiles()) {
if (config.getManifest().getFiles().stream().noneMatch(newManifestFile::equals)) {
Path newFile = run.resolve(newManifestFile.getPath());
if (Files.exists(newFile)) {
Files.deleteIfExists(newFile);
}
}
}
}

Path root = repository.getVersionRoot(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,21 @@ public class ModrinthManifest implements ModpackManifest, Validation {
private final @Nullable String summary;
private final List<File> files;
private final Map<String, String> dependencies;
private final @Nullable String fileApi;

public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List<File> files, Map<String, String> dependencies) {
public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List<File> files, Map<String, String> dependencies, @Nullable String fileApi) {
this.game = game;
this.formatVersion = formatVersion;
this.versionId = versionId;
this.name = name;
this.summary = summary;
this.files = files;
this.dependencies = dependencies;
this.fileApi = fileApi;
}

public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List<File> files, Map<String, String> dependencies) {
this(game, formatVersion, versionId, name, summary, files, dependencies, null);
}

public String getGame() {
Expand Down Expand Up @@ -80,6 +86,11 @@ public String getGameVersion() {
return dependencies.get("minecraft");
}

@Nullable
public String getFileApi() {
return fileApi;
}

@Override
public ModpackProvider getProvider() {
return ModrinthModpackProvider.INSTANCE;
Expand Down
Loading