feat: import .minecraft modpack. Closes #973.

This commit is contained in:
huanghongxun 2021-09-25 20:24:50 +08:00
parent d075dd27bc
commit 7fd8e0721f
19 changed files with 343 additions and 61 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ hs_err_pid*
*.2
*.log
.mine*
/externalgames
NVIDIA
# gradle build

View File

@ -32,7 +32,6 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@ -49,6 +48,7 @@ import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@ -114,7 +114,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
}
// https://github.com/huanghongxun/HMCL/issues/938
@ -267,7 +267,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
return false;
}
}

View File

@ -18,11 +18,14 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
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 java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@ -45,15 +48,23 @@ public final class HMCLModpackManager {
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class).setEncoding(encoding);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
else
return manifest.setManifest(HMCLModpackManifest.INSTANCE);
manifest.setManifest(HMCLModpackManifest.INSTANCE);
else
return manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
return manifest;
}
private static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import java.nio.file.Path;
public class ManuallyCreatedModpackException extends Exception {
private final Path path;
public ManuallyCreatedModpackException(Path path) {
this.path = path;
}
public Path getPath() {
return path;
}
}

View File

@ -0,0 +1,61 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
private final Profile profile;
private final Path zipFile;
private final Charset charset;
private final String name;
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
this.profile = profile;
this.zipFile = zipFile;
this.charset = charset;
this.name = name;
}
@Override
public void execute() throws Exception {
Path subdirectory;
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
}
Path dest = Paths.get("externalgames").resolve(name);
setResult(dest);
new Unzipper(zipFile, dest)
.setSubDirectory(subdirectory.toString())
.setTerminateIfSubDirectoryNotExists()
.setEncoding(charset)
.unzip();
}
}

View File

