feat(mod): mod update.
This commit is contained in:
@@ -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 -> {
|
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
|
||||||
mods.forEach(it -> {
|
mods.forEach(it -> {
|
||||||
try {
|
try {
|
||||||
modManager.addMod(it);
|
modManager.addMod(it.toPath());
|
||||||
} catch (IOException | IllegalArgumentException e) {
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, 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(() -> {
|
Task.runAsync(() -> {
|
||||||
for (File file : res) {
|
for (File file : res) {
|
||||||
try {
|
try {
|
||||||
modManager.addMod(file);
|
modManager.addMod(file.toPath());
|
||||||
succeeded.add(file.getName());
|
succeeded.add(file.getName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logging.LOG.log(Level.WARNING, "Unable to add mod " + file, 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() {
|
public boolean isModded() {
|
||||||
return modded.get();
|
return modded.get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.*;
|
||||||
import com.jfoenix.controls.JFXCheckBox;
|
|
||||||
import com.jfoenix.controls.JFXDialogLayout;
|
|
||||||
import com.jfoenix.controls.JFXListView;
|
|
||||||
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.BooleanProperty;
|
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.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.construct.*;
|
import org.jackhuang.hmcl.ui.construct.*;
|
||||||
|
import org.jackhuang.hmcl.util.Lazy;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
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.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
|
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();
|
JFXCheckBox checkBox = new JFXCheckBox();
|
||||||
TwoLineListItem content = new TwoLineListItem();
|
TwoLineListItem content = new TwoLineListItem();
|
||||||
|
JFXButton restoreButton = new JFXButton();
|
||||||
JFXButton infoButton = new JFXButton();
|
JFXButton infoButton = new JFXButton();
|
||||||
JFXButton revealButton = new JFXButton();
|
JFXButton revealButton = new JFXButton();
|
||||||
BooleanProperty booleanProperty;
|
BooleanProperty booleanProperty;
|
||||||
@@ -276,13 +279,18 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
content.setMouseTransparent(true);
|
content.setMouseTransparent(true);
|
||||||
setSelectable();
|
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.getStyleClass().add("toggle-icon4");
|
||||||
revealButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
revealButton.setGraphic(FXUtils.limitingSize(SVG.folderOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
||||||
|
|
||||||
infoButton.getStyleClass().add("toggle-icon4");
|
infoButton.getStyleClass().add("toggle-icon4");
|
||||||
infoButton.setGraphic(FXUtils.limitingSize(SVG.informationOutline(Theme.blackFillBinding(), 24, 24), 24, 24));
|
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));
|
StackPane.setMargin(container, new Insets(8));
|
||||||
getContainer().getChildren().setAll(container);
|
getContainer().getChildren().setAll(container);
|
||||||
@@ -302,6 +310,17 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
|||||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||||
}
|
}
|
||||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
|
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 -> {
|
revealButton.setOnMouseClicked(e -> {
|
||||||
FXUtils.showFileInExplorer(dataItem.getModInfo().getFile());
|
FXUtils.showFileInExplorer(dataItem.getModInfo().getFile());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,15 +32,16 @@ import org.jackhuang.hmcl.mod.ModManager;
|
|||||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||||
import org.jackhuang.hmcl.mod.curse.CurseAddon;
|
import org.jackhuang.hmcl.mod.curse.CurseAddon;
|
||||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
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.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTreeTableCell;
|
import org.jackhuang.hmcl.ui.construct.*;
|
||||||
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.decorator.DecoratorPage;
|
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
import org.jackhuang.hmcl.util.Pair;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -120,13 +121,26 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateMods() {
|
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(
|
Controllers.taskDialog(
|
||||||
new ModUpdateTask(
|
task.whenComplete(Schedulers.javafx(), exception -> {
|
||||||
modManager,
|
fireEvent(new PageCloseEvent());
|
||||||
objects.stream()
|
if (!task.getFailedMods().isEmpty()) {
|
||||||
.filter(o -> o.enabled.get())
|
Controllers.dialog(i18n("mods.check_updates.failed") + "\n" +
|
||||||
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidates().get(0)))
|
task.getFailedMods().stream().map(LocalModFile::getFileName).collect(Collectors.joining("\n")),
|
||||||
.collect(Collectors.toList())),
|
i18n("install.failed"),
|
||||||
|
MessageDialogPane.MessageType.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception == null) {
|
||||||
|
Controllers.dialog(i18n("install.success"));
|
||||||
|
}
|
||||||
|
}),
|
||||||
i18n("mods.check_updates.update"),
|
i18n("mods.check_updates.update"),
|
||||||
t -> {
|
t -> {
|
||||||
});
|
});
|
||||||
@@ -249,6 +263,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
|||||||
|
|
||||||
public static class ModUpdateTask extends Task<Void> {
|
public static class ModUpdateTask extends Task<Void> {
|
||||||
private final Collection<Task<?>> dependents;
|
private final Collection<Task<?>> dependents;
|
||||||
|
private final List<LocalModFile> failedMods = new ArrayList<>();
|
||||||
|
|
||||||
ModUpdateTask(ModManager modManager, List<Pair<LocalModFile, RemoteMod.Version>> mods) {
|
ModUpdateTask(ModManager modManager, List<Pair<LocalModFile, RemoteMod.Version>> mods) {
|
||||||
setStage("mods.check_updates.update");
|
setStage("mods.check_updates.update");
|
||||||
@@ -257,16 +272,33 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
|||||||
dependents = mods.stream()
|
dependents = mods.stream()
|
||||||
.map(mod -> {
|
.map(mod -> {
|
||||||
return Task
|
return Task
|
||||||
.supplyAsync(() -> {
|
.runAsync(Schedulers.javafx(), () -> {
|
||||||
return null;
|
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");
|
.withCounter("mods.check_updates.update");
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<LocalModFile> getFailedMods() {
|
||||||
|
return failedMods;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Task<?>> getDependents() {
|
public Collection<Task<?>> getDependents() {
|
||||||
return dependents;
|
return dependents;
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ message.default=Default
|
|||||||
message.doing=Please wait
|
message.doing=Please wait
|
||||||
message.downloading=Downloading...
|
message.downloading=Downloading...
|
||||||
message.error=Error
|
message.error=Error
|
||||||
|
message.failed=Operation failed
|
||||||
message.info=Info
|
message.info=Info
|
||||||
message.success=Job completed successfully
|
message.success=Job completed successfully
|
||||||
message.unknown=Unknown
|
message.unknown=Unknown
|
||||||
@@ -583,6 +584,7 @@ mods.add.success=Successfully installed mods %s.
|
|||||||
mods.category=Category
|
mods.category=Category
|
||||||
mods.check_updates=Check updates
|
mods.check_updates=Check updates
|
||||||
mods.check_updates.current_version=Current
|
mods.check_updates.current_version=Current
|
||||||
|
mods.check_updates.failed=Failed to download some of files
|
||||||
mods.check_updates.file=File
|
mods.check_updates.file=File
|
||||||
mods.check_updates.source=Source
|
mods.check_updates.source=Source
|
||||||
mods.check_updates.target_version=Target
|
mods.check_updates.target_version=Target
|
||||||
@@ -602,6 +604,7 @@ mods.mcmod.search=Search in MCMOD
|
|||||||
mods.modrinth=Modrinth
|
mods.modrinth=Modrinth
|
||||||
mods.name=Name
|
mods.name=Name
|
||||||
mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader)
|
mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader)
|
||||||
|
mods.restore=Restore
|
||||||
mods.url=Official Page
|
mods.url=Official Page
|
||||||
|
|
||||||
multiplayer=Multiplayer
|
multiplayer=Multiplayer
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ message.default=預設
|
|||||||
message.doing=請耐心等待
|
message.doing=請耐心等待
|
||||||
message.downloading=正在下載…
|
message.downloading=正在下載…
|
||||||
message.error=錯誤
|
message.error=錯誤
|
||||||
|
message.failed=操作失敗
|
||||||
message.info=資訊
|
message.info=資訊
|
||||||
message.success=完成
|
message.success=完成
|
||||||
message.unknown=未知
|
message.unknown=未知
|
||||||
@@ -583,6 +584,7 @@ mods.add.success=成功新增模組 %s。
|
|||||||
mods.category=類別
|
mods.category=類別
|
||||||
mods.check_updates=檢查模組更新
|
mods.check_updates=檢查模組更新
|
||||||
mods.check_updates.current_version=當前版本
|
mods.check_updates.current_version=當前版本
|
||||||
|
mods.check_updates.failed=部分文件下載失敗
|
||||||
mods.check_updates.file=文件
|
mods.check_updates.file=文件
|
||||||
mods.check_updates.source=來源
|
mods.check_updates.source=來源
|
||||||
mods.check_updates.target_version=目標版本
|
mods.check_updates.target_version=目標版本
|
||||||
@@ -602,6 +604,7 @@ mods.mcmod.search=MC 百科蒐索
|
|||||||
mods.modrinth=Modrinth
|
mods.modrinth=Modrinth
|
||||||
mods.name=名稱
|
mods.name=名稱
|
||||||
mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。
|
mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。
|
||||||
|
mods.restore=回退
|
||||||
mods.url=官方頁面
|
mods.url=官方頁面
|
||||||
|
|
||||||
multiplayer=多人聯機
|
multiplayer=多人聯機
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ message.default=默认
|
|||||||
message.doing=请耐心等待
|
message.doing=请耐心等待
|
||||||
message.downloading=正在下载
|
message.downloading=正在下载
|
||||||
message.error=错误
|
message.error=错误
|
||||||
|
message.failed=操作失败
|
||||||
message.info=提示
|
message.info=提示
|
||||||
message.success=已完成
|
message.success=已完成
|
||||||
message.unknown=未知
|
message.unknown=未知
|
||||||
@@ -583,6 +584,7 @@ mods.add.success=成功添加模组 %s。
|
|||||||
mods.category=类别
|
mods.category=类别
|
||||||
mods.check_updates=检查模组更新
|
mods.check_updates=检查模组更新
|
||||||
mods.check_updates.current_version=当前版本
|
mods.check_updates.current_version=当前版本
|
||||||
|
mods.check_updates.failed=部分文件下载失败
|
||||||
mods.check_updates.file=文件
|
mods.check_updates.file=文件
|
||||||
mods.check_updates.source=来源
|
mods.check_updates.source=来源
|
||||||
mods.check_updates.target_version=目标版本
|
mods.check_updates.target_version=目标版本
|
||||||
@@ -602,6 +604,7 @@ mods.mcmod.search=MC 百科搜索
|
|||||||
mods.modrinth=Modrinth
|
mods.modrinth=Modrinth
|
||||||
mods.name=名称
|
mods.name=名称
|
||||||
mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。
|
mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。
|
||||||
|
mods.restore=回退
|
||||||
mods.url=官方页面
|
mods.url=官方页面
|
||||||
|
|
||||||
multiplayer=多人联机
|
multiplayer=多人联机
|
||||||
|
|||||||
@@ -17,27 +17,21 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod;
|
||||||
|
|
||||||
import com.google.gson.JsonDeserializationContext;
|
import com.google.gson.*;
|
||||||
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.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
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;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -64,14 +58,14 @@ public final class FabricModMetadata {
|
|||||||
this.contact = contact;
|
this.contact = contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||||
Path mcmod = fs.getPath("fabric.mod.json");
|
Path mcmod = fs.getPath("fabric.mod.json");
|
||||||
if (Files.notExists(mcmod))
|
if (Files.notExists(mcmod))
|
||||||
throw new IOException("File " + modFile + " is not a Fabric mod.");
|
throw new IOException("File " + modFile + " is not a Fabric mod.");
|
||||||
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
|
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
|
||||||
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
|
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);
|
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.jackhuang.hmcl.util.Immutable;
|
|||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -116,8 +115,8 @@ public final class ForgeNewModMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||||
Path modstoml = fs.getPath("META-INF/mods.toml");
|
Path modstoml = fs.getPath("META-INF/mods.toml");
|
||||||
if (Files.notExists(modstoml))
|
if (Files.notExists(modstoml))
|
||||||
throw new IOException("File " + modFile + " is not a Forge 1.13+ mod.");
|
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));
|
Manifest manifest = new Manifest(Files.newInputStream(manifestMF));
|
||||||
jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
|
jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
|
||||||
} catch (IOException e) {
|
} 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.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||||
mod.getDisplayURL(),
|
mod.getDisplayURL(),
|
||||||
metadata.getLogoFile());
|
metadata.getLogoFile());
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ package org.jackhuang.hmcl.mod;
|
|||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.google.gson.reflect.TypeToken;
|
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.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -120,8 +120,8 @@ public final class ForgeOldModMetadata {
|
|||||||
return authors;
|
return authors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||||
Path mcmod = fs.getPath("mcmod.info");
|
Path mcmod = fs.getPath("mcmod.info");
|
||||||
if (Files.notExists(mcmod))
|
if (Files.notExists(mcmod))
|
||||||
throw new IOException("File " + modFile + " is not a Forge mod.");
|
throw new IOException("File " + modFile + " is not a Forge mod.");
|
||||||
@@ -138,7 +138,7 @@ public final class ForgeOldModMetadata {
|
|||||||
authors = String.join(", ", metadata.getAuthorList());
|
authors = String.join(", ", metadata.getAuthorList());
|
||||||
if (StringUtils.isBlank(authors))
|
if (StringUtils.isBlank(authors))
|
||||||
authors = metadata.getCredits();
|
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(),
|
authors, metadata.getVersion(), metadata.getGameVersion(),
|
||||||
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url,
|
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url,
|
||||||
metadata.getLogoFile());
|
metadata.getLogoFile());
|
||||||
|
|||||||
@@ -18,13 +18,12 @@
|
|||||||
package org.jackhuang.hmcl.mod;
|
package org.jackhuang.hmcl.mod;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.Immutable;
|
import org.jackhuang.hmcl.util.Immutable;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
@@ -108,15 +107,15 @@ public final class LiteModMetadata {
|
|||||||
return updateURI;
|
return updateURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||||
try (ZipFile zipFile = new ZipFile(modFile)) {
|
try (ZipFile zipFile = new ZipFile(modFile.toFile())) {
|
||||||
ZipEntry entry = zipFile.getEntry("litemod.json");
|
ZipEntry entry = zipFile.getEntry("litemod.json");
|
||||||
if (entry == null)
|
if (entry == null)
|
||||||
throw new IOException("File " + modFile + "is not a LiteLoader mod.");
|
throw new IOException("File " + modFile + "is not a LiteLoader mod.");
|
||||||
LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class);
|
LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class);
|
||||||
if (metadata == null)
|
if (metadata == null)
|
||||||
throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
|
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(), "");
|
metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java
Normal file
63
HMCLCore/src/main/java/org/jackhuang/hmcl/mod/LocalMod.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Hello Minecraft! Launcher
|
* 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
|
* 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
|
* 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.BooleanProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -37,8 +35,8 @@ import java.util.stream.Collectors;
|
|||||||
public final class LocalModFile implements Comparable<LocalModFile> {
|
public final class LocalModFile implements Comparable<LocalModFile> {
|
||||||
|
|
||||||
private Path file;
|
private Path file;
|
||||||
private final ModLoaderType modLoaderType;
|
private final ModManager modManager;
|
||||||
private final String id;
|
private final LocalMod mod;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final Description description;
|
private final Description description;
|
||||||
private final String authors;
|
private final String authors;
|
||||||
@@ -49,14 +47,14 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
|||||||
private final String logoPath;
|
private final String logoPath;
|
||||||
private final BooleanProperty activeProperty;
|
private final BooleanProperty activeProperty;
|
||||||
|
|
||||||
public LocalModFile(File file, ModLoaderType modLoaderType, String id, String name, Description description) {
|
public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description) {
|
||||||
this(file, modLoaderType, id, name, 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) {
|
public LocalModFile(ModManager modManager, LocalMod mod, Path file, String name, Description description, String authors, String version, String gameVersion, String url, String logoPath) {
|
||||||
this.file = file.toPath();
|
this.modManager = modManager;
|
||||||
this.modLoaderType = modLoaderType;
|
this.mod = mod;
|
||||||
this.id = id;
|
this.file = file;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.authors = authors;
|
this.authors = authors;
|
||||||
@@ -65,23 +63,39 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
|||||||
this.url = url;
|
this.url = url;
|
||||||
this.logoPath = logoPath;
|
this.logoPath = logoPath;
|
||||||
|
|
||||||
activeProperty = new SimpleBooleanProperty(this, "active", !ModManager.isDisabled(file)) {
|
activeProperty = new SimpleBooleanProperty(this, "active", !modManager.isDisabled(file)) {
|
||||||
@Override
|
@Override
|
||||||
protected void invalidated() {
|
protected void invalidated() {
|
||||||
|
if (isOld()) return;
|
||||||
|
|
||||||
Path path = LocalModFile.this.file.toAbsolutePath();
|
Path path = LocalModFile.this.file.toAbsolutePath();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (get())
|
if (get())
|
||||||
LocalModFile.this.file = ModManager.enableMod(path);
|
LocalModFile.this.file = modManager.enableMod(path);
|
||||||
else
|
else
|
||||||
LocalModFile.this.file = ModManager.disableMod(path);
|
LocalModFile.this.file = modManager.disableMod(path);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logging.LOG.log(Level.SEVERE, "Unable to invert state of mod file " + path, 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() {
|
public Path getFile() {
|
||||||
@@ -89,11 +103,11 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ModLoaderType getModLoaderType() {
|
public ModLoaderType getModLoaderType() {
|
||||||
return modLoaderType;
|
return mod.getModLoaderType();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return mod.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@@ -140,12 +154,29 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
|||||||
return fileName;
|
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 {
|
public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException {
|
||||||
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
|
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
|
||||||
if (!currentVersion.isPresent()) return null;
|
if (!currentVersion.isPresent()) return null;
|
||||||
List<RemoteMod.Version> remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid())
|
List<RemoteMod.Version> remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid())
|
||||||
.filter(version -> version.getGameVersions().contains(gameVersion))
|
.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())
|
.sorted(Comparator.comparing(RemoteMod.Version::getDatePublished).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (remoteVersions.isEmpty()) return null;
|
if (remoteVersions.isEmpty()) return null;
|
||||||
|
|||||||
@@ -23,16 +23,18 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
|
|||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
public final class ModManager {
|
public final class ModManager {
|
||||||
private final GameRepository repository;
|
private final GameRepository repository;
|
||||||
private final String id;
|
private final String id;
|
||||||
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
|
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
|
||||||
|
private final HashMap<LocalMod, LocalMod> localMods = new HashMap<>();
|
||||||
|
|
||||||
private boolean loaded = false;
|
private boolean loaded = false;
|
||||||
|
|
||||||
@@ -49,60 +51,68 @@ public final class ModManager {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path getModsDirectory() {
|
public Path getModsDirectory() {
|
||||||
return repository.getRunDirectory(id).toPath().resolve("mods");
|
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 {
|
try {
|
||||||
localModFiles.add(getModInfo(file));
|
LocalModFile localModFile = getModInfo(file);
|
||||||
|
if (!localModFile.isOld()) {
|
||||||
|
localModFiles.add(localModFile);
|
||||||
|
}
|
||||||
} catch (IllegalArgumentException ignore) {
|
} catch (IllegalArgumentException ignore) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile getModInfo(File modFile) {
|
public LocalModFile getModInfo(Path modFile) {
|
||||||
File file = isDisabled(modFile) ? new File(modFile.getAbsoluteFile().getParentFile(), FileUtils.getNameWithoutExtension(modFile)) : modFile;
|
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||||
String description, extension = FileUtils.getExtension(file);
|
String description;
|
||||||
switch (extension) {
|
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
|
||||||
case "zip":
|
try {
|
||||||
case "jar":
|
return ForgeOldModMetadata.fromFile(this, modFile);
|
||||||
try {
|
} catch (Exception ignore) {
|
||||||
return ForgeOldModMetadata.fromFile(modFile);
|
}
|
||||||
} catch (Exception ignore) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return ForgeNewModMetadata.fromFile(modFile);
|
return ForgeNewModMetadata.fromFile(this, modFile);
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return FabricModMetadata.fromFile(modFile);
|
return FabricModMetadata.fromFile(this, modFile);
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return PackMcMeta.fromFile(modFile);
|
return PackMcMeta.fromFile(this, modFile);
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
description = "";
|
description = "";
|
||||||
break;
|
} else if (fileName.endsWith(".litemod")) {
|
||||||
case "litemod":
|
try {
|
||||||
try {
|
return LiteModMetadata.fromFile(this, modFile);
|
||||||
return LiteModMetadata.fromFile(modFile);
|
} catch (Exception ignore) {
|
||||||
} catch (Exception ignore) {
|
description = "LiteLoader Mod";
|
||||||
description = "LiteLoader Mod";
|
}
|
||||||
}
|
} else {
|
||||||
break;
|
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
|
||||||
default:
|
|
||||||
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 {
|
public void refreshMods() throws IOException {
|
||||||
localModFiles.clear();
|
localModFiles.clear();
|
||||||
|
localMods.clear();
|
||||||
if (Files.isDirectory(getModsDirectory())) {
|
if (Files.isDirectory(getModsDirectory())) {
|
||||||
try (DirectoryStream<Path> modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) {
|
try (DirectoryStream<Path> modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) {
|
||||||
for (Path subitem : modsDirectoryStream) {
|
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
|
// If the folder name is game version, forge will search mod in this subdirectory
|
||||||
try (DirectoryStream<Path> subitemDirectoryStream = Files.newDirectoryStream(subitem)) {
|
try (DirectoryStream<Path> subitemDirectoryStream = Files.newDirectoryStream(subitem)) {
|
||||||
for (Path subsubitem : subitemDirectoryStream) {
|
for (Path subsubitem : subitemDirectoryStream) {
|
||||||
addModInfo(subsubitem.toFile());
|
addModInfo(subsubitem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addModInfo(subitem.toFile());
|
addModInfo(subitem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,18 +138,17 @@ public final class ModManager {
|
|||||||
return localModFiles;
|
return localModFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addMod(File file) throws IOException {
|
public void addMod(Path file) throws IOException {
|
||||||
if (!isFileNameMod(file))
|
if (!isFileNameMod(file))
|
||||||
throw new IllegalArgumentException("File " + file + " is not a valid mod file.");
|
throw new IllegalArgumentException("File " + file + " is not a valid mod file.");
|
||||||
|
|
||||||
if (!loaded)
|
if (!loaded)
|
||||||
refreshMods();
|
refreshMods();
|
||||||
|
|
||||||
File modsDirectory = new File(repository.getRunDirectory(id), "mods");
|
Path modsDirectory = getModsDirectory();
|
||||||
if (!FileUtils.makeDirectory(modsDirectory))
|
Files.createDirectories(modsDirectory);
|
||||||
throw new IOException("Cannot make directory " + modsDirectory);
|
|
||||||
|
|
||||||
File newFile = new File(modsDirectory, file.getName());
|
Path newFile = modsDirectory.resolve(file.getFileName());
|
||||||
FileUtils.copyFile(file, newFile);
|
FileUtils.copyFile(file, newFile);
|
||||||
|
|
||||||
addModInfo(newFile);
|
addModInfo(newFile);
|
||||||
@@ -151,32 +160,105 @@ public final class ModManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Path disableMod(Path file) throws IOException {
|
public void rollback(LocalModFile from, LocalModFile to) throws IOException {
|
||||||
Path disabled = file.getParent().resolve(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
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))
|
if (Files.exists(file))
|
||||||
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
|
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
|
||||||
return disabled;
|
return disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Path enableMod(Path file) throws IOException {
|
public Path enableMod(Path file) throws IOException {
|
||||||
Path enabled = file.getParent().resolve(StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
if (isOld(file)) return file;
|
||||||
|
Path enabled = file.resolveSibling(StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
||||||
if (Files.exists(file))
|
if (Files.exists(file))
|
||||||
Files.move(file, enabled, StandardCopyOption.REPLACE_EXISTING);
|
Files.move(file, enabled, StandardCopyOption.REPLACE_EXISTING);
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isOld(File file) {
|
public static String getModName(Path file) {
|
||||||
return file.getPath().endsWith(OLD_EXTENSION);
|
return StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isDisabled(File file) {
|
public boolean isOld(Path file) {
|
||||||
return file.getPath().endsWith(DISABLED_EXTENSION);
|
return FileUtils.getName(file).endsWith(OLD_EXTENSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isFileNameMod(File file) {
|
public boolean isDisabled(Path file) {
|
||||||
String name = file.getName();
|
return FileUtils.getName(file).endsWith(DISABLED_EXTENSION);
|
||||||
if (isDisabled(file))
|
}
|
||||||
name = FileUtils.getNameWithoutExtension(file);
|
|
||||||
|
public static boolean isFileNameMod(Path file) {
|
||||||
|
String name = getModName(file);
|
||||||
return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod");
|
return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.util.gson.Validation;
|
|||||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
@@ -144,13 +143,19 @@ public class PackMcMeta implements Validation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(File modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||||
Path mcmod = fs.getPath("pack.mcmeta");
|
Path mcmod = fs.getPath("pack.mcmeta");
|
||||||
if (Files.notExists(mcmod))
|
if (Files.notExists(mcmod))
|
||||||
throw new IOException("File " + modFile + " is not a resource pack.");
|
throw new IOException("File " + modFile + " is not a resource pack.");
|
||||||
PackMcMeta metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), PackMcMeta.class);
|
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,
|
||||||
|
"", "", "", "", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ public class CurseAddon implements RemoteMod.IMod {
|
|||||||
this,
|
this,
|
||||||
Integer.toString(projectId),
|
Integer.toString(projectId),
|
||||||
getDisplayName(),
|
getDisplayName(),
|
||||||
null,
|
getFileName(),
|
||||||
null,
|
null,
|
||||||
getParsedFileDate(),
|
getParsedFileDate(),
|
||||||
versionType,
|
versionType,
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ public final class FileUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getNameWithoutExtension(String fileName) {
|
||||||
|
return StringUtils.substringBeforeLast(fileName, '.');
|
||||||
|
}
|
||||||
|
|
||||||
public static String getNameWithoutExtension(File file) {
|
public static String getNameWithoutExtension(File file) {
|
||||||
return StringUtils.substringBeforeLast(file.getName(), '.');
|
return StringUtils.substringBeforeLast(file.getName(), '.');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user