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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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();
Expand All @@ -48,7 +49,12 @@ public String getName() {
}

@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
public Task<?> createUpdateTask(
DefaultDependencyManager dependencyManager,
DefaultGameInstance instance,
Path zipFile,
Modpack modpack,
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());

Expand Down Expand Up @@ -77,7 +83,12 @@ public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding)

private final static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
public Task<?> getInstallTask(
DefaultDependencyManager dependencyManager,
Path zipFile,
GameInstanceID instanceId,
String iconUrl,
@Nullable Set<String> excludedFiles) {
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, instanceId);
}
}
Expand Down
52 changes: 47 additions & 5 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
Expand Down Expand Up @@ -196,6 +197,26 @@ public static Task<?> getInstallManuallyCreatedModpackTask(Path zipFile, String
}

public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) {
return getInstallTask(repository, zipFile, instanceId, modpack, iconUrl, null);
}

/// Creates an install task that respects optional-file selection.
///
/// @param repository the target repository
/// @param zipFile the modpack archive
/// @param instanceId the new instance id
/// @param modpack the parsed modpack
/// @param iconUrl the optional icon URL, or `null`
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
/// When non-null, must not contain `null` elements.
/// @return the install task
public static Task<?> getInstallTask(
HMCLGameRepository repository,
Path zipFile,
GameInstanceID instanceId,
Modpack modpack,
@Nullable String iconUrl,
@Nullable Set<String> excludedFiles) {
ExceptionalRunnable<?> success = () -> {
repository.refresh();
repository.getInstance(instanceId).enableIsolation();
Expand All @@ -209,17 +230,17 @@ public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile
};

if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
.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(repository.getDependency(), zipFile, instanceId, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
Expand All @@ -240,17 +261,38 @@ public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModp
}

public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
return getUpdateTask(repository, zipFile, charset, instanceId, configuration, null);
}

/// Creates an update task that respects optional-file selection.
///
/// @param repository the target repository
/// @param zipFile the modpack archive
/// @param charset the archive encoding
/// @param instanceId the instance to update
/// @param configuration the existing modpack configuration
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
/// When non-null, must not contain `null` elements.
/// @return the update task
public static Task<?> getUpdateTask(
HMCLGameRepository repository,
Path zipFile,
Charset charset,
GameInstanceID instanceId,
ModpackConfiguration<?> configuration,
@Nullable Set<String> excludedFiles)
throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
.thenComposeAsync(repository.refreshAsync());
else
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
.thenComposeAsync(repository.refreshAsync());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
import javafx.beans.binding.DoubleBinding;
import javafx.css.PseudoClass;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;

public abstract class MDListCell<T> extends ListCell<T> {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static final PseudoClass LAST = PseudoClass.getPseudoClass("last");

private final StackPane container = new StackPane();
private final StackPane root = new StackPane();
Expand Down Expand Up @@ -58,7 +60,10 @@ protected void updateItem(T item, boolean empty) {

super.updateItem(item, empty);

if (oldItem == item && oldEmpty == empty) return;
if (oldItem == item && oldEmpty == empty) {
updateLastPseudoClass(empty);
return;
}

ripplerContainer.releaseRippleImmediately();

Expand All @@ -68,6 +73,19 @@ protected void updateItem(T item, boolean empty) {
} else {
setGraphic(root);
}
updateLastPseudoClass(empty || item == null);
}

/// Updates the `:last` pseudo-class so the trailing divider can be suppressed in CSS.
///
/// @param empty whether this cell is empty
private void updateLastPseudoClass(boolean empty) {
ListView<T> listView = getListView();
boolean last = !empty
&& listView != null
&& getIndex() >= 0
&& getIndex() == listView.getItems().size() - 1;
root.pseudoClassStateChanged(LAST, last);
}

protected StackPane getContainer() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.modpack.Modpack;
import org.jackhuang.hmcl.modpack.ModpackFile;
import org.jackhuang.hmcl.modpack.ModpackManifest;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
Expand All @@ -39,9 +46,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.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
Expand All @@ -51,10 +64,22 @@ public final class LocalModpackPage extends ModpackPage {
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
private Modpack manifest = null;
private Charset charset;
private final ObservableList<ModpackFile> allFiles = FXCollections.observableList(new ArrayList<>());
private final ObservableSet<String> 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::optional),
excludedFiles)));

