fix: mcbbs modpack completion

This commit is contained in:
huanghongxun 2021-01-26 12:25:06 +08:00
parent 94cd33af5c
commit 13fa713d58
15 changed files with 534 additions and 166 deletions

View File

@ -35,7 +35,11 @@ import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@ -140,10 +144,12 @@ public final class LauncherHelper {
}), Task.composeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if ("Server".equals(configuration.getType()))
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {

View File

@ -199,6 +199,11 @@ public final class ModpackHelper {
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
case McbbsModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
case ServerModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof ServerModpackManifest))
throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));

View File

@ -82,7 +82,7 @@ public class GameItem extends Control {
CompletableFuture.runAsync(() -> {
try {
ModpackConfiguration<Void> config = profile.getRepository().readModpackConfiguration(version);
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
if (config == null) return;
tag.set(config.getVersion());
} catch (IOException e) {

View File

@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
@ -40,10 +39,8 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

View File

@ -238,11 +238,11 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
modpack.choose.remote=从互联网下载整合包
modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 Markdown(图片请用网络图)。
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图)。
modpack.description=整合包描述
modpack.enter_name=给游戏起个你喜欢的名字
modpack.export=导出整合包
modpack.export.as=请选择整合包类型 (若无法决定,请选择 HMCL 类型)
modpack.export.as=请选择整合包类型 (若无法决定,请选择我的世界中文论坛整合包标准)
modpack.file_api=整合包下载链接前缀
modpack.files.blueprints=BuildCraft 蓝图
modpack.files.config=Mod 配置文件
@ -278,7 +278,7 @@ modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
modpack.type.mcbbs=我的世界中文论坛整合包标准
modpack.type.hmcl.export=可以被 Hello Minecraft! Launcher (HMCL) 导入
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 导入
modpack.type.multimc=MultiMC
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入
modpack.type.server=服务器自动更新整合包

View File

@ -33,7 +33,7 @@ import java.util.List;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public final class MinecraftInstanceTask<T> extends Task<Void> {
public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>> {
private final File zipFile;
private final Charset encoding;
@ -73,6 +73,8 @@ public final class MinecraftInstanceTask<T> extends Task<Void> {
});
}
FileUtils.writeText(jsonFile, JsonUtils.GSON.toJson(new ModpackConfiguration<>(manifest, type, name, version, overrides)));
ModpackConfiguration<T> configuration = new ModpackConfiguration<>(manifest, type, name, version, overrides);
FileUtils.writeText(jsonFile, JsonUtils.GSON.toJson(configuration));
setResult(configuration);
}
}

View File

@ -42,6 +42,14 @@ public final class ModManager {
this.id = id;
}
public GameRepository getRepository() {
return repository;
}
public String getVersion() {
return id;
}
private Path getModsDirectory() {
return repository.getRunDirectory(id).toPath().resolve("mods");
}

View File

@ -21,181 +21,302 @@ import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseMetaMod;
import org.jackhuang.hmcl.task.*;
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;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class McbbsModpackCompletionTask extends Task<Void> {
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
private final DefaultDependencyManager dependency;
private final DefaultGameRepository repository;
private final ModManager modManager;
private final String version;
private ModpackConfiguration<McbbsModpackManifest> manifest;
private final File configurationFile;
private ModpackConfiguration<McbbsModpackManifest> configuration;
private GetTask dependent;
private McbbsModpackManifest remoteManifest;
private McbbsModpackManifest manifest;
private final List<Task<?>> dependencies = new LinkedList<>();
private final AtomicBoolean allNameKnown = new AtomicBoolean(true);
private final AtomicInteger finished = new AtomicInteger(0);
private final AtomicBoolean notFound = new AtomicBoolean(false);
public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version) {
this(dependencyManager, version, null);
}
public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version, ModpackConfiguration<McbbsModpackManifest> manifest) {
public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version, ModpackConfiguration<McbbsModpackManifest> configuration) {
this.dependency = dependencyManager;
this.repository = dependencyManager.getGameRepository();
this.modManager = repository.getModManager(version);
this.version = version;
this.configurationFile = repository.getModpackConfiguration(version);
this.configuration = configuration;
}
if (manifest == null) {
try {
File manifestFile = repository.getModpackConfiguration(version);
if (manifestFile.exists()) {
this.manifest = JsonUtils.GSON.fromJson(FileUtils.readText(manifestFile), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
@Override
public CompletableFuture<Void> getFuture(TaskCompletableFuture executor) {
return breakable(CompletableFuture.runAsync(wrap(() -> {
if (configuration == null) {
// Load configuration from disk
try {
configuration = JsonUtils.fromNonNullJson(FileUtils.readText(configurationFile), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
} catch (IOException | JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to read mcbbs modpack manifest.json", e);
}
manifest = configuration.getManifest();
if (manifest == null) throw new CustomException();
})).thenComposeAsync(unused -> {
// we first download latest manifest
return breakable(CompletableFuture.runAsync(wrap(() -> {
if (StringUtils.isBlank(manifest.getFileApi())) {
// skip this phase
throw new CustomException();
}
})).thenComposeAsync(wrap(unused1 -> {
return executor.one(new GetTask(new URL(manifest.getFileApi() + "/manifest.json")));
})).thenComposeAsync(wrap(unused1 -> {
McbbsModpackManifest remoteManifest;
// We needs to update modpack from online server.
try {
remoteManifest = JsonUtils.fromNonNullJson(dependent.getResult(), McbbsModpackManifest.class);
} catch (JsonParseException e) {
throw new IOException("Unable to parse server manifest.json from " + manifest.getFileApi(), e);
}
Path rootPath = repository.getVersionRoot(version).toPath();
Map<McbbsModpackManifest.File, McbbsModpackManifest.File> localFiles = manifest.getFiles().stream().collect(Collectors.toMap(Function.identity(), Function.identity()));
// for files in new modpack
List<McbbsModpackManifest.File> newFiles = new ArrayList<>(remoteManifest.getFiles().size());
List<Task<?>> tasks = new ArrayList<>();
for (McbbsModpackManifest.File file : remoteManifest.getFiles()) {
Path actualPath = getFilePath(file);
McbbsModpackManifest.File oldFile = localFiles.remove(file);
boolean download = false;
if (oldFile == null) {
// If old modpack does not have this entry, download it
download = true;
} else if (actualPath != null) {
if (!Files.exists(actualPath)) {
// If both old and new modpacks have this entry, but the file is missing...
// Re-download it since network problem may cause file missing
download = true;
} else if (getFileHash(file) != null) {
// If user modified this entry file, we will not replace this file since this modified file is what user expects.
// Or we have downloaded latest file in previous completion task, this time we have no need to download it again.
String fileHash = encodeHex(digest("SHA-1", actualPath));
String oldHash = getFileHash(oldFile);
String newHash = getFileHash(file);
if (oldHash == null) {
// We don't know whether the file is modified or not, just update it.
download = true;
} else if (!Objects.equals(fileHash, newHash)) {
if (file.isForce()) {
// this file is not allowed to be modified, required by modpack author.
download = true;
} else if (Objects.equals(oldHash, fileHash)) {
download = true;
}
}
}
} else {
// we resolve files with unknown path later.
}
if (download) {
tasks.add(downloadFile(remoteManifest, file));
}
newFiles.add(mergeFile(oldFile, file));
}
// If old modpack have this entry, and new modpack deleted it. Delete this file.
// for-loop above removes still existing file in localFiles. Remaining elements
// are files removed by next modpack version.
// Notice that this loop will also remove Curse mods.
for (McbbsModpackManifest.File file : localFiles.keySet()) {
Path actualPath = getFilePath(file);
if (actualPath != null && Files.exists(actualPath))
Files.deleteIfExists(actualPath);
}
manifest = remoteManifest.setFiles(newFiles);
return executor.all(tasks.stream().filter(Objects::nonNull).collect(Collectors.toList()));
})).thenAcceptAsync(wrap(unused1 -> {
File manifestFile = repository.getModpackConfiguration(version);
FileUtils.writeText(manifestFile, JsonUtils.GSON.toJson(
new ModpackConfiguration<>(manifest, this.configuration.getType(), this.manifest.getName(), this.manifest.getVersion(),
this.manifest.getFiles().stream()
.flatMap(file -> file instanceof McbbsModpackManifest.AddonFile
? Stream.of((McbbsModpackManifest.AddonFile) file)
: Stream.empty())
.map(file -> new ModpackConfiguration.FileInformation(file.getPath(), file.getHash()))
.collect(Collectors.toList()))));
})));
}).thenComposeAsync(unused -> {
AtomicBoolean allNameKnown = new AtomicBoolean(true);
AtomicInteger finished = new AtomicInteger(0);
AtomicBoolean notFound = new AtomicBoolean(false);
return breakable(CompletableFuture.completedFuture(null)
.thenComposeAsync(wrap(unused1 -> {
List<Task<?>> dependencies = new ArrayList<>();
// Because in China, Curse is too difficult to visit,
// if failed, ignore it and retry next time.
McbbsModpackManifest newManifest = manifest.setFiles(
manifest.getFiles().parallelStream()
.map(rawFile -> {
updateProgress(finished.incrementAndGet(), manifest.getFiles().size());
if (rawFile instanceof McbbsModpackManifest.CurseFile) {
McbbsModpackManifest.CurseFile file = (McbbsModpackManifest.CurseFile) rawFile;
if (StringUtils.isBlank(file.getFileName())) {
try {
return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
} catch (IOException e) {
try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) {
Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), fof);
notFound.set(true);
return file;
} catch (IOException | JsonParseException e2) {
try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) {
Logging.LOG.log(Level.WARNING, "Could not query forgesvc for deleted mods: " + file.getUrl(), fof);
notFound.set(true);
return file;
} catch (IOException | JsonParseException e3) {
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e);
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e2);
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e3);
allNameKnown.set(false);
return file;
}
}
}
} else {
return file;
}
} else {
return rawFile;
}
})
.collect(Collectors.toList()));
manifest = newManifest;
configuration = configuration.setManifest(newManifest);
FileUtils.writeText(configurationFile, JsonUtils.GSON.toJson(configuration));
for (McbbsModpackManifest.File file : newManifest.getFiles())
if (file instanceof McbbsModpackManifest.CurseFile) {
McbbsModpackManifest.CurseFile curseFile = (McbbsModpackManifest.CurseFile) file;
if (StringUtils.isNotBlank(curseFile.getFileName())) {
if (!modManager.hasSimpleMod(curseFile.getFileName())) {
FileDownloadTask task = new FileDownloadTask(curseFile.getUrl(), modManager.getSimpleModPath(curseFile.getFileName()).toFile());
task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true);
dependencies.add(task.withCounter());
}
}
}
if (!dependencies.isEmpty()) {
getProperties().put("total", dependencies.size());
}
return executor.all(dependencies);
})).whenComplete(wrap((unused1, ex) -> {
// Let this task fail if the curse manifest has not been completed.
// But continue other downloads.
if (notFound.get())
throw new CurseCompletionException(new FileNotFoundException());
if (!allNameKnown.get() || ex != null)
throw new CurseCompletionException();
})));
}));
}
@Nullable
private Path getFilePath(McbbsModpackManifest.File file) {
if (file instanceof McbbsModpackManifest.AddonFile) {
return modManager.getRepository().getRunDirectory(modManager.getVersion()).toPath().resolve(((McbbsModpackManifest.AddonFile) file).getPath());
} else if (file instanceof McbbsModpackManifest.CurseFile) {
String fileName = ((McbbsModpackManifest.CurseFile) file).getFileName();
if (fileName == null) return null;
return modManager.getSimpleModPath(fileName);
} else {
this.manifest = manifest;
throw new IllegalArgumentException();
}
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
dependent = new GetTask(new URL(manifest.getManifest().getFileApi() + "/manifest.json"));
}
@Override
public Collection<Task<?>> getDependencies() {
return dependencies;
}
@Override
public Collection<Task<?>> getDependents() {
return dependent == null ? Collections.emptySet() : Collections.singleton(dependent);
}
@Override
public void execute() throws Exception {
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
try {
remoteManifest = JsonUtils.fromNonNullJson(dependent.getResult(), McbbsModpackManifest.class);
} catch (JsonParseException e) {
throw new IOException(e);
private String getFileHash(McbbsModpackManifest.File file) {
if (file instanceof McbbsModpackManifest.AddonFile) {
return ((McbbsModpackManifest.AddonFile) file).getHash();
} else {
return null;
}
Path rootPath = repository.getVersionRoot(version).toPath();
// Because in China, Curse is too difficult to visit,
// if failed, ignore it and retry next time.
// CurseManifest newManifest = manifest.setFiles(
// manifest.getFiles().parallelStream()
// .map(file -> {
// updateProgress(finished.incrementAndGet(), manifest.getFiles().size());
// if (StringUtils.isBlank(file.getFileName())) {
// try {
// return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
// } catch (IOException e) {
// try {
// String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
// CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
// return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
// } catch (FileNotFoundException fof) {
// Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), fof);
// notFound.set(true);
// return file;
// } catch (IOException | JsonParseException e2) {
// try {
// String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
// CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
// return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
// } catch (FileNotFoundException fof) {
// Logging.LOG.log(Level.WARNING, "Could not query forgesvc for deleted mods: " + file.getUrl(), fof);
// notFound.set(true);
// return file;
// } catch (IOException | JsonParseException e3) {
// Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e);
// Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e2);
// Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e3);
// allNameKnown.set(false);
// return file;
// }
// }
// }
// } else {
// return file;
// }
// })
// .collect(Collectors.toList()));
//
// Map<String, ModpackConfiguration.FileInformation> files = manifest.getManifest().getFiles().stream()
// .collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath,
// Function.identity()));
//
// Set<String> remoteFiles = remoteManifest.getFiles().stream().map(ModpackConfiguration.FileInformation::getPath)
// .collect(Collectors.toSet());
//
// // for files in new modpack
// for (ModpackConfiguration.FileInformation file : remoteManifest.getFiles()) {
// Path actualPath = rootPath.resolve(file.getPath());
// boolean download;
// if (!files.containsKey(file.getPath())) {
// // If old modpack does not have this entry, download it
// download = true;
// } else if (!Files.exists(actualPath)) {
// // If both old and new modpacks have this entry, but the file is missing...
// // Re-download it since network problem may cause file missing
// download = true;
// } else {
// // If user modified this entry file, we will not replace this file since this modified file is that user expects.
// String fileHash = encodeHex(digest("SHA-1", actualPath));
// String oldHash = files.get(file.getPath()).getHash();
// download = !Objects.equals(oldHash, file.getHash()) && Objects.equals(oldHash, fileHash);
// }
//
// if (download) {
// dependencies.add(new FileDownloadTask(
// new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(file.getPath())),
// actualPath.toFile(),
// new FileDownloadTask.IntegrityCheck("SHA-1", file.getHash())));
// }
// }
//
// // If old modpack have this entry, and new modpack deleted it. Delete this file.
// for (ModpackConfiguration.FileInformation file : manifest.getManifest().getFiles()) {
// Path actualPath = rootPath.resolve(file.getPath());
// if (Files.exists(actualPath) && !remoteFiles.contains(file.getPath()))
// Files.deleteIfExists(actualPath);
// }
}
@Override
public boolean doPostExecute() {
return true;
private Task<?> downloadFile(McbbsModpackManifest remoteManifest, McbbsModpackManifest.File file) throws IOException {
if (file instanceof McbbsModpackManifest.AddonFile) {
McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file;
return new FileDownloadTask(
new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())),
modManager.getSimpleModPath(addonFile.getPath()).toFile(),
addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null);
} else if (file instanceof McbbsModpackManifest.CurseFile) {
// we download it later.
return null;
} else {
throw new IllegalArgumentException();
}
}
@Override
public void postExecute() throws Exception {
// if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
// File manifestFile = repository.getModpackConfiguration(version);
// FileUtils.writeText(manifestFile, JsonUtils.GSON.toJson(new ModpackConfiguration<>(remoteManifest, this.manifest.getType(), this.manifest.getName(), this.manifest.getVersion(), remoteManifest.getFiles())));
@NotNull
private McbbsModpackManifest.File mergeFile(@Nullable McbbsModpackManifest.File oldFile, @NotNull McbbsModpackManifest.File newFile) {
if (newFile instanceof McbbsModpackManifest.AddonFile) {
return newFile;
} else if (newFile instanceof McbbsModpackManifest.CurseFile) {
// Preserves prefetched file names and urls.
return oldFile != null ? oldFile : newFile;
} else {
throw new IllegalArgumentException();
}
}
}

View File

@ -41,16 +41,19 @@ import java.util.stream.Stream;
public class McbbsModpackLocalInstallTask extends Task<Void> {
private final DefaultDependencyManager dependencyManager;
private final File zipFile;
private final Modpack modpack;
private final McbbsModpackManifest manifest;
private final String name;
private final boolean update;
private final DefaultGameRepository repository;
private final MinecraftInstanceTask<McbbsModpackManifest> instanceTask;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, McbbsModpackManifest manifest, String name) {
this.dependencyManager = dependencyManager;
this.zipFile = zipFile;
this.modpack = modpack;
this.manifest = manifest;
@ -87,7 +90,8 @@ public class McbbsModpackLocalInstallTask extends Task<Void> {
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/overrides", any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/overrides", manifest, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/overrides", manifest, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name));
dependents.add(instanceTask.withStage("hmcl.modpack"));
}
@Override
@ -115,6 +119,8 @@ public class McbbsModpackLocalInstallTask extends Task<Void> {
// This mcbbs modpack was installed by other launchers.
// TODO: maintain libraries.
}
dependencies.add(new McbbsModpackCompletionTask(dependencyManager, name, instanceTask.getResult()).withStage("hmcl.modpack.download"));
}
@Override

View File

@ -143,6 +143,10 @@ public class McbbsModpackManifest implements Validation {
return launchInfo;
}
public McbbsModpackManifest setFiles(List<File> files) {
return new McbbsModpackManifest(manifestType, manifestVersion, name, version, author, description, fileApi, url, forceUpdate, origins, addons, libraries, files, settings, launchInfo);
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (!MANIFEST_TYPE.equals(manifestType))
@ -230,7 +234,7 @@ public class McbbsModpackManifest implements Validation {
}
)
public static abstract class File implements Validation {
private final boolean force;
protected final boolean force;
public File(boolean force) {
this.force = force;
@ -251,8 +255,8 @@ public class McbbsModpackManifest implements Validation {
public AddonFile(boolean force, String path, String hash) {
super(force);
this.path = path;
this.hash = hash;
this.path = Objects.requireNonNull(path);
this.hash = Objects.requireNonNull(hash);
}
public String getPath() {
@ -270,6 +274,19 @@ public class McbbsModpackManifest implements Validation {
Validation.requireNonNull(path, "AddonFile.path cannot be null");
Validation.requireNonNull(hash, "AddonFile.hash cannot be null");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddonFile addonFile = (AddonFile) o;
return path.equals(addonFile.path);
}
@Override
public int hashCode() {
return Objects.hash(path);
}
}
public static final class CurseFile extends File {
@ -298,6 +315,7 @@ public class McbbsModpackManifest implements Validation {
return fileID;
}
@Nullable
public String getFileName() {
return fileName;
}
@ -307,6 +325,14 @@ public class McbbsModpackManifest implements Validation {
: NetworkUtils.toURL(NetworkUtils.encodeLocation(url));
}
public CurseFile withFileName(String fileName) {
return new CurseFile(force, projectID, fileID, fileName, url);
}
public CurseFile withURL(String url) {
return new CurseFile(force, projectID, fileID, fileName, url);
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
super.validate();

View File

@ -23,13 +23,13 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -40,9 +40,9 @@ public class McbbsModpackRemoteInstallTask extends Task<Void> {
private final DefaultGameRepository repository;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
private final ServerModpackManifest manifest;
private final McbbsModpackManifest manifest;
public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, String name) {
public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, McbbsModpackManifest manifest, String name) {
this.name = name;
this.dependency = dependencyManager;
this.repository = dependencyManager.getGameRepository();
@ -53,7 +53,7 @@ public class McbbsModpackRemoteInstallTask extends Task<Void> {
throw new IllegalArgumentException("Version " + name + " already exists.");
GameBuilder builder = dependencyManager.gameBuilder().name(name);
for (ServerModpackManifest.Addon addon : manifest.getAddons()) {
for (McbbsModpackManifest.Addon addon : manifest.getAddons()) {
builder.version(addon.getId(), addon.getVersion());
}
@ -63,14 +63,14 @@ public class McbbsModpackRemoteInstallTask extends Task<Void> {
repository.removeVersionFromDisk(name);
});
ModpackConfiguration<ServerModpackManifest> config = null;
ModpackConfiguration<McbbsModpackManifest> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<ServerModpackManifest>>() {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Server modpack. Cannot update this version.");
throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
@ -88,7 +88,7 @@ public class McbbsModpackRemoteInstallTask extends Task<Void> {
@Override
public void execute() throws Exception {
// dependencies.add(new McbbsModpackCompletionTask(dependency, name, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList())));
dependencies.add(new McbbsModpackCompletionTask(dependency, name, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList())));
}
public static final String MODPACK_TYPE = "Server";

View File

@ -99,7 +99,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
future.cancel(true);
}
private CompletableFuture<Exception> executeTasks(Task<?> parentTask, Collection<Task<?>> tasks) {
private CompletableFuture<?> executeTasksExceptionally(Task<?> parentTask, Collection<Task<?>> tasks) {
if (tasks == null || tasks.isEmpty())
return CompletableFuture.completedFuture(null);
@ -114,7 +114,11 @@ public final class AsyncTaskExecutor extends TaskExecutor {
.map(task -> CompletableFuture.completedFuture(null)
.thenComposeAsync(unused2 -> executeTask(parentTask, task))
).toArray(CompletableFuture<?>[]::new));
})
});
}
private CompletableFuture<Exception> executeTasks(Task<?> parentTask, Collection<Task<?>> tasks) {
return executeTasksExceptionally(parentTask, tasks)
.thenApplyAsync(unused -> (Exception) null)
.exceptionally(throwable -> {
Throwable resolved = resolveException(throwable);
@ -127,7 +131,79 @@ public final class AsyncTaskExecutor extends TaskExecutor {
});
}
private CompletableFuture<?> executeTask(Task<?> parentTask, Task<?> task) {
private <T> CompletableFuture<T> executeCompletableFutureTask(Task<?> parentTask, CompletableFutureTask<T> task) {
checkCancellation();
return CompletableFuture.completedFuture(null)
.thenComposeAsync(unused -> {
checkCancellation();
task.setCancelled(this::isCancelled);
task.setState(Task.TaskState.READY);
if (parentTask != null && task.getStage() == null)
task.setStage(parentTask.getStage());
if (task.getSignificance().shouldLog())
Logging.LOG.log(Level.FINE, "Executing task: " + task.getName());
taskListeners.forEach(it -> it.onReady(task));
return task.getFuture(new TaskCompletableFuture() {
@Override
public <T2> CompletableFuture<T2> one(Task<T2> subtask) {
return executeTask(task, subtask);
}
@Override
public CompletableFuture<?> all(Collection<Task<?>> tasks) {
return executeTasksExceptionally(task, tasks);
}
});
})
.thenApplyAsync(result -> {
checkCancellation();
if (task.getSignificance().shouldLog()) {
Logging.LOG.log(Level.FINER, "Task finished: " + task.getName());
}
task.setResult(result);
task.onDone().fireEvent(new TaskEvent(this, task, false));
taskListeners.forEach(it -> it.onFinished(task));
task.setState(Task.TaskState.SUCCEEDED);
return result;
})
.exceptionally(throwable -> {
Throwable resolved = resolveException(throwable);
if (resolved instanceof Exception) {
Exception e = (Exception) resolved;
if (e instanceof InterruptedException || e instanceof CancellationException) {
task.setException(null);
if (task.getSignificance().shouldLog()) {
Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName());
}
task.onDone().fireEvent(new TaskEvent(this, task, true));
taskListeners.forEach(it -> it.onFailed(task, e));
} else {
task.setException(e);
exception = e;
if (task.getSignificance().shouldLog()) {
Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e);
}
task.onDone().fireEvent(new TaskEvent(this, task, true));
taskListeners.forEach(it -> it.onFailed(task, e));
}
task.setState(Task.TaskState.FAILED);
}
throw new CompletionException(resolved); // rethrow error
});
}
private <T> CompletableFuture<T> executeNormalTask(Task<?> parentTask, Task<T> task) {
checkCancellation();
return CompletableFuture.completedFuture(null)
@ -185,7 +261,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
return CompletableFuture.completedFuture(dependenciesException);
}
})
.thenAcceptAsync(dependenciesException -> {
.thenApplyAsync(dependenciesException -> {
boolean isDependenciesSucceeded = dependenciesException == null;
if (!isDependenciesSucceeded && task.isRelyingOnDependencies()) {
@ -204,6 +280,8 @@ public final class AsyncTaskExecutor extends TaskExecutor {
taskListeners.forEach(it -> it.onFinished(task));
task.setState(Task.TaskState.SUCCEEDED);
return task.getResult();
})
.exceptionally(throwable -> {
Throwable resolved = resolveException(throwable);
@ -233,6 +311,14 @@ public final class AsyncTaskExecutor extends TaskExecutor {
});
}
private <T> CompletableFuture<T> executeTask(Task<?> parentTask, Task<T> task) {
if (task instanceof CompletableFutureTask<?>) {
return executeCompletableFutureTask(parentTask, (CompletableFutureTask<T>) task);
} else {
return executeNormalTask(parentTask, task);
}
}
private static Throwable resolveException(Throwable e) {
if (e instanceof ExecutionException || e instanceof CompletionException)
return resolveException(e.getCause());

View File

@ -0,0 +1,92 @@
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.function.ExceptionalBiConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
public abstract class CompletableFutureTask<T> extends Task<T> {
@Override
public void execute() throws Exception {
}
public abstract CompletableFuture<T> getFuture(TaskCompletableFuture executor);
protected static Runnable wrap(ExceptionalRunnable<?> runnable) {
return () -> {
try {
runnable.run();
} catch (Exception e) {
rethrow(e);
}
};
}
protected static <T, R> Function<T, R> wrap(ExceptionalFunction<T, R, ?> fn) {
return t -> {
try {
return fn.apply(t);
} catch (Exception e) {
rethrow(e);
throw new InternalError("Unreachable code");
}
};
}
protected static <T> Consumer<T> wrap(ExceptionalConsumer<T, ?> fn) {
return t -> {
try {
fn.accept(t);
} catch (Exception e) {
rethrow(e);
}
};
}
protected static <T, E> BiConsumer<T, E> wrap(ExceptionalBiConsumer<T, E, ?> fn) {
return (t, e) -> {
try {
fn.accept(t, e);
} catch (Exception ex) {
rethrow(ex);
}
};
}
protected static void rethrow(Throwable e) {
if (e == null)
return;
if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable
rethrow(e.getCause());
} else if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new CompletionException(e);
}
}
protected static Throwable resolveException(Throwable e) {
if (e instanceof ExecutionException || e instanceof CompletionException)
return resolveException(e.getCause());
else
return e;
}
public static class CustomException extends RuntimeException {}
protected static CompletableFuture<Void> breakable(CompletableFuture<?> future) {
return future.thenApplyAsync(unused1 -> (Void) null).exceptionally(throwable -> {
if (resolveException(throwable) instanceof CustomException) return null;
else throw new CompletionException(throwable);
});
}
}

View File

@ -0,0 +1,11 @@
package org.jackhuang.hmcl.task;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
public interface TaskCompletableFuture {
<T> CompletableFuture<T> one(Task<T> task);
CompletableFuture<?> all(Collection<Task<?>> tasks);
}

View File

@ -23,6 +23,7 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import java.io.File;
import java.lang.reflect.Type;
import java.util.Date;
import java.util.UUID;
@ -43,6 +44,13 @@ public final class JsonUtils {
return parsed;
}
public static <T> T fromNonNullJson(String json, Type type) throws JsonParseException {
T parsed = GSON.fromJson(json, type);
if (parsed == null)
throw new JsonParseException("Json object cannot be null.");
return parsed;
}
public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException {
try {
return GSON.fromJson(json, classOfT);