feat(modpack): support installing Modrinth modpack.

This commit is contained in:
huanghongxun
2022-05-22 00:39:33 +08:00
parent c0df55ee62
commit 9684ce4a1b
45 changed files with 1522 additions and 661 deletions

View File

@@ -44,7 +44,7 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
@Override
public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> {
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.INSTANCE.getRemoteVersionsById("P7dR8mSH"))) {
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("P7dR8mSH"))) {
for (String gameVersion : modVersion.getGameVersions()) {
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
Collections.singletonList(modVersion.getFile().getUrl())));

View File

@@ -29,6 +29,7 @@ import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
@@ -37,20 +38,20 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
private final File zipFile;
private final Charset encoding;
private final String subDirectory;
private final List<String> subDirectories;
private final File jsonFile;
private final T manifest;
private final String type;
private final String name;
private final String version;
public MinecraftInstanceTask(File zipFile, Charset encoding, String subDirectory, T manifest, String type, String name, String version, File jsonFile) {
public MinecraftInstanceTask(File zipFile, Charset encoding, List<String> subDirectories, T manifest, ModpackProvider modpackProvider, String name, String version, File jsonFile) {
this.zipFile = zipFile;
this.encoding = encoding;
this.subDirectory = FileUtils.normalizePath(subDirectory);
this.subDirectories = subDirectories.stream().map(FileUtils::normalizePath).collect(Collectors.toList());
this.manifest = manifest;
this.jsonFile = jsonFile;
this.type = type;
this.type = modpackProvider.getName();
this.name = name;
this.version = version;
}
@@ -60,17 +61,19 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
List<ModpackConfiguration.FileInformation> overrides = new ArrayList<>();
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setEncoding(encoding).build()) {
Path root = fs.getPath(subDirectory);
for (String subDirectory : subDirectories) {
Path root = fs.getPath(subDirectory);
if (Files.exists(root))
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String relativePath = root.relativize(file).normalize().toString().replace(File.separatorChar, '/');
overrides.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", file))));
return FileVisitResult.CONTINUE;
}
});
if (Files.exists(root))
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String relativePath = root.relativize(file).normalize().toString().replace(File.separatorChar, '/');
overrides.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", file))));
return FileVisitResult.CONTINUE;
}
});
}
}
ModpackConfiguration<T> configuration = new ModpackConfiguration<>(manifest, type, name, version, overrides);

View File

