diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index e11262d86..27dbc0187 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -20,7 +20,6 @@ package org.jackhuang.hmcl.game; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; import javafx.scene.image.Image; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.LibraryAnalyzer; @@ -28,8 +27,7 @@ import org.jackhuang.hmcl.event.Event; import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackConfiguration; -import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask; -import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest; +import org.jackhuang.hmcl.mod.ModpackProvider; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.ProxyManager; import org.jackhuang.hmcl.setting.VersionIconType; @@ -374,11 +372,8 @@ public class HMCLGameRepository extends DefaultGameRepository { try { String jsonText = FileUtils.readText(json); ModpackConfiguration modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class); - if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) { - ModpackConfiguration config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { - }.getType()); - config.getManifest().injectLaunchOptions(builder); - } + ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType()); + if (provider != null) provider.injectLaunchOptions(jsonText, builder); } catch (IOException | JsonParseException e) { e.printStackTrace(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index 4f30b3cfb..df1e69162 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -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; public final class HMCLModpackInstallTask extends Task { @@ -69,13 +70,13 @@ public final class HMCLModpackInstallTask extends Task { config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { }.getType()); - if (!MODPACK_TYPE.equals(config.getType())) + if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType())) throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version."); } } catch (JsonParseException | IOException ignore) { } - dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/minecraft", it -> !"pack.json".equals(it), config)); - dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); + dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config)); + dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); } @Override @@ -104,6 +105,4 @@ public final class HMCLModpackInstallTask extends Task { dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync)); } - - public static final String MODPACK_TYPE = "HMCL"; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManifest.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManifest.java index 204d8e16b..ba091d5ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManifest.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManifest.java @@ -17,8 +17,16 @@ */ package org.jackhuang.hmcl.game; -public final class HMCLModpackManifest { +import org.jackhuang.hmcl.mod.ModpackManifest; +import org.jackhuang.hmcl.mod.ModpackProvider; + +public final class HMCLModpackManifest implements ModpackManifest { public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest(); private HMCLModpackManifest() {} + + @Override + public ModpackProvider getProvider() { + return HMCLModpackProvider.INSTANCE; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java similarity index 58% rename from HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java rename to HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java index 6c0ee5bdb..c95192f3d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2022 huangyuhui and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,7 +20,11 @@ package org.jackhuang.hmcl.game; 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.setting.Profile; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -29,24 +33,38 @@ 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; -/** - * @author huangyuhui - */ -public final class HMCLModpackManager { - private HMCLModpackManager() { +public final class HMCLModpackProvider implements ModpackProvider { + public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider(); + + @Override + public String getName() { + return "HMCL"; } - /** - * Read the manifest in a HMCL modpack. - * - * @param file a HMCL modpack file. - * @param encoding encoding of modpack zip file. - * @return the manifest of HMCL modpack. - * @throws IOException if the file is not a valid zip file. - * @throws JsonParseException if the manifest.json is missing or malformed. - */ - public static Modpack readHMCLModpackManifest(ZipFile file, Charset encoding) throws IOException, JsonParseException { + @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 HMCLModpackManifest)) + throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); + + if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) { + throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository"); + } + + HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository(); + Profile profile = repository.getProfile(); + + return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name)); + } + + @Override + public Modpack readManifest(ZipFile file, Path path, Charset encoding) throws IOException, JsonParseException { String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json"); Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding); String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json"); @@ -67,4 +85,5 @@ public final class HMCLModpackManager { return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name); } } + } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 934351f45..fb7f67eea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -31,14 +31,9 @@ import org.jackhuang.hmcl.download.game.GameVerificationFixTask; import org.jackhuang.hmcl.download.game.LibraryDownloadException; import org.jackhuang.hmcl.download.java.JavaRepository; import org.jackhuang.hmcl.launch.*; +import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.mod.ModpackConfiguration; -import org.jackhuang.hmcl.mod.curse.CurseCompletionException; -import org.jackhuang.hmcl.mod.curse.CurseCompletionTask; -import org.jackhuang.hmcl.mod.curse.CurseInstallTask; -import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask; -import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask; -import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask; -import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask; +import org.jackhuang.hmcl.mod.ModpackProvider; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.LauncherVisibility; import org.jackhuang.hmcl.setting.Profile; @@ -64,7 +59,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; @@ -151,14 +149,9 @@ public final class LauncherHelper { } else { try { ModpackConfiguration configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion)); - if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType())) - return new CurseCompletionTask(dependencyManager, selectedVersion); - else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType())) - return new ServerModpackCompletionTask(dependencyManager, selectedVersion); - else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType())) - return new McbbsModpackCompletionTask(dependencyManager, selectedVersion); - else - return null; + ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType()); + if (provider == null) return null; + else return provider.createCompletionTask(dependencyManager, selectedVersion); } catch (IOException e) { return null; } @@ -237,7 +230,7 @@ public final class LauncherHelper { Exception ex = executor.getException(); if (!(ex instanceof CancellationException)) { String message; - if (ex instanceof CurseCompletionException) { + if (ex instanceof ModpackCompletionException) { if (ex.getCause() instanceof FileNotFoundException) message = i18n("modpack.type.curse.not_found"); else diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java new file mode 100644 index 000000000..998402e22 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteModRepository.java @@ -0,0 +1,88 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.game; + +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.RemoteModRepository; +import org.jackhuang.hmcl.ui.versions.ModTranslations; +import org.jackhuang.hmcl.util.StringUtils; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public abstract class LocalizedRemoteModRepository implements RemoteModRepository { + + protected abstract RemoteModRepository getBackedRemoteModRepository(); + + @Override + public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { + String newSearchFilter; + if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { + ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType()); + List mods = modTranslations.searchMod(searchFilter); + List searchFilters = new ArrayList<>(); + int count = 0; + for (ModTranslations.Mod mod : mods) { + String englishName = mod.getName(); + if (StringUtils.isNotBlank(mod.getSubname())) { + englishName = mod.getSubname(); + } + + searchFilters.add(englishName); + + count++; + if (count >= 3) break; + } + newSearchFilter = String.join(" ", searchFilters); + } else { + newSearchFilter = searchFilter; + } + + return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder); + } + + @Override + public Stream getCategories() throws IOException { + return getBackedRemoteModRepository().getCategories(); + } + + @Override + public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { + return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file); + } + + @Override + public RemoteMod getModById(String id) throws IOException { + return getBackedRemoteModRepository().getModById(id); + } + + @Override + public RemoteMod.File getModFile(String modId, String fileId) throws IOException { + return getBackedRemoteModRepository().getModFile(modId, fileId); + } + + @Override + public Stream getRemoteVersionsById(String id) throws IOException { + return getBackedRemoteModRepository().getRemoteVersionsById(id); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 925615d8e..c07676597 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -21,15 +21,14 @@ import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; import org.apache.commons.compress.archivers.zip.ZipFile; import org.jackhuang.hmcl.mod.*; -import org.jackhuang.hmcl.mod.curse.CurseCompletionException; -import org.jackhuang.hmcl.mod.curse.CurseInstallTask; -import org.jackhuang.hmcl.mod.curse.CurseManifest; -import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask; +import org.jackhuang.hmcl.mod.curse.CurseModpackProvider; import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest; +import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider; +import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider; import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration; -import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask; -import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask; +import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider; import org.jackhuang.hmcl.mod.server.ServerModpackManifest; +import org.jackhuang.hmcl.mod.server.ServerModpackProvider; import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -42,6 +41,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileNotFoundException; @@ -52,47 +52,53 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; +import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.toIterable; +import static org.jackhuang.hmcl.util.Pair.pair; public final class ModpackHelper { private ModpackHelper() {} + private static final Map providers = mapOf( + pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE), + pair(McbbsModpackProvider.INSTANCE.getName(), McbbsModpackProvider.INSTANCE), + pair(ModrinthModpackProvider.INSTANCE.getName(), ModrinthModpackProvider.INSTANCE), + pair(MultiMCModpackProvider.INSTANCE.getName(), MultiMCModpackProvider.INSTANCE), + pair(ServerModpackProvider.INSTANCE.getName(), ServerModpackProvider.INSTANCE), + pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE) + ); + + @Nullable + public static ModpackProvider getProviderByType(String type) { + return providers.get(type); + } + + public static boolean isFileModpackByExtension(File file) { + String ext = FileUtils.getExtension(file); + return "zip".equals(ext) || "mrpack".equals(ext); + } + public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException { try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) { - try { - return McbbsModpackManifest.readManifest(zipFile, charset); - } catch (Exception ignored) { - // ignore it, not a valid MCBBS modpack. + // Order for trying detecting manifest is necessary here. + // Do not change to iterating providers. + for (ModpackProvider provider : new ModpackProvider[]{ + McbbsModpackProvider.INSTANCE, + CurseModpackProvider.INSTANCE, + ModrinthModpackProvider.INSTANCE, + HMCLModpackProvider.INSTANCE, + MultiMCModpackProvider.INSTANCE, + ServerModpackProvider.INSTANCE}) { + try { + return provider.readManifest(zipFile, file, charset); + } catch (Exception ignored) { + } } - - try { - return CurseManifest.readCurseForgeModpackManifest(zipFile, charset); - } catch (Exception e) { - // ignore it, not a valid CurseForge modpack. - } - - try { - return HMCLModpackManager.readHMCLModpackManifest(zipFile, charset); - } catch (Exception e) { - // ignore it, not a valid HMCL modpack. - } - - try { - return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zipFile, file, charset); - } catch (Exception e) { - // ignore it, not a valid MultiMC modpack. - } - - try { - return ServerModpackManifest.readManifest(zipFile, charset); - } catch (Exception e) { - // ignore it, not a valid Server modpack. - } - } catch (IOException ignored) { } @@ -142,17 +148,6 @@ public final class ModpackHelper { } } - private static String getManifestType(Object manifest) throws UnsupportedModpackException { - if (manifest instanceof HMCLModpackManifest) - return HMCLModpackInstallTask.MODPACK_TYPE; - else if (manifest instanceof MultiMCInstanceConfiguration) - return MultiMCModpackInstallTask.MODPACK_TYPE; - else if (manifest instanceof CurseManifest) - return CurseInstallTask.MODPACK_TYPE; - else - throw new UnsupportedModpackException(); - } - public static Task getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) { profile.getRepository().markVersionAsModpack(name); @@ -166,7 +161,7 @@ public final class ModpackHelper { }; ExceptionalConsumer failure = ex -> { - if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) { + if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) { success.run(); // This is tolerable and we will not delete the game } @@ -208,7 +203,7 @@ public final class ModpackHelper { }; ExceptionalConsumer failure = ex -> { - if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) { + if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) { success.run(); // This is tolerable and we will not delete the game } @@ -237,38 +232,13 @@ public final class ModpackHelper { } } - public static Task getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException { + public static Task getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException { Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset); - - switch (configuration.getType()) { - case CurseInstallTask.MODPACK_TYPE: - if (!(modpack.getManifest() instanceof CurseManifest)) - throw new MismatchedModpackTypeException(CurseInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest())); - - return new ModpackUpdateTask(profile.getRepository(), name, new CurseInstallTask(profile.getDependency(), zipFile, modpack, (CurseManifest) modpack.getManifest(), name)); - case MultiMCModpackInstallTask.MODPACK_TYPE: - if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration)) - throw new MismatchedModpackTypeException(MultiMCModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest())); - - return new ModpackUpdateTask(profile.getRepository(), name, new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)); - case HMCLModpackInstallTask.MODPACK_TYPE: - if (!(modpack.getManifest() instanceof HMCLModpackManifest)) - throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest())); - - return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name)); - case McbbsModpackLocalInstallTask.MODPACK_TYPE: - if (!(modpack.getManifest() instanceof McbbsModpackManifest)) - throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest())); - - return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)); - case ServerModpackLocalInstallTask.MODPACK_TYPE: - if (!(modpack.getManifest() instanceof ServerModpackManifest)) - throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest())); - - return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)); - default: - throw new UnsupportedModpackException(); + ModpackProvider provider = getProviderByType(configuration.getType()); + if (provider == null) { + throw new UnsupportedModpackException(); } + return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack); } public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 9e604ef17..68dd83c57 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -32,6 +32,7 @@ import javafx.stage.StageStyle; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.java.JavaRepository; +import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.EnumCommonDirectory; import org.jackhuang.hmcl.setting.Profiles; @@ -75,7 +76,7 @@ public final class Controllers { GameListPage gameListPage = new GameListPage(); gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty()); gameListPage.profilesProperty().bindContent(Profiles.profilesProperty()); - FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { + FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> { File modpack = modpacks.get(0); Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index ab87bb1bb..71a392a91 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -42,10 +42,15 @@ import org.jackhuang.hmcl.mod.ModpackInstallTask; import org.jackhuang.hmcl.mod.ModpackUpdateTask; import org.jackhuang.hmcl.mod.curse.CurseCompletionTask; import org.jackhuang.hmcl.mod.curse.CurseInstallTask; +import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask; import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask; +import org.jackhuang.hmcl.mod.modrinth.ModrinthCompletionTask; +import org.jackhuang.hmcl.mod.modrinth.ModrinthInstallTask; import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask; import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask; +import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask; import org.jackhuang.hmcl.mod.server.ServerModpackExportTask; +import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; @@ -123,8 +128,8 @@ public final class TaskListPane extends StackPane { task.setName(i18n("install.installer.install", i18n("install.installer.fabric"))); } else if (task instanceof FabricAPIInstallTask) { task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api"))); - } else if (task instanceof CurseCompletionTask) { - task.setName(i18n("modpack.type.curse.completion")); + } else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) { + task.setName(i18n("modpack.completion")); } else if (task instanceof ModpackInstallTask) { task.setName(i18n("modpack.installing")); } else if (task instanceof ModpackUpdateTask) { @@ -133,6 +138,10 @@ public final class TaskListPane extends StackPane { task.setName(i18n("modpack.install", i18n("modpack.type.curse"))); } else if (task instanceof MultiMCModpackInstallTask) { task.setName(i18n("modpack.install", i18n("modpack.type.multimc"))); + } else if (task instanceof ModrinthInstallTask) { + task.setName(i18n("modpack.install", i18n("modpack.type.modrinth"))); + } else if (task instanceof ServerModpackLocalInstallTask) { + task.setName(i18n("modpack.install", i18n("modpack.type.server"))); } else if (task instanceof HMCLModpackInstallTask) { task.setName(i18n("modpack.install", i18n("modpack.type.hmcl"))); } else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index f79ba070d..cc75b176f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -20,7 +20,7 @@ package org.jackhuang.hmcl.ui.download; import javafx.scene.Node; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.game.ManuallyCreatedModpackException; -import org.jackhuang.hmcl.mod.curse.CurseCompletionException; +import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.mod.MismatchedModpackTypeException; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.UnsupportedModpackException; @@ -125,7 +125,7 @@ public class ModpackInstallWizardProvider implements WizardProvider { settings.put("failure_callback", new FailureCallback() { @Override public void onFail(Map settings, Exception exception, Runnable next) { - if (exception instanceof CurseCompletionException) { + if (exception instanceof ModpackCompletionException) { if (exception.getCause() instanceof FileNotFoundException) { Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next); } else { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 12fcf0249..b25d7b9c9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -243,13 +243,19 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP { int rowIndex = 0; - if (control.versionSelection) { - JFXComboBox versionsComboBox = new JFXComboBox<>(); - versionsComboBox.setMaxWidth(Double.MAX_VALUE); - Bindings.bindContent(versionsComboBox.getItems(), control.versions); - selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion); + if (control.versionSelection || !control.downloadSources.isEmpty()) { + searchPane.addRow(rowIndex); + int columns = 0; + Node lastNode = null; + if (control.versionSelection) { + JFXComboBox versionsComboBox = new JFXComboBox<>(); + versionsComboBox.setMaxWidth(Double.MAX_VALUE); + Bindings.bindContent(versionsComboBox.getItems(), control.versions); + selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion); - searchPane.addRow(rowIndex, new Label(i18n("version")), versionsComboBox); + searchPane.add(new Label(i18n("version")), columns++, rowIndex); + searchPane.add(lastNode = versionsComboBox, columns++, rowIndex); + } if (control.downloadSources.getSize() > 1) { JFXComboBox downloadSourceComboBox = new JFXComboBox<>(); @@ -258,10 +264,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP downloadSourceComboBox.setConverter(stringConverter(I18n::i18n)); selectedItemPropertyFor(downloadSourceComboBox).bindBidirectional(control.downloadSource); - searchPane.add(new Label(i18n("settings.launcher.download_source")), 2, rowIndex); - searchPane.add(downloadSourceComboBox, 3, rowIndex); - } else { - GridPane.setColumnSpan(versionsComboBox, 3); + searchPane.add(new Label(i18n("settings.launcher.download_source")), columns++, rowIndex); + searchPane.add(lastNode = downloadSourceComboBox, columns++, rowIndex); + } + + if (columns == 2) { + GridPane.setColumnSpan(lastNode, 3); } rowIndex++; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java index 3c4f03d98..0af781f97 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModDownloadListPage.java @@ -17,19 +17,10 @@ */ package org.jackhuang.hmcl.ui.versions; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.game.LocalizedRemoteModRepository; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; -import org.jackhuang.hmcl.util.StringUtils; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -39,17 +30,17 @@ public class ModDownloadListPage extends DownloadListPage { repository = new Repository(); - supportChinese.set(true); downloadSources.get().setAll("mods.curseforge", "mods.modrinth"); downloadSource.set("mods.curseforge"); } - private class Repository implements RemoteModRepository { + private class Repository extends LocalizedRemoteModRepository { - private RemoteModRepository getBackedRemoteModRepository() { + @Override + protected RemoteModRepository getBackedRemoteModRepository() { if ("mods.modrinth".equals(downloadSource.get())) { - return ModrinthRemoteModRepository.INSTANCE; + return ModrinthRemoteModRepository.MODS; } else { return CurseForgeRemoteModRepository.MODS; } @@ -59,57 +50,6 @@ public class ModDownloadListPage extends DownloadListPage { public Type getType() { return Type.MOD; } - - @Override - public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { - String newSearchFilter; - if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { - List mods = ModTranslations.MOD.searchMod(searchFilter); - List searchFilters = new ArrayList<>(); - int count = 0; - for (ModTranslations.Mod mod : mods) { - String englishName = mod.getName(); - if (StringUtils.isNotBlank(mod.getSubname())) { - englishName = mod.getSubname(); - } - - searchFilters.add(englishName); - - count++; - if (count >= 3) break; - } - newSearchFilter = String.join(" ", searchFilters); - } else { - newSearchFilter = searchFilter; - } - - return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder); - } - - @Override - public Stream getCategories() throws IOException { - return getBackedRemoteModRepository().getCategories(); - } - - @Override - public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { - return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file); - } - - @Override - public RemoteMod getModById(String id) throws IOException { - return getBackedRemoteModRepository().getModById(id); - } - - @Override - public RemoteMod.File getModFile(String modId, String fileId) throws IOException { - return getBackedRemoteModRepository().getModFile(modId, fileId); - } - - @Override - public Stream getRemoteVersionsById(String id) throws IOException { - return getBackedRemoteModRepository().getRemoteVersionsById(id); - } } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java index bab76367b..47a38fd7e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java @@ -41,7 +41,6 @@ public final class ModTranslations { public static ModTranslations MODPACK = new ModTranslations("/assets/modpack_data.txt"); public static ModTranslations EMPTY = new ModTranslations(""); - @Nullable public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) { switch (type) { case MOD: @@ -101,7 +100,11 @@ public final class ModTranslations { } private boolean loadFromResource() { - if (mods != null || StringUtils.isBlank(resourceName)) return true; + if (mods != null) return true; + if (StringUtils.isBlank(resourceName)) { + mods = Collections.emptyList(); + return true; + } try { String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8); mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java index 9709cb257..5f21966d5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java @@ -172,7 +172,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage { if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) { content.getTags().add("Curseforge"); - } else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ModVersion) { + } else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ProjectVersion) { content.getTags().add("Modrinth"); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java index 3dd7aab7d..e8be19219 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModpackDownloadListPage.java @@ -17,18 +17,10 @@ */ package org.jackhuang.hmcl.ui.versions; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.game.LocalizedRemoteModRepository; import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; -import org.jackhuang.hmcl.util.StringUtils; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; +import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -38,76 +30,43 @@ public class ModpackDownloadListPage extends DownloadListPage { repository = new Repository(); - supportChinese.set(true); + downloadSources.get().setAll("mods.curseforge", "mods.modrinth"); + downloadSource.set("mods.curseforge"); } - private class Repository implements RemoteModRepository { + private class Repository extends LocalizedRemoteModRepository { + + @Override + protected RemoteModRepository getBackedRemoteModRepository() { + if ("mods.modrinth".equals(downloadSource.get())) { + return ModrinthRemoteModRepository.MODPACKS; + } else { + return CurseForgeRemoteModRepository.MODPACKS; + } + } @Override public Type getType() { return Type.MODPACK; } - - @Override - public Stream search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { - String newSearchFilter; - if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) { - List mods = ModTranslations.MODPACK.searchMod(searchFilter); - List searchFilters = new ArrayList<>(); - int count = 0; - for (ModTranslations.Mod mod : mods) { - String englishName = mod.getName(); - if (StringUtils.isNotBlank(mod.getSubname())) { - englishName = mod.getSubname(); - } - - searchFilters.add(englishName); - - count++; - if (count >= 3) break; - } - newSearchFilter = String.join(" ", searchFilters); - } else { - newSearchFilter = searchFilter; - } - - return CurseForgeRemoteModRepository.MODPACKS.search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder); - } - - @Override - public Stream getCategories() throws IOException { - return CurseForgeRemoteModRepository.MODPACKS.getCategories(); - } - - @Override - public Optional getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { - return CurseForgeRemoteModRepository.MODPACKS.getRemoteVersionByLocalFile(localModFile, file); - } - - @Override - public RemoteMod getModById(String id) throws IOException { - return CurseForgeRemoteModRepository.MODPACKS.getModById(id); - } - - @Override - public RemoteMod.File getModFile(String modId, String fileId) throws IOException { - return CurseForgeRemoteModRepository.MODPACKS.getModFile(modId, fileId); - } - - @Override - public Stream getRemoteVersionsById(String id) throws IOException { - return CurseForgeRemoteModRepository.MODPACKS.getRemoteVersionsById(id); - } } @Override protected String getLocalizedCategory(String category) { - return i18n("curse.category." + category); + if ("mods.modrinth".equals(downloadSource.get())) { + return i18n("modrinth.category." + category); + } else { + return i18n("curse.category." + category); + } } @Override protected String getLocalizedOfficialPage() { - return i18n("mods.curseforge"); + if ("mods.modrinth".equals(downloadSource.get())) { + return i18n("mods.modrinth"); + } else { + return i18n("mods.curseforge"); + } } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 1832b1b21..d8406d086 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -553,6 +553,7 @@ modpack.choose.local.detail=You can drag the modpack file into this page to inst modpack.choose.remote=Download modpack from Internet modpack.choose.remote.detail=Requires a direct download link to the remote modpack file modpack.choose.remote.tooltip=A direct download link to the remote modpack file +modpack.completion=Install files related to modpack modpack.desc=Describe your modpack, including precautions and changelog. Markdown and online pictures are supported. modpack.description=Description modpack.download=Modpack Downloads @@ -586,16 +587,16 @@ modpack.origin.mcbbs=MCBBS modpack.origin.mcbbs.prompt=Thread id modpack.scan=Scanning this modpack modpack.task.install=Import Modpack -modpack.task.install.error=This modpack file cannot be recognized. Only Curse and MultiMC modpacks are supported. +modpack.task.install.error=This modpack file cannot be recognized. Only Curse, Modrinth, MCBBS and MultiMC modpacks are supported. modpack.task.install.will=Install the modpack: modpack.type.curse=Curse -modpack.type.curse.completion=Install files related to Curse modpack modpack.type.curse.tolerable_error=We cannot complete the download of all files of this Curse modpack. You can retry the download when starting corresponding game version. You may retry for a couple of times due to network problems. modpack.type.curse.error=Unable to install this Curse modpack. Please retry. modpack.type.curse.not_found=Some of required resources are missing and thus could not be downloaded. Please consider the latest version or other modpacks. modpack.type.manual.warning=You probably need to directly decompress this modpack file, instead of importing this modpack. And launch the game using bundled launcher. This modpack is manually created by compressing .minecraft directory, not exported by launcher. HMCL can try to import this modpack, continue? modpack.type.mcbbs=MCBBS Standard modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher and MultiMC +modpack.type.modrinth=Modrinth modpack.type.multimc=MultiMC modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC modpack.type.server=Server Auto-Update Modpack diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 23c618273..7e856f64b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -553,6 +553,7 @@ modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以 modpack.choose.remote=從網路下載模組包 modpack.choose.remote.detail=需要提供模組包的下載連結 modpack.choose.remote.tooltip=要下載的模組包的連結 +modpack.completion=下載模組包相關檔案 modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新記錄,支援 Markdown(圖片請上傳至網路)。 modpack.description=模組包描述 modpack.download=下載模組包 @@ -586,16 +587,16 @@ modpack.origin.mcbbs=MCBBS modpack.origin.mcbbs.prompt=貼子 id modpack.scan=解析模組包 modpack.task.install=匯入模組包 -modpack.task.install.error=無法識別該模組包,目前僅支援匯入 Curse、MultiMC、HMCL 模組包。 +modpack.task.install.error=無法識別該模組包,目前僅支援匯入 Curse、Modrinth、MultiMC、HMCL 模組包。 modpack.task.install.will=將會安裝模組包: modpack.type.curse=Curse -modpack.type.curse.completion=下載 Curse 模組包相關檔案 modpack.type.curse.tolerable_error=但未能完成 Curse 模組包檔案的下載,您可以在啟動該遊戲版本時繼續 Curse 模組包檔案的下載。由於網路問題,您可能需要重試多次。 modpack.type.curse.error=無法完成 Curse 模組包的下載,請多次重試或設定代理 modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該模組包的最新版本或者安裝其他模組包。 modpack.type.manual.warning=您大概需要直接解压该模組包,并使用其自带的启动器即可开始游戏,而不需要导入模組包。该模組包不是由启动器导出的模組包,而是人工打包 .minecraft 资料夹而来的,这类模組包通常附带游戏启动器。HMCL 可以尝试导入该模組包,但不保证可用性,是否继续? modpack.type.mcbbs=我的世界中文論壇模組包標準 modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 匯入 +modpack.type.modrinth=Modrinth modpack.type.multimc=MultiMC modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 匯入 modpack.type.server=伺服器自動更新模組包 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 289e8f1cc..977eb1b4a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -553,6 +553,7 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以 modpack.choose.remote=从互联网下载整合包 modpack.choose.remote.detail=需要提供整合包的下载链接 modpack.choose.remote.tooltip=要下载的整合包的链接 +modpack.completion=下载整合包相关文件 modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图) modpack.description=整合包描述 modpack.download=下载整合包 @@ -586,16 +587,16 @@ modpack.origin.mcbbs=MCBBS modpack.origin.mcbbs.prompt=贴子 id modpack.scan=解析整合包 modpack.task.install=导入整合包 -modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。 +modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、Modrinth、MultiMC、HMCL 整合包。 modpack.task.install.will=将会安装整合包: modpack.type.curse=Curse -modpack.type.curse.completion=下载 Curse 整合包相关文件 modpack.type.curse.tolerable_error=但未能完成 Curse 整合包所需的依赖下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次…… modpack.type.curse.error=未能完成 Curse 整合包所需的依赖下载,请多次重试或设置代理 modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。 modpack.type.manual.warning=您大概需要直接解压该整合包,并使用其自带的启动器即可开始游戏,而不需要导入整合包!该整合包不是由启动器导出的整合包,而是人工打包 .minecraft 文件夹而来的,这类整合包通常附带游戏启动器。HMCL 可以尝试导入该整合包,但不保证可用性,是否继续? modpack.type.mcbbs=我的世界中文论坛整合包标准 modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入 +modpack.type.modrinth=Modrinth modpack.type.multimc=MultiMC modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入 modpack.type.server=服务器自动更新整合包 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java index 43a3dc6fa..74c862874 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIVersionList.java @@ -44,7 +44,7 @@ public class FabricAPIVersionList extends VersionList { @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()))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java index 4d4df3d6c..c4e518924 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/MinecraftInstanceTask.java @@ -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 extends Task private final File zipFile; private final Charset encoding; - private final String subDirectory; + private final List 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 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 extends Task List 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() { - @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() { + @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 configuration = new ModpackConfiguration<>(manifest, type, name, version, overrides); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java index 173ba0e9a..1a3b05fa3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Modpack.java @@ -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; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackCompletionException.java similarity index 73% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionException.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackCompletionException.java index d6cb1f3ea..238eca917 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackCompletionException.java @@ -15,21 +15,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java index bab40ed90..4d66088da 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackInstallTask.java @@ -36,7 +36,7 @@ public class ModpackInstallTask extends Task { private final File modpackFile; private final File dest; private final Charset charset; - private final String subDirectory; + private final List subDirectories; private final List overrides; private final Predicate callback; @@ -45,15 +45,15 @@ public class ModpackInstallTask extends Task { * @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 callback, ModpackConfiguration oldConfiguration) { + public ModpackInstallTask(File modpackFile, File dest, Charset charset, List subDirectories, Predicate callback, ModpackConfiguration 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 extends Task { 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) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackManifest.java new file mode 100644 index 000000000..0dcd03dc1 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackManifest.java @@ -0,0 +1,22 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod; + +public interface ModpackManifest { + ModpackProvider getProvider(); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackProvider.java new file mode 100644 index 000000000..5b340edf9 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModpackProvider.java @@ -0,0 +1,51 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod; + +import 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) { + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java index 6a2c156be..f8cd2dde2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseCompletionTask.java @@ -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 { 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 { // 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(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java index 6876fdcbf..a195a1120 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseInstallTask.java @@ -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 { 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 { config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { }.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 { File root = repository.getVersionRoot(name); FileUtils.writeText(new File(root, "manifest.json"), JsonUtils.GSON.toJson(manifest)); } - - public static final String MODPACK_TYPE = "Curse"; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java index 1ecaf0845..6c1d697fa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseManifest.java @@ -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"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModpackProvider.java new file mode 100644 index 000000000..3893856e6 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/curse/CurseModpackProvider.java @@ -0,0 +1,78 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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); + } + }; + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackCompletionTask.java index 4ed4083ae..4f507d8d8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackCompletionTask.java @@ -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 { // 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(); }))); })); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackLocalInstallTask.java index c3ba91615..3cee33c60 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackLocalInstallTask.java @@ -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 { config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { }.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 { } private static final String PATCH_NAME = "mcbbs"; - public static final String MODPACK_TYPE = "Mcbbs"; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java index 51799856a..7e5984f10 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackManifest.java @@ -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"); - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java new file mode 100644 index 000000000..90eaa4a83 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackProvider.java @@ -0,0 +1,86 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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 config = JsonUtils.GSON.fromJson(modpackConfigurationJson, new TypeToken>() { + }.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"); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java new file mode 100644 index 000000000..a3332c661 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthCompletionTask.java @@ -0,0 +1,134 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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 { + + private final DefaultDependencyManager dependency; + private final DefaultGameRepository repository; + private final String version; + private ModrinthManifest manifest; + private final List> 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> 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(); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java new file mode 100644 index 000000000..f2c5c720d --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthInstallTask.java @@ -0,0 +1,137 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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 { + + 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 config; + private final List> dependents = new ArrayList<>(4); + private final List> 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 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 config = null; + try { + if (json.exists()) { + config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { + }.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 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> getDependents() { + return dependents; + } + + @Override + public Collection> 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)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java new file mode 100644 index 000000000..19b3d19fc --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthManifest.java @@ -0,0 +1,146 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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 files; + private final Map dependencies; + + public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List files, Map 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 getFiles() { + return files; + } + + public Map 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 hashes; + private final Map env; + private final List downloads; + private final int fileSize; + + public File(String path, Map hashes, Map env, List 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 getHashes() { + return hashes; + } + + public Map getEnv() { + return env; + } + + public List 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); + } + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackProvider.java new file mode 100644 index 000000000..30b33559e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackProvider.java @@ -0,0 +1,68 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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); + } + }; + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java index 9b9f54032..c8ef0807d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthRemoteModRepository.java @@ -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 search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException { + List> facets = new ArrayList<>(); + facets.add(Collections.singletonList("project_type:" + projectType)); + if (StringUtils.isNotBlank(gameVersion)) { + facets.add(Collections.singletonList("versions:" + gameVersion)); + } Map 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 response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/api/v1/mod", query)) - .getJson(new TypeToken>() { + Response response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query)) + .getJson(new TypeToken>() { }.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 getRemoteVersionsById(String id) throws IOException { id = StringUtils.removePrefix(id, "local-"); - List versions = HttpRequest.GET("https://api.modrinth.com/api/v1/mod/" + id + "/version") - .getJson(new TypeToken>() { + List versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version") + .getJson(new TypeToken>() { }.getType()); - return versions.stream().map(ModVersion::toVersion).flatMap(Lang::toStream); + return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream); } public List getCategoriesImpl() throws IOException { - return HttpRequest.GET("https://api.modrinth.com/api/v1/tag/category").getJson(new TypeToken>() { + return HttpRequest.GET(PREFIX + "/v2/tag/category").getJson(new TypeToken>() { }.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 categories; - private final List 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 categories, List 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 versions; + + public Project(String slug, String title, String description, List categories, String body, String projectType, int downloads, String iconUrl, String id, String team, Date published, Date updated, List 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 getCategories() { return categories; } - public List 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 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 dependencies; + + @SerializedName("game_versions") + private final List gameVersions; + + @SerializedName("version_type") + private final String versionType; + + private final List 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 files; + private final List files; - private final List dependencies; - - @SerializedName("game_versions") - private final List gameVersions; - - private final List loaders; - - public ModVersion(String id, String modId, String authorId, String name, String versionNumber, String changelog, Date datePublished, int downloads, String versionType, List files, List dependencies, List gameVersions, List loaders) { - this.id = id; - this.modId = modId; - this.authorId = authorId; + public ProjectVersion(String name, String versionNumber, String changelog, List dependencies, List gameVersions, String versionType, List loaders, boolean featured, String id, String projectId, String authorId, Date datePublished, int downloads, String changelogUrl, List 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 getDependencies() { + return dependencies; + } + + public List getGameVersions() { + return gameVersions; + } + + public String getVersionType() { + return versionType; + } + + public List 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 getFiles() { + public List getFiles() { return files; } - public List getDependencies() { - return dependencies; - } - - public List getGameVersions() { - return gameVersions; - } - - public List 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 hashes; private final String url; private final String filename; + private final boolean primary; + private final int size; - public ModVersionFile(Map hashes, String url, String filename) { + public ProjectVersionFile(Map 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 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 categories; - private final List 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 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 categories, List 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 categories, String projectType, int downloads, String iconUrl, String projectId, String author, List 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 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 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 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 ); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java index 223d0b2bd..4cbc228c6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCInstanceConfiguration.java @@ -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 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 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; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java index 54fbb414d..6989ef270 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCManifest.java @@ -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 { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java index 4470efafc..3f3281042 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackInstallTask.java @@ -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 { config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { }.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 { 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 { dependencies.add(repository.saveAsync(version)); } - - public static final String MODPACK_TYPE = "MultiMC"; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java new file mode 100644 index 000000000..a66716c03 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/multimc/MultiMCModpackProvider.java @@ -0,0 +1,115 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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 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 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); + } + }; + } + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackLocalInstallTask.java index 83f96789f..b23753f30 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackLocalInstallTask.java @@ -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 { @@ -74,13 +75,13 @@ public class ServerModpackLocalInstallTask extends Task { config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken>() { }.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 { @Override public void execute() throws Exception { } - - public static final String MODPACK_TYPE = "Server"; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java index 1d48284b9..5b3db2e77 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackManifest.java @@ -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); - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackProvider.java new file mode 100644 index 000000000..4c4c28d0a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/server/ServerModpackProvider.java @@ -0,0 +1,63 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2022 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.mod.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); + } +}