@ -31,6 +31,7 @@ import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@ -38,20 +39,27 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.toIterable;
public final class ModpackHelper {
private ModpackHelper() {}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
@ -82,9 +90,40 @@ public final class ModpackHelper {
// ignore it, not a valid Server modpack.
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
throw new ManuallyCreatedModpackException(file);
} catch (IOException e) {
// ignore it
}
throw new UnsupportedModpackException(file.toString());
}
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
Path root = fs.getPath("/");
if (isMinecraftDirectory(root)) return root;
try (Stream<Path> firstLayer = Files.list(root)) {
for (Path dir : toIterable(firstLayer)) {
if (isMinecraftDirectory(dir)) return dir;
try (Stream<Path> secondLayer = Files.list(dir)) {
for (Path subdir : toIterable(secondLayer)) {
if (isMinecraftDirectory(subdir)) return subdir;
}
} catch (IOException ignored) {
}
}
} catch (IOException ignored) {
}
throw new UnsupportedModpackException(modpackName);
}
private static boolean isMinecraftDirectory(Path path) {
return Files.isDirectory(path.resolve("versions")) &&
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
}
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
throw new FileNotFoundException(file.getPath());
@ -108,7 +147,7 @@ public final class ModpackHelper {
throw new UnsupportedModpackException();
}
public static Task<Void> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@ -131,7 +170,25 @@ public final class ModpackHelper {
.whenComplete(Schedulers.defaultScheduler(), success, failure);
}
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
public static boolean isExternalGameNameConflicts(String name) {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
Profile newProfile = new Profile(name, location.toFile());
newProfile.setUseRelativePath(true);
Profiles.getProfiles().add(newProfile);
Profiles.setSelectedProfile(newProfile);
});
}
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@ -150,24 +207,17 @@ public final class ModpackHelper {
}
};
if (modpack.getManifest() instanceof CurseManifest)
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof HMCLModpackManifest)
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.javafx(), success, failure);
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
@ -179,7 +229,7 @@ public final class ModpackHelper {
}
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
switch (configuration.getType()) {

View File

@ -20,34 +20,41 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
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.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LocalModpackPage extends StackPane implements WizardPage {
@ -79,6 +86,9 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
@FXML
private SpinnerPane spinnerPane;
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
private Charset charset;
public LocalModpackPage(WizardController controller) {
this.controller = controller;
@ -93,10 +103,20 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
txtModpackName.setText(name.get());
txtModpackName.setDisable(true);
} else {
txtModpackName.getValidators().addAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
FXUtils.onChangeAndOperate(installAsVersion, installAsVersion -> {
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.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.malformed"), HMCLGameRepository::isValidVersionId));
}
});
btnInstall.disableProperty().bind(
createBooleanBinding(txtModpackName::validate, txtModpackName.textProperty())
.not());
@ -120,23 +140,46 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
spinnerPane.showSpinner();
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
.thenApplyAsync(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
.whenComplete(Schedulers.javafx(), manifest -> {
spinnerPane.hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
.thenApplyAsync(encoding -> {
charset = encoding;
manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding);
return manifest;
})
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
if (exception instanceof ManuallyCreatedModpackException) {
spinnerPane.hideSpinner();
lblName.setText(selectedFile.getName());
installAsVersion.set(false);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(FileUtils.getNameWithoutExtension(selectedFile));
}
if (!name.isPresent()) {
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
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.log(Level.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 {
spinnerPane.hideSpinner();
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/huanghongxun/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
}
}
}, e -> {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
Platform.runLater(controller::onEnd);
}).start();
}
@ -149,6 +192,7 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
private void onInstall() {
if (!txtModpackName.validate()) return;
controller.getSettings().put(MODPACK_NAME, txtModpackName.getText());
controller.getSettings().put(MODPACK_CHARSET, charset);
controller.onFinish();
}
@ -167,4 +211,6 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
public static final String MODPACK_FILE = "MODPACK_FILE";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
public static final String MODPACK_CHARSET = "MODPACK_CHARSET";
public static final String MODPACK_MANUALLY_CREATED = "MODPACK_MANUALLY_CREATED";
}

View File

@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
@ -35,6 +36,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
@ -72,11 +74,18 @@ public class ModpackInstallWizardProvider implements WizardProvider {
settings.put(PROFILE, profile);
}
private Task<Void> finishModpackInstallingAsync(Map<String, Object> settings) {
private Task<?> finishModpackInstallingAsync(Map<String, Object> settings) {
File selected = tryCast(settings.get(LocalModpackPage.MODPACK_FILE), File.class).orElse(null);
ServerModpackManifest serverModpackManifest = tryCast(settings.get(RemoteModpackPage.MODPACK_SERVER_MANIFEST), ServerModpackManifest.class).orElse(null);
Modpack modpack = tryCast(settings.get(LocalModpackPage.MODPACK_MANIFEST), Modpack.class).orElse(null);
String name = tryCast(settings.get(LocalModpackPage.MODPACK_NAME), String.class).orElse(null);
Charset charset = tryCast(settings.get(LocalModpackPage.MODPACK_CHARSET), Charset.class).orElse(null);
boolean isManuallyCreated = tryCast(settings.get(LocalModpackPage.MODPACK_MANUALLY_CREATED), Boolean.class).orElse(false);
if (isManuallyCreated) {
return ModpackHelper.getInstallManuallyCreatedModpackTask(profile, selected, name, charset);
}
if ((selected == null && serverModpackManifest == null) || modpack == null || name == null) return null;
if (updateVersion != null) {
@ -90,7 +99,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
} else {
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
}
} catch (UnsupportedModpackException e) {
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);

View File

@ -240,7 +240,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
txtModpackFileApi.getValidators().add(new URLValidator());
txtModpackFileApi.getValidators().add(new URLValidator(true));
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
}

View File

@ -522,6 +522,7 @@ modpack.type.curse.completion=Install files related to Curse modpack
modpack.type.curse.tolerable_error=We cannot complete the download of all files of this Curse modpack. You can retry the download when starting corresponding game version. You may retry for a couple of times due to network problems.
modpack.type.curse.error=Unable to install this Curse modpack. Please retry.
modpack.type.curse.not_found=Some of required resources are missing and thus could not be downloaded. Please consider the latest version or other modpacks.
modpack.type.manual.warning=You probably need to directly decompress this modpack file, instead of importing this modpack. And launch the game using bundled launcher. This modpack is manually created by compressing .minecraft directory, not exported by launcher. HMCL can try to import this modpack, continue?
modpack.type.mcbbs=MCBBS Standard
modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher and MultiMC
modpack.type.multimc=MultiMC

View File

@ -522,6 +522,7 @@ modpack.type.curse.completion=下載 Curse 整合包相關檔案
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包檔案的下載,您可以在啟動該遊戲版本時繼續 Curse 整合包檔案的下載。由於網路問題,您可能需要重試多次。
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該整合包的最新版本或者安裝其他整合包。
modpack.type.manual.warning=您大概需要直接解压该整合包,并使用其自带的启动器即可开始游戏,而不需要导入整合包。该整合包不是由启动器导出的整合包,而是人工打包 .minecraft 资料夹而来的这类整合包通常附带游戏启动器。HMCL 可以尝试导入该整合包,但不保证可用性,是否继续?
modpack.type.mcbbs=我的世界中文論壇整合包標準
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 匯入
modpack.type.multimc=MultiMC

View File

@ -522,6 +522,7 @@ modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次。
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
modpack.type.manual.warning=您大概需要直接解压该整合包,并使用其自带的启动器即可开始游戏,而不需要导入整合包。该整合包不是由启动器导出的整合包,而是人工打包 .minecraft 文件夹而来的这类整合包通常附带游戏启动器。HMCL 可以尝试导入该整合包,但不保证可用性,是否继续?
modpack.type.mcbbs=我的世界中文论坛整合包标准
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入
modpack.type.multimc=MultiMC

View File

@ -17,6 +17,10 @@
*/
package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.task.Task;
import java.io.File;
import java.nio.charset.Charset;
import java.util.List;
@ -24,14 +28,14 @@ import java.util.List;
*
* @author huangyuhui
*/
public final class Modpack {
private final String name;
private final String author;
private final String version;
private final String gameVersion;
private final String description;
private final transient Charset encoding;
private final Object manifest;
public abstract class Modpack {
private String name;
private String author;
private String version;
private String gameVersion;
private String description;
private transient Charset encoding;
private Object manifest;
public Modpack() {
this("", null, null, null, null, null, null);
@ -51,32 +55,54 @@ public final class Modpack {
return name;
}
public Modpack setName(String name) {
this.name = name;
return this;
}
public String getAuthor() {
return author;
}
public Modpack setAuthor(String author) {
this.author = author;
return this;
}
public String getVersion() {
return version;
}
public Modpack setVersion(String version) {
this.version = version;
return this;
}
public String getGameVersion() {
return gameVersion;
}
public Modpack setGameVersion(String gameVersion) {
return new Modpack(name, author, version, gameVersion, description, encoding, manifest);
this.gameVersion = gameVersion;
return this;
}
public String getDescription() {
return description;
}
public Modpack setDescription(String description) {
this.description = description;
return this;
}
public Charset getEncoding() {
return encoding;
}
public Modpack setEncoding(Charset encoding) {
return new Modpack(name, author, version, gameVersion, description, encoding, manifest);
this.encoding = encoding;
return this;
}
public Object getManifest() {
@ -84,9 +110,12 @@ public final class Modpack {
}
public Modpack setManifest(Object manifest) {
return new Modpack(name, author, version, gameVersion, description, encoding, manifest);
this.manifest = manifest;
return this;
}
public abstract Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name);
public static boolean acceptFile(String path, List<String> blackList, List<String> whiteList) {
if (path.isEmpty())
return true;

View File

@ -19,11 +19,14 @@ package org.jackhuang.hmcl.mod.curse;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@ -122,7 +125,12 @@ public final class CurseManifest {
String json = CompressingUtils.readTextZipEntry(zip, "manifest.json", encoding);
CurseManifest manifest = JsonUtils.fromNonNullJson(json, CurseManifest.class);
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(),
CompressingUtils.readTextZipEntryQuietly(zip, "modlist.html", encoding).orElse( "No description"), encoding, manifest);
CompressingUtils.readTextZipEntryQuietly(zip, "modlist.html", encoding).orElse( "No description"), encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
}
};
}
public static final String MINECRAFT_MODPACK = "minecraftModpack";

View File

@ -19,9 +19,11 @@ package org.jackhuang.hmcl.mod.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@ -419,7 +421,12 @@ public class McbbsModpackManifest implements Validation {
public Modpack toModpack(Charset encoding) throws IOException {
String gameVersion = addons.stream().filter(x -> MINECRAFT.getPatchId().equals(x.id)).findAny()
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
return new Modpack(name, author, version, gameVersion, description, encoding, this);
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, java.io.File zipFile, String name) {
return new McbbsModpackLocalInstallTask(dependencyManager, zipFile, this, McbbsModpackManifest.this, name);
}
};
}
public void injectLaunchOptions(LaunchOptions.Builder launchOptions) {

View File

@ -17,11 +17,14 @@
*/
package org.jackhuang.hmcl.mod.multimc;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -356,7 +359,12 @@ public final class MultiMCInstanceConfiguration {
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
try (InputStream instanceStream = Files.newInputStream(instancePath)) {
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
}
};
}
}
}

View File

@ -18,13 +18,16 @@
package org.jackhuang.hmcl.mod.server;
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.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@ -117,7 +120,12 @@ public class ServerModpackManifest implements Validation {
public Modpack toModpack(Charset encoding) throws IOException {
String gameVersion = addons.stream().filter(x -> MINECRAFT.getPatchId().equals(x.id)).findAny()
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
return new Modpack(name, author, version, gameVersion, description, encoding, this);
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new ServerModpackLocalInstallTask(dependencyManager, zipFile, this, ServerModpackManifest.this, name);
}
};
}
/**

View File

@ -330,6 +330,14 @@ public final class Lang {
return optional.map(Stream::of).orElseGet(Stream::empty);
}
public static <T> Iterable<T> toIterable(Stream<T> stream) {
return stream::iterator;
}
public static <T> Iterable<T> toIterable(Iterator<T> iterator) {
return () -> iterator;
}
/**
* This is a useful function to prevent exceptions being eaten when using CompletableFuture.
* You can write:

View File

@ -99,6 +99,7 @@ public final class FileUtils {
}
public static String getName(Path path) {
if (path.getFileName() == null) return "";
return StringUtils.removeSuffix(path.getFileName().toString(), "/", "\\");
}