From e8316de160820c32bbaba03c6ce435ba1e4f89e2 Mon Sep 17 00:00:00 2001 From: huangyuhui Date: Sat, 27 Jan 2018 01:19:13 +0800 Subject: [PATCH] Modpack updates --- .../hmcl/game/HMCLModpackInstallTask.java | 41 ++++--- .../hmcl/game/HMCLModpackManager.java | 22 +++- .../jackhuang/hmcl/game/ModpackHelper.java | 2 +- .../java/org/jackhuang/hmcl/ui/Decorator.java | 6 + .../hmcl/ui/construct/TaskListPane.java | 3 + .../assets/css/jfoenix-main-demo.css | 8 ++ .../main/resources/assets/fxml/decorator.fxml | 2 +- .../resources/assets/lang/I18N.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../jackhuang/hmcl/mod/CurseInstallTask.java | 35 +++++- .../jackhuang/hmcl/mod/CurseManifestFile.java | 15 +++ .../hmcl/mod/MinecraftInstanceTask.java | 54 ++++++++- .../hmcl/mod/ModpackConfiguration.java | 105 +++++++++++++++++ .../hmcl/mod/ModpackInstallTask.java | 109 ++++++++++++++++++ .../hmcl/mod/MultiMCModpackInstallTask.java | 28 +++-- .../jackhuang/hmcl/task/FileDownloadTask.java | 12 +- .../java/org/jackhuang/hmcl/task/Task.java | 21 +++- .../org/jackhuang/hmcl/task/TaskExecutor.java | 15 ++- .../jackhuang/hmcl/util/CompressingUtils.java | 65 +++++------ .../org/jackhuang/hmcl/util/Constants.java | 8 ++ 20 files changed, 472 insertions(+), 81 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackConfiguration.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 9191b9727..e15c36355 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -17,41 +17,58 @@ */ package org.jackhuang.hmcl.game; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.download.DependencyManager; import org.jackhuang.hmcl.download.game.VersionJsonSaveTask; -import org.jackhuang.hmcl.mod.Modpack; +import org.jackhuang.hmcl.mod.*; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.CompressingUtils; import org.jackhuang.hmcl.util.Constants; +import org.jackhuang.hmcl.util.FileUtils; import java.io.File; +import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Objects; public final class HMCLModpackInstallTask extends Task { private final File zipFile; - private final String id; + private final String name; private final HMCLGameRepository repository; - private final DefaultDependencyManager dependency; + private final Modpack modpack; + private final File run; private final List dependencies = new LinkedList<>(); private final List dependents = new LinkedList<>(); - public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String id) { - dependency = profile.getDependency(); + public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) { + DependencyManager dependency = profile.getDependency(); repository = profile.getRepository(); this.zipFile = zipFile; - this.id = id; + this.name = name; + this.modpack = modpack; + this.run = repository.getRunDirectory(name); - if (repository.hasVersion(id)) - throw new IllegalArgumentException("Version " + id + " already exists"); + File json = new File(run, "modpack.json"); + if (repository.hasVersion(name) && !json.exists()) + throw new IllegalArgumentException("Version " + name + " already exists"); - dependents.add(dependency.gameBuilder().name(id).gameVersion(modpack.getGameVersion()).buildAsync()); + dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync()); onDone().register(event -> { - if (event.isFailed()) repository.removeVersionFromDisk(id); + if (event.isFailed()) repository.removeVersionFromDisk(name); }); + + ModpackConfiguration config = null; + try { + if (json.exists()) + config = Constants.GSON.fromJson(FileUtils.readText(json), new TypeToken>(){}.getType()); + } catch (JsonParseException | IOException ignore) { + } + dependents.add(new ModpackInstallTask<>(zipFile, run, "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), config)); } @Override @@ -69,8 +86,6 @@ public final class HMCLModpackInstallTask extends Task { String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json"); Version version = Constants.GSON.fromJson(json, Version.class).setJar(null); dependencies.add(new VersionJsonSaveTask(repository, version)); - - CompressingUtils.unzip(zipFile, repository.getRunDirectory(id), - "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), false); + dependencies.add(new MinecraftInstanceTask<>(zipFile, "minecraft/", modpack, new File(run, "modpack.json"))); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java index 4efbfccdb..07640f1a2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java @@ -33,10 +33,26 @@ import java.util.List; */ public final class HMCLModpackManager { - public static final List MODPACK_BLACK_LIST = Arrays.asList("usernamecache.json", "asm", "logs", "backups", "versions", "assets", "usercache.json", "libraries", "crash-reports", "launcher_profiles.json", "NVIDIA", "AMD", "TCNodeTracker", "screenshots", "natives", "native", "$native", "pack.json", "launcher.jar", "minetweaker.log", "launcher.pack.lzma", "hmclmc.log"); - public static final List MODPACK_SUGGESTED_BLACK_LIST = Arrays.asList("fonts", "saves", "servers.dat", "options.txt", "optionsof.txt", "journeymap", "optionsshaders.txt", "mods/VoxelMods"); + public static final List MODPACK_BLACK_LIST = Arrays.asList( + "usernamecache.json", "usercache.json", // Minecraft + "launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher + "pack.json", "launcher.jar", "hmclmc.log", // HMCL + "manifest.json", "minecraftinstance.json", ".curseclient", // Curse + "minetweaker.log", // Mods + "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft + "downloads", // Curse + "asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods + ); + public static final List MODPACK_SUGGESTED_BLACK_LIST = Arrays.asList( + "fonts", // BetterFonts + "saves", "servers.dat", "options.txt", // Minecraft + "blueprints" /* BuildCraft */, + "optionsof.txt" /* OptiFine */, + "journeymap" /* JourneyMap */, + "optionsshaders.txt", + "mods/VoxelMods"); - public static ModAdviser MODPACK_PREDICATE = (String fileName, boolean isDirectory) -> { + public static ModAdviser.ModSuggestion suggestMod(String fileName, boolean isDirectory) { if (match(MODPACK_BLACK_LIST, fileName, isDirectory)) return ModAdviser.ModSuggestion.HIDDEN; if (match(MODPACK_SUGGESTED_BLACK_LIST, fileName, isDirectory)) 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 df91ed950..dd8864856 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -61,7 +61,7 @@ public final class ModpackHelper { } if (c.isOverrideMemory()) { - vs.setPermSize(Optional.ofNullable(c.getPermGen()).map(i -> i.toString()).orElse("")); + vs.setPermSize(Optional.ofNullable(c.getPermGen()).map(Object::toString).orElse("")); if (c.getMaxMemory() != null) vs.setMaxMemory(c.getMaxMemory()); vs.setMinMemory(c.getMinMemory()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java index 55a8d686c..66763ac96 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Decorator.java @@ -405,6 +405,10 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza private Node nowPage; public void showPage(Node content) { + contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background"); + if (content != null) + contentPlaceHolder.getStyleClass().add("gray-background"); + Node c = content == null ? mainPage : content; onEnd(); if (nowPage instanceof DecoratorPage) @@ -457,6 +461,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza @Override public void navigateTo(Node page, Navigation.NavigationDirection nav) { + contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background"); + contentPlaceHolder.getStyleClass().add("white-background"); setContent(page, nav.getAnimation().getAnimationProducer()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index 70b23dee2..4db86f333 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -34,6 +34,7 @@ import org.jackhuang.hmcl.game.HMCLModpackExportTask; import org.jackhuang.hmcl.game.HMCLModpackInstallTask; import org.jackhuang.hmcl.mod.CurseCompletionTask; import org.jackhuang.hmcl.mod.CurseInstallTask; +import org.jackhuang.hmcl.mod.MinecraftInstanceTask; import org.jackhuang.hmcl.mod.MultiMCModpackInstallTask; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; @@ -87,6 +88,8 @@ public final class TaskListPane extends StackPane { task.setName(Main.i18n("modpack.install", Main.i18n("modpack.type.curse"))); } else if (task instanceof HMCLModpackExportTask) { task.setName(Main.i18n("modpack.export")); + } else if (task instanceof MinecraftInstanceTask) { + task.setName(Main.i18n("modpack.scan")); } ProgressListNode node = new ProgressListNode(task); diff --git a/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css b/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css index e153296c5..095e5ab76 100644 --- a/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css +++ b/HMCL/src/main/resources/assets/css/jfoenix-main-demo.css @@ -983,6 +983,14 @@ -fx-border-width: 1; } +.gray-background { + -fx-background-color: rgba(244, 244, 244, 0.5); +} + +.white-background { + -fx-background-color: rgba(255, 255, 255); +} + /******************************************************************************* * * * Tree Table View * diff --git a/HMCL/src/main/resources/assets/fxml/decorator.fxml b/HMCL/src/main/resources/assets/fxml/decorator.fxml index 81e5e5508..afb0b9855 100644 --- a/HMCL/src/main/resources/assets/fxml/decorator.fxml +++ b/HMCL/src/main/resources/assets/fxml/decorator.fxml @@ -23,7 +23,7 @@
- +
diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index af6023bb7..ac799eba1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -412,3 +412,4 @@ extension.mod=Mod file extension.png=Image file message.success=Tasks succeeded message.doing=Please wait +modpack.scan=Scanning this modpack 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 d42c9c5b5..1da8044e7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -412,3 +412,4 @@ extension.mod=模组文件 extension.png=图片文件 message.success=已完成 message.doing=请耐心等待 +modpack.scan=解析整合包 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java index acd541136..71a62e4b5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseInstallTask.java @@ -17,16 +17,22 @@ */ package org.jackhuang.hmcl.mod; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.CompressingUtils; +import org.jackhuang.hmcl.util.Constants; +import org.jackhuang.hmcl.util.FileUtils; import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.function.Predicate; /** * Install a downloaded CurseForge modpack. @@ -40,6 +46,8 @@ public final class CurseInstallTask extends Task { private final File zipFile; private final CurseManifest manifest; private final String name; + private final File run; + private final ModpackConfiguration config; private final List dependents = new LinkedList<>(); private final List dependencies = new LinkedList<>(); @@ -57,10 +65,11 @@ public final class CurseInstallTask extends Task { this.zipFile = zipFile; this.manifest = manifest; this.name = name; + this.repository = dependencyManager.getGameRepository(); + this.run = repository.getRunDirectory(name); - repository = dependencyManager.getGameRepository(); - - if (repository.hasVersion(name)) + File json = new File(run, "modpack.json"); + if (repository.hasVersion(name) && !json.exists()) throw new IllegalArgumentException("Version " + name + " already exists."); GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.getMinecraft().getGameVersion()); @@ -68,6 +77,15 @@ public final class CurseInstallTask extends Task { if (modLoader.getId().startsWith("forge-")) builder.version("forge", modLoader.getId().substring("forge-".length())); dependents.add(builder.buildAsync()); + + ModpackConfiguration config = null; + try { + if (json.exists()) + config = Constants.GSON.fromJson(FileUtils.readText(json), new TypeToken>(){}.getType()); + } catch (JsonParseException | IOException ignore) { + } + this.config = config; + dependents.add(new ModpackInstallTask<>(zipFile, run, manifest.getOverrides(), Constants.truePredicate(), config)); } @Override @@ -82,10 +100,17 @@ public final class CurseInstallTask extends Task { @Override public void execute() throws Exception { - File run = repository.getRunDirectory(name); - CompressingUtils.unzip(zipFile, run, manifest.getOverrides()); + if (config != null) + for (CurseManifestFile oldCurseManifestFile : config.getManifest().getFiles()) { + if (oldCurseManifestFile.getFileName() == null) continue; + File oldFile = new File(run, "mods/" + oldCurseManifestFile.getFileName()); + if (!oldFile.exists()) continue; + if (manifest.getFiles().stream().noneMatch(oldCurseManifestFile::equals)) + oldFile.delete(); + } dependencies.add(new CurseCompletionTask(dependencyManager, name)); + dependencies.add(new MinecraftInstanceTask<>(zipFile, manifest.getOverrides(), false, manifest, new File(run, "modpack.json"))); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifestFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifestFile.java index 4b009f212..5c5ed4b2d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifestFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/CurseManifestFile.java @@ -24,6 +24,7 @@ import org.jackhuang.hmcl.util.NetworkUtils; import org.jackhuang.hmcl.util.Validation; import java.net.URL; +import java.util.Objects; /** * @@ -84,4 +85,18 @@ public final class CurseManifestFile implements Validation { public CurseManifestFile setFileName(String fileName) { return new CurseManifestFile(projectID, fileID, fileName, required); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CurseManifestFile that = (CurseManifestFile) o; + return projectID == that.projectID && + fileID == that.fileID; + } + + @Override + public int hashCode() { + return Objects.hash(projectID, fileID); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java index db595621c..7a4b72cfa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java @@ -17,5 +17,57 @@ */ package org.jackhuang.hmcl.mod; -public class MinecraftInstanceTask { +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.Constants; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.FileUtils; +import org.jackhuang.hmcl.util.IOUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.util.HashMap; +import java.util.Map; + +public final class MinecraftInstanceTask extends Task { + + private final File zipFile; + private final String subDirectory; + private final File jsonFile; + private final T manifest; + + public MinecraftInstanceTask(File zipFile, String subDirectory, T manifest, File jsonFile) { + this.zipFile = zipFile; + this.subDirectory = subDirectory; + this.manifest = manifest; + this.jsonFile = jsonFile; + + if (!zipFile.exists()) + throw new IllegalArgumentException("File " + zipFile + " does not exist. Cannot parse this modpack."); + } + + @Override + public void execute() throws Exception { + Map overrides = new HashMap<>(); + + byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; + try (ZipArchiveInputStream zip = new ZipArchiveInputStream(new FileInputStream(zipFile), null, true, true)) { + ArchiveEntry entry; + while ((entry = zip.getNextEntry()) != null) { + String path = entry.getName(); + if (!path.startsWith(subDirectory) || entry.isDirectory()) + continue; + path = path.substring(subDirectory.length()); + if (path.startsWith("/") || path.startsWith("\\")) + path = path.substring(1); + + overrides.put(path, new ModpackConfiguration.FileInformation( + path, DigestUtils.sha1Hex(zip) + )); + } + } + + FileUtils.writeText(jsonFile, Constants.GSON.toJson(new ModpackConfiguration<>(manifest, overrides))); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackConfiguration.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackConfiguration.java new file mode 100644 index 000000000..520d0f00a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackConfiguration.java @@ -0,0 +1,105 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.mod; + +import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.Validation; + +import java.util.Collections; +import java.util.Map; + +@Immutable +public final class ModpackConfiguration implements Validation { + + private final T manifest; + + private final Map overrides; + + public ModpackConfiguration() { + this(null, Collections.emptyMap()); + } + + public ModpackConfiguration(T manifest, Map overrides) { + this.manifest = manifest; + this.overrides = overrides; + } + + public T getManifest() { + return manifest; + } + + public ModpackConfiguration setManifest(T manifest) { + return new ModpackConfiguration<>(manifest, overrides); + } + + public ModpackConfiguration setOverrides(Map overrides) { + return new ModpackConfiguration<>(manifest, overrides); + } + + public Map getOverrides() { + return Collections.unmodifiableMap(overrides); + } + + @Override + public void validate() throws JsonParseException { + if (manifest == null) + throw new JsonParseException("MinecraftInstanceConfiguration missing `manifest`"); + } + + @Immutable + public static class FileInformation implements Validation { + private final String location; // relative + private final String hash; + private final String downloadURL; + + public FileInformation() { + this(null, null); + } + + public FileInformation(String location, String hash) { + this(location, hash, null); + } + + public FileInformation(String location, String hash, String downloadURL) { + this.location = location; + this.hash = hash; + this.downloadURL = downloadURL; + } + + public String getLocation() { + return location; + } + + public String getDownloadURL() { + return downloadURL; + } + + public String getHash() { + return hash; + } + + @Override + public void validate() throws JsonParseException { + if (location == null) + throw new JsonParseException("FileInformation missing `location`."); + if (hash == null) + throw new JsonParseException("FileInformation missing file hash code."); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java new file mode 100644 index 000000000..6e515811d --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java @@ -0,0 +1,109 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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 {http://www.gnu.org/licenses/}. + */ +package org.jackhuang.hmcl.mod; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.FileUtils; +import org.jackhuang.hmcl.util.IOUtils; + +import java.io.*; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +public class ModpackInstallTask extends Task { + + private final File modpackFile; + private final File dest; + private final String subDirectory; + private final Map overrides; + private final Predicate callback; + + public ModpackInstallTask(File modpackFile, File dest, String subDirectory, Predicate callback, ModpackConfiguration oldConfiguration) { + this.modpackFile = modpackFile; + this.dest = dest; + this.subDirectory = subDirectory; + this.callback = callback; + + if (oldConfiguration == null) + overrides = Collections.emptyMap(); + else + overrides = oldConfiguration.getOverrides(); + } + + @Override + public void execute() throws Exception { + Set entries = new HashSet<>(); + byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; + if (!FileUtils.makeDirectory(dest)) + throw new IOException("Unable to make directory " + dest); + try (ZipArchiveInputStream zipStream = new ZipArchiveInputStream(new FileInputStream(modpackFile), null, true, true)) { + ArchiveEntry entry; + while ((entry = zipStream.getNextEntry()) != null) { + String path = entry.getName(); + + if (!path.startsWith(subDirectory)) + continue; + path = path.substring(subDirectory.length()); + if (path.startsWith("/") || path.startsWith("\\")) + path = path.substring(1); + File entryFile = new File(dest, path); + + if (callback != null) + if (!callback.test(path)) + continue; + + if (entry.isDirectory()) { + if (!FileUtils.makeDirectory(entryFile)) + throw new IOException("Unable to make directory: " + entryFile); + } else { + if (!FileUtils.makeDirectory(entryFile.getAbsoluteFile().getParentFile())) + throw new IOException("Unable to make parent directory for file " + entryFile); + + entries.add(path); + + ByteArrayOutputStream os = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE); + IOUtils.copyTo(zipStream, os, buf); + byte[] data = os.toByteArray(); + + if (!overrides.containsKey(path) || entryFile.exists()) { + String oldHash = DigestUtils.sha1Hex(new FileInputStream(entryFile)); + String newHash = DigestUtils.sha1Hex(new ByteArrayInputStream(data)); + if (!oldHash.equals(newHash)) { + try (FileOutputStream fos = new FileOutputStream(entryFile)) { + IOUtils.copyTo(new ByteArrayInputStream(data), fos, buf); + } + } + } + + } + } + } + + for (String path : overrides.keySet()) { + File original = new File(dest, path); + if (original.exists() && !entries.contains(path)) + original.delete(); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java index 9d2d684d2..a7b3c827f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MultiMCModpackInstallTask.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.mod; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; import org.jackhuang.hmcl.download.DefaultDependencyManager; @@ -25,12 +27,10 @@ import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.CompressingUtils; -import org.jackhuang.hmcl.util.Constants; -import org.jackhuang.hmcl.util.IOUtils; -import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.*; import java.io.File; +import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -57,14 +57,24 @@ public final class MultiMCModpackInstallTask extends Task { this.name = name; this.repository = dependencyManager.getGameRepository(); this.run = repository.getRunDirectory(name); - - if (repository.hasVersion(name)) + + File json = new File(run, "modpack.json"); + if (repository.hasVersion(name) && !json.exists()) throw new IllegalArgumentException("Version " + name + " already exists."); dependents.add(dependencyManager.gameBuilder().name(name).gameVersion(manifest.getGameVersion()).buildAsync()); onDone().register(event -> { if (event.isFailed()) repository.removeVersionFromDisk(name); }); + + ModpackConfiguration config = null; + try { + if (json.exists()) + config = Constants.GSON.fromJson(FileUtils.readText(json), new TypeToken>(){}.getType()); + } catch (JsonParseException | IOException ignore) { + } + + dependents.add(new ModpackInstallTask<>(zipFile, run, manifest.getName() + "/minecraft/", Constants.truePredicate(), config)); } @Override @@ -80,8 +90,7 @@ public final class MultiMCModpackInstallTask extends Task { @Override public void execute() throws Exception { Version version = Objects.requireNonNull(repository.readVersionJson(name)); - CompressingUtils.unzip(zipFile, run, manifest.getName() + "/minecraft/", null, false, true); - + try (ZipFile zip = new ZipFile(zipFile)) { for (ZipArchiveEntry entry : Lang.asIterable(zip.getEntries())) { // ensure that this entry is in folder 'patches' and is a json file. @@ -99,8 +108,9 @@ public final class MultiMCModpackInstallTask extends Task { } } } - + dependencies.add(new VersionJsonSaveTask(repository, version)); + dependencies.add(new MinecraftInstanceTask<>(zipFile, manifest.getName() + "/minecraft/", manifest, new File(run, "modpack.json"))); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java index a4539bb54..d7517170a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/FileDownloadTask.java @@ -107,6 +107,10 @@ public class FileDownloadTask extends Task { return url; } + public File getFile() { + return file; + } + @Override public void execute() throws Exception { URL currentURL = url; @@ -189,12 +193,12 @@ public class FileDownloadTask extends Task { Thread.currentThread().interrupt(); break; } else { - if (file.exists()) - file.delete(); + if (file.exists() || !file.delete()) + throw new IOException("Unable to delete existent file " + file); if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile())) - throw new IOException("Cannot make parent directory " + file); + throw new IOException("Unable to make parent directory " + file); if (!temp.renameTo(file)) - throw new IOException("Cannot move temp file to " + file); + throw new IOException("Unable move temp file to " + file); } if (downloaded != contentLength) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index a95d850ff..90be54e6a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -52,6 +52,16 @@ public abstract class Task { this.significance = significance; } + private TaskState state = TaskState.READY; + + public TaskState getState() { + return state; + } + + void setState(TaskState state) { + this.state = state; + } + /** * The scheduler that decides how this task runs. */ @@ -111,14 +121,14 @@ public abstract class Task { /** * The collection of sub-tasks that should execute **before** this task running. */ - public Collection getDependents() { + public Collection getDependents() { return Collections.emptySet(); } /** * The collection of sub-tasks that should execute **after** this task running. */ - public Collection getDependencies() { + public Collection getDependencies() { return Collections.emptySet(); } @@ -298,4 +308,11 @@ public abstract class Task { return this == MAJOR; } } + + public enum TaskState { + READY, + RUNNING, + SUCCEEDED, + FAILED + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java index 590b0ad09..f549ec447 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java @@ -108,7 +108,7 @@ public final class TaskExecutor { } } - private boolean executeTasks(Collection tasks) throws InterruptedException { + private boolean executeTasks(Collection tasks) throws InterruptedException { if (tasks.isEmpty()) return true; @@ -140,8 +140,12 @@ public final class TaskExecutor { } private boolean executeTask(Task task) { - if (canceled) + if (canceled) { + task.setState(Task.TaskState.FAILED); return false; + } + + task.setState(Task.TaskState.RUNNING); if (task.getSignificance().shouldLog()) Logging.LOG.log(Level.FINE, "Executing task: {0}", task.getName()); @@ -165,7 +169,7 @@ public final class TaskExecutor { if (!executeTasks(task.getDependencies()) && task.isRelyingOnDependencies()) { Logging.LOG.severe("Subtasks failed for " + task.getName()); - return false; + throw new SilentException(); } flag = true; @@ -182,10 +186,8 @@ public final class TaskExecutor { task.onDone().fireEvent(new TaskEvent(this, task, true)); taskListeners.forEach(it -> it.onFailed(task, e)); } - } catch (SilentException e) { + } catch (SilentException | RejectedExecutionException e) { // do nothing - } catch (RejectedExecutionException e) { - return false; } catch (Exception e) { lastException = e; Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e); @@ -194,6 +196,7 @@ public final class TaskExecutor { } finally { task.setVariables(null); } + task.setState(flag ? Task.TaskState.SUCCEEDED : Task.TaskState.FAILED); return flag; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java index 8b7290378..1f0598852 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/CompressingUtils.java @@ -43,7 +43,7 @@ public final class CompressingUtils { * @param sourceDir the source directory or a file. * @param zipFile the location of dest zip file. * @param pathNameCallback callback(pathName, isDirectory) returns your modified pathName - * @throws IOException + * @throws IOException if there is filesystem error. */ public static void zip(File sourceDir, File zipFile, BiFunction pathNameCallback) throws IOException { try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(new FileOutputStream(zipFile))) { @@ -71,6 +71,8 @@ public final class CompressingUtils { File[] files = src.isDirectory() ? src.listFiles() : new File[] { src }; String pathName;// the relative path (relative to the root directory to be compressed) byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; + + if (files == null) return; for (File file : files) if (file.isDirectory()) { pathName = file.getPath().substring(basePath.length() + 1) + "/"; @@ -152,48 +154,39 @@ public final class CompressingUtils { * @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed. * @param ignoreExistentFile true if skip all existent files. * @param allowStoredEntriesWithDataDescriptor whether the zip stream will try to read STORED entries that use a data descriptor - * @throws IOException + * @throws IOException if zip file is malformed or filesystem error. */ public static void unzip(File src, File dest, String subDirectory, Predicate callback, boolean ignoreExistentFile, boolean allowStoredEntriesWithDataDescriptor) throws IOException { byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; - dest.mkdirs(); - try (ZipArchiveInputStream zipFile = new ZipArchiveInputStream(new FileInputStream(src), null, true, allowStoredEntriesWithDataDescriptor)) { - if (src.exists()) { - String strPath, gbkPath, strtemp; - strPath = dest.getAbsolutePath(); - ArchiveEntry zipEnt; - while ((zipEnt = zipFile.getNextEntry()) != null) { - gbkPath = zipEnt.getName(); + if (!FileUtils.makeDirectory(dest)) + throw new IOException("Unable to make directory " + dest); + try (ZipArchiveInputStream zipStream = new ZipArchiveInputStream(new FileInputStream(src), null, true, allowStoredEntriesWithDataDescriptor)) { + ArchiveEntry entry; + while ((entry = zipStream.getNextEntry()) != null) { + String path = entry.getName(); - if (!gbkPath.startsWith(subDirectory)) + if (!path.startsWith(subDirectory)) + continue; + path = path.substring(subDirectory.length()); + if (path.startsWith("/") || path.startsWith("\\")) + path = path.substring(1); + File entryFile = new File(dest, path); + + if (callback != null) + if (!callback.test(path)) continue; - gbkPath = gbkPath.substring(subDirectory.length()); - if (gbkPath.startsWith("/") || gbkPath.startsWith("\\")) - gbkPath = gbkPath.substring(1); - strtemp = strPath + File.separator + gbkPath; - if (callback != null) - if (!callback.test(gbkPath)) - continue; + if (entry.isDirectory()) { + if (!FileUtils.makeDirectory(entryFile)) + throw new IOException("Unable to make directory: " + entryFile); + } else { + if (!FileUtils.makeDirectory(entryFile.getAbsoluteFile().getParentFile())) + throw new IOException("Unable to make parent directory for file " + entryFile); - if (zipEnt.isDirectory()) { - File dir = new File(strtemp); - dir.mkdirs(); - } else { - // create directories - String strsubdir = gbkPath; - for (int i = 0; i < strsubdir.length(); i++) - if (strsubdir.substring(i, i + 1).equalsIgnoreCase("/")) { - String temp = strPath + File.separator + strsubdir.substring(0, i); - File subdir = new File(temp); - if (!subdir.exists()) - subdir.mkdir(); - } - if (ignoreExistentFile && new File(strtemp).exists()) - continue; - try (FileOutputStream fos = new FileOutputStream(new File(strtemp))) { - IOUtils.copyTo(zipFile, fos, buf); - } + if (ignoreExistentFile && entryFile.exists()) + continue; + try (FileOutputStream fos = new FileOutputStream(entryFile)) { + IOUtils.copyTo(zipStream, fos, buf); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Constants.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Constants.java index 804ba0d09..08ac96a2e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Constants.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Constants.java @@ -32,6 +32,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID; import java.util.function.Consumer; +import java.util.function.Predicate; /** * Constants. @@ -86,4 +87,11 @@ public final class Constants { .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE) .create(); + public static Predicate truePredicate() { + return s -> true; + } + + public static Predicate falsePredicate() { + return s -> false; + } }