@@ -35,13 +35,13 @@ public abstract class Modpack {
private String gameVersion;
private String description;
private transient Charset encoding;
private Object manifest;
private ModpackManifest manifest;
public Modpack() {
this("", null, null, null, null, null, null);
}
public Modpack(String name, String author, String version, String gameVersion, String description, Charset encoding, Object manifest) {
public Modpack(String name, String author, String version, String gameVersion, String description, Charset encoding, ModpackManifest manifest) {
this.name = name;
this.author = author;
this.version = version;
@@ -105,11 +105,11 @@ public abstract class Modpack {
return this;
}
public Object getManifest() {
public ModpackManifest getManifest() {
return manifest;
}
public Modpack setManifest(Object manifest) {
public Modpack setManifest(ModpackManifest manifest) {
this.manifest = manifest;
return this;
}

View File

@@ -15,21 +15,21 @@
* 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.curse;
package org.jackhuang.hmcl.mod;
public class CurseCompletionException extends Exception {
public CurseCompletionException() {
public class ModpackCompletionException extends Exception {
public ModpackCompletionException() {
}
public CurseCompletionException(String message) {
public ModpackCompletionException(String message) {
super(message);
}
public CurseCompletionException(String message, Throwable cause) {
public ModpackCompletionException(String message, Throwable cause) {
super(message, cause);
}
public CurseCompletionException(Throwable cause) {
public ModpackCompletionException(Throwable cause) {
super(cause);
}
}

View File

@@ -36,7 +36,7 @@ public class ModpackInstallTask<T> extends Task<Void> {
private final File modpackFile;
private final File dest;
private final Charset charset;
private final String subDirectory;
private final List<String> subDirectories;
private final List<ModpackConfiguration.FileInformation> overrides;
private final Predicate<String> callback;
@@ -45,15 +45,15 @@ public class ModpackInstallTask<T> extends Task<Void> {
* @param modpackFile a zip file
* @param dest destination to store unpacked files
* @param charset charset of the zip file
* @param subDirectory the subdirectory of zip file to unpack
* @param subDirectories the subdirectory of zip file to unpack
* @param callback test whether the file (given full path) in zip file should be unpacked or not
* @param oldConfiguration old modpack information if upgrade
*/
public ModpackInstallTask(File modpackFile, File dest, Charset charset, String subDirectory, Predicate<String> callback, ModpackConfiguration<T> oldConfiguration) {
public ModpackInstallTask(File modpackFile, File dest, Charset charset, List<String> subDirectories, Predicate<String> callback, ModpackConfiguration<T> oldConfiguration) {
this.modpackFile = modpackFile;
this.dest = dest;
this.charset = charset;
this.subDirectory = subDirectory;
this.subDirectories = subDirectories;
this.callback = callback;
if (oldConfiguration == null)
@@ -72,30 +72,33 @@ public class ModpackInstallTask<T> extends Task<Void> {
for (ModpackConfiguration.FileInformation file : overrides)
files.put(file.getPath(), file);
new Unzipper(modpackFile, dest)
.setSubDirectory(subDirectory)
.setTerminateIfSubDirectoryNotExists()
.setReplaceExistentFile(true)
.setEncoding(charset)
.setFilter((destPath, isDirectory, zipEntry, entryPath) -> {
if (isDirectory) return true;
if (!callback.test(entryPath)) return false;
entries.add(entryPath);
if (!files.containsKey(entryPath)) {
// If old modpack does not have this entry, add this entry or override the file that user added.
return true;
} else if (!Files.exists(destPath)) {
// If both old and new modpacks have this entry, but the file is deleted by user, leave it missing.
return false;
} else {
// If both old and new modpacks have this entry, and user has modified this file,
// we will not replace it since this modified file is what user expects.
String fileHash = encodeHex(digest("SHA-1", destPath));
String oldHash = files.get(entryPath).getHash();
return Objects.equals(oldHash, fileHash);
}
}).unzip();
for (String subDirectory : subDirectories) {
new Unzipper(modpackFile, dest)
.setSubDirectory(subDirectory)
.setTerminateIfSubDirectoryNotExists()
.setReplaceExistentFile(true)
.setEncoding(charset)
.setFilter((destPath, isDirectory, zipEntry, entryPath) -> {
if (isDirectory) return true;
if (!callback.test(entryPath)) return false;
entries.add(entryPath);
if (!files.containsKey(entryPath)) {
// If old modpack does not have this entry, add this entry or override the file that user added.
return true;
} else if (!Files.exists(destPath)) {
// If both old and new modpacks have this entry, but the file is deleted by user, leave it missing.
return false;
} else {
// If both old and new modpacks have this entry, and user has modified this file,
// we will not replace it since this modified file is what user expects.
String fileHash = encodeHex(digest("SHA-1", destPath));
String oldHash = files.get(entryPath).getHash();
return Objects.equals(oldHash, fileHash);
}
}).unzip();
}
// If old modpack have this entry, and new modpack deleted it. Delete this file.
for (ModpackConfiguration.FileInformation file : overrides) {

View File

@@ -0,0 +1,22 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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;
public interface ModpackManifest {
ModpackProvider getProvider();
}

View File

@@ -0,0 +1,51 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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 com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.task.Task;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public interface ModpackProvider {
String getName();
Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version);
Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException;
/**
* @param zipFile the opened modpack zip file.
* @param file the modpack zip file path.
* @param encoding encoding of zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest.
*/
Modpack readManifest(ZipFile zipFile, Path file, Charset encoding) throws IOException, JsonParseException;
default void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
}
}

View File

@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
@@ -122,11 +123,11 @@ public final class CurseCompletionTask extends Task<Void> {
RemoteMod.File remoteFile = CurseForgeRemoteModRepository.MODS.getModFile(Integer.toString(file.getProjectID()), Integer.toString(file.getFileID()));
return file.withFileName(remoteFile.getFilename()).withURL(remoteFile.getUrl());
} catch (FileNotFoundException fof) {
Logging.LOG.log(Level.WARNING, "Could not query api.curseforge.com for deleted mods: " + file.getProjectID() + ", " +file.getFileID(), fof);
Logging.LOG.log(Level.WARNING, "Could not query api.curseforge.com for deleted mods: " + file.getProjectID() + ", " + file.getFileID(), fof);
notFound.set(true);
return file;
} catch (IOException | JsonParseException e) {
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name projectID=" + file.getProjectID() + ", fileID=" +file.getFileID(), e);
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name projectID=" + file.getProjectID() + ", fileID=" + file.getFileID(), e);
allNameKnown.set(false);
return file;
}
@@ -163,8 +164,8 @@ public final class CurseCompletionTask extends Task<Void> {
// Let this task fail if the curse manifest has not been completed.
// But continue other downloads.
if (notFound.get())
throw new CurseCompletionException(new FileNotFoundException());
throw new ModpackCompletionException(new FileNotFoundException());
if (!allNameKnown.get() || !isDependenciesSucceeded())
throw new CurseCompletionException();
throw new ModpackCompletionException();
}
}

View File

@@ -22,10 +22,7 @@ import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -35,6 +32,7 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
@@ -78,17 +76,19 @@ public final class CurseInstallTask extends Task<Void> {
throw new IllegalArgumentException("Version " + name + " already exists.");
GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.getMinecraft().getGameVersion());
for (CurseManifestModLoader modLoader : manifest.getMinecraft().getModLoaders())
if (modLoader.getId().startsWith("forge-"))
for (CurseManifestModLoader modLoader : manifest.getMinecraft().getModLoaders()) {
if (modLoader.getId().startsWith("forge-")) {
builder.version("forge", modLoader.getId().substring("forge-".length()));
else if (modLoader.getId().startsWith("fabric-"))
} else if (modLoader.getId().startsWith("fabric-")) {
builder.version("fabric", modLoader.getId().substring("fabric-".length()));
}
}
dependents.add(builder.buildAsync());
onDone().register(event -> {
Exception ex = event.getTask().getException();
if (event.isFailed()) {
if (!(ex instanceof CurseCompletionException)) {
if (!(ex instanceof ModpackCompletionException)) {
repository.removeVersionFromDisk(name);
}
}
@@ -100,14 +100,14 @@ public final class CurseInstallTask extends Task<Void> {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<CurseManifest>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
if (!CurseModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Curse modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
this.config = config;
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), manifest.getOverrides(), any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), manifest.getOverrides(), manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(manifest.getOverrides()), any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(manifest.getOverrides()), manifest, CurseModpackProvider.INSTANCE, manifest.getName(), manifest.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependencies.add(new CurseCompletionTask(dependencyManager, name, manifest));
}
@@ -139,6 +139,4 @@ public final class CurseInstallTask extends Task<Void> {
File root = repository.getVersionRoot(name);
FileUtils.writeText(new File(root, "manifest.json"), JsonUtils.GSON.toJson(manifest));
}
public static final String MODPACK_TYPE = "Curse";
}

View File

@@ -17,21 +17,11 @@
*/
package org.jackhuang.hmcl.mod.curse;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
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.IOUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
@@ -40,7 +30,7 @@ import java.util.List;
* @author huangyuhui
*/
@Immutable
public final class CurseManifest {
public final class CurseManifest implements ModpackManifest {
@SerializedName("manifestType")
private final String manifestType;
@@ -117,28 +107,9 @@ public final class CurseManifest {
return new CurseManifest(manifestType, manifestVersion, name, version, author, overrides, minecraft, files);
}
/**
* @param zip the CurseForge modpack file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest.
*/
public static Modpack readCurseForgeModpackManifest(ZipFile zip, Charset encoding) throws IOException, JsonParseException {
CurseManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "manifest.json"), CurseManifest.class);
String description = "No description";
try {
ZipArchiveEntry modlist = zip.getEntry("modlist.html");
if (modlist != null)
description = IOUtils.readFullyAsString(zip.getInputStream(modlist));
} catch (Throwable ignored) {
}
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(), description, encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
}
};
@Override
public ModpackProvider getProvider() {
return CurseModpackProvider.INSTANCE;
}
public static final String MINECRAFT_MODPACK = "minecraftModpack";

View File

@@ -0,0 +1,78 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.curse;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class CurseModpackProvider implements ModpackProvider {
public static final CurseModpackProvider INSTANCE = new CurseModpackProvider();
@Override
public String getName() {
return "Curse";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new CurseCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof CurseManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, (CurseManifest) modpack.getManifest(), name));
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
CurseManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "manifest.json"), CurseManifest.class);
String description = "No description";
try {
ZipArchiveEntry modlist = zip.getEntry("modlist.html");
if (modlist != null)
description = IOUtils.readFullyAsString(zip.getInputStream(modlist));
} catch (Throwable ignored) {
}
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(), description, encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
}
};
}
}

View File

@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseMetaMod;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.util.Logging;
@@ -269,9 +269,9 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
// Let this task fail if the curse manifest has not been completed.
// But continue other downloads.
if (notFound.get())
throw new CurseCompletionException(new FileNotFoundException());
throw new ModpackCompletionException(new FileNotFoundException());
if (!allNameKnown.get() || ex != null)
throw new CurseCompletionException();
throw new ModpackCompletionException();
})));
}));
}

