Fix redownloading disabled mods for curse modpack

This commit is contained in:
huanghongxun 2019-01-09 13:18:04 +08:00
parent d655c9ec6b
commit fb3ba220b6
10 changed files with 177 additions and 125 deletions

View File

@ -31,7 +31,6 @@ import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.javafx.ImmediateObjectProperty;
@ -52,7 +51,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
public final class Profile implements Observable {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private final HMCLGameRepository repository;
private final ModManager modManager;
private final StringProperty selectedVersion = new SimpleStringProperty();
@ -136,7 +134,6 @@ public final class Profile implements Observable {
this.name = new ImmediateStringProperty(this, "name", name);
gameDir = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
repository = new HMCLGameRepository(this, initialGameDir);
modManager = new ModManager(repository);
this.global.set(global == null ? new VersionSetting() : global);
this.selectedVersion.set(selectedVersion);
this.useRelativePath.set(useRelativePath);
@ -166,10 +163,6 @@ public final class Profile implements Observable {
return repository;
}
public ModManager getModManager() {
return modManager;
}
public DefaultDependencyManager getDependency() {
return new DefaultDependencyManager(repository, DownloadProviders.getDownloadProvider(), HMCLCacheRepository.REPOSITORY);
}

View File

@ -44,7 +44,6 @@ public final class ModListPage extends ListPage<ModItem> {
private JFXTabPane parentTab;
private ModManager modManager;
private String versionId;
public ModListPage() {
setRefreshable(true);
@ -52,41 +51,44 @@ public final class ModListPage extends ListPage<ModItem> {
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
mods.forEach(it -> {
try {
modManager.addMod(versionId, it);
modManager.addMod(it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
loadMods(modManager);
});
}
@Override
public void refresh() {
loadMods(modManager, versionId);
loadMods(modManager);
}
public void loadVersion(Profile profile, String id) {
loadMods(profile.getModManager(), id);
loadMods(profile.getRepository().getModManager(id));
}
public void loadMods(ModManager modManager, String versionId) {
public void loadMods(ModManager modManager) {
this.modManager = modManager;
this.versionId = versionId;
Task.of(variables -> {
synchronized (ModListPage.this) {
JFXUtilities.runInFX(() -> loadingProperty().set(true));
modManager.refreshMods(versionId);
modManager.refreshMods();
// Surprisingly, if there are a great number of mods, this processing will cause a long UI pause,
// constructing UI elements.
// We must do this asynchronously.
LinkedList<ModItem> list = new LinkedList<>();
for (ModInfo modInfo : modManager.getMods(versionId)) {
for (ModInfo modInfo : modManager.getMods()) {
ModItem item = new ModItem(modInfo, i -> {
modManager.removeMods(versionId, modInfo);
loadMods(modManager, versionId);
try {
modManager.removeMods(modInfo);
} catch (IOException ignore) {
// Fail to remove mods if the game is running or the mod is absent.
}
loadMods(modManager);
});
modInfo.activeProperty().addListener((a, b, newValue) -> {
if (newValue)
@ -126,7 +128,7 @@ public final class ModListPage extends ListPage<ModItem> {
Task.of(variables -> {
for (File file : res) {
try {
modManager.addMod(versionId, file);
modManager.addMod(file);
succeeded.add(file.getName());
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to add mod " + file, e);
@ -142,7 +144,7 @@ public final class ModListPage extends ListPage<ModItem> {
if (!failed.isEmpty())
prompt.add(i18n("mods.add.failed", String.join(", ", failed)));
Controllers.dialog(String.join("\n", prompt), i18n("mods.add"));
loadMods(modManager, versionId);
loadMods(modManager);
})).start();
}

View File

@ -19,6 +19,7 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ToStringBuilder;
@ -366,6 +367,10 @@ public class DefaultGameRepository implements GameRepository {
return getModpackConfiguration(version).exists();
}
public ModManager getModManager(String version) {
return new ModManager(this, version);
}
@Override
public String toString() {
return new ToStringBuilder(this)

View File

@ -19,7 +19,7 @@ package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.*;
@ -45,8 +45,8 @@ import java.util.stream.Collectors;
*/
public final class CurseCompletionTask extends Task {
private final DefaultDependencyManager dependencyManager;
private final GameRepository repository;
private final DefaultGameRepository repository;
private final ModManager modManager;
private final String version;
private CurseManifest manifest = null;
private final List<Task> dependents = new LinkedList<>();
@ -70,8 +70,8 @@ public final class CurseCompletionTask extends Task {
* @param manifest the CurseForgeModpack manifest.
*/
public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version, CurseManifest manifest) {
this.dependencyManager = dependencyManager;
this.repository = dependencyManager.getGameRepository();
this.modManager = repository.getModManager(version);
this.version = version;
this.manifest = manifest;
@ -101,7 +101,6 @@ public final class CurseCompletionTask extends Task {
return;
File root = repository.getVersionRoot(version);
File run = repository.getRunDirectory(version);
AtomicBoolean flag = new AtomicBoolean(true);
AtomicInteger finished = new AtomicInteger(0);
@ -140,9 +139,9 @@ public final class CurseCompletionTask extends Task {
for (CurseManifestFile file : newManifest.getFiles())
if (StringUtils.isNotBlank(file.getFileName())) {
File dest = new File(run, "mods/" + file.getFileName());
if (!dest.exists())
dependencies.add(new FileDownloadTask(file.getUrl(), dest));
if (!modManager.hasSimpleMod(file.getFileName())) {
dependencies.add(new FileDownloadTask(file.getUrl(), modManager.getSimpleModPath(file.getFileName()).toFile()));
}
}
// Let this task fail if the curse manifest has not been completed.

View File

@ -114,7 +114,7 @@ public final class ForgeModMetadata {
return authors;
}
public static ModInfo fromFile(File modFile) throws IOException, JsonParseException {
public static ModInfo fromFile(ModManager modManager, File modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
Path mcmod = fs.getPath("mcmod.info");
if (Files.notExists(mcmod))
@ -132,7 +132,7 @@ public final class ForgeModMetadata {
authors = String.join(", ", metadata.getAuthorList());
if (StringUtils.isBlank(authors))
authors = metadata.getCredits();
return new ModInfo(modFile, metadata.getName(), metadata.getDescription(),
return new ModInfo(modManager, modFile, metadata.getName(), metadata.getDescription(),
authors, metadata.getVersion(), metadata.getGameVersion(),
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url);
}

View File

@ -108,7 +108,7 @@ public final class LiteModMetadata {
return updateURI;
}
public static ModInfo fromFile(File modFile) throws IOException, JsonParseException {
public static ModInfo fromFile(ModManager modManager, File modFile) throws IOException, JsonParseException {
try (ZipFile zipFile = new ZipFile(modFile)) {
ZipEntry entry = zipFile.getEntry("litemod.json");
if (entry == null)
@ -116,7 +116,8 @@ public final class LiteModMetadata {
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 ModInfo(modFile, metadata.getName(), metadata.getDescription(), metadata.getAuthor(), metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI());
return new ModInfo(modManager, modFile, metadata.getName(), metadata.getDescription(), metadata.getAuthor(),
metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI());
}
}

View File

@ -23,7 +23,10 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ImmediateBooleanProperty;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.logging.Level;
/**
*
@ -31,7 +34,7 @@ import java.util.Objects;
*/
public final class ModInfo implements Comparable<ModInfo> {
private File file;
private Path file;
private final String name;
private final String description;
private final String authors;
@ -41,16 +44,12 @@ public final class ModInfo implements Comparable<ModInfo> {
private final String fileName;
private final ImmediateBooleanProperty activeProperty;
public ModInfo(File file, String name) {
this(file, name, "");
public ModInfo(ModManager modManager, File file, String name, String description) {
this(modManager, file, name, description, "", "", "", "");
}
public ModInfo(File file, String name, String description) {
this(file, name, description, "", "", "", "");
}
public ModInfo(File file, String name, String description, String authors, String version, String gameVersion, String url) {
this.file = file;
public ModInfo(ModManager modManager, File file, String name, String description, String authors, String version, String gameVersion, String url) {
this.file = file.toPath();
this.name = name;
this.description = description;
this.authors = authors;
@ -58,26 +57,26 @@ public final class ModInfo implements Comparable<ModInfo> {
this.gameVersion = gameVersion;
this.url = url;
activeProperty = new ImmediateBooleanProperty(this, "active", !DISABLED_EXTENSION.equals(FileUtils.getExtension(file))) {
activeProperty = new ImmediateBooleanProperty(this, "active", !modManager.isDisabled(file)) {
@Override
protected void invalidated() {
File f = ModInfo.this.file.getAbsoluteFile(), newF;
if (DISABLED_EXTENSION.equals(FileUtils.getExtension(f)))
newF = new File(f.getParentFile(), FileUtils.getNameWithoutExtension(f));
else
newF = new File(f.getParentFile(), f.getName() + "." + DISABLED_EXTENSION);
Path path = ModInfo.this.file.toAbsolutePath();
if (f.renameTo(newF))
ModInfo.this.file = newF;
else
Logging.LOG.severe("Unable to rename file " + f + " to " + newF);
try {
if (get())
ModInfo.this.file = modManager.enableMod(path);
else
ModInfo.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), '.');
}
public File getFile() {
public Path getFile() {
return file;
}
@ -135,48 +134,4 @@ public final class ModInfo implements Comparable<ModInfo> {
public int hashCode() {
return Objects.hash(getFileName());
}
public static boolean isFileMod(File file) {
String name = file.getName();
if (isDisabled(file))
name = FileUtils.getNameWithoutExtension(file);
return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod");
}
public static ModInfo fromFile(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 ForgeModMetadata.fromFile(modFile);
} catch (Exception ignore) {
}
try {
return RiftModMetadata.fromFile(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.");
}
return new ModInfo(modFile, FileUtils.getNameWithoutExtension(modFile), description);
}
public static boolean isDisabled(File file) {
return DISABLED_EXTENSION.equals(FileUtils.getExtension(file));
}
public static final String DISABLED_EXTENSION = "disabled";
}

View File

@ -18,50 +18,97 @@
package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.StringUtils;
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.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.function.Consumer;
public final class ModManager {
private final GameRepository repository;
private final SimpleMultimap<String, ModInfo> modCache = new SimpleMultimap<String, ModInfo>(HashMap::new, TreeSet::new);
private final String id;
private final TreeSet<ModInfo> modInfos = new TreeSet<>();
public ModManager(GameRepository repository) {
private boolean loaded = false;
public ModManager(GameRepository repository, String id) {
this.repository = repository;
this.id = id;
}
public void refreshMods(String id) {
modCache.removeKey(id);
File modsDirectory = new File(repository.getRunDirectory(id), "mods");
Consumer<File> puter = modFile -> Lang.ignoringException(() -> modCache.put(id, ModInfo.fromFile(modFile)));
Optional.ofNullable(modsDirectory.listFiles()).map(Arrays::stream).ifPresent(files -> files.forEach(modFile -> {
if (modFile.isDirectory() && VersionNumber.isIntVersionNumber(modFile.getName())) {
private Path getModsDirectory() {
return repository.getRunDirectory(id).toPath().resolve("mods");
}
private void addModInfo(File file) {
try {
modInfos.add(getModInfo(file));
} catch (IllegalArgumentException ignore) {
}
}
public ModInfo 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 ForgeModMetadata.fromFile(this, modFile);
} catch (Exception ignore) {
}
try {
return RiftModMetadata.fromFile(this, modFile);
} catch (Exception ignore) {
}
description = "";
break;
case "litemod":
try {
return LiteModMetadata.fromFile(this, modFile);
} catch (Exception ignore) {
description = "LiteLoader Mod";
}
break;
default:
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
}
return new ModInfo(this, modFile, FileUtils.getNameWithoutExtension(modFile), description);
}
public void refreshMods() throws IOException {
modInfos.clear();
for (Path subitem : Files.newDirectoryStream(getModsDirectory())) {
if (Files.isDirectory(subitem) && VersionNumber.isIntVersionNumber(FileUtils.getName(subitem))) {
// If the folder name is game version, forge will search mod in this subdirectory
Optional.ofNullable(modFile.listFiles()).map(Arrays::stream).ifPresent(x -> x.forEach(puter));
for (Path subsubitem : Files.newDirectoryStream(subitem))
addModInfo(subsubitem.toFile());
} else {
puter.accept(modFile);
addModInfo(subitem.toFile());
}
}));
}
loaded = true;
}
public Collection<ModInfo> getMods(String id) {
if (!modCache.containsKey(id))
refreshMods(id);
return modCache.get(id);
public Collection<ModInfo> getMods() throws IOException {
if (!loaded)
refreshMods();
return modInfos;
}
public void addMod(String id, File file) throws IOException {
if (!ModInfo.isFileMod(file))
public void addMod(File file) throws IOException {
if (!isFileMod(file))
throw new IllegalArgumentException("File " + file + " is not a valid mod file.");
if (!modCache.containsKey(id))
refreshMods(id);
if (!loaded)
refreshMods();
File modsDirectory = new File(repository.getRunDirectory(id), "mods");
if (!FileUtils.makeDirectory(modsDirectory))
@ -70,12 +117,55 @@ public final class ModManager {
File newFile = new File(modsDirectory, file.getName());
FileUtils.copyFile(file, newFile);
modCache.put(id, ModInfo.fromFile(newFile));
addModInfo(newFile);
}
public boolean removeMods(String id, ModInfo... modInfos) {
boolean result = Arrays.stream(modInfos).reduce(true, (acc, modInfo) -> acc && modInfo.getFile().delete(), Boolean::logicalAnd);
refreshMods(id);
return result;
public void removeMods(ModInfo... modInfos) throws IOException {
for (ModInfo modInfo : modInfos) {
Files.deleteIfExists(modInfo.getFile());
}
refreshMods();
}
public Path disableMod(Path file) throws IOException {
Path disabled = file.getParent().resolve(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
if (Files.exists(file))
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
return disabled;
}
public Path enableMod(Path file) throws IOException {
Path enabled = file.getParent().resolve(StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
if (Files.exists(file))
Files.move(file, enabled, StandardCopyOption.REPLACE_EXISTING);
return enabled;
}
public boolean isDisabled(File file) {
return file.getPath().endsWith(DISABLED_EXTENSION);
}
public boolean isFileMod(File file) {
String name = file.getName();
if (isDisabled(file))
name = FileUtils.getNameWithoutExtension(file);
return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".litemod");
}
/**
* Check if "mods" directory has mod file named "fileName" no matter the mod is disabled or not
*
* @param fileName name of the file whose existence is being checked
* @return true if the file exists
*/
public boolean hasSimpleMod(String fileName) {
return Files.exists(getModsDirectory().resolve(StringUtils.removeSuffix(fileName, DISABLED_EXTENSION)))
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION)));
}
public Path getSimpleModPath(String fileName) {
return getModsDirectory().resolve(fileName);
}
public static final String DISABLED_EXTENSION = ".disabled";
}

View File

@ -60,14 +60,14 @@ public final class RiftModMetadata {
return authors;
}
public static ModInfo fromFile(File modFile) throws IOException, JsonParseException {
public static ModInfo fromFile(ModManager modManager, File modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
Path mcmod = fs.getPath("riftmod.json");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod.");
RiftModMetadata metadata = JsonUtils.fromNonNullJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)), RiftModMetadata.class);
String authors = metadata.getAuthors() == null ? "" : String.join(", ", metadata.getAuthors());
return new ModInfo(modFile, metadata.getName(), "",
return new ModInfo(modManager, modFile, metadata.getName(), "",
authors, "", "", "");
}
}

View File

@ -146,6 +146,13 @@ public final class StringUtils {
return prefix + str;
}
public static String addSuffix(String str, String suffix) {
if (str.endsWith(suffix))
return str;
else
return str + suffix;
}
public static String removePrefix(String str, String... prefixes) {
for (String prefix : prefixes)
if (str.startsWith(prefix))