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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 selectedFiles) throws MismatchedModpackTypeException { if (!(modpack.getManifest() instanceof ServerModpackManifest serverModpackManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());