View File

@@ -34,6 +34,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -82,13 +83,13 @@ public class McbbsModpackLocalInstallTask extends Task<Void> {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
if (!McbbsModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/overrides", any -> true, config).withStage("hmcl.modpack"));
instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/overrides", manifest, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name));
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack"));
instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, McbbsModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name));
dependents.add(instanceTask.withStage("hmcl.modpack"));
}
@@ -122,5 +123,4 @@ public class McbbsModpackLocalInstallTask extends Task<Void> {
}
private static final String PATCH_NAME = "mcbbs";
public static final String MODPACK_TYPE = "Mcbbs";
}

View File

@@ -19,15 +19,14 @@ package org.jackhuang.hmcl.mod.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable;
@@ -41,7 +40,7 @@ import java.util.Optional;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
public class McbbsModpackManifest implements Validation {
public class McbbsModpackManifest implements ModpackManifest, Validation {
public static final String MANIFEST_TYPE = "minecraftModpack";
private final String manifestType;
@@ -150,6 +149,11 @@ public class McbbsModpackManifest implements Validation {
return new McbbsModpackManifest(manifestType, manifestVersion, name, version, author, description, fileApi, url, forceUpdate, origins, addons, libraries, files, settings, launchInfo);
}
@Override
public ModpackProvider getProvider() {
return McbbsModpackProvider.INSTANCE;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (!MANIFEST_TYPE.equals(manifestType))
@@ -431,27 +435,4 @@ public class McbbsModpackManifest implements Validation {
launchOptions.getJavaArguments().addAll(launchInfo.getJavaArguments());
}
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding);
}
/**
* @param zip the MCBBS modpack file.
* @param encoding the modpack zip file encoding.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the server-manifest.json is missing or malformed.
* @return the manifest.
*/
public static Modpack readManifest(ZipFile zip, Charset encoding) throws IOException, JsonParseException {
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
if (mcbbsPackMeta != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
}
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
if (manifestJson != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(manifestJson)), encoding);
}
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
}
}

View File

@@ -0,0 +1,86 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
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.charset.Charset;
import java.nio.file.Path;
public final class McbbsModpackProvider implements ModpackProvider {
public static final McbbsModpackProvider INSTANCE = new McbbsModpackProvider();
@Override
public String getName() {
return "Mcbbs";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new McbbsModpackCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new McbbsModpackLocalInstallTask(dependencyManager, zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
}
@Override
public void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(modpackConfigurationJson, new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
if (!getName().equals(config.getType())) {
throw new IllegalArgumentException("Incorrect manifest type, actual=" + config.getType() + ", expected=" + getName());
}
config.getManifest().injectLaunchOptions(builder);
}
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding);
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
if (mcbbsPackMeta != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
}
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
if (manifestJson != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(manifestJson)), encoding);
}
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
}
}

View File