HMCLGameRepository repository = controller.getSettings().get(ModpackPage.REPOSITORY);

String name = controller.getSettings().get(MODPACK_NAME);
Expand Down Expand Up @@ -103,8 +128,7 @@ 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) -> {
if (exception instanceof ManuallyCreatedModpackException) {
Expand All @@ -126,7 +150,11 @@ public LocalModpackPage(WizardController controller) {
LOG.warning("Failed to read modpack manifest", exception);
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
Platform.runLater(controller::onEnd);
} else if (manifest == null) {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
Platform.runLater(controller::onEnd);
} else {
this.manifest = manifest;
hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
nameProperty.set(manifest.getName());
Expand All @@ -139,6 +167,33 @@ public LocalModpackPage(WizardController controller) {
}

btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription()));

if (manifest.getManifest() instanceof ModpackManifest.SupportOptional supportOptional) {
allFiles.setAll(supportOptional.getFiles());
if (allFiles.stream().anyMatch(ModpackFile::optional)) {
loadOptionalFiles();
btnOptionalFiles.setVisible(true);
btnOptionalFiles.setManaged(true);
}
}
}
}).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) -> {
loadingOptionalFiles.set(false);
List<? extends ModpackFile> files = ((ModpackManifest.SupportOptional) manifest1).getFiles();
manifest.setManifest(manifest1);
allFiles.setAll(files);
if (files.stream().anyMatch(s -> s.optional() && (!s.addonQueried() || s.fileName() == null))) {
LOG.warning("Failed to load optional files");
} else {
loadedOptionalFiles.set(true);
}
}).start();
}
Expand All @@ -157,21 +212,28 @@ protected void onInstall() {
i18n("install.name.invalid"),
i18n("message.warning"),
MessageDialogPane.MessageType.QUESTION)
.yesOrNo(() -> {
controller.getSettings().put(MODPACK_NAME, name);
controller.getSettings().put(MODPACK_CHARSET, charset);
controller.onFinish();
}, () -> {
.yesOrNo(() -> finishInstall(name), () -> {
// The user selects Cancel and does nothing.
})
.build());
} else {
controller.getSettings().put(MODPACK_NAME, name);
controller.getSettings().put(MODPACK_CHARSET, charset);
controller.onFinish();
finishInstall(name);
}
}

private void finishInstall(String name) {
controller.getSettings().put(MODPACK_NAME, name);
controller.getSettings().put(MODPACK_CHARSET, charset);
controller.getSettings().put(MODPACK_EXCLUDED_FILES, getExcludedFiles());
controller.onFinish();
}

private @Nullable Set<String> getExcludedFiles() {
if (allFiles.isEmpty())
return null;
return Set.copyOf(excludedFiles);
}

protected void onDescribe() {
if (manifest != null)
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
Expand All @@ -183,4 +245,5 @@ protected void onDescribe() {
public static final SettingsMap.Key<Charset> MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
public static final SettingsMap.Key<Boolean> MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>("MODPACK_MANUALLY_CREATED");
public static final SettingsMap.Key<String> MODPACK_ICON_URL = new SettingsMap.Key<>("MODPACK_ICON_URL");
public static final SettingsMap.Key<Set<String>> MODPACK_EXCLUDED_FILES = new SettingsMap.Key<>("MODPACK_EXCLUDED_FILES");
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ private Task<?> finishModpackInstallingAsync(SettingsMap settings) {
String iconUrl = settings.get(LocalModpackPage.MODPACK_ICON_URL);
Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET);
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
var excludedFiles = settings.get(LocalModpackPage.MODPACK_EXCLUDED_FILES);

if (isManuallyCreated) {
return ModpackHelper.getInstallManuallyCreatedModpackTask(selected, name, charset);
Expand All @@ -108,7 +109,7 @@ private Task<?> finishModpackInstallingAsync(SettingsMap settings) {
if (serverModpackManifest != null) {
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
} else {
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)), excludedFiles);
}
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
Expand All @@ -123,7 +124,7 @@ private Task<?> finishModpackInstallingAsync(SettingsMap settings) {
return ModpackHelper.getInstallTask(repository, serverModpackManifest, instanceId, modpack)
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
} else {
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl)
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl, excludedFiles)
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
}
}
Expand Down
Loading