feat(mod): mod update.

This commit is contained in:
huanghongxun
2021-10-06 02:55:19 +08:00
parent d48591a4cd
commit 6c3178a831
16 changed files with 376 additions and 130 deletions

View File

@@ -60,7 +60,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
FXUtils.applyDragListener(this, it -> 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<ModListPageSkin.ModInfoObjec
Task.runAsync(() -> {
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<ModListPageSkin.ModInfoObjec
});
}
public void rollback(LocalModFile from, LocalModFile to) {
try {
modManager.rollback(from, to);
refresh();
} catch (IOException ex) {
Controllers.showToast(i18n("message.failed"));
}
}
public boolean isModded() {
return modded.get();
}

View File

@@ -17,10 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.*;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
@@ -43,6 +40,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -56,6 +54,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
@@ -259,9 +258,13 @@ class ModListPageSkin extends SkinBase<ModListPage> {
}
}
static class ModInfoListCell extends MDListCell<ModInfoObject> {
private static Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
private static Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
class ModInfoListCell extends MDListCell<ModInfoObject> {
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<ModListPage> {
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<ModListPage> {
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());
});

View File

@@ -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<Void> {
private final Collection<Task<?>> dependents;
private final List<LocalModFile> failedMods = new ArrayList<>();
ModUpdateTask(ModManager modManager, List<Pair<LocalModFile, RemoteMod.Version>> 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<LocalModFile> getFailedMods() {
return failedMods;
}
@Override
public Collection<Task<?>> getDependents() {
return dependents;

View File

@@ -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

View File

@@ -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=多人聯機

View File

@@ -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=多人联机

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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());

View File

@@ -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(), "");
}
}

View File

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

View File

@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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<LocalModFile> {
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<LocalModFile> {
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<LocalModFile> {
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<LocalModFile> {
}
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<LocalModFile> {
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<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
if (!currentVersion.isPresent()) return null;
List<RemoteMod.Version> 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;

View File

@@ -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<LocalModFile> localModFiles = new TreeSet<>();
private final HashMap<LocalMod, LocalMod> 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<Path> 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<Path> 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");
}

View File

@@ -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,
"", "", "", "", "");
}
}
}

View File

@@ -507,7 +507,7 @@ public class CurseAddon implements RemoteMod.IMod {
this,
Integer.toString(projectId),
getDisplayName(),
null,
getFileName(),
null,
getParsedFileDate(),
versionType,

View File

@@ -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(), '.');
}