@@ -0,0 +1,134 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.modrinth;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
public class ModrinthCompletionTask extends Task<Void> {
private final DefaultDependencyManager dependency;
private final DefaultGameRepository repository;
private final String version;
private ModrinthManifest manifest;
private final List<Task<?>> dependencies = new ArrayList<>();
private final AtomicBoolean allNameKnown = new AtomicBoolean(true);
private final AtomicInteger finished = new AtomicInteger(0);
private final AtomicBoolean notFound = new AtomicBoolean(false);
/**
* Constructor.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
*/
public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version) {
this(dependencyManager, version, null);
}
/**
* Constructor.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
* @param manifest the CurseForgeModpack manifest.
*/
public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) {
this.dependency = dependencyManager;
this.repository = dependencyManager.getGameRepository();
this.version = version;
this.manifest = manifest;
if (manifest == null)
try {
File manifestFile = new File(repository.getVersionRoot(version), "modrinth.index.json");
if (manifestFile.exists())
this.manifest = JsonUtils.GSON.fromJson(FileUtils.readText(manifestFile), ModrinthManifest.class);
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to read Modrinth modpack manifest.json", e);
}
setStage("hmcl.modpack.download");
}
@Override
public Collection<Task<?>> getDependencies() {
return dependencies;
}
@Override
public boolean isRelyingOnDependencies() {
return false;
}
@Override
public void execute() throws Exception {
if (manifest == null)
return;
Path runDirectory = repository.getRunDirectory(version).toPath();
for (ModrinthManifest.File file : manifest.getFiles()) {
Path filePath = runDirectory.resolve(file.getPath());
if (!Files.exists(filePath) && !file.getDownloads().isEmpty()) {
FileDownloadTask task = new FileDownloadTask(file.getDownloads().get(0), filePath.toFile());
task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true);
dependencies.add(task.withCounter("hmcl.modpack.download"));
}
}
if (!dependencies.isEmpty()) {
getProperties().put("total", dependencies.size());
notifyPropertiesChanged();
}
}
@Override
public boolean doPostExecute() {
return true;
}
@Override
public void postExecute() throws Exception {
// Let this task fail if the curse manifest has not been completed.
// But continue other downloads.
if (notFound.get())
throw new ModpackCompletionException(new FileNotFoundException());
if (!allNameKnown.get() || !isDependenciesSucceeded())
throw new ModpackCompletionException();
}
}

View File

@@ -0,0 +1,137 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.modrinth;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseManifest;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class ModrinthInstallTask extends Task<Void> {
private final DefaultDependencyManager dependencyManager;
private final DefaultGameRepository repository;
private final File zipFile;
private final Modpack modpack;
private final ModrinthManifest manifest;
private final String name;
private final File run;
private final ModpackConfiguration<ModrinthManifest> config;
private final List<Task<?>> dependents = new ArrayList<>(4);
private final List<Task<?>> dependencies = new ArrayList<>(1);
public ModrinthInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, ModrinthManifest manifest, String name) {
this.dependencyManager = dependencyManager;
this.zipFile = zipFile;
this.modpack = modpack;
this.manifest = manifest;
this.name = name;
this.repository = dependencyManager.getGameRepository();
this.run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists.");
GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.getGameVersion());
for (Map.Entry<String, String> modLoader : manifest.getDependencies().entrySet()) {
switch (modLoader.getKey()) {
case "minecraft":
break;
case "forge":
builder.version("forge", modLoader.getValue());
break;
case "fabric-loader":
builder.version("fabric", modLoader.getValue());
break;
case "quilt-loader":
throw new IllegalStateException("Quilt Modloader is not supported");
default:
throw new IllegalStateException("Unsupported mod loader " + modLoader.getKey());
}
}
dependents.add(builder.buildAsync());
onDone().register(event -> {
Exception ex = event.getTask().getException();
if (event.isFailed()) {
if (!(ex instanceof ModpackCompletionException)) {
repository.removeVersionFromDisk(name);
}
}
});
ModpackConfiguration<ModrinthManifest> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<CurseManifest>>() {
}.getType());
if (!ModrinthModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Modrinth modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
this.config = config;
List<String> subDirectories = Arrays.asList("/client-overrides", "/overrides");
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), subDirectories, any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependencies.add(new ModrinthCompletionTask(dependencyManager, name, manifest));
}
@Override
public Collection<Task<?>> getDependents() {
return dependents;
}
@Override
public Collection<Task<?>> getDependencies() {
return dependencies;
}
@Override
public void execute() throws Exception {
if (config != null) {
// For update, remove mods not listed in new manifest
for (ModrinthManifest.File oldManifestFile : config.getManifest().getFiles()) {
Path oldFile = run.toPath().resolve(oldManifestFile.getPath());
if (!Files.exists(oldFile)) continue;
if (manifest.getFiles().stream().noneMatch(oldManifestFile::equals)) {
Files.deleteIfExists(oldFile);
}
}
}
File root = repository.getVersionRoot(name);
FileUtils.writeText(new File(root, "modrinth.index.json"), JsonUtils.GSON.toJson(manifest));
}
}

View File

