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 b96bd92c1c..cddd8ca3ea 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java
@@ -20,10 +20,7 @@
import com.google.gson.JsonParseException;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
-import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
-import org.jackhuang.hmcl.mod.Modpack;
-import org.jackhuang.hmcl.mod.ModpackProvider;
-import org.jackhuang.hmcl.mod.ModpackUpdateTask;
+import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
@@ -33,6 +30,7 @@
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Set;
public final class HMCLModpackProvider implements ModpackProvider {
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
@@ -48,7 +46,7 @@ public Task> createCompletionTask(DefaultDependencyManager dependencyManager,
}
@Override
- public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
+ public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack, Set extends ModpackFile> selectedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
@@ -79,7 +77,7 @@ public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding)
private final static class HMCLModpack extends Modpack {
@Override
- public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
+ public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
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 109e951420..49f3f335f7 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java
@@ -55,6 +55,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -191,7 +192,7 @@ public static Task> getInstallManuallyCreatedModpackTask(Profile profile, Path
});
}
- public static Task> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack, String iconUrl) {
+ public static Task> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack, String iconUrl, Set extends ModpackFile> selectedFiles) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable> success = () -> {
@@ -211,17 +212,17 @@ public static Task> getInstallTask(Profile profile, Path zipFile, String name,
};
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
- return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
+ return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl, selectedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
- return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
+ return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl, selectedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
- return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
+ return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl, selectedFiles)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
@@ -237,18 +238,19 @@ public static Task getUpdateTask(Profile profile, ServerModpackManifest ma
}
}
- public static Task> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
+ public static Task> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration> configuration, Set extends ModpackFile> selectedFiles) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
+ Task> updateTask = provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack, selectedFiles);
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
- return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
+ return updateTask
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
else
- return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
+ return updateTask
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
}
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java
index 064f580d52..34c75df440 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java
@@ -20,11 +20,18 @@
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableSet;
+import javafx.collections.transformation.FilteredList;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
+import org.jackhuang.hmcl.mod.ModpackFile;
+import org.jackhuang.hmcl.mod.ModpackManifest;
+import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
@@ -40,8 +47,15 @@
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
+import org.jetbrains.annotations.Nullable;
import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -52,9 +66,18 @@ public final class LocalModpackPage extends ModpackPage {
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
private Modpack manifest = null;
private Charset charset;
+ private final ObservableList allFiles = FXCollections.observableList(new ArrayList<>());
+ private final ObservableSet excludedFiles = FXCollections.observableSet(new HashSet<>());
+ private final BooleanProperty loadingOptionalFiles = new SimpleBooleanProperty(false);
+ private final BooleanProperty loadedOptionalFiles = new SimpleBooleanProperty(true);
public LocalModpackPage(WizardController controller) {
super(controller);
+ btnOptionalFiles.setOnAction((ev) -> {
+ controller.onNext(new OptionalFilesPage(this::onInstall, this::loadOptionalFiles,
+ loadingOptionalFiles, loadedOptionalFiles,
+ new FilteredList<>(allFiles, ModpackFile::isOptional), excludedFiles));
+ });
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
@@ -67,12 +90,15 @@ public LocalModpackPage(WizardController controller) {
if (installAsVersion) {
txtModpackName.getValidators().setAll(
new RequiredValidator(),
- new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
+ new Validator(i18n("install.new_game.already_exists"),
+ str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
} else {
txtModpackName.getValidators().setAll(
new RequiredValidator(),
- new Validator(i18n("install.new_game.already_exists"), str -> !ModpackHelper.isExternalGameNameConflicts(str) && Profiles.getProfiles().stream().noneMatch(p -> p.getName().equals(str))),
+ new Validator(i18n("install.new_game.already_exists"),
+ str -> !ModpackHelper.isExternalGameNameConflicts(str)
+ && Profiles.getProfiles().stream().noneMatch(p -> p.getName().equals(str))),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
}
});
@@ -101,10 +127,17 @@ public LocalModpackPage(WizardController controller) {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile))
.thenApplyAsync(encoding -> {
charset = encoding;
- manifest = ModpackHelper.readModpackManifest(selectedFile, encoding);
- return manifest;
+ return ModpackHelper.readModpackManifest(selectedFile, encoding);
})
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
+ this.manifest = manifest;
+ if (manifest.getManifest() instanceof ModpackManifest.SupportOptional) {
+ allFiles.setAll(((ModpackManifest.SupportOptional) manifest.getManifest()).getFiles());
+ if (allFiles.stream().anyMatch(ModpackFile::isOptional)) {
+ loadOptionalFiles();
+ btnOptionalFiles.setVisible(true);
+ }
+ }
if (exception instanceof ManuallyCreatedModpackException) {
hideSpinner();
nameProperty.set(FileUtils.getName(selectedFile));
@@ -115,14 +148,17 @@ public LocalModpackPage(WizardController controller) {
txtModpackName.setText(FileUtils.getNameWithoutExtension(selectedFile));
}
- Controllers.confirm(i18n("modpack.type.manual.warning"), i18n("install.modpack"), MessageDialogPane.MessageType.WARNING,
- () -> {},
+ Controllers.confirm(i18n("modpack.type.manual.warning"), i18n("install.modpack"),
+ MessageDialogPane.MessageType.WARNING,
+ () -> {
+ },
controller::onEnd);
controller.getSettings().put(MODPACK_MANUALLY_CREATED, true);
} else if (exception != null) {
LOG.warning("Failed to read modpack manifest", exception);
- Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
+ Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"),
+ MessageDialogPane.MessageType.ERROR);
Platform.runLater(controller::onEnd);
} else {
hideSpinner();
@@ -141,6 +177,25 @@ public LocalModpackPage(WizardController controller) {
}).start();
}
+ private void loadOptionalFiles() {
+ Objects.requireNonNull(manifest);
+ loadingOptionalFiles.set(true);
+ loadedOptionalFiles.set(false);
+ Task.supplyAsync(() -> manifest.getManifest().getProvider().loadFiles(DownloadProviders.getDownloadProvider(), manifest.getManifest()))
+ .whenComplete(Schedulers.javafx(), (manifest1, exception) -> {
+ List extends ModpackFile> files = ((ModpackManifest.SupportOptional) manifest
+ .setManifest(manifest1).getManifest()).getFiles();
+ allFiles.setAll(files);
+ loadingOptionalFiles.set(false);
+ if (exception != null || files.stream()
+ .anyMatch(s -> s.isOptional() && (s.getMod() == null || s.getFileName() == null))) {
+ LOG.warning("Failed to load optional files", exception);
+ } else {
+ loadedOptionalFiles.set(true);
+ }
+ }).start();
+ }
+
@Override
public void cleanup(SettingsMap settings) {
settings.remove(MODPACK_FILE);
@@ -158,6 +213,7 @@ protected void onInstall() {
.yesOrNo(() -> {
controller.getSettings().put(MODPACK_NAME, name);
controller.getSettings().put(MODPACK_CHARSET, charset);
+ controller.getSettings().put(MODPACK_SELECTED_FILES, getSelectedFiles());
controller.onFinish();
}, () -> {
// The user selects Cancel and does nothing.
@@ -166,10 +222,15 @@ protected void onInstall() {
} else {
controller.getSettings().put(MODPACK_NAME, name);
controller.getSettings().put(MODPACK_CHARSET, charset);
+ controller.getSettings().put(MODPACK_SELECTED_FILES, getSelectedFiles());
controller.onFinish();
}
}
+ private @Nullable Set extends ModpackFile> getSelectedFiles() {
+ return allFiles.stream().filter(file -> !excludedFiles.contains(file)).collect(Collectors.toSet());
+ }
+
protected void onDescribe() {
if (manifest != null)
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
@@ -179,6 +240,9 @@ protected void onDescribe() {
public static final SettingsMap.Key MODPACK_NAME = new SettingsMap.Key<>("MODPACK_NAME");
public static final SettingsMap.Key MODPACK_MANIFEST = new SettingsMap.Key<>("MODPACK_MANIFEST");
public static final SettingsMap.Key MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
- public static final SettingsMap.Key MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>("MODPACK_MANUALLY_CREATED");
+ public static final SettingsMap.Key MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>(
+ "MODPACK_MANUALLY_CREATED");
public static final SettingsMap.Key MODPACK_ICON_URL = new SettingsMap.Key<>("MODPACK_ICON_URL");
+ public static final SettingsMap.Key> MODPACK_SELECTED_FILES = new SettingsMap.Key<>(
+ "MODPACK_SELECTED_FILES");
}
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java
index 1826421b10..695be94b0b 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java
@@ -24,6 +24,7 @@
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
+import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
@@ -39,6 +40,7 @@
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Set;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -89,6 +91,7 @@ private Task> finishModpackInstallingAsync(SettingsMap settings) {
Modpack modpack = settings.get(LocalModpackPage.MODPACK_MANIFEST);
String name = settings.get(LocalModpackPage.MODPACK_NAME);
String iconUrl = settings.get(LocalModpackPage.MODPACK_ICON_URL);
+ Set extends ModpackFile> selectedFiles = settings.get(LocalModpackPage.MODPACK_SELECTED_FILES);
Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET);
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
@@ -107,7 +110,7 @@ private Task> finishModpackInstallingAsync(SettingsMap settings) {
if (serverModpackManifest != null) {
return ModpackHelper.getUpdateTask(profile, serverModpackManifest, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
} else {
- return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
+ return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)), selectedFiles);
}
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
@@ -122,7 +125,7 @@ private Task> finishModpackInstallingAsync(SettingsMap settings) {
return ModpackHelper.getInstallTask(profile, serverModpackManifest, name, modpack)
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
} else {
- return ModpackHelper.getInstallTask(profile, selected, name, modpack, iconUrl)
+ return ModpackHelper.getInstallTask(profile, selected, name, modpack, iconUrl, selectedFiles)
.thenRunAsync(Schedulers.javafx(), () -> profile.setSelectedVersion(name));
}
}
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java
index e88d7453db..8df34903f0 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java
@@ -22,6 +22,7 @@
import javafx.beans.property.StringProperty;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -33,7 +34,6 @@
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap;
-import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class ModpackPage extends SpinnerPane implements WizardPage {
@@ -47,6 +47,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected final JFXTextField txtModpackName;
protected final JFXButton btnInstall;
protected final JFXButton btnDescription;
+ protected final JFXButton btnOptionalFiles;
protected ModpackPage(WizardController controller) {
this.controller = controller;
@@ -64,7 +65,6 @@ protected ModpackPage(WizardController controller) {
txtModpackName = new JFXTextField();
txtModpackName.setPrefWidth(300);
FXUtils.setLimitHeight(archiveNamePane, 75);
- // BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
archiveNamePane.setRight(txtModpackName);
}
@@ -93,18 +93,30 @@ protected ModpackPage(WizardController controller) {
btnDescription.setOnAction(e -> onDescribe());
descriptionPane.setLeft(btnDescription);
+ var installHBox = new HBox(8);
+ btnOptionalFiles = FXUtils.newRaisedButton(i18n("modpack.optional_files"));
+ installHBox.getChildren().add(btnOptionalFiles);
+
btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
btnInstall.setOnAction(e -> onInstall());
- btnInstall.disableProperty().bind(createBooleanBinding(() -> !txtModpackName.validate(), txtModpackName.textProperty()));
- descriptionPane.setRight(btnInstall);
+ installHBox.getChildren().add(btnInstall);
+
+ descriptionPane.setRight(installHBox);
}
componentList.getContent().setAll(
archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
}
-
borderPane.getChildren().setAll(componentList);
- this.setContent(borderPane);
+ setContent(borderPane);
+ }
+
+ public void showSpinner() {
+ super.showSpinner();
+ }
+
+ public void hideSpinner() {
+ super.hideSpinner();
}
protected abstract void onInstall();
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/OptionalFilesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/OptionalFilesPage.java
new file mode 100644
index 0000000000..ce12442f71
--- /dev/null
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/OptionalFilesPage.java
@@ -0,0 +1,218 @@
+/*
+ * Hello Minecraft! Launcher
+ * Copyright (C) 2024 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.ui.download;
+
+import com.jfoenix.controls.JFXButton;
+import com.jfoenix.controls.JFXCheckBox;
+import com.jfoenix.controls.JFXDialogLayout;
+import com.jfoenix.controls.JFXListView;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.collections.ObservableList;
+import javafx.collections.ObservableSet;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.*;
+import org.jackhuang.hmcl.mod.ModpackFile;
+import org.jackhuang.hmcl.mod.RemoteMod;
+import org.jackhuang.hmcl.task.Schedulers;
+import org.jackhuang.hmcl.ui.Controllers;
+import org.jackhuang.hmcl.ui.FXUtils;
+import org.jackhuang.hmcl.ui.SVG;
+import org.jackhuang.hmcl.ui.construct.ComponentList;
+import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
+import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
+import org.jackhuang.hmcl.ui.construct.MDListCell;
+import org.jackhuang.hmcl.ui.construct.SpinnerPane;
+import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
+import org.jackhuang.hmcl.ui.wizard.WizardPage;
+
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
+import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
+
+/**
+ * This page is used to ask player which optional file they want to install
+ * Support CurseForge modpack yet
+ */
+public class OptionalFilesPage extends SpinnerPane implements WizardPage {
+ private final ObservableSet excludedFiles;
+ private final VBox head = new VBox();
+ private final JFXListView body = new JFXListView<>();
+ private final VBox tail = new VBox();
+ private final Label lblRetry = new Label(i18n("modpack.retry_optional_files"));
+
+ public OptionalFilesPage(Runnable install, Runnable retry, ObservableBooleanValue loading,
+ ObservableBooleanValue successful, ObservableList optionalFiles,
+ ObservableSet excludedFiles) {
+ this.excludedFiles = excludedFiles;
+
+ VBox borderPane = new VBox();
+ borderPane.setAlignment(Pos.CENTER);
+ FXUtils.setLimitWidth(borderPane, 500);
+ ComponentList componentList = new ComponentList();
+ {
+ head.getChildren().add(new Label(i18n("modpack.optional_files")));
+
+ var descPane = new BorderPane();
+ var btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
+ descPane.setRight(btnInstall);
+ btnInstall.setOnAction(e -> install.run());
+ tail.getChildren().add(descPane);
+
+ lblRetry.setOnMouseClicked(e -> retry.run());
+
+ if (successful.get()) {
+ componentList.getContent().setAll(head, body, tail);
+ } else {
+ componentList.getContent().setAll(head, lblRetry, body, tail);
+ }
+ successful.addListener((obs, oldVal, newVal) -> {
+ if (newVal && !oldVal) {
+ componentList.getContent().setAll(head, body, tail);
+ } else if (!newVal && oldVal) {
+ componentList.getContent().setAll(head, lblRetry, body, tail);
+ }
+ });
+ }
+
+ borderPane.getChildren().setAll(componentList);
+ setContent(borderPane);
+ body.setCellFactory(it -> new OptionalFileEntry(body));
+ body.setItems(optionalFiles);
+
+ loadingProperty().bind(loading);
+ }
+
+ private class OptionalFileEntry extends MDListCell {
+ private JFXCheckBox checkBox = new JFXCheckBox();
+ private TwoLineListItem content = new TwoLineListItem();
+ private JFXButton infoButton = new JFXButton();
+ private HBox container = new HBox(8);
+ private Label text1 = new Label();
+ private ModpackFile currentFile = null;
+ private ChangeListener selectedListener = (observable, oldValue, newValue) -> {
+ if (currentFile != null) {
+ if (newValue) {
+ excludedFiles.remove(currentFile);
+ } else {
+ excludedFiles.add(currentFile);
+ }
+ }
+ };
+
+ public OptionalFileEntry(JFXListView listView) {
+ super(listView);
+ container.setPickOnBounds(false);
+ container.setAlignment(Pos.CENTER_LEFT);
+ HBox.setHgrow(content, Priority.ALWAYS);
+ content.setMouseTransparent(true);
+ setSelectable();
+ container.getChildren().setAll(checkBox, content);
+
+ infoButton.getStyleClass().add("toggle-icon4");
+ infoButton.setGraphic(SVG.INFO.createIcon());
+ container.getChildren().add(infoButton);
+ getContainer().getChildren().setAll(container);
+ }
+
+ @Override
+ protected void updateControl(ModpackFile item, boolean empty) {
+ if (empty)
+ return;
+ checkBox.selectedProperty().removeListener(selectedListener);
+ currentFile = item;
+ String name = item.getFileName();
+ text1.setText(name);
+ if (name != null) {
+ content.setTitle(name);
+ } else {
+ content.setTitle(i18n("modpack.unknown_optional_file"));
+ }
+ Optional mod = item.getMod();
+ RemoteMod mod1 = mod == null ? null : mod.orElse(null);
+ if (mod1 != null) {
+ content.setSubtitle(mod1.getTitle());
+ infoButton.setOnMouseClicked(e -> Controllers.dialog(new ModInfo(mod1)));
+ infoButton.setManaged(true);
+ infoButton.setVisible(true);
+ } else {
+ content.setSubtitle("");
+ infoButton.setOnMouseClicked(null);
+ infoButton.setManaged(false);
+ infoButton.setVisible(false);
+ }
+ checkBox.setSelected(!excludedFiles.contains(item));
+ checkBox.selectedProperty().addListener(selectedListener);
+ }
+ }
+
+ private static class ModInfo extends JFXDialogLayout {
+ public ModInfo(RemoteMod mod) {
+ HBox container = new HBox(8);
+ SpinnerPane spinnerPane = new SpinnerPane();
+ ImageView imageView = new ImageView();
+ imageView.setFitHeight(32);
+ imageView.setFitWidth(32);
+ spinnerPane.setContent(imageView);
+ spinnerPane.setPrefSize(32, 32);
+ spinnerPane.setLoading(true);
+ CompletableFuture.supplyAsync(() -> {
+ try {
+ return FXUtils.getRemoteImageTask(mod.getIconUrl(), 32, 32, true, true).run();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }, Schedulers.io()).thenAcceptAsync((image) -> {
+ imageView.setImage(image);
+ spinnerPane.setLoading(false);
+ }, Schedulers.javafx());
+ container.getChildren().add(spinnerPane);
+
+ TwoLineListItem title = new TwoLineListItem();
+ title.setTitle(mod.getTitle());
+ title.setSubtitle(mod.getAuthor());
+ container.getChildren().add(title);
+ setHeading(container);
+
+ Label description = new Label(mod.getDescription());
+ setBody(description);
+
+ JFXHyperlink pageButton = new JFXHyperlink(i18n("mods.url"));
+ pageButton.setOnAction(e -> FXUtils.openLink(mod.getPageUrl()));
+ getActions().add(pageButton);
+
+ JFXButton okButton = new JFXButton();
+ okButton.getStyleClass().add("dialog-accept");
+ okButton.setText(i18n("button.ok"));
+ okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
+ getActions().add(okButton);
+
+ onEscPressed(this, okButton::fire);
+ }
+ }
+
+ @Override
+ public String getTitle() {
+ return i18n("modpack.optional_files");
+ }
+}
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java
index 2ace1017fe..4321bb3382 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java
@@ -40,6 +40,7 @@ public final class RemoteModpackPage extends ModpackPage {
public RemoteModpackPage(WizardController controller) {
super(controller);
+ btnOptionalFiles.setVisible(false);
manifest = controller.getSettings().get(MODPACK_SERVER_MANIFEST);
if (manifest == null)
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java
index 4875e75bd8..3a891deee5 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java
@@ -283,7 +283,7 @@ private void onRefreshedVersions(HMCLGameRepository repository) {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile))
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
.thenApplyAsync(modpack -> ModpackHelper
- .getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack, null)
+ .getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack, null, null)
.executor())
.thenAcceptAsync(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL);
diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties
index 6850bbc2ba..2d6101db4a 100644
--- a/HMCL/src/main/resources/assets/lang/I18N.properties
+++ b/HMCL/src/main/resources/assets/lang/I18N.properties
@@ -902,6 +902,10 @@ message.warning=Warning
message.question=Question
modpack=Modpacks
+modpack.optional_files=Optional Files
+modpack.unknown_optional_file=Unknown file
+modpack.retry_optional_files=Failed to load some optional files, click here to retry
+modpack.no_optional_files=No optional files in this modpack
modpack.choose=Choose Modpack
modpack.choose.local=Import from Local File
modpack.choose.local.detail=You can drag the modpack file here.
diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties
index d2cecb300c..ef68d656a5 100644
--- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties
+++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties
@@ -708,6 +708,10 @@ message.warning=警告
message.question=確認
modpack=模組包
+modpack.optional_files=可選檔案
+modpack.unknown_optional_file=未知檔案
+modpack.retry_optional_files=未能載入一些可選檔案,點擊重試
+modpack.no_optional_files=該模組包沒有可選檔案
modpack.choose=選取要安裝的遊戲模組包檔案
modpack.choose.local=匯入本機模組包檔案
modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以安裝
diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
index a942727b08..e3b77dc1cf 100644
--- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
+++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties
@@ -713,6 +713,10 @@ message.warning=警告
message.question=确认
modpack=整合包
+modpack.optional_files=可选文件
+modpack.unknown_optional_file=未知文件
+modpack.retry_optional_files=未能加载一些可选文件,点击重试
+modpack.no_optional_files=该整合包没有可选文件
modpack.choose=选择要安装的游戏整合包文件
modpack.choose.local=导入本地整合包文件
modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以安装
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java
index 07876ebf81..913eb6e949 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java
@@ -119,7 +119,7 @@ public Modpack setManifest(ModpackManifest manifest) {
return this;
}
- public abstract Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl);
+ public abstract Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl, Set extends ModpackFile> selectedFiles);
public static boolean acceptFile(String path, List blackList, List whiteList) {
if (path.isEmpty())
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackFile.java
new file mode 100644
index 0000000000..6e17a44440
--- /dev/null
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackFile.java
@@ -0,0 +1,55 @@
+/*
+ * Hello Minecraft! Launcher
+ * Copyright (C) 2024 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.mod;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+
+/**
+ * Representing a file entry which allow modpack developer declare it's optional or not
+ * */
+public interface ModpackFile {
+
+ /**
+ * Get the file name for the file
+ */
+ String getFileName();
+
+ /**
+ * Return if the file optional on the client side
+ */
+ boolean isOptional();
+
+ /**
+ * Return the path of the file
+ */
+ String getPath();
+
+ /**
+ * Return the mod the file belongs to
+ *
+ * About null and Optional.empty():
+ *
+ * - If the file hasn't been queried from remote, the mod will be null
+ * - If the file has been queried from remote but not found, the mod will be Optional.empty()
+ *
+ *
+ */
+ @Nullable Optional getMod();
+}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackManifest.java
index 0dcd03dc13..48e481a727 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackManifest.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackManifest.java
@@ -17,6 +17,12 @@
*/
package org.jackhuang.hmcl.mod;
+import java.util.List;
+
public interface ModpackManifest {
ModpackProvider getProvider();
+
+ interface SupportOptional {
+ List extends ModpackFile> getFiles();
+ }
}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackProvider.java
index d244095904..186d2ef6f2 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackProvider.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackProvider.java
@@ -20,12 +20,14 @@
import com.google.gson.JsonParseException;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
+import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.task.Task;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Set;
public interface ModpackProvider {
@@ -33,7 +35,7 @@ public interface ModpackProvider {
Task> createCompletionTask(DefaultDependencyManager dependencyManager, String version);
- Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException;
+ Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack, Set extends ModpackFile> selectedFiles) throws MismatchedModpackTypeException;
/**
* @param zipFile the opened modpack zip file.
@@ -47,4 +49,9 @@ public interface ModpackProvider {
default void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
}
+
+ // Complete the manifest with additional information
+ default ModpackManifest loadFiles(DownloadProvider downloadProvider, ModpackManifest manifest) {
+ return manifest;
+ }
}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java
index 87a81716bd..c3d2028a87 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java
@@ -23,6 +23,7 @@
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
+import org.jackhuang.hmcl.mod.ModpackFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
@@ -34,7 +35,9 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -54,6 +57,7 @@ public final class CurseCompletionTask extends Task {
private final ModManager modManager;
private final String version;
private CurseManifest manifest;
+ private Set extends ModpackFile> selectedFiles;
private List> dependencies;
private final AtomicBoolean allNameKnown = new AtomicBoolean(true);
@@ -67,7 +71,7 @@ public final class CurseCompletionTask extends Task {
* @param version the existent and physical version.
*/
public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version) {
- this(dependencyManager, version, null);
+ this(dependencyManager, version, null, null);
}
/**
@@ -77,18 +81,29 @@ public CurseCompletionTask(DefaultDependencyManager dependencyManager, String ve
* @param version the existent and physical version.
* @param manifest the CurseForgeModpack manifest.
*/
- public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version, CurseManifest manifest) {
+ public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version, CurseManifest manifest, Set extends ModpackFile> selectedFiles) {
this.dependency = dependencyManager;
this.repository = dependencyManager.getGameRepository();
this.modManager = repository.getModManager(version);
this.version = version;
this.manifest = manifest;
+ this.selectedFiles = selectedFiles;
if (manifest == null)
try {
- Path manifestFile = repository.getVersionRoot(version).resolve("manifest.json");
+ Path versionRoot = repository.getVersionRoot(version);
+ Path manifestFile = versionRoot.resolve("manifest.json");
if (Files.exists(manifestFile))
this.manifest = JsonUtils.fromJsonFile(manifestFile, CurseManifest.class);
+ Path filesFile = versionRoot.resolve("files.json");
+ if (this.manifest != null && Files.exists(filesFile)) {
+ Set files = new HashSet<>(JsonUtils.fromJsonFile(filesFile, JsonUtils.listTypeOf(String.class)));
+ this.selectedFiles = this.manifest.getFiles().stream()
+ .filter(f -> files.contains(f.getPath()))
+ .collect(Collectors.toSet());
+ } else {
+ this.selectedFiles = null;
+ }
} catch (Exception e) {
LOG.warning("Unable to read CurseForge modpack manifest.json", e);
}
@@ -138,6 +153,9 @@ public void execute() throws Exception {
})
.collect(Collectors.toList()));
JsonUtils.writeToJsonFile(root.resolve("manifest.json"), newManifest);
+ if (selectedFiles != null) {
+ JsonUtils.writeToJsonFile(root.resolve("files.json"), selectedFiles.stream().map(ModpackFile::getPath).collect(Collectors.toList()));
+ }
Path versionRoot = repository.getVersionRoot(modManager.getInstanceId());
Path resourcePacksRoot = versionRoot.resolve("resourcepacks");
@@ -146,6 +164,7 @@ public void execute() throws Exception {
dependencies = newManifest.files()
.stream().parallel()
.filter(f -> f.fileName() != null)
+ .filter(f -> selectedFiles == null || selectedFiles.contains(f))
.flatMap(f -> {
try {
Path path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot);
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java
index 60a2a1c533..e05c6904f9 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java
@@ -66,7 +66,7 @@ public final class CurseInstallTask extends Task {
* @param manifest The manifest content of given CurseForge modpack.
* @param name the new version name
*/
- public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, String name, String iconUrl) {
+ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
this.dependencyManager = dependencyManager;
this.zipFile = zipFile;
this.modpack = modpack;
@@ -123,7 +123,7 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile
dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl)));
}
}
- dependencies.add(new CurseCompletionTask(dependencyManager, name, manifest));
+ dependencies.add(new CurseCompletionTask(dependencyManager, name, manifest, selectedFiles));
}
@Override
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java
index 2fde2789a5..b7ad96d92d 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java
@@ -20,21 +20,124 @@
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
+import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jetbrains.annotations.Unmodifiable;
+import java.util.Collections;
import java.util.List;
-/// @author huangyuhui
+/**
+ *
+ * @author huangyuhui
+ */
+@Immutable
@JsonSerializable
-public record CurseManifest(@SerializedName("manifestType") String manifestType,
- @SerializedName("manifestVersion") int manifestVersion,
- @SerializedName("name") String name,
- @SerializedName("version") String version,
- @SerializedName("author") String author,
- @SerializedName("overrides") String overrides,
- @SerializedName("minecraft") CurseManifestMinecraft minecraft,
- @SerializedName("files") @Unmodifiable List files) implements ModpackManifest {
+public final class CurseManifest implements ModpackManifest, ModpackManifest.SupportOptional {
+
+ @SerializedName("manifestType")
+ private final String manifestType;
+
+ @SerializedName("manifestVersion")
+ private final int manifestVersion;
+
+ @SerializedName("name")
+ private final String name;
+
+ @SerializedName("version")
+ private final String version;
+
+ @SerializedName("author")
+ private final String author;
+
+ @SerializedName("overrides")
+ private final String overrides;
+
+ @SerializedName("minecraft")
+ private final CurseManifestMinecraft minecraft;
+
+ @SerializedName("files")
+ @Unmodifiable
+ private final List files;
+
+ public CurseManifest() {
+ this(MINECRAFT_MODPACK, 1, "", "1.0", "", "overrides", new CurseManifestMinecraft("", Collections.emptyList()), Collections.emptyList());
+ }
+
+ public CurseManifest(String manifestType, int manifestVersion, String name, String version, String author, String overrides, CurseManifestMinecraft minecraft, List files) {
+ this.manifestType = manifestType;
+ this.manifestVersion = manifestVersion;
+ this.name = name;
+ this.version = version;
+ this.author = author;
+ this.overrides = overrides;
+ this.minecraft = minecraft;
+ this.files = files;
+ }
+
+ public String getManifestType() {
+ return manifestType;
+ }
+
+ public int getManifestVersion() {
+ return manifestVersion;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public String getOverrides() {
+ return overrides;
+ }
+
+ public CurseManifestMinecraft getMinecraft() {
+ return minecraft;
+ }
+
+ public List getFiles() {
+ return files;
+ }
+
+ public String manifestType() {
+ return manifestType;
+ }
+
+ public int manifestVersion() {
+ return manifestVersion;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String version() {
+ return version;
+ }
+
+ public String author() {
+ return author;
+ }
+
+ public String overrides() {
+ return overrides;
+ }
+
+ public CurseManifestMinecraft minecraft() {
+ return minecraft;
+ }
+
+ public List files() {
+ return files;
+ }
public CurseManifest setFiles(List files) {
return new CurseManifest(manifestType, manifestVersion, name, version, author, overrides, minecraft, files);
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java
index bc772760d6..06bb85c812 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifestFile.java
@@ -19,19 +19,101 @@
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
+import org.jackhuang.hmcl.mod.ModpackFile;
+import org.jackhuang.hmcl.mod.RemoteMod;
+import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
+import java.util.Optional;
-/// @author huangyuhui
+/**
+ *
+ * @author huangyuhui
+ */
+@Immutable
@JsonSerializable
-public record CurseManifestFile(@SerializedName("projectID") int projectID,
- @SerializedName("fileID") int fileID,
- @SerializedName("fileName") String fileName,
- @SerializedName("url") String url,
- @SerializedName("required") boolean required) implements Validation {
+public final class CurseManifestFile implements Validation, ModpackFile {
+
+ @SerializedName("projectID")
+ private final int projectID;
+
+ @SerializedName("fileID")
+ private final int fileID;
+
+ @SerializedName("fileName")
+ private final String fileName;
+
+ @SerializedName("url")
+ private final String url;
+
+ @SerializedName("required")
+ private final boolean required;
+
+ @Nullable
+ private transient final RemoteMod mod;
+
+ public CurseManifestFile() {
+ this(0, 0, null, null, true, null);
+ }
+
+ public CurseManifestFile(int projectID, int fileID, String fileName, String url, boolean required, RemoteMod mod) {
+ this.projectID = projectID;
+ this.fileID = fileID;
+ this.fileName = fileName;
+ this.url = url;
+ this.required = required;
+ this.mod = mod;
+ }
+
+ public CurseManifestFile(int projectID, int fileID, String fileName, String url, boolean required) {
+ this(projectID, fileID, fileName, url, required, null);
+ }
+
+ public int getProjectID() {
+ return projectID;
+ }
+
+ public int getFileID() {
+ return fileID;
+ }
+
+ @Override
+ public String getFileName() {
+ return fileName;
+ }
+
+ @Override
+ public boolean isOptional() {
+ return !isRequired();
+ }
+
+ @Override
+ public String getPath() {
+ return "mods/" + getFileName();
+ }
+
+ public boolean isRequired() {
+ return required;
+ }
+
+ public int projectID() {
+ return projectID;
+ }
+
+ public int fileID() {
+ return fileID;
+ }
+
+ public String fileName() {
+ return fileName;
+ }
+
+ public boolean required() {
+ return required;
+ }
@Override
public void validate() throws JsonParseException {
@@ -39,7 +121,6 @@ public void validate() throws JsonParseException {
throw new JsonParseException("Missing Project ID or File ID.");
}
- @Override
@Nullable
public String url() {
if (url == null) {
@@ -51,12 +132,27 @@ public String url() {
}
}
+ @Nullable
+ public String getUrl() {
+ return url();
+ }
+
+ @SuppressWarnings("OptionalAssignedToNull")
+ @Override
+ public @Nullable Optional getMod() {
+ return mod == null ? null : Optional.of(mod);
+ }
+
public CurseManifestFile withFileName(String fileName) {
- return new CurseManifestFile(projectID, fileID, fileName, url, required);
+ return new CurseManifestFile(projectID, fileID, fileName, url, required, mod);
}
public CurseManifestFile withURL(String url) {
- return new CurseManifestFile(projectID, fileID, fileName, url, required);
+ return new CurseManifestFile(projectID, fileID, fileName, url, required, mod);
+ }
+
+ public CurseManifestFile withMod(@Nullable RemoteMod mod) {
+ return new CurseManifestFile(projectID, fileID, fileName, url, required, mod);
}
@Override
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModpackProvider.java
index aae1b7ec83..7989dee524 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModpackProvider.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModpackProvider.java
@@ -21,18 +21,22 @@
import kala.compress.archivers.zip.ZipArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
-import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
-import org.jackhuang.hmcl.mod.Modpack;
-import org.jackhuang.hmcl.mod.ModpackProvider;
-import org.jackhuang.hmcl.mod.ModpackUpdateTask;
+import org.jackhuang.hmcl.download.DownloadProvider;
+import org.jackhuang.hmcl.mod.*;
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.io.IOUtils;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class CurseModpackProvider implements ModpackProvider {
public static final CurseModpackProvider INSTANCE = new CurseModpackProvider();
@@ -48,11 +52,11 @@ public Task> createCompletionTask(DefaultDependencyManager dependencyManager,
}
@Override
- public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
+ public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack, Set extends ModpackFile> selectedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof CurseManifest curseManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
- return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, name, null));
+ return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, name, null, selectedFiles));
}
@Override
@@ -68,10 +72,36 @@ public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) t
return new Modpack(manifest.name(), manifest.author(), manifest.version(), manifest.minecraft().gameVersion(), description, encoding, manifest) {
@Override
- public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
- return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl);
+ public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
+ return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl, selectedFiles);
}
};
}
+ @Override
+ public CurseManifest loadFiles(DownloadProvider downloadProvider, ModpackManifest manifest1) {
+ if (!(manifest1 instanceof CurseManifest))
+ throw new IllegalArgumentException("manifest1 is not a CurseManifest");
+ CurseManifest manifest = (CurseManifest) manifest1;
+ return manifest.setFiles(
+ manifest.getFiles().parallelStream()
+ .map(file -> {
+ if ((StringUtils.isBlank(file.getFileName()) || file.getUrl() == null) && file.isOptional()) {
+ try {
+ RemoteMod mod = CurseForgeRemoteModRepository.MODS.getModById(downloadProvider, Integer.toString(file.getProjectID()));
+ RemoteMod.File remoteFile = CurseForgeRemoteModRepository.MODS.getModFile(Integer.toString(file.getProjectID()), Integer.toString(file.getFileID()));
+ return file.withFileName(remoteFile.getFilename()).withURL(remoteFile.getUrl()).withMod(mod);
+ } catch (FileNotFoundException fof) {
+ LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.getProjectID() + ", " + file.getFileID(), fof);
+ return file;
+ } catch (IOException | JsonParseException e) {
+ LOG.warning("Unable to fetch the file name projectID=" + file.getProjectID() + ", fileID=" + file.getFileID(), e);
+ return file;
+ }
+ } else {
+ return file;
+ }
+ })
+ .collect(Collectors.toList()));
+ }
}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java
index 403a30ff74..eaec64c719 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java
@@ -23,6 +23,7 @@
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.mod.Modpack;
+import org.jackhuang.hmcl.mod.ModpackFile;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.task.Task;
@@ -36,6 +37,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
@@ -424,7 +426,7 @@ public Modpack toModpack(Charset encoding) throws IOException {
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
@Override
- public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
+ public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
return new McbbsModpackLocalInstallTask(dependencyManager, zipFile, this, McbbsModpackManifest.this, name);
}
};
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java
index ac04cf736d..f06ff53008 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java
@@ -30,6 +30,7 @@
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Set;
public final class McbbsModpackProvider implements ModpackProvider {
public static final McbbsModpackProvider INSTANCE = new McbbsModpackProvider();
@@ -45,7 +46,7 @@ public Task> createCompletionTask(DefaultDependencyManager dependencyManager,
}
@Override
- public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
+ public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack, Set extends ModpackFile> selectedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof McbbsModpackManifest mcbbsModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java
index 682690c365..a64ca83952 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java
@@ -21,6 +21,7 @@
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
+import org.jackhuang.hmcl.mod.ModpackFile;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -30,11 +31,10 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -44,6 +44,7 @@ public class ModrinthCompletionTask extends Task {
private final DefaultGameRepository repository;
private final ModManager modManager;
private final String version;
+ private Set extends ModpackFile> selectedFiles;
private ModrinthManifest manifest;
private final List> dependencies = new ArrayList<>();
@@ -58,7 +59,7 @@ public class ModrinthCompletionTask extends Task {
* @param version the existent and physical version.
*/
public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version) {
- this(dependencyManager, version, null);
+ this(dependencyManager, version, null, null);
}
/**
@@ -68,18 +69,30 @@ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String
* @param version the existent and physical version.
* @param manifest the CurseForgeModpack manifest.
*/
- public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) {
+ public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest,
+ Set extends ModpackFile> selectedFiles) {
this.dependency = dependencyManager;
this.repository = dependencyManager.getGameRepository();
this.modManager = repository.getModManager(version);
this.version = version;
this.manifest = manifest;
+ this.selectedFiles = selectedFiles;
if (manifest == null)
try {
- Path manifestFile = repository.getVersionRoot(version).resolve("modrinth.index.json");
+ Path versionRoot = repository.getVersionRoot(version);
+ Path manifestFile = versionRoot.resolve("modrinth.index.json");
if (Files.exists(manifestFile))
this.manifest = JsonUtils.fromJsonFile(manifestFile, ModrinthManifest.class);
+ Path filesFile = versionRoot.resolve("files.json");
+ if (this.manifest != null && Files.exists(filesFile)) {
+ Set files = new HashSet<>(
+ JsonUtils.fromJsonFile(filesFile, JsonUtils.listTypeOf(String.class)));
+ this.selectedFiles = this.manifest.getFiles().stream().filter(f -> files.contains(f.getPath()))
+ .collect(Collectors.toSet());
+ } else {
+ this.selectedFiles = null;
+ }
} catch (Exception e) {
LOG.warning("Unable to read Modrinth modpack manifest.json", e);
}
@@ -105,11 +118,18 @@ public void execute() throws Exception {
Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(version));
Path modsDirectory = runDirectory.resolve("mods");
+ if (selectedFiles != null) {
+ JsonUtils.writeToJsonFile(repository.getVersionRoot(version).resolve("files.json"),
+ selectedFiles.stream().map(ModpackFile::getPath).collect(Collectors.toList()));
+ }
+
for (ModrinthManifest.File file : manifest.getFiles()) {
if (file.getEnv() != null && file.getEnv().getOrDefault("client", "required").equals("unsupported"))
continue;
if (file.getDownloads().isEmpty())
continue;
+ if (selectedFiles != null && !selectedFiles.contains(file))
+ continue;
Path filePath = runDirectory.resolve(file.getPath()).toAbsolutePath().normalize();
if (!filePath.startsWith(runDirectory))
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java
index c977b10176..c968c1c0f8 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java
@@ -52,8 +52,9 @@ public class ModrinthInstallTask extends Task {
private Task downloadIconTask;
private final List> dependents = new ArrayList<>(4);
private final List> dependencies = new ArrayList<>(1);
+ private final Set extends ModpackFile> selectedFiles;
- public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ModrinthManifest manifest, String name, String iconUrl) {
+ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ModrinthManifest manifest, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
this.dependencyManager = dependencyManager;
this.zipFile = zipFile;
this.modpack = modpack;
@@ -62,6 +63,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF
this.iconUrl = iconUrl;
this.repository = dependencyManager.getGameRepository();
this.run = repository.getRunDirectory(name);
+ this.selectedFiles = selectedFiles;
Path json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && Files.notExists(json))
@@ -126,7 +128,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF
dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl)));
}
}
- dependencies.add(new ModrinthCompletionTask(dependencyManager, name, manifest));
+ dependencies.add(new ModrinthCompletionTask(dependencyManager, name, manifest, selectedFiles));
}
@Override
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java
index ee5ac48e6f..7872df040a 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java
@@ -18,8 +18,10 @@
package org.jackhuang.hmcl.mod.modrinth;
import com.google.gson.JsonParseException;
+import org.jackhuang.hmcl.mod.ModpackFile;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
+import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable;
@@ -27,8 +29,9 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
-public class ModrinthManifest implements ModpackManifest, Validation {
+public class ModrinthManifest implements ModpackManifest, ModpackManifest.SupportOptional, Validation {
private final String game;
private final int formatVersion;
@@ -72,6 +75,10 @@ public List getFiles() {
return files;
}
+ public ModrinthManifest withFiles(List files) {
+ return new ModrinthManifest(game, formatVersion, versionId, name, summary, files, dependencies);
+ }
+
public Map getDependencies() {
return dependencies;
}
@@ -92,7 +99,7 @@ public void validate() throws JsonParseException, TolerableValidationException {
}
}
- public static class File {
+ public static class File implements ModpackFile {
private final String path;
private final Map hashes;
@Nullable
@@ -100,12 +107,22 @@ public static class File {
private final List downloads;
private final int fileSize;
- public File(String path, Map hashes, @Nullable Map env, List downloads, int fileSize) {
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ @Nullable
+ private transient final Optional mod;
+
+ public File(String path, Map hashes, @Nullable Map env, List downloads, int fileSize, @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nullable Optional mod) {
this.path = path;
this.hashes = hashes;
this.env = env;
this.downloads = downloads;
this.fileSize = fileSize;
+ this.mod = mod;
+ }
+
+ @SuppressWarnings("OptionalAssignedToNull")
+ public File(String path, Map hashes, @Nullable Map env, List downloads, int fileSize) {
+ this(path, hashes, env, downloads, fileSize, null);
}
public String getPath() {
@@ -141,6 +158,25 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(path, hashes, env, downloads, fileSize);
}
- }
+ @Override
+ public String getFileName() {
+ return new java.io.File(path).getName();
+ }
+
+ @Override
+ public @Nullable Optional getMod() {
+ return mod;
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public File withMod(@Nullable Optional mod) {
+ return new File(path, hashes, env, downloads, fileSize, mod);
+ }
+
+ @Override
+ public boolean isOptional() {
+ return env != null && "optional".equals(env.get("client"));
+ }
+ }
}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackProvider.java
index c8adc3da62..4bfc75e062 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackProvider.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackProvider.java
@@ -20,17 +20,21 @@
import com.google.gson.JsonParseException;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
-import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
-import org.jackhuang.hmcl.mod.Modpack;
-import org.jackhuang.hmcl.mod.ModpackProvider;
-import org.jackhuang.hmcl.mod.ModpackUpdateTask;
+import org.jackhuang.hmcl.download.DownloadProvider;
+import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModrinthModpackProvider implements ModpackProvider {
public static final ModrinthModpackProvider INSTANCE = new ModrinthModpackProvider();
@@ -46,11 +50,11 @@ public Task> createCompletionTask(DefaultDependencyManager dependencyManager,
}
@Override
- public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
+ public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack, Set extends ModpackFile> selectedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof ModrinthManifest modrinthManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
- return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, name, null));
+ return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, name, null, selectedFiles));
}
@Override
@@ -58,10 +62,36 @@ public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) t
ModrinthManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class);
return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), encoding, manifest) {
@Override
- public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
- return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl);
+ public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
+ return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl, selectedFiles);
}
};
}
+ @Override
+ public ModpackManifest loadFiles(DownloadProvider downloadProvider, ModpackManifest manifest1) {
+ if (!(manifest1 instanceof ModrinthManifest))
+ throw new IllegalArgumentException("Manifest is not a ModrinthManifest");
+ ModrinthManifest manifest = (ModrinthManifest) manifest1;
+ return manifest.withFiles(manifest.getFiles().parallelStream().map(file -> {
+ if (file.isOptional() && file.getMod() == null) {
+ try {
+ RemoteMod.Version version = ModrinthRemoteModRepository.MODS.getRemoteVersionBySHA1(file.getHashes().get("sha1")).orElse(null);
+ if (version == null) {
+ return file.withMod(Optional.empty());
+ }
+ RemoteMod mod = ModrinthRemoteModRepository.MODS.getModById(downloadProvider, version.getModid());
+ return file.withMod(Optional.ofNullable(mod));
+ } catch (FileNotFoundException fof) {
+ LOG.warning("Could not query modrinth for deleted mods: " + file.getFileName(), fof);
+ return file;
+ } catch (IOException | JsonParseException e) {
+ LOG.warning("Unable to fetch the modid for" + file.getFileName(), e);
+ return file;
+ }
+ } else {
+ return file;
+ }
+ }).collect(Collectors.toList()));
+ }
}
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java
index 72c30f7bd7..b236aa784e 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java
@@ -184,8 +184,10 @@ public SearchResult search(DownloadProvider downloadProvider, String gameVersion
@Override
public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
- String sha1 = DigestUtils.digestToString("SHA-1", file);
+ return getRemoteVersionBySHA1(DigestUtils.digestToString("SHA-1", file));
+ }
+ public Optional getRemoteVersionBySHA1(String sha1) throws IOException {
SEMAPHORE.acquireUninterruptibly();
try {
ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java
index 1bb7c8e939..c3705ce2c5 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java
@@ -20,10 +20,7 @@
import kala.compress.archivers.zip.ZipArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
-import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
-import org.jackhuang.hmcl.mod.Modpack;
-import org.jackhuang.hmcl.mod.ModpackProvider;
-import org.jackhuang.hmcl.mod.ModpackUpdateTask;
+import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -31,6 +28,7 @@
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Set;
public final class MultiMCModpackProvider implements ModpackProvider {
public static final MultiMCModpackProvider INSTANCE = new MultiMCModpackProvider();
@@ -46,7 +44,7 @@ public Task> createCompletionTask(DefaultDependencyManager dependencyManager,
}
@Override
- public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
+ public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack, Set extends ModpackFile> selectedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration multiMCInstanceConfiguration))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
@@ -85,7 +83,7 @@ public Modpack readManifest(ZipArchiveReader modpackFile, Path modpackPath, Char
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
@Override
- public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
+ public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
}
};
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java
index b8075c09f0..6fd111998b 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java
@@ -19,10 +19,7 @@
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
-import org.jackhuang.hmcl.mod.Modpack;
-import org.jackhuang.hmcl.mod.ModpackConfiguration;
-import org.jackhuang.hmcl.mod.ModpackManifest;
-import org.jackhuang.hmcl.mod.ModpackProvider;
+import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
@@ -32,6 +29,7 @@
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
@@ -126,7 +124,7 @@ public Modpack toModpack(Charset encoding) throws IOException {
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
@Override
- public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
+ public Task> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl, Set extends ModpackFile> selectedFiles) {
return new ServerModpackLocalInstallTask(dependencyManager, zipFile, this, ServerModpackManifest.this, name);
}
};
diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackProvider.java
index c29da8e5d2..2447443d99 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackProvider.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackProvider.java
@@ -20,10 +20,7 @@
import com.google.gson.JsonParseException;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
-import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
-import org.jackhuang.hmcl.mod.Modpack;
-import org.jackhuang.hmcl.mod.ModpackProvider;
-import org.jackhuang.hmcl.mod.ModpackUpdateTask;
+import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -31,6 +28,7 @@
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
+import java.util.Set;
public final class ServerModpackProvider implements ModpackProvider {
public static final ServerModpackProvider INSTANCE = new ServerModpackProvider();
@@ -46,7 +44,7 @@ public Task> createCompletionTask(DefaultDependencyManager dependencyManager,
}
@Override
- public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
+ public Task> createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack, Set extends ModpackFile> selectedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof ServerModpackManifest serverModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());