From 6c3178a83102e7e088c163868a69330291a8026d Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Wed, 6 Oct 2021 02:55:19 +0800 Subject: [PATCH] feat(mod): mod update. --- .../hmcl/ui/versions/ModListPage.java | 13 +- .../hmcl/ui/versions/ModListPageSkin.java | 31 ++- .../hmcl/ui/versions/ModUpdatesPage.java | 60 ++++-- .../resources/assets/lang/I18N.properties | 3 + .../resources/assets/lang/I18N_zh.properties | 3 + .../assets/lang/I18N_zh_CN.properties | 3 + .../jackhuang/hmcl/mod/FabricModMetadata.java | 20 +- .../hmcl/mod/ForgeNewModMetadata.java | 9 +- .../hmcl/mod/ForgeOldModMetadata.java | 10 +- .../jackhuang/hmcl/mod/LiteModMetadata.java | 9 +- .../java/org/jackhuang/hmcl/mod/LocalMod.java | 63 ++++++ .../org/jackhuang/hmcl/mod/LocalModFile.java | 67 ++++-- .../org/jackhuang/hmcl/mod/ModManager.java | 196 +++++++++++++----- .../org/jackhuang/hmcl/mod/PackMcMeta.java | 13 +- .../jackhuang/hmcl/mod/curse/CurseAddon.java | 2 +- .../org/jackhuang/hmcl/util/io/FileUtils.java | 4 + 16 files changed, 376 insertions(+), 130 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index a9ca7a57b..c41fc7492 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -60,7 +60,7 @@ public final class ModListPage extends ListPageBase Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> { mods.forEach(it -> { try { - modManager.addMod(it); + modManager.addMod(it.toPath()); } catch (IOException | IllegalArgumentException e) { Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e); } @@ -125,7 +125,7 @@ public final class ModListPage extends ListPageBase { for (File file : res) { try { - modManager.addMod(file); + modManager.addMod(file.toPath()); succeeded.add(file.getName()); } catch (Exception e) { Logging.LOG.log(Level.WARNING, "Unable to add mod " + file, e); @@ -196,6 +196,15 @@ public final class ModListPage extends ListPageBase { } } - static class ModInfoListCell extends MDListCell { + private static Lazy menu = new Lazy<>(PopupMenu::new); + private static Lazy popup = new Lazy<>(() -> new JFXPopup(menu.get())); + + class ModInfoListCell extends MDListCell { JFXCheckBox checkBox = new JFXCheckBox(); TwoLineListItem content = new TwoLineListItem(); + JFXButton restoreButton = new JFXButton(); JFXButton infoButton = new JFXButton(); JFXButton revealButton = new JFXButton(); BooleanProperty booleanProperty; @@ -276,13 +279,18 @@ class ModListPageSkin extends SkinBase { content.setMouseTransparent(true); setSelectable(); + restoreButton.getStyleClass().add("toggle-icon4"); + restoreButton.setGraphic(FXUtils.limitingSize(SVG.restore(Theme.blackFillBinding(), 24, 24), 24, 24)); + + FXUtils.installFastTooltip(restoreButton, i18n("mods.restore")); + revealButton.getStyleClass().add("toggle-icon4"); revealButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); infoButton.getStyleClass().add("toggle-icon4"); infoButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); - container.getChildren().setAll(checkBox, content, revealButton, infoButton); + container.getChildren().setAll(checkBox, content, restoreButton, revealButton, infoButton); StackPane.setMargin(container, new Insets(8)); getContainer().getChildren().setAll(container); @@ -302,6 +310,17 @@ class ModListPageSkin extends SkinBase { checkBox.selectedProperty().unbindBidirectional(booleanProperty); } checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active); + restoreButton.setVisible(!dataItem.getModInfo().getMod().getOldFiles().isEmpty()); + restoreButton.setOnMouseClicked(e -> { + menu.get().getContent().setAll(dataItem.getModInfo().getMod().getOldFiles().stream() + .map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(), () -> { + getSkinnable().rollback(dataItem.getModInfo(), localModFile); + })) + .collect(Collectors.toList()) + ); + + popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight()); + }); revealButton.setOnMouseClicked(e -> { FXUtils.showFileInExplorer(dataItem.getModInfo().getFile()); }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index b6b8ca8e0..b00de0ee1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -32,15 +32,16 @@ import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.curse.CurseAddon; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; +import org.jackhuang.hmcl.task.FileDownloadTask; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell; -import org.jackhuang.hmcl.ui.construct.MDListCell; -import org.jackhuang.hmcl.ui.construct.PageCloseEvent; -import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.Pair; +import java.net.URL; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; @@ -120,13 +121,26 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage { } private void updateMods() { + ModUpdateTask task = new ModUpdateTask( + modManager, + objects.stream() + .filter(o -> o.enabled.get()) + .map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0))) + .collect(Collectors.toList())); Controllers.taskDialog( - new ModUpdateTask( - modManager, - objects.stream() - .filter(o -> o.enabled.get()) - .map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0))) - .collect(Collectors.toList())), + task.whenComplete(Schedulers.javafx(), exception -> { + fireEvent(new PageCloseEvent()); + if (!task.getFailedMods().isEmpty()) { + Controllers.dialog(i18n("mods.check_updates.failed") + "\n" + + task.getFailedMods().stream().map(LocalModFile::getFileName).collect(Collectors.joining("\n")), + i18n("install.failed"), + MessageDialogPane.MessageType.ERROR); + } + + if (exception == null) { + Controllers.dialog(i18n("install.success")); + } + }), i18n("mods.check_updates.update"), t -> { }); @@ -249,6 +263,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage { public static class ModUpdateTask extends Task { private final Collection> dependents; + private final List failedMods = new ArrayList<>(); ModUpdateTask(ModManager modManager, List> mods) { setStage("mods.check_updates.update"); @@ -257,16 +272,33 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage { dependents = mods.stream() .map(mod -> { return Task - .supplyAsync(() -> { - return null; + .runAsync(Schedulers.javafx(), () -> { + mod.getKey().setOld(true); + }) + .thenComposeAsync(() -> { + FileDownloadTask task = new FileDownloadTask( + new URL(mod.getValue().getFile().getUrl()), + modManager.getModsDirectory().resolve(mod.getValue().getFile().getFilename()).toFile()); + + task.setName(mod.getValue().getName()); + return task; + }) + .whenComplete(Schedulers.javafx(), exception -> { + if (exception != null) { + // restore state if failed + mod.getKey().setOld(false); + failedMods.add(mod.getKey()); + } }) - .setName(mod.getKey().getName()) - .setSignificance(TaskSignificance.MAJOR) .withCounter("mods.check_updates.update"); }) .collect(Collectors.toList()); } + public List getFailedMods() { + return failedMods; + } + @Override public Collection> getDependents() { return dependents; diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index f9fb34785..66f6f85a6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -486,6 +486,7 @@ message.default=Default message.doing=Please wait message.downloading=Downloading... message.error=Error +message.failed=Operation failed message.info=Info message.success=Job completed successfully message.unknown=Unknown @@ -583,6 +584,7 @@ mods.add.success=Successfully installed mods %s. mods.category=Category mods.check_updates=Check updates mods.check_updates.current_version=Current +mods.check_updates.failed=Failed to download some of files mods.check_updates.file=File mods.check_updates.source=Source mods.check_updates.target_version=Target @@ -602,6 +604,7 @@ mods.mcmod.search=Search in MCMOD mods.modrinth=Modrinth mods.name=Name mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader) +mods.restore=Restore mods.url=Official Page multiplayer=Multiplayer diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index a1f7cadc8..41d605f99 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -486,6 +486,7 @@ message.default=預設 message.doing=請耐心等待 message.downloading=正在下載… message.error=錯誤 +message.failed=操作失敗 message.info=資訊 message.success=完成 message.unknown=未知 @@ -583,6 +584,7 @@ mods.add.success=成功新增模組 %s。 mods.category=類別 mods.check_updates=檢查模組更新 mods.check_updates.current_version=當前版本 +mods.check_updates.failed=部分文件下載失敗 mods.check_updates.file=文件 mods.check_updates.source=來源 mods.check_updates.target_version=目標版本 @@ -602,6 +604,7 @@ mods.mcmod.search=MC 百科蒐索 mods.modrinth=Modrinth mods.name=名稱 mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。 +mods.restore=回退 mods.url=官方頁面 multiplayer=多人聯機 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 1d65b7c50..a8d9f72f8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -486,6 +486,7 @@ message.default=默认 message.doing=请耐心等待 message.downloading=正在下载 message.error=错误 +message.failed=操作失败 message.info=提示 message.success=已完成 message.unknown=未知 @@ -583,6 +584,7 @@ mods.add.success=成功添加模组 %s。 mods.category=类别 mods.check_updates=检查模组更新 mods.check_updates.current_version=当前版本 +mods.check_updates.failed=部分文件下载失败 mods.check_updates.file=文件 mods.check_updates.source=来源 mods.check_updates.target_version=目标版本 @@ -602,6 +604,7 @@ mods.mcmod.search=MC 百科搜索 mods.modrinth=Modrinth mods.name=名称 mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。 +mods.restore=回退 mods.url=官方页面 multiplayer=多人联机 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/FabricModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/FabricModMetadata.java index f28ab1c6f..b53c24c2f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/FabricModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/FabricModMetadata.java @@ -17,27 +17,21 @@ */ package org.jackhuang.hmcl.mod; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; +import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; import org.jackhuang.hmcl.util.Immutable; 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.IOException; import java.lang.reflect.Type; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Immutable @@ -64,14 +58,14 @@ public final class FabricModMetadata { this.contact = contact; } - public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) { + public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { Path mcmod = fs.getPath("fabric.mod.json"); if (Files.notExists(mcmod)) throw new IOException("File " + modFile + " is not a Fabric mod."); FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class); String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", ")); - return new LocalModFile(modFile, ModLoaderType.FABRIC, metadata.id, metadata.name, new LocalModFile.Description(metadata.description), + return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalModFile.Description(metadata.description), authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeNewModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeNewModMetadata.java index 1245d3af8..23ebfc2ab 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeNewModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeNewModMetadata.java @@ -6,7 +6,6 @@ import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import java.io.File; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -116,8 +115,8 @@ public final class ForgeNewModMetadata { } } - public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) { + public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { Path modstoml = fs.getPath("META-INF/mods.toml"); if (Files.notExists(modstoml)) throw new IOException("File " + modFile + " is not a Forge 1.13+ mod."); @@ -132,10 +131,10 @@ public final class ForgeNewModMetadata { Manifest manifest = new Manifest(Files.newInputStream(manifestMF)); jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); } catch (IOException e) { - LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile.getPath()); + LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile); } } - return new LocalModFile(modFile, ModLoaderType.FORGE, mod.getModId(), mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()), + return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()), mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "", mod.getDisplayURL(), metadata.getLogoFile()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeOldModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeOldModMetadata.java index 81680926a..e22b9c464 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeOldModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ForgeOldModMetadata.java @@ -20,12 +20,12 @@ package org.jackhuang.hmcl.mod; import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; -import org.jackhuang.hmcl.util.*; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import java.io.File; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -120,8 +120,8 @@ public final class ForgeOldModMetadata { return authors; } - public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) { + public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { Path mcmod = fs.getPath("mcmod.info"); if (Files.notExists(mcmod)) throw new IOException("File " + modFile + " is not a Forge mod."); @@ -138,7 +138,7 @@ public final class ForgeOldModMetadata { authors = String.join(", ", metadata.getAuthorList()); if (StringUtils.isBlank(authors)) authors = metadata.getCredits(); - return new LocalModFile(modFile, ModLoaderType.FORGE, metadata.getModId(), metadata.getName(), new LocalModFile.Description(metadata.getDescription()), + return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), authors, metadata.getVersion(), metadata.getGameVersion(), StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url, metadata.getLogoFile()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java index 98289ff11..30a129bbd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LiteModMetadata.java @@ -18,13 +18,12 @@ package org.jackhuang.hmcl.mod; import com.google.gson.JsonParseException; - import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.IOUtils; -import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -108,15 +107,15 @@ public final class LiteModMetadata { return updateURI; } - public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException { - try (ZipFile zipFile = new ZipFile(modFile)) { + public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { + try (ZipFile zipFile = new ZipFile(modFile.toFile())) { ZipEntry entry = zipFile.getEntry("litemod.json"); if (entry == null) throw new IOException("File " + modFile + "is not a LiteLoader mod."); LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class); if (metadata == null) throw new IOException("Mod " + modFile + " `litemod.json` is malformed."); - return new LocalModFile(modFile, ModLoaderType.FORGE, null, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(), + return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(), metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), ""); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java new file mode 100644 index 000000000..98233fa9f --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java @@ -0,0 +1,63 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod; + +import java.util.HashSet; +import java.util.Objects; + +public class LocalMod { + + private final String id; + private final ModLoaderType modLoaderType; + private final HashSet files = new HashSet<>(); + private final HashSet oldFiles = new HashSet<>(); + + public LocalMod(String id, ModLoaderType modLoaderType) { + this.id = id; + this.modLoaderType = modLoaderType; + } + + public String getId() { + return id; + } + + public ModLoaderType getModLoaderType() { + return modLoaderType; + } + + public HashSet getFiles() { + return files; + } + + public HashSet getOldFiles() { + return oldFiles; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LocalMod localMod = (LocalMod) o; + return Objects.equals(id, localMod.id) && modLoaderType == localMod.modLoaderType; + } + + @Override + public int hashCode() { + return Objects.hash(id, modLoaderType); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java index 72dbaea10..b39d3757b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalModFile.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,10 +20,8 @@ package org.jackhuang.hmcl.mod; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import org.jackhuang.hmcl.util.Logging; -import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.*; @@ -37,8 +35,8 @@ import java.util.stream.Collectors; public final class LocalModFile implements Comparable { private Path file; - private final ModLoaderType modLoaderType; - private final String id; + private final ModManager modManager; + private final LocalMod mod; private final String name; private final Description description; private final String authors; @@ -49,14 +47,14 @@ public final class LocalModFile implements Comparable { private final String logoPath; private final BooleanProperty activeProperty; - public LocalModFile(File file, ModLoaderType modLoaderType, String id, String name, Description description) { - this(file, modLoaderType, id, name, description, "", "", "", "", ""); + public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description) { + this(modManager, mod, file, name, description, "", "", "", "", ""); } - public LocalModFile(File file, ModLoaderType modLoaderType, String id, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath) { - this.file = file.toPath(); - this.modLoaderType = modLoaderType; - this.id = id; + public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath) { + this.modManager = modManager; + this.mod = mod; + this.file = file; this.name = name; this.description = description; this.authors = authors; @@ -65,23 +63,39 @@ public final class LocalModFile implements Comparable { this.url = url; this.logoPath = logoPath; - activeProperty = new SimpleBooleanProperty(this, "active", !ModManager.isDisabled(file)) { + activeProperty = new SimpleBooleanProperty(this, "active", !modManager.isDisabled(file)) { @Override protected void invalidated() { + if (isOld()) return; + Path path = LocalModFile.this.file.toAbsolutePath(); try { if (get()) - LocalModFile.this.file = ModManager.enableMod(path); + LocalModFile.this.file = modManager.enableMod(path); else - LocalModFile.this.file = ModManager.disableMod(path); + LocalModFile.this.file = modManager.disableMod(path); } catch (IOException e) { Logging.LOG.log(Level.SEVERE, "Unable to invert state of mod file " + path, e); } } }; - fileName = StringUtils.substringBeforeLast(isActive() ? file.getName() : FileUtils.getNameWithoutExtension(file), '.'); + fileName = FileUtils.getNameWithoutExtension(ModManager.getModName(file)); + + if (isOld()) { + mod.getOldFiles().add(this); + } else { + mod.getFiles().add(this); + } + } + + public ModManager getModManager() { + return modManager; + } + + public LocalMod getMod() { + return mod; } public Path getFile() { @@ -89,11 +103,11 @@ public final class LocalModFile implements Comparable { } public ModLoaderType getModLoaderType() { - return modLoaderType; + return mod.getModLoaderType(); } public String getId() { - return id; + return mod.getId(); } public String getName() { @@ -140,12 +154,29 @@ public final class LocalModFile implements Comparable { return fileName; } + public boolean isOld() { + return modManager.isOld(file); + } + + public void setOld(boolean old) throws IOException { + file = modManager.setOld(this, old); + + if (old) { + mod.getFiles().remove(this); + mod.getOldFiles().add(this); + } else { + mod.getOldFiles().remove(this); + mod.getFiles().add(this); + } + } + public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException { Optional currentVersion = repository.getRemoteVersionByLocalFile(this, file); if (!currentVersion.isPresent()) return null; List remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid()) .filter(version -> version.getGameVersions().contains(gameVersion)) - .filter(version -> version.getLoaders().contains(modLoaderType)) + .filter(version -> version.getLoaders().contains(getModLoaderType())) + .filter(version -> version.getDatePublished().compareTo(currentVersion.get().getDatePublished()) > 0) .sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed()) .collect(Collectors.toList()); if (remoteVersions.isEmpty()) return null; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index a468e2ef5..2699501d6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -23,16 +23,18 @@ import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.versioning.VersionNumber; -import java.io.File; import java.io.IOException; import java.nio.file.*; import java.util.Collection; +import java.util.HashMap; +import java.util.Objects; import java.util.TreeSet; public final class ModManager { private final GameRepository repository; private final String id; private final TreeSet localModFiles = new TreeSet<>(); + private final HashMap localMods = new HashMap<>(); private boolean loaded = false; @@ -49,60 +51,68 @@ public final class ModManager { return id; } - private Path getModsDirectory() { + public Path getModsDirectory() { return repository.getRunDirectory(id).toPath().resolve("mods"); } - private void addModInfo(File file) { + public LocalMod getLocalMod(String id, ModLoaderType modLoaderType) { + return localMods.computeIfAbsent(new LocalMod(id, modLoaderType), x -> x); + } + + private void addModInfo(Path file) { try { - localModFiles.add(getModInfo(file)); + LocalModFile localModFile = getModInfo(file); + if (!localModFile.isOld()) { + localModFiles.add(localModFile); + } } catch (IllegalArgumentException ignore) { } } - public static LocalModFile getModInfo(File modFile) { - File file = isDisabled(modFile) ? new File(modFile.getAbsoluteFile().getParentFile(), FileUtils.getNameWithoutExtension(modFile)) : modFile; - String description, extension = FileUtils.getExtension(file); - switch (extension) { - case "zip": - case "jar": - try { - return ForgeOldModMetadata.fromFile(modFile); - } catch (Exception ignore) { - } + public LocalModFile getModInfo(Path modFile) { + String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION); + String description; + if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) { + try { + return ForgeOldModMetadata.fromFile(this, modFile); + } catch (Exception ignore) { + } - try { - return ForgeNewModMetadata.fromFile(modFile); - } catch (Exception ignore) { - } + try { + return ForgeNewModMetadata.fromFile(this, modFile); + } catch (Exception ignore) { + } - try { - return FabricModMetadata.fromFile(modFile); - } catch (Exception ignore) { - } + try { + return FabricModMetadata.fromFile(this, modFile); + } catch (Exception ignore) { + } - try { - return PackMcMeta.fromFile(modFile); - } catch (Exception ignore) { - } + try { + return PackMcMeta.fromFile(this, modFile); + } catch (Exception ignore) { + } - description = ""; - break; - case "litemod": - try { - return LiteModMetadata.fromFile(modFile); - } catch (Exception ignore) { - description = "LiteLoader Mod"; - } - break; - default: - throw new IllegalArgumentException("File " + modFile + " is not a mod file."); + description = ""; + } else if (fileName.endsWith(".litemod")) { + try { + return LiteModMetadata.fromFile(this, modFile); + } catch (Exception ignore) { + description = "LiteLoader Mod"; + } + } else { + throw new IllegalArgumentException("File " + modFile + " is not a mod file."); } - return new LocalModFile(modFile, ModLoaderType.UNKNOWN, null, FileUtils.getNameWithoutExtension(modFile), new LocalModFile.Description(description)); + return new LocalModFile(this, + getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN), + modFile, + FileUtils.getNameWithoutExtension(modFile), + new LocalModFile.Description(description)); } public void refreshMods() throws IOException { localModFiles.clear(); + localMods.clear(); if (Files.isDirectory(getModsDirectory())) { try (DirectoryStream modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) { for (Path subitem : modsDirectoryStream) { @@ -110,11 +120,11 @@ public final class ModManager { // If the folder name is game version, forge will search mod in this subdirectory try (DirectoryStream subitemDirectoryStream = Files.newDirectoryStream(subitem)) { for (Path subsubitem : subitemDirectoryStream) { - addModInfo(subsubitem.toFile()); + addModInfo(subsubitem); } } } else { - addModInfo(subitem.toFile()); + addModInfo(subitem); } } } @@ -128,18 +138,17 @@ public final class ModManager { return localModFiles; } - public void addMod(File file) throws IOException { + public void addMod(Path file) throws IOException { if (!isFileNameMod(file)) throw new IllegalArgumentException("File " + file + " is not a valid mod file."); if (!loaded) refreshMods(); - File modsDirectory = new File(repository.getRunDirectory(id), "mods"); - if (!FileUtils.makeDirectory(modsDirectory)) - throw new IOException("Cannot make directory " + modsDirectory); + Path modsDirectory = getModsDirectory(); + Files.createDirectories(modsDirectory); - File newFile = new File(modsDirectory, file.getName()); + Path newFile = modsDirectory.resolve(file.getFileName()); FileUtils.copyFile(file, newFile); addModInfo(newFile); @@ -151,32 +160,105 @@ public final class ModManager { } } - public static Path disableMod(Path file) throws IOException { - Path disabled = file.getParent().resolve(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION)); + public void rollback(LocalModFile from, LocalModFile to) throws IOException { + if (!loaded) { + throw new IllegalStateException("ModManager Not loaded"); + } + if (!localModFiles.contains(from)) { + throw new IllegalStateException("Rolling back an unknown mod " + from.getFileName()); + } + if (from.isOld()) { + throw new IllegalArgumentException("Rolling back an old mod " + from.getFileName()); + } + if (!to.isOld()) { + throw new IllegalArgumentException("Rolling back to an old path " + to.getFileName()); + } + if (from.getFileName().equals(to.getFileName())) { + // We cannot roll back to the mod with the same name. + return; + } + + LocalMod mod = Objects.requireNonNull(from.getMod()); + if (mod != to.getMod()) { + throw new IllegalArgumentException("Rolling back mod " + from.getFileName() + " to a different mod " + to.getFileName()); + } + if (!mod.getFiles().contains(from) + || !mod.getOldFiles().contains(to)) { + throw new IllegalStateException("LocalMod state corrupt"); + } + + boolean active = from.isActive(); + from.setActive(true); + from.setOld(true); + to.setOld(false); + to.setActive(active); + } + + private Path backupMod(Path file) throws IOException { + Path newPath = file.resolveSibling( + StringUtils.addSuffix( + StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION), + OLD_EXTENSION + ) + ); + if (Files.exists(file)) { + Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING); + } + return newPath; + } + + private Path restoreMod(Path file) throws IOException { + Path newPath = file.resolveSibling( + StringUtils.removeSuffix(FileUtils.getName(file), OLD_EXTENSION) + ); + if (Files.exists(file)) { + Files.move(file, newPath, StandardCopyOption.REPLACE_EXISTING); + } + return newPath; + } + + public Path setOld(LocalModFile modFile, boolean old) throws IOException { + Path newPath; + if (old) { + newPath = backupMod(modFile.getFile()); + localModFiles.remove(modFile); + } else { + newPath = restoreMod(modFile.getFile()); + localModFiles.add(modFile); + } + return newPath; + } + + public Path disableMod(Path file) throws IOException { + if (isOld(file)) return file; // no need to disable an old mod. + Path disabled = file.resolveSibling(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION)); if (Files.exists(file)) Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING); return disabled; } - public static Path enableMod(Path file) throws IOException { - Path enabled = file.getParent().resolve(StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION)); + public Path enableMod(Path file) throws IOException { + if (isOld(file)) return file; + Path enabled = file.resolveSibling(StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION)); if (Files.exists(file)) Files.move(file, enabled, StandardCopyOption.REPLACE_EXISTING); return enabled; } - public static boolean isOld(File file) { - return file.getPath().endsWith(OLD_EXTENSION); + public static String getModName(Path file) { + return StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION); } - public static boolean isDisabled(File file) { - return file.getPath().endsWith(DISABLED_EXTENSION); + public boolean isOld(Path file) { + return FileUtils.getName(file).endsWith(OLD_EXTENSION); } - public static boolean isFileNameMod(File file) { - String name = file.getName(); - if (isDisabled(file)) - name = FileUtils.getNameWithoutExtension(file); + public boolean isDisabled(Path file) { + return FileUtils.getName(file).endsWith(DISABLED_EXTENSION); + } + + public static boolean isFileNameMod(Path file) { + String name = getModName(file); return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod"); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/PackMcMeta.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/PackMcMeta.java index a8b1136a6..1c856bae4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/PackMcMeta.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/PackMcMeta.java @@ -26,7 +26,6 @@ import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import java.io.File; import java.io.IOException; import java.lang.reflect.Type; import java.nio.file.FileSystem; @@ -144,13 +143,19 @@ public class PackMcMeta implements Validation { } } - public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException { - try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) { + public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { Path mcmod = fs.getPath("pack.mcmeta"); if (Files.notExists(mcmod)) throw new IOException("File " + modFile + " is not a resource pack."); PackMcMeta metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), PackMcMeta.class); - return new LocalModFile(modFile, ModLoaderType.PACK, null, FileUtils.getNameWithoutExtension(modFile), metadata.pack.description, "", "", "", "", ""); + return new LocalModFile( + modManager, + modManager.getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.PACK), + modFile, + FileUtils.getNameWithoutExtension(modFile), + metadata.pack.description, + "", "", "", "", ""); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java index f1f4920df..9f278595f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseAddon.java @@ -507,7 +507,7 @@ public class CurseAddon implements RemoteMod.IMod { this, Integer.toString(projectId), getDisplayName(), - null, + getFileName(), null, getParsedFileDate(), versionType, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index 86e9371db..920a7360d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -75,6 +75,10 @@ public final class FileUtils { } } + public static String getNameWithoutExtension(String fileName) { + return StringUtils.substringBeforeLast(fileName, '.'); + } + public static String getNameWithoutExtension(File file) { return StringUtils.substringBeforeLast(file.getName(), '.'); }