@@ -0,0 +1,146 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.modrinth;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class ModrinthManifest implements ModpackManifest, Validation {
private final String game;
private final int formatVersion;
private final String versionId;
private final String name;
@Nullable
private final String summary;
private final List<File> files;
private final Map<String, String> dependencies;
public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List<File> files, Map<String, String> dependencies) {
this.game = game;
this.formatVersion = formatVersion;
this.versionId = versionId;
this.name = name;
this.summary = summary;
this.files = files;
this.dependencies = dependencies;
}
public String getGame() {
return game;
}
public int getFormatVersion() {
return formatVersion;
}
public String getVersionId() {
return versionId;
}
public String getName() {
return name;
}
public String getSummary() {
return summary == null ? "" : summary;
}
public List<File> getFiles() {
return files;
}
public Map<String, String> getDependencies() {
return dependencies;
}
public String getGameVersion() {
return dependencies.get("minecraft");
}
@Override
public ModpackProvider getProvider() {
return ModrinthModpackProvider.INSTANCE;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (dependencies == null || dependencies.get("minecraft") == null) {
throw new JsonParseException("missing Modrinth.dependencies.minecraft");
}
}
public static class File {
private final String path;
private final Map<String, String> hashes;
private final Map<String, String> env;
private final List<URL> downloads;
private final int fileSize;
public File(String path, Map<String, String> hashes, Map<String, String> env, List<URL> downloads, int fileSize) {
this.path = path;
this.hashes = hashes;
this.env = env;
this.downloads = downloads;
this.fileSize = fileSize;
}
public String getPath() {
return path;
}
public Map<String, String> getHashes() {
return hashes;
}
public Map<String, String> getEnv() {
return env;
}
public List<URL> getDownloads() {
return downloads;
}
public int getFileSize() {
return fileSize;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
File file = (File) o;
return fileSize == file.fileSize && path.equals(file.path) && hashes.equals(file.hashes) && env.equals(file.env) && downloads.equals(file.downloads);
}
@Override
public int hashCode() {
return Objects.hash(path, hashes, env, downloads, fileSize);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.modrinth;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class ModrinthModpackProvider implements ModpackProvider {
public static final ModrinthModpackProvider INSTANCE = new ModrinthModpackProvider();
@Override
public String getName() {
return "Modrinth";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new ModrinthCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof ModrinthManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ModrinthInstallTask(dependencyManager, zipFile, modpack, (ModrinthManifest) modpack.getManifest(), name));
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
ModrinthManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class);
return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, java.io.File zipFile, String name) {
return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, name);
}
};
}
}

View File

@@ -23,17 +23,14 @@ import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -42,11 +39,15 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class ModrinthRemoteModRepository implements RemoteModRepository {
public static final ModrinthRemoteModRepository INSTANCE = new ModrinthRemoteModRepository();
public static final ModrinthRemoteModRepository MODS = new ModrinthRemoteModRepository("mod");
public static final ModrinthRemoteModRepository MODPACKS = new ModrinthRemoteModRepository("modpack");
private static final String PREFIX = "https://api.modrinth.com";
private ModrinthRemoteModRepository() {
private final String projectType;
private ModrinthRemoteModRepository(String projectType) {
this.projectType = projectType;
}
@Override
@@ -73,19 +74,22 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
@Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
List<List<String>> facets = new ArrayList<>();
facets.add(Collections.singletonList("project_type:" + projectType));
if (StringUtils.isNotBlank(gameVersion)) {
facets.add(Collections.singletonList("versions:" + gameVersion));
}
Map<String, String> query = mapOf(
pair("query", searchFilter),
pair("facets", JsonUtils.UGLY_GSON.toJson(facets)),
pair("offset", Integer.toString(pageOffset)),
pair("limit", Integer.toString(pageSize)),
pair("index", convertSortType(sort))
);
if (StringUtils.isNotBlank(gameVersion)) {
query.put("version", "versions=" + gameVersion);
}
Response<ModResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/api/v1/mod", query))
.getJson(new TypeToken<Response<ModResult>>() {
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
.getJson(new TypeToken<Response<ProjectSearchResult>>() {
}.getType());
return response.getHits().stream().map(ModResult::toMod);
return response.getHits().stream().map(ProjectSearchResult::toMod);
}
@Override
@@ -93,9 +97,9 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
String sha1 = Hex.encodeHex(DigestUtils.digest("SHA-1", file));
try {
ModVersion mod = HttpRequest.GET(PREFIX + "/api/v1/version_file/" + sha1,
ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
pair("algorithm", "sha1"))
.getJson(ModVersion.class);
.getJson(ProjectVersion.class);
return mod.toVersion();
} catch (ResponseCodeException e) {
if (e.getResponseCode() == 404) {
@@ -119,14 +123,14 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
@Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
id = StringUtils.removePrefix(id, "local-");
List<ModVersion> versions = HttpRequest.GET("https://api.modrinth.com/api/v1/mod/" + id + "/version")
.getJson(new TypeToken<List<ModVersion>>() {
List<ProjectVersion> versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version")
.getJson(new TypeToken<List<ProjectVersion>>() {
}.getType());
return versions.stream().map(ModVersion::toVersion).flatMap(Lang::toStream);
return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream);
}
public List<String> getCategoriesImpl() throws IOException {
return HttpRequest.GET("https://api.modrinth.com/api/v1/tag/category").getJson(new TypeToken<List<String>>() {
return HttpRequest.GET(PREFIX + "/v2/tag/category").getJson(new TypeToken<List<String>>() {
}.getType());
}
@@ -135,56 +139,58 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
.map(name -> new Category(null, name, Collections.emptyList()));
}
public static class Mod {
private final String id;
public static class Project {
private final String slug;
private final String team;
private final String title;
private final String description;
private final Instant published;
private final Instant updated;
private final List<String> categories;
private final List<String> versions;
/**
* A long body describing project in detail.
*/
private final String body;
@SerializedName("project_type")
private final String projectType;
private final int downloads;
@SerializedName("icon_url")
private final String iconUrl;
public Mod(String id, String slug, String team, String title, String description, Instant published, Instant updated, List<String> categories, List<String> versions, int downloads, String iconUrl) {
this.id = id;
private final String id;
private final String team;
private final Date published;
private final Date updated;
private final List<String> versions;
public Project(String slug, String title, String description, List<String> categories, String body, String projectType, int downloads, String iconUrl, String id, String team, Date published, Date updated, List<String> versions) {
this.slug = slug;
this.team = team;
this.title = title;
this.description = description;
this.published = published;
this.updated = updated;
this.categories = categories;
this.versions = versions;
this.body = body;
this.projectType = projectType;
this.downloads = downloads;
this.iconUrl = iconUrl;
}
public String getId() {
return id;
this.id = id;
this.team = team;
this.published = published;
this.updated = updated;
this.versions = versions;
}
public String getSlug() {
return slug;
}
public String getTeam() {
return team;
}
public String getTitle() {
return title;
}
@@ -193,20 +199,16 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
return description;
}
public Instant getPublished() {
return published;
}
public Instant getUpdated() {
return updated;
}
public List<String> getCategories() {
return categories;
}
public List<String> getVersions() {
return versions;
public String getBody() {
return body;
}
public String getProjectType() {
return projectType;
}
public int getDownloads() {
@@ -216,17 +218,59 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
public String getIconUrl() {
return iconUrl;
}
public String getId() {
return id;
}
public String getTeam() {
return team;
}
public Date getPublished() {
return published;
}
public Date getUpdated() {
return updated;
}
public List<String> getVersions() {
return versions;
}
}
public static class ModVersion implements RemoteMod.IVersion {
private final String id;
@Immutable
public static class Dependency {
@SerializedName("version_id")
private final String versionId;
@SerializedName("mod_id")
private final String modId;
@SerializedName("project_id")
private final String projectId;
@SerializedName("author_id")
private final String authorId;
@SerializedName("dependency_type")
private final String dependencyType;
public Dependency(String versionId, String projectId, String dependencyType) {
this.versionId = versionId;
this.projectId = projectId;
this.dependencyType = dependencyType;
}
public String getVersionId() {
return versionId;
}
public String getProjectId() {
return projectId;
}
public String getDependencyType() {
return dependencyType;
}
}
public static class ProjectVersion implements RemoteMod.IVersion {
private final String name;
@SerializedName("version_number")
@@ -234,49 +278,52 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
private final String changelog;
private final List<Dependency> dependencies;
@SerializedName("game_versions")
private final List<String> gameVersions;
@SerializedName("version_type")
private final String versionType;
private final List<String> loaders;
private final boolean featured;
private final String id;
@SerializedName("project_id")
private final String projectId;
@SerializedName("author_id")
private final String authorId;
@SerializedName("date_published")
private final Date datePublished;
private final int downloads;
@SerializedName("version_type")
private final String versionType;
@SerializedName("changelog_url")
private final String changelogUrl;
private final List<ModVersionFile> files;
private final List<ProjectVersionFile> files;
private final List<String> dependencies;
@SerializedName("game_versions")
private final List<String> gameVersions;
private final List<String> loaders;
public ModVersion(String id, String modId, String authorId, String name, String versionNumber, String changelog, Date datePublished, int downloads, String versionType, List<ModVersionFile> files, List<String> dependencies, List<String> gameVersions, List<String> loaders) {
this.id = id;
this.modId = modId;
this.authorId = authorId;
public ProjectVersion(String name, String versionNumber, String changelog, List<Dependency> dependencies, List<String> gameVersions, String versionType, List<String> loaders, boolean featured, String id, String projectId, String authorId, Date datePublished, int downloads, String changelogUrl, List<ProjectVersionFile> files) {
this.name = name;
this.versionNumber = versionNumber;
this.changelog = changelog;
this.datePublished = datePublished;
this.downloads = downloads;
this.versionType = versionType;
this.files = files;
this.dependencies = dependencies;
this.gameVersions = gameVersions;
this.versionType = versionType;
this.loaders = loaders;
}
public String getId() {
return id;
}
public String getModId() {
return modId;
}
public String getAuthorId() {
return authorId;
this.featured = featured;
this.id = id;
this.projectId = projectId;
this.authorId = authorId;
this.datePublished = datePublished;
this.downloads = downloads;
this.changelogUrl = changelogUrl;
this.files = files;
}
public String getName() {
@@ -291,6 +338,38 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
return changelog;
}
public List<Dependency> getDependencies() {
return dependencies;
}
public List<String> getGameVersions() {
return gameVersions;
}
public String getVersionType() {
return versionType;
}
public List<String> getLoaders() {
return loaders;
}
public boolean isFeatured() {
return featured;
}
public String getId() {
return id;
}
public String getProjectId() {
return projectId;
}
public String getAuthorId() {
return authorId;
}
public Date getDatePublished() {
return datePublished;
}
@@ -299,26 +378,14 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
return downloads;
}
public String getVersionType() {
return versionType;
public String getChangelogUrl() {
return changelogUrl;
}
public List<ModVersionFile> getFiles() {
public List<ProjectVersionFile> getFiles() {
return files;
}
public List<String> getDependencies() {
return dependencies;
}
public List<String> getGameVersions() {
return gameVersions;
}
public List<String> getLoaders() {
return loaders;
}
@Override
public RemoteMod.Type getType() {
return RemoteMod.Type.MODRINTH;
@@ -342,14 +409,14 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
return Optional.of(new RemoteMod.Version(
this,
modId,
projectId,
name,
versionNumber,
changelog,
datePublished,
type,
files.get(0).toFile(),
dependencies,
dependencies.stream().map(Dependency::getProjectId).collect(Collectors.toList()),
gameVersions,
loaders.stream().flatMap(loader -> {
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
@@ -360,15 +427,19 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
}
}
public static class ModVersionFile {
public static class ProjectVersionFile {
private final Map<String, String> hashes;
private final String url;
private final String filename;
private final boolean primary;
private final int size;
public ModVersionFile(Map<String, String> hashes, String url, String filename) {
public ProjectVersionFile(Map<String, String> hashes, String url, String filename, boolean primary, int size) {
this.hashes = hashes;
this.url = url;
this.filename = filename;
this.primary = primary;
this.size = size;
}
public Map<String, String> getHashes() {
@@ -383,76 +454,72 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
return filename;
}
public boolean isPrimary() {
return primary;
}
public int getSize() {
return size;
}
public RemoteMod.File toFile() {
return new RemoteMod.File(hashes, url, filename);
}
}
public static class ModResult implements RemoteMod.IMod {
@SerializedName("mod_id")
private final String modId;
public static class ProjectSearchResult implements RemoteMod.IMod {
private final String slug;
private final String author;
private final String title;
private final String description;
private final List<String> categories;
private final List<String> versions;
@SerializedName("project_type")
private final String projectType;
private final int downloads;
@SerializedName("page_url")
private final String pageUrl;
@SerializedName("icon_url")
private final String iconUrl;
@SerializedName("author_url")
private final String authorUrl;
@SerializedName("project_id")
private final String projectId;
private final String author;
private final List<String> versions;
@SerializedName("date_created")
private final Instant dateCreated;
private final Date dateCreated;
@SerializedName("date_modified")
private final Instant dateModified;
private final Date dateModified;
@SerializedName("latest_version")
private final String latestVersion;
public ModResult(String modId, String slug, String author, String title, String description, List<String> categories, List<String> versions, int downloads, String pageUrl, String iconUrl, String authorUrl, Instant dateCreated, Instant dateModified, String latestVersion) {
this.modId = modId;
public ProjectSearchResult(String slug, String title, String description, List<String> categories, String projectType, int downloads, String iconUrl, String projectId, String author, List<String> versions, Date dateCreated, Date dateModified, String latestVersion) {
this.slug = slug;
this.author = author;
this.title = title;
this.description = description;
this.categories = categories;
this.versions = versions;
this.projectType = projectType;
this.downloads = downloads;
this.pageUrl = pageUrl;
this.iconUrl = iconUrl;
this.authorUrl = authorUrl;
this.projectId = projectId;
this.author = author;
this.versions = versions;
this.dateCreated = dateCreated;
this.dateModified = dateModified;
this.latestVersion = latestVersion;
}
public String getModId() {
return modId;
}
public String getSlug() {
return slug;
}
public String getAuthor() {
return author;
}
public String getTitle() {
return title;
}
@@ -465,31 +532,35 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
return categories;
}
public List<String> getVersions() {
return versions;
public String getProjectType() {
return projectType;
}
public int getDownloads() {
return downloads;
}
public String getPageUrl() {
return pageUrl;
}
public String getIconUrl() {
return iconUrl;
}
public String getAuthorUrl() {
return authorUrl;
public String getProjectId() {
return projectId;
}
public Instant getDateCreated() {
public String getAuthor() {
return author;
}
public List<String> getVersions() {
return versions;
}
public Date getDateCreated() {
return dateCreated;
}
public Instant getDateModified() {
public Date getDateModified() {
return dateModified;
}
@@ -504,7 +575,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
@Override
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
return modRepository.getRemoteVersionsById(getModId());
return modRepository.getRemoteVersionsById(getProjectId());
}
public RemoteMod toMod() {
@@ -514,7 +585,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
title,
description,
categories,
pageUrl,
String.format("https://modrinth.com/%s/%s", projectType, projectId),
iconUrl,
this
);

View File

@@ -17,32 +17,22 @@
*/
package org.jackhuang.hmcl.mod.multimc;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Stream;
/**
*
* @author huangyuhui
*/
public final class MultiMCInstanceConfiguration {
public final class MultiMCInstanceConfiguration implements ModpackManifest {
private final String instanceType; // InstanceType
private final String name; // name
@@ -71,7 +61,7 @@ public final class MultiMCInstanceConfiguration {
private final MultiMCManifest mmcPack;
private MultiMCInstanceConfiguration(String defaultName, InputStream contentStream, MultiMCManifest mmcPack) throws IOException {
MultiMCInstanceConfiguration(String defaultName, InputStream contentStream, MultiMCManifest mmcPack) throws IOException {
Properties p = new Properties();
p.load(new InputStreamReader(contentStream, StandardCharsets.UTF_8));
@@ -335,58 +325,9 @@ public final class MultiMCInstanceConfiguration {
return mmcPack;
}
private static boolean testPath(Path root) {
return Files.exists(root.resolve("instance.cfg"));
}
public static Path getRootPath(Path root) throws IOException {
if (testPath(root)) return root;
try (Stream<Path> stream = Files.list(root)) {
Path candidate = stream.filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
if (testPath(candidate)) return candidate;
throw new IOException("Not a valid MultiMC modpack");
}
}
public static String getRootEntryName(ZipFile file) throws IOException {
final String instanceFileName = "instance.cfg";
if (file.getEntry(instanceFileName) != null) return "";
Enumeration<ZipArchiveEntry> entries = file.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
String entryName = entry.getName();
int idx = entryName.indexOf('/');
if (idx >= 0
&& entryName.length() == idx + instanceFileName.length() + 1
&& entryName.startsWith(instanceFileName, idx + 1))
return entryName.substring(0, idx + 1);
}
throw new IOException("Not a valid MultiMC modpack");
}
public static Modpack readMultiMCModpackManifest(ZipFile modpackFile, Path modpackPath, Charset encoding) throws IOException {
String rootEntryName = getRootEntryName(modpackFile);
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile, rootEntryName);
String name = rootEntryName.isEmpty() ? FileUtils.getNameWithoutExtension(modpackPath) : rootEntryName.substring(0, rootEntryName.length() - 1);
ZipArchiveEntry instanceEntry = modpackFile.getEntry(rootEntryName + "instance.cfg");
if (instanceEntry == null)
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
try (InputStream instanceStream = modpackFile.getInputStream(instanceEntry)) {
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
}
};
}
@Override
public ModpackProvider getProvider() {
return MultiMCModpackProvider.INSTANCE;
}
}

View File

@@ -25,7 +25,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.IOException;
import java.util.*;
import java.util.List;
@Immutable
public final class MultiMCManifest {

View File

@@ -40,6 +40,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -118,20 +119,26 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<MultiMCInstanceConfiguration>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
if (!MultiMCModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a MultiMC modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
String subDirectory;
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setEncoding(modpack.getEncoding()).build()) {
if (Files.exists(fs.getPath("/" + manifest.getName() + "/.minecraft")))
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/" + manifest.getName() + "/.minecraft", any -> true, config).withStage("hmcl.modpack"));
else if (Files.exists(fs.getPath("/" + manifest.getName() + "/minecraft")))
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/" + manifest.getName() + "/minecraft", any -> true, config).withStage("hmcl.modpack"));
if (Files.exists(fs.getPath("/" + manifest.getName() + "/.minecraft"))) {
subDirectory = "/" + manifest.getName() + "/.minecraft";
} else if (Files.exists(fs.getPath("/" + manifest.getName() + "/minecraft"))) {
subDirectory = "/" + manifest.getName() + "/minecraft";
} else {
subDirectory = "/" + manifest.getName() + "/minecraft";
}
}
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/" + manifest.getName() + "/minecraft", manifest, MODPACK_TYPE, manifest.getName(), null, repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(subDirectory), any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(subDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
@@ -144,7 +151,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
Version version = repository.readVersionJson(name);
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) {
Path root = MultiMCInstanceConfiguration.getRootPath(fs.getPath("/"));
Path root = MultiMCModpackProvider.getRootPath(fs.getPath("/"));
Path patches = root.resolve("patches");
if (Files.exists(patches)) {
@@ -178,6 +185,4 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
dependencies.add(repository.saveAsync(version));
}
public static final String MODPACK_TYPE = "MultiMC";
}

View File

@@ -0,0 +1,115 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.multimc;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.stream.Stream;
public final class MultiMCModpackProvider implements ModpackProvider {
public static final MultiMCModpackProvider INSTANCE = new MultiMCModpackProvider();
@Override
public String getName() {
return "MultiMC";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return null;
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
}
private static boolean testPath(Path root) {
return Files.exists(root.resolve("instance.cfg"));
}
public static Path getRootPath(Path root) throws IOException {
if (testPath(root)) return root;
try (Stream<Path> stream = Files.list(root)) {
Path candidate = stream.filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
if (testPath(candidate)) return candidate;
throw new IOException("Not a valid MultiMC modpack");
}
}
private static String getRootEntryName(ZipFile file) throws IOException {
final String instanceFileName = "instance.cfg";
if (file.getEntry(instanceFileName) != null) return "";
Enumeration<ZipArchiveEntry> entries = file.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
String entryName = entry.getName();
int idx = entryName.indexOf('/');
if (idx >= 0
&& entryName.length() == idx + instanceFileName.length() + 1
&& entryName.startsWith(instanceFileName, idx + 1))
return entryName.substring(0, idx + 1);
}
throw new IOException("Not a valid MultiMC modpack");
}
@Override
public Modpack readManifest(ZipFile modpackFile, Path modpackPath, Charset encoding) throws IOException {
String rootEntryName = getRootEntryName(modpackFile);
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile, rootEntryName);
String name = rootEntryName.isEmpty() ? FileUtils.getNameWithoutExtension(modpackPath) : rootEntryName.substring(0, rootEntryName.length() - 1);
ZipArchiveEntry instanceEntry = modpackFile.getEntry(rootEntryName + "instance.cfg");
if (instanceEntry == null)
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
try (InputStream instanceStream = modpackFile.getInputStream(instanceEntry)) {
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
}
};
}
}
}

View File

@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ServerModpackLocalInstallTask extends Task<Void> {
@@ -74,13 +75,13 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<ServerModpackManifest>>() {
}.getType());
if (!MODPACK_TYPE.equals(config.getType()))
if (!ServerModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Server modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/overrides", any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/overrides", manifest, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, ServerModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
@@ -96,6 +97,4 @@ public class ServerModpackLocalInstallTask extends Task<Void> {
@Override
public void execute() throws Exception {
}
public static final String MODPACK_TYPE = "Server";
}

View File

@@ -18,15 +18,14 @@
package org.jackhuang.hmcl.mod.server;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
@@ -36,7 +35,7 @@ import java.util.List;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
public class ServerModpackManifest implements Validation {
public class ServerModpackManifest implements ModpackManifest, Validation {
private final String name;
private final String author;
private final String version;
@@ -87,6 +86,11 @@ public class ServerModpackManifest implements Validation {
return addons;
}
@Override
public ModpackProvider getProvider() {
return ServerModpackProvider.INSTANCE;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (fileApi == null)
@@ -128,15 +132,4 @@ public class ServerModpackManifest implements Validation {
};
}
/**
* @param zip the CurseForge modpack file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the server-manifest.json is missing or malformed.
* @return the manifest.
*/
public static Modpack readManifest(ZipFile zip, Charset encoding) throws IOException, JsonParseException {
String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json");
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
return manifest.toModpack(encoding);
}
}

View File

@@ -0,0 +1,63 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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.server;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class ServerModpackProvider implements ModpackProvider {
public static final ServerModpackProvider INSTANCE = new ServerModpackProvider();
@Override
public String getName() {
return "Server";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new ServerModpackCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof ServerModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ServerModpackLocalInstallTask(dependencyManager, zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name));
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json");
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
return manifest.toModpack(encoding);
}
}