feat(modpack): support installing Modrinth modpack.

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

View File

@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer; 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.event.EventManager;
import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask; import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager; import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.setting.VersionIconType;
@@ -374,11 +372,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
try { try {
String jsonText = FileUtils.readText(json); String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class); ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) { ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() { if (provider != null) provider.injectLaunchOptions(jsonText, builder);
}.getType());
config.getManifest().injectLaunchOptions(builder);
}
} catch (IOException | JsonParseException e) { } catch (IOException | JsonParseException e) {
e.printStackTrace(); e.printStackTrace();
} }

View File

@@ -34,6 +34,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
public final class HMCLModpackInstallTask extends Task<Void> { public final class HMCLModpackInstallTask extends Task<Void> {
@@ -69,13 +70,13 @@ public final class HMCLModpackInstallTask extends Task<Void> {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() { config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType()); }.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."); throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
} }
} catch (JsonParseException | IOException ignore) { } catch (JsonParseException | IOException ignore) {
} }
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/minecraft", it -> !"pack.json".equals(it), config)); dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/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 MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
} }
@Override @Override
@@ -104,6 +105,4 @@ public final class HMCLModpackInstallTask extends Task<Void> {
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync)); dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
} }
public static final String MODPACK_TYPE = "HMCL";
} }

View File

@@ -17,8 +17,16 @@
*/ */
package org.jackhuang.hmcl.game; 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(); public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest();
private HMCLModpackManifest() {} private HMCLModpackManifest() {}
@Override
public ModpackProvider getProvider() {
return HMCLModpackProvider.INSTANCE;
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Hello Minecraft! Launcher * Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors * Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,11 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack; 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.task.Task;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; 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.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Path;
/** public final class HMCLModpackProvider implements ModpackProvider {
* @author huangyuhui public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
*/
public final class HMCLModpackManager { @Override
private HMCLModpackManager() { public String getName() {
return "HMCL";
} }
/** @Override
* Read the manifest in a HMCL modpack. public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
* return null;
* @param file a HMCL modpack file. }
* @param encoding encoding of modpack zip file.
* @return the manifest of HMCL modpack. @Override
* @throws IOException if the file is not a valid zip file. public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
* @throws JsonParseException if the manifest.json is missing or malformed. if (!(modpack.getManifest() instanceof HMCLModpackManifest))
*/ throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
public static Modpack readHMCLModpackManifest(ZipFile file, Charset encoding) throws IOException, JsonParseException {
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"); String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding); Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json"); 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); return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
} }
} }
} }

View File

@@ -31,14 +31,9 @@ import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException; import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.java.JavaRepository; import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.launch.*; import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException; import org.jackhuang.hmcl.mod.ModpackProvider;
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.setting.DownloadProviders; import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.LauncherVisibility; import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
@@ -64,7 +59,10 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.*; 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.concurrent.atomic.AtomicReference;
import java.util.logging.Level; import java.util.logging.Level;
@@ -151,14 +149,9 @@ public final class LauncherHelper {
} else { } else {
try { try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion)); ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType())) ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType());
return new CurseCompletionTask(dependencyManager, selectedVersion); if (provider == null) return null;
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType())) else return provider.createCompletionTask(dependencyManager, selectedVersion);
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
@@ -237,7 +230,7 @@ public final class LauncherHelper {
Exception ex = executor.getException(); Exception ex = executor.getException();
if (!(ex instanceof CancellationException)) { if (!(ex instanceof CancellationException)) {
String message; String message;
if (ex instanceof CurseCompletionException) { if (ex instanceof ModpackCompletionException) {
if (ex.getCause() instanceof FileNotFoundException) if (ex.getCause() instanceof FileNotFoundException)
message = i18n("modpack.type.curse.not_found"); message = i18n("modpack.type.curse.not_found");
else else

View File

@@ -0,0 +1,88 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.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<RemoteMod> 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<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
List<String> 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<Category> getCategories() throws IOException {
return getBackedRemoteModRepository().getCategories();
}
@Override
public Optional<RemoteMod.Version> 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<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}
}

View File

@@ -21,15 +21,14 @@ import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.mod.*; import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException; import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
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.mcbbs.McbbsModpackManifest; 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.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask; import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest; 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.mod.server.ServerModpackRemoteInstallTask;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; 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.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -52,47 +52,53 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; 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.Lang.toIterable;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class ModpackHelper { public final class ModpackHelper {
private ModpackHelper() {} private ModpackHelper() {}
private static final Map<String, ModpackProvider> 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 { public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) { try (ZipFile zipFile = CompressingUtils.openZipFile(file, charset)) {
// 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 { try {
return McbbsModpackManifest.readManifest(zipFile, charset); return provider.readManifest(zipFile, file, charset);
} catch (Exception ignored) { } catch (Exception ignored) {
// ignore it, not a valid MCBBS modpack.
} }
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) { } 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) { public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name); profile.getRepository().markVersionAsModpack(name);
@@ -166,7 +161,7 @@ public final class ModpackHelper {
}; };
ExceptionalConsumer<Exception, ?> failure = ex -> { ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) { if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run(); success.run();
// This is tolerable and we will not delete the game // This is tolerable and we will not delete the game
} }
@@ -208,7 +203,7 @@ public final class ModpackHelper {
}; };
ExceptionalConsumer<Exception, ?> failure = ex -> { ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) { if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run(); success.run();
// This is tolerable and we will not delete the game // This is tolerable and we will not delete the game
} }
@@ -237,38 +232,13 @@ public final class ModpackHelper {
} }
} }
public static Task<Void> 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); Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
ModpackProvider provider = getProviderByType(configuration.getType());
switch (configuration.getType()) { if (provider == null) {
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(); throw new UnsupportedModpackException();
} }
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
} }
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) { public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {

View File

@@ -32,6 +32,7 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository; import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory; import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
@@ -75,7 +76,7 @@ public final class Controllers {
GameListPage gameListPage = new GameListPage(); GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty()); gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty()); 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); File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack")); Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
}); });

View File

@@ -42,10 +42,15 @@ import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask; import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask; import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask; 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.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.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask; 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.ServerModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor; 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"))); task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
} else if (task instanceof FabricAPIInstallTask) { } else if (task instanceof FabricAPIInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api"))); task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api")));
} else if (task instanceof CurseCompletionTask) { } else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
task.setName(i18n("modpack.type.curse.completion")); task.setName(i18n("modpack.completion"));
} else if (task instanceof ModpackInstallTask) { } else if (task instanceof ModpackInstallTask) {
task.setName(i18n("modpack.installing")); task.setName(i18n("modpack.installing"));
} else if (task instanceof ModpackUpdateTask) { } else if (task instanceof ModpackUpdateTask) {
@@ -133,6 +138,10 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.install", i18n("modpack.type.curse"))); task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) { } else if (task instanceof MultiMCModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.multimc"))); 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) { } else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl"))); task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) { } else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {

View File

@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node; import javafx.scene.Node;
import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException; 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.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException; import org.jackhuang.hmcl.mod.UnsupportedModpackException;
@@ -125,7 +125,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
settings.put("failure_callback", new FailureCallback() { settings.put("failure_callback", new FailureCallback() {
@Override @Override
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) { public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof CurseCompletionException) { if (exception instanceof ModpackCompletionException) {
if (exception.getCause() instanceof FileNotFoundException) { if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next); Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
} else { } else {

View File

@@ -243,13 +243,19 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
{ {
int rowIndex = 0; int rowIndex = 0;
if (control.versionSelection || !control.downloadSources.isEmpty()) {
searchPane.addRow(rowIndex);
int columns = 0;
Node lastNode = null;
if (control.versionSelection) { if (control.versionSelection) {
JFXComboBox<String> versionsComboBox = new JFXComboBox<>(); JFXComboBox<String> versionsComboBox = new JFXComboBox<>();
versionsComboBox.setMaxWidth(Double.MAX_VALUE); versionsComboBox.setMaxWidth(Double.MAX_VALUE);
Bindings.bindContent(versionsComboBox.getItems(), control.versions); Bindings.bindContent(versionsComboBox.getItems(), control.versions);
selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion); 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) { if (control.downloadSources.getSize() > 1) {
JFXComboBox<String> downloadSourceComboBox = new JFXComboBox<>(); JFXComboBox<String> downloadSourceComboBox = new JFXComboBox<>();
@@ -258,10 +264,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
downloadSourceComboBox.setConverter(stringConverter(I18n::i18n)); downloadSourceComboBox.setConverter(stringConverter(I18n::i18n));
selectedItemPropertyFor(downloadSourceComboBox).bindBidirectional(control.downloadSource); selectedItemPropertyFor(downloadSourceComboBox).bindBidirectional(control.downloadSource);
searchPane.add(new Label(i18n("settings.launcher.download_source")), 2, rowIndex); searchPane.add(new Label(i18n("settings.launcher.download_source")), columns++, rowIndex);
searchPane.add(downloadSourceComboBox, 3, rowIndex); searchPane.add(lastNode = downloadSourceComboBox, columns++, rowIndex);
} else { }
GridPane.setColumnSpan(versionsComboBox, 3);
if (columns == 2) {
GridPane.setColumnSpan(lastNode, 3);
} }
rowIndex++; rowIndex++;

View File

@@ -17,19 +17,10 @@
*/ */
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; 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; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -39,17 +30,17 @@ public class ModDownloadListPage extends DownloadListPage {
repository = new Repository(); repository = new Repository();
supportChinese.set(true); supportChinese.set(true);
downloadSources.get().setAll("mods.curseforge", "mods.modrinth"); downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
downloadSource.set("mods.curseforge"); 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())) { if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.INSTANCE; return ModrinthRemoteModRepository.MODS;
} else { } else {
return CurseForgeRemoteModRepository.MODS; return CurseForgeRemoteModRepository.MODS;
} }
@@ -59,57 +50,6 @@ public class ModDownloadListPage extends DownloadListPage {
public Type getType() { public Type getType() {
return Type.MOD; return Type.MOD;
} }
@Override
public Stream<RemoteMod> 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<ModTranslations.Mod> mods = ModTranslations.MOD.searchMod(searchFilter);
List<String> 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<Category> getCategories() throws IOException {
return getBackedRemoteModRepository().getCategories();
}
@Override
public Optional<RemoteMod.Version> 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<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}
} }
@Override @Override

View File

@@ -41,7 +41,6 @@ public final class ModTranslations {
public static ModTranslations MODPACK = new ModTranslations("/assets/modpack_data.txt"); public static ModTranslations MODPACK = new ModTranslations("/assets/modpack_data.txt");
public static ModTranslations EMPTY = new ModTranslations(""); public static ModTranslations EMPTY = new ModTranslations("");
@Nullable
public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) { public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) {
switch (type) { switch (type) {
case MOD: case MOD:
@@ -101,7 +100,11 @@ public final class ModTranslations {
} }
private boolean loadFromResource() { 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 { try {
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8); 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()); mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());

View File

@@ -172,7 +172,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) { if (item.getCurrentVersion().getSelf() instanceof CurseAddon.LatestFile) {
content.getTags().add("Curseforge"); content.getTags().add("Curseforge");
} else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ModVersion) { } else if (item.getCurrentVersion().getSelf() instanceof ModrinthRemoteModRepository.ProjectVersion) {
content.getTags().add("Modrinth"); content.getTags().add("Modrinth");
} }
} }

View File

@@ -17,18 +17,10 @@
*/ */
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository; import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
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; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -38,76 +30,43 @@ public class ModpackDownloadListPage extends DownloadListPage {
repository = new Repository(); repository = new Repository();
supportChinese.set(true); 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 @Override
public Type getType() { public Type getType() {
return Type.MODPACK; return Type.MODPACK;
} }
@Override
public Stream<RemoteMod> 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<ModTranslations.Mod> mods = ModTranslations.MODPACK.searchMod(searchFilter);
List<String> 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<Category> getCategories() throws IOException {
return CurseForgeRemoteModRepository.MODPACKS.getCategories();
}
@Override
public Optional<RemoteMod.Version> 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<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return CurseForgeRemoteModRepository.MODPACKS.getRemoteVersionsById(id);
}
} }
@Override @Override
protected String getLocalizedCategory(String category) { protected String getLocalizedCategory(String category) {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("modrinth.category." + category);
} else {
return i18n("curse.category." + category); return i18n("curse.category." + category);
} }
}
@Override @Override
protected String getLocalizedOfficialPage() { protected String getLocalizedOfficialPage() {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("mods.modrinth");
} else {
return i18n("mods.curseforge"); return i18n("mods.curseforge");
} }
} }
}

View File

@@ -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=Download modpack from Internet
modpack.choose.remote.detail=Requires a direct download link to the remote modpack file 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.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.desc=Describe your modpack, including precautions and changelog. Markdown and online pictures are supported.
modpack.description=Description modpack.description=Description
modpack.download=Modpack Downloads modpack.download=Modpack Downloads
@@ -586,16 +587,16 @@ modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=Thread id modpack.origin.mcbbs.prompt=Thread id
modpack.scan=Scanning this modpack modpack.scan=Scanning this modpack
modpack.task.install=Import 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.task.install.will=Install the modpack:
modpack.type.curse=Curse 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.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.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.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.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=MCBBS Standard
modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher and MultiMC modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher and MultiMC
modpack.type.modrinth=Modrinth
modpack.type.multimc=MultiMC modpack.type.multimc=MultiMC
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
modpack.type.server=Server Auto-Update Modpack modpack.type.server=Server Auto-Update Modpack

View File

@@ -553,6 +553,7 @@ modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以
modpack.choose.remote=從網路下載模組包 modpack.choose.remote=從網路下載模組包
modpack.choose.remote.detail=需要提供模組包的下載連結 modpack.choose.remote.detail=需要提供模組包的下載連結
modpack.choose.remote.tooltip=要下載的模組包的連結 modpack.choose.remote.tooltip=要下載的模組包的連結
modpack.completion=下載模組包相關檔案
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新記錄,支援 Markdown圖片請上傳至網路 modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新記錄,支援 Markdown圖片請上傳至網路
modpack.description=模組包描述 modpack.description=模組包描述
modpack.download=下載模組包 modpack.download=下載模組包
@@ -586,16 +587,16 @@ modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=貼子 id modpack.origin.mcbbs.prompt=貼子 id
modpack.scan=解析模組包 modpack.scan=解析模組包
modpack.task.install=匯入模組包 modpack.task.install=匯入模組包
modpack.task.install.error=無法識別該模組包,目前僅支援匯入 Curse、MultiMC、HMCL 模組包。 modpack.task.install.error=無法識別該模組包,目前僅支援匯入 Curse、Modrinth、MultiMC、HMCL 模組包。
modpack.task.install.will=將會安裝模組包: modpack.task.install.will=將會安裝模組包:
modpack.type.curse=Curse modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 模組包相關檔案
modpack.type.curse.tolerable_error=但未能完成 Curse 模組包檔案的下載,您可以在啟動該遊戲版本時繼續 Curse 模組包檔案的下載。由於網路問題,您可能需要重試多次。 modpack.type.curse.tolerable_error=但未能完成 Curse 模組包檔案的下載,您可以在啟動該遊戲版本時繼續 Curse 模組包檔案的下載。由於網路問題,您可能需要重試多次。
modpack.type.curse.error=無法完成 Curse 模組包的下載,請多次重試或設定代理 modpack.type.curse.error=無法完成 Curse 模組包的下載,請多次重試或設定代理
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該模組包的最新版本或者安裝其他模組包。 modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載,請嘗試該模組包的最新版本或者安裝其他模組包。
modpack.type.manual.warning=您大概需要直接解压该模組包,并使用其自带的启动器即可开始游戏,而不需要导入模組包。该模組包不是由启动器导出的模組包,而是人工打包 .minecraft 资料夹而来的这类模組包通常附带游戏启动器。HMCL 可以尝试导入该模組包,但不保证可用性,是否继续? modpack.type.manual.warning=您大概需要直接解压该模組包,并使用其自带的启动器即可开始游戏,而不需要导入模組包。该模組包不是由启动器导出的模組包,而是人工打包 .minecraft 资料夹而来的这类模組包通常附带游戏启动器。HMCL 可以尝试导入该模組包,但不保证可用性,是否继续?
modpack.type.mcbbs=我的世界中文論壇模組包標準 modpack.type.mcbbs=我的世界中文論壇模組包標準
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 匯入 modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 匯入
modpack.type.modrinth=Modrinth
modpack.type.multimc=MultiMC modpack.type.multimc=MultiMC
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 匯入 modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 匯入
modpack.type.server=伺服器自動更新模組包 modpack.type.server=伺服器自動更新模組包

View File

@@ -553,6 +553,7 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
modpack.choose.remote=从互联网下载整合包 modpack.choose.remote=从互联网下载整合包
modpack.choose.remote.detail=需要提供整合包的下载链接 modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.remote.tooltip=要下载的整合包的链接 modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.completion=下载整合包相关文件
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML图片请用网络图 modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML图片请用网络图
modpack.description=整合包描述 modpack.description=整合包描述
modpack.download=下载整合包 modpack.download=下载整合包
@@ -586,16 +587,16 @@ modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=贴子 id modpack.origin.mcbbs.prompt=贴子 id
modpack.scan=解析整合包 modpack.scan=解析整合包
modpack.task.install=导入整合包 modpack.task.install=导入整合包
modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。 modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、Modrinth、MultiMC、HMCL 整合包。
modpack.task.install.will=将会安装整合包: modpack.task.install.will=将会安装整合包:
modpack.type.curse=Curse modpack.type.curse=Curse
modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包所需的依赖下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次…… modpack.type.curse.tolerable_error=但未能完成 Curse 整合包所需的依赖下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次……
modpack.type.curse.error=未能完成 Curse 整合包所需的依赖下载,请多次重试或设置代理 modpack.type.curse.error=未能完成 Curse 整合包所需的依赖下载,请多次重试或设置代理
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。 modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
modpack.type.manual.warning=您大概需要直接解压该整合包,并使用其自带的启动器即可开始游戏,而不需要导入整合包!该整合包不是由启动器导出的整合包,而是人工打包 .minecraft 文件夹而来的这类整合包通常附带游戏启动器。HMCL 可以尝试导入该整合包,但不保证可用性,是否继续? modpack.type.manual.warning=您大概需要直接解压该整合包,并使用其自带的启动器即可开始游戏,而不需要导入整合包!该整合包不是由启动器导出的整合包,而是人工打包 .minecraft 文件夹而来的这类整合包通常附带游戏启动器。HMCL 可以尝试导入该整合包,但不保证可用性,是否继续?
modpack.type.mcbbs=我的世界中文论坛整合包标准 modpack.type.mcbbs=我的世界中文论坛整合包标准
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入 modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入
modpack.type.modrinth=Modrinth
modpack.type.multimc=MultiMC modpack.type.multimc=MultiMC
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入 modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入
modpack.type.server=服务器自动更新整合包 modpack.type.server=服务器自动更新整合包

View File

@@ -44,7 +44,7 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
@Override @Override
public CompletableFuture<?> refreshAsync() { public CompletableFuture<?> refreshAsync() {
return CompletableFuture.runAsync(wrap(() -> { 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()) { for (String gameVersion : modVersion.getGameVersions()) {
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion, versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
Collections.singletonList(modVersion.getFile().getUrl()))); Collections.singletonList(modVersion.getFile().getUrl())));

View File

@@ -29,6 +29,7 @@ import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DigestUtils.digest; import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex; import static org.jackhuang.hmcl.util.Hex.encodeHex;
@@ -37,20 +38,20 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
private final File zipFile; private final File zipFile;
private final Charset encoding; private final Charset encoding;
private final String subDirectory; private final List<String> subDirectories;
private final File jsonFile; private final File jsonFile;
private final T manifest; private final T manifest;
private final String type; private final String type;
private final String name; private final String name;
private final String version; private final String version;
public MinecraftInstanceTask(File zipFile, Charset encoding, String subDirectory, T manifest, String type, String name, String version, File jsonFile) { public MinecraftInstanceTask(File zipFile, Charset encoding, List<String> subDirectories, T manifest, ModpackProvider modpackProvider, String name, String version, File jsonFile) {
this.zipFile = zipFile; this.zipFile = zipFile;
this.encoding = encoding; this.encoding = encoding;
this.subDirectory = FileUtils.normalizePath(subDirectory); this.subDirectories = subDirectories.stream().map(FileUtils::normalizePath).collect(Collectors.toList());
this.manifest = manifest; this.manifest = manifest;
this.jsonFile = jsonFile; this.jsonFile = jsonFile;
this.type = type; this.type = modpackProvider.getName();
this.name = name; this.name = name;
this.version = version; this.version = version;
} }
@@ -60,6 +61,7 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
List<ModpackConfiguration.FileInformation> overrides = new ArrayList<>(); List<ModpackConfiguration.FileInformation> overrides = new ArrayList<>();
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setEncoding(encoding).build()) { try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setEncoding(encoding).build()) {
for (String subDirectory : subDirectories) {
Path root = fs.getPath(subDirectory); Path root = fs.getPath(subDirectory);
if (Files.exists(root)) if (Files.exists(root))
@@ -72,6 +74,7 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
} }
}); });
} }
}
ModpackConfiguration<T> configuration = new ModpackConfiguration<>(manifest, type, name, version, overrides); ModpackConfiguration<T> configuration = new ModpackConfiguration<>(manifest, type, name, version, overrides);
FileUtils.writeText(jsonFile, JsonUtils.GSON.toJson(configuration)); FileUtils.writeText(jsonFile, JsonUtils.GSON.toJson(configuration));

View File

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

View File

@@ -15,21 +15,21 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.jackhuang.hmcl.mod.curse; package org.jackhuang.hmcl.mod;
public class CurseCompletionException extends Exception { public class ModpackCompletionException extends Exception {
public CurseCompletionException() { public ModpackCompletionException() {
} }
public CurseCompletionException(String message) { public ModpackCompletionException(String message) {
super(message); super(message);
} }
public CurseCompletionException(String message, Throwable cause) { public ModpackCompletionException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
public CurseCompletionException(Throwable cause) { public ModpackCompletionException(Throwable cause) {
super(cause); super(cause);
} }
} }

View File

@@ -36,7 +36,7 @@ public class ModpackInstallTask<T> extends Task<Void> {
private final File modpackFile; private final File modpackFile;
private final File dest; private final File dest;
private final Charset charset; private final Charset charset;
private final String subDirectory; private final List<String> subDirectories;
private final List<ModpackConfiguration.FileInformation> overrides; private final List<ModpackConfiguration.FileInformation> overrides;
private final Predicate<String> callback; private final Predicate<String> callback;
@@ -45,15 +45,15 @@ public class ModpackInstallTask<T> extends Task<Void> {
* @param modpackFile a zip file * @param modpackFile a zip file
* @param dest destination to store unpacked files * @param dest destination to store unpacked files
* @param charset charset of the zip file * @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 callback test whether the file (given full path) in zip file should be unpacked or not
* @param oldConfiguration old modpack information if upgrade * @param oldConfiguration old modpack information if upgrade
*/ */
public ModpackInstallTask(File modpackFile, File dest, Charset charset, String subDirectory, Predicate<String> callback, ModpackConfiguration<T> oldConfiguration) { public ModpackInstallTask(File modpackFile, File dest, Charset charset, List<String> subDirectories, Predicate<String> callback, ModpackConfiguration<T> oldConfiguration) {
this.modpackFile = modpackFile; this.modpackFile = modpackFile;
this.dest = dest; this.dest = dest;
this.charset = charset; this.charset = charset;
this.subDirectory = subDirectory; this.subDirectories = subDirectories;
this.callback = callback; this.callback = callback;
if (oldConfiguration == null) if (oldConfiguration == null)
@@ -72,6 +72,8 @@ public class ModpackInstallTask<T> extends Task<Void> {
for (ModpackConfiguration.FileInformation file : overrides) for (ModpackConfiguration.FileInformation file : overrides)
files.put(file.getPath(), file); files.put(file.getPath(), file);
for (String subDirectory : subDirectories) {
new Unzipper(modpackFile, dest) new Unzipper(modpackFile, dest)
.setSubDirectory(subDirectory) .setSubDirectory(subDirectory)
.setTerminateIfSubDirectoryNotExists() .setTerminateIfSubDirectoryNotExists()
@@ -96,6 +98,7 @@ public class ModpackInstallTask<T> extends Task<Void> {
return Objects.equals(oldHash, fileHash); return Objects.equals(oldHash, fileHash);
} }
}).unzip(); }).unzip();
}
// If old modpack have this entry, and new modpack deleted it. Delete this file. // If old modpack have this entry, and new modpack deleted it. Delete this file.
for (ModpackConfiguration.FileInformation file : overrides) { for (ModpackConfiguration.FileInformation file : overrides) {

View File

@@ -0,0 +1,22 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod;
public interface ModpackManifest {
ModpackProvider getProvider();
}

View File

@@ -0,0 +1,51 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.task.Task;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public interface ModpackProvider {
String getName();
Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version);
Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException;
/**
* @param zipFile the opened modpack zip file.
* @param file the modpack zip file path.
* @param encoding encoding of zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest.
*/
Modpack readManifest(ZipFile zipFile, Path file, Charset encoding) throws IOException, JsonParseException;
default void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
}
}

View File

@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
@@ -163,8 +164,8 @@ public final class CurseCompletionTask extends Task<Void> {
// Let this task fail if the curse manifest has not been completed. // Let this task fail if the curse manifest has not been completed.
// But continue other downloads. // But continue other downloads.
if (notFound.get()) if (notFound.get())
throw new CurseCompletionException(new FileNotFoundException()); throw new ModpackCompletionException(new FileNotFoundException());
if (!allNameKnown.get() || !isDependenciesSucceeded()) if (!allNameKnown.get() || !isDependenciesSucceeded())
throw new CurseCompletionException(); throw new ModpackCompletionException();
} }
} }

View File

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

View File

@@ -17,21 +17,11 @@
*/ */
package org.jackhuang.hmcl.mod.curse; package org.jackhuang.hmcl.mod.curse;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.jackhuang.hmcl.mod.ModpackManifest;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable; 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.Collections;
import java.util.List; import java.util.List;
@@ -40,7 +30,7 @@ import java.util.List;
* @author huangyuhui * @author huangyuhui
*/ */
@Immutable @Immutable
public final class CurseManifest { public final class CurseManifest implements ModpackManifest {
@SerializedName("manifestType") @SerializedName("manifestType")
private final String manifestType; private final String manifestType;
@@ -117,28 +107,9 @@ public final class CurseManifest {
return new CurseManifest(manifestType, manifestVersion, name, version, author, overrides, minecraft, files); 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 @Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) { public ModpackProvider getProvider() {
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name); return CurseModpackProvider.INSTANCE;
}
};
} }
public static final String MINECRAFT_MODPACK = "minecraftModpack"; public static final String MINECRAFT_MODPACK = "minecraftModpack";

View File

@@ -0,0 +1,78 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.curse;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class CurseModpackProvider implements ModpackProvider {
public static final CurseModpackProvider INSTANCE = new CurseModpackProvider();
@Override
public String getName() {
return "Curse";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new CurseCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof CurseManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, (CurseManifest) modpack.getManifest(), name));
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
CurseManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "manifest.json"), CurseManifest.class);
String description = "No description";
try {
ZipArchiveEntry modlist = zip.getEntry("modlist.html");
if (modlist != null)
description = IOUtils.readFullyAsString(zip.getInputStream(modlist));
} catch (Throwable ignored) {
}
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(), description, encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
}
};
}
}

View File

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

View File

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

View File

@@ -19,15 +19,14 @@ package org.jackhuang.hmcl.mod.mcbbs;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; 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.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.LaunchOptions; import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.mod.Modpack; 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.task.Task;
import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -41,7 +40,7 @@ import java.util.Optional;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; 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"; public static final String MANIFEST_TYPE = "minecraftModpack";
private final String manifestType; 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); 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 @Override
public void validate() throws JsonParseException, TolerableValidationException { public void validate() throws JsonParseException, TolerableValidationException {
if (!MANIFEST_TYPE.equals(manifestType)) if (!MANIFEST_TYPE.equals(manifestType))
@@ -431,27 +435,4 @@ public class McbbsModpackManifest implements Validation {
launchOptions.getJavaArguments().addAll(launchInfo.getJavaArguments()); launchOptions.getJavaArguments().addAll(launchInfo.getJavaArguments());
} }
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding);
}
/**
* @param zip the MCBBS modpack file.
* @param encoding the modpack zip file encoding.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the server-manifest.json is missing or malformed.
* @return the manifest.
*/
public static Modpack readManifest(ZipFile zip, Charset encoding) throws IOException, JsonParseException {
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
if (mcbbsPackMeta != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
}
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
if (manifestJson != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(manifestJson)), encoding);
}
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
}
} }

View File

@@ -0,0 +1,86 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.mcbbs;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class McbbsModpackProvider implements ModpackProvider {
public static final McbbsModpackProvider INSTANCE = new McbbsModpackProvider();
@Override
public String getName() {
return "Mcbbs";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new McbbsModpackCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new McbbsModpackLocalInstallTask(dependencyManager, zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
}
@Override
public void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(modpackConfigurationJson, new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
if (!getName().equals(config.getType())) {
throw new IllegalArgumentException("Incorrect manifest type, actual=" + config.getType() + ", expected=" + getName());
}
config.getManifest().injectLaunchOptions(builder);
}
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding);
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
if (mcbbsPackMeta != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
}
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
if (manifestJson != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(manifestJson)), encoding);
}
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
}
}

View File

@@ -0,0 +1,134 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modrinth;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
public class ModrinthCompletionTask extends Task<Void> {
private final DefaultDependencyManager dependency;
private final DefaultGameRepository repository;
private final String version;
private ModrinthManifest manifest;
private final List<Task<?>> dependencies = new ArrayList<>();
private final AtomicBoolean allNameKnown = new AtomicBoolean(true);
private final AtomicInteger finished = new AtomicInteger(0);
private final AtomicBoolean notFound = new AtomicBoolean(false);
/**
* Constructor.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
*/
public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version) {
this(dependencyManager, version, null);
}
/**
* Constructor.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
* @param manifest the CurseForgeModpack manifest.
*/
public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) {
this.dependency = dependencyManager;
this.repository = dependencyManager.getGameRepository();
this.version = version;
this.manifest = manifest;
if (manifest == null)
try {
File manifestFile = new File(repository.getVersionRoot(version), "modrinth.index.json");
if (manifestFile.exists())
this.manifest = JsonUtils.GSON.fromJson(FileUtils.readText(manifestFile), ModrinthManifest.class);
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to read Modrinth modpack manifest.json", e);
}
setStage("hmcl.modpack.download");
}
@Override
public Collection<Task<?>> getDependencies() {
return dependencies;
}
@Override
public boolean isRelyingOnDependencies() {
return false;
}
@Override
public void execute() throws Exception {
if (manifest == null)
return;
Path runDirectory = repository.getRunDirectory(version).toPath();
for (ModrinthManifest.File file : manifest.getFiles()) {
Path filePath = runDirectory.resolve(file.getPath());
if (!Files.exists(filePath) && !file.getDownloads().isEmpty()) {
FileDownloadTask task = new FileDownloadTask(file.getDownloads().get(0), filePath.toFile());
task.setCacheRepository(dependency.getCacheRepository());
task.setCaching(true);
dependencies.add(task.withCounter("hmcl.modpack.download"));
}
}
if (!dependencies.isEmpty()) {
getProperties().put("total", dependencies.size());
notifyPropertiesChanged();
}
}
@Override
public boolean doPostExecute() {
return true;
}
@Override
public void postExecute() throws Exception {
// Let this task fail if the curse manifest has not been completed.
// But continue other downloads.
if (notFound.get())
throw new ModpackCompletionException(new FileNotFoundException());
if (!allNameKnown.get() || !isDependenciesSucceeded())
throw new ModpackCompletionException();
}
}

View File

@@ -0,0 +1,137 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modrinth;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseManifest;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
public class ModrinthInstallTask extends Task<Void> {
private final DefaultDependencyManager dependencyManager;
private final DefaultGameRepository repository;
private final File zipFile;
private final Modpack modpack;
private final ModrinthManifest manifest;
private final String name;
private final File run;
private final ModpackConfiguration<ModrinthManifest> config;
private final List<Task<?>> dependents = new ArrayList<>(4);
private final List<Task<?>> dependencies = new ArrayList<>(1);
public ModrinthInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, ModrinthManifest manifest, String name) {
this.dependencyManager = dependencyManager;
this.zipFile = zipFile;
this.modpack = modpack;
this.manifest = manifest;
this.name = name;
this.repository = dependencyManager.getGameRepository();
this.run = repository.getRunDirectory(name);
File json = repository.getModpackConfiguration(name);
if (repository.hasVersion(name) && !json.exists())
throw new IllegalArgumentException("Version " + name + " already exists.");
GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.getGameVersion());
for (Map.Entry<String, String> modLoader : manifest.getDependencies().entrySet()) {
switch (modLoader.getKey()) {
case "minecraft":
break;
case "forge":
builder.version("forge", modLoader.getValue());
break;
case "fabric-loader":
builder.version("fabric", modLoader.getValue());
break;
case "quilt-loader":
throw new IllegalStateException("Quilt Modloader is not supported");
default:
throw new IllegalStateException("Unsupported mod loader " + modLoader.getKey());
}
}
dependents.add(builder.buildAsync());
onDone().register(event -> {
Exception ex = event.getTask().getException();
if (event.isFailed()) {
if (!(ex instanceof ModpackCompletionException)) {
repository.removeVersionFromDisk(name);
}
}
});
ModpackConfiguration<ModrinthManifest> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<CurseManifest>>() {
}.getType());
if (!ModrinthModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a Modrinth modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
this.config = config;
List<String> subDirectories = Arrays.asList("/client-overrides", "/overrides");
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), subDirectories, any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependencies.add(new ModrinthCompletionTask(dependencyManager, name, manifest));
}
@Override
public Collection<Task<?>> getDependents() {
return dependents;
}
@Override
public Collection<Task<?>> getDependencies() {
return dependencies;
}
@Override
public void execute() throws Exception {
if (config != null) {
// For update, remove mods not listed in new manifest
for (ModrinthManifest.File oldManifestFile : config.getManifest().getFiles()) {
Path oldFile = run.toPath().resolve(oldManifestFile.getPath());
if (!Files.exists(oldFile)) continue;
if (manifest.getFiles().stream().noneMatch(oldManifestFile::equals)) {
Files.deleteIfExists(oldFile);
}
}
}
File root = repository.getVersionRoot(name);
FileUtils.writeText(new File(root, "modrinth.index.json"), JsonUtils.GSON.toJson(manifest));
}
}

View File

@@ -0,0 +1,146 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modrinth;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class ModrinthManifest implements ModpackManifest, Validation {
private final String game;
private final int formatVersion;
private final String versionId;
private final String name;
@Nullable
private final String summary;
private final List<File> files;
private final Map<String, String> dependencies;
public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List<File> files, Map<String, String> dependencies) {
this.game = game;
this.formatVersion = formatVersion;
this.versionId = versionId;
this.name = name;
this.summary = summary;
this.files = files;
this.dependencies = dependencies;
}
public String getGame() {
return game;
}
public int getFormatVersion() {
return formatVersion;
}
public String getVersionId() {
return versionId;
}
public String getName() {
return name;
}
public String getSummary() {
return summary == null ? "" : summary;
}
public List<File> getFiles() {
return files;
}
public Map<String, String> getDependencies() {
return dependencies;
}
public String getGameVersion() {
return dependencies.get("minecraft");
}
@Override
public ModpackProvider getProvider() {
return ModrinthModpackProvider.INSTANCE;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (dependencies == null || dependencies.get("minecraft") == null) {
throw new JsonParseException("missing Modrinth.dependencies.minecraft");
}
}
public static class File {
private final String path;
private final Map<String, String> hashes;
private final Map<String, String> env;
private final List<URL> downloads;
private final int fileSize;
public File(String path, Map<String, String> hashes, Map<String, String> env, List<URL> downloads, int fileSize) {
this.path = path;
this.hashes = hashes;
this.env = env;
this.downloads = downloads;
this.fileSize = fileSize;
}
public String getPath() {
return path;
}
public Map<String, String> getHashes() {
return hashes;
}
public Map<String, String> getEnv() {
return env;
}
public List<URL> getDownloads() {
return downloads;
}
public int getFileSize() {
return fileSize;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
File file = (File) o;
return fileSize == file.fileSize && path.equals(file.path) && hashes.equals(file.hashes) && env.equals(file.env) && downloads.equals(file.downloads);
}
@Override
public int hashCode() {
return Objects.hash(path, hashes, env, downloads, fileSize);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modrinth;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class ModrinthModpackProvider implements ModpackProvider {
public static final ModrinthModpackProvider INSTANCE = new ModrinthModpackProvider();
@Override
public String getName() {
return "Modrinth";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new ModrinthCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof ModrinthManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ModrinthInstallTask(dependencyManager, zipFile, modpack, (ModrinthManifest) modpack.getManifest(), name));
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
ModrinthManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class);
return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, java.io.File zipFile, String name) {
return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, name);
}
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,115 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.multimc;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.stream.Stream;
public final class MultiMCModpackProvider implements ModpackProvider {
public static final MultiMCModpackProvider INSTANCE = new MultiMCModpackProvider();
@Override
public String getName() {
return "MultiMC";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return null;
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
}
private static boolean testPath(Path root) {
return Files.exists(root.resolve("instance.cfg"));
}
public static Path getRootPath(Path root) throws IOException {
if (testPath(root)) return root;
try (Stream<Path> stream = Files.list(root)) {
Path candidate = stream.filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
if (testPath(candidate)) return candidate;
throw new IOException("Not a valid MultiMC modpack");
}
}
private static String getRootEntryName(ZipFile file) throws IOException {
final String instanceFileName = "instance.cfg";
if (file.getEntry(instanceFileName) != null) return "";
Enumeration<ZipArchiveEntry> entries = file.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
String entryName = entry.getName();
int idx = entryName.indexOf('/');
if (idx >= 0
&& entryName.length() == idx + instanceFileName.length() + 1
&& entryName.startsWith(instanceFileName, idx + 1))
return entryName.substring(0, idx + 1);
}
throw new IOException("Not a valid MultiMC modpack");
}
@Override
public Modpack readManifest(ZipFile modpackFile, Path modpackPath, Charset encoding) throws IOException {
String rootEntryName = getRootEntryName(modpackFile);
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile, rootEntryName);
String name = rootEntryName.isEmpty() ? FileUtils.getNameWithoutExtension(modpackPath) : rootEntryName.substring(0, rootEntryName.length() - 1);
ZipArchiveEntry instanceEntry = modpackFile.getEntry(rootEntryName + "instance.cfg");
if (instanceEntry == null)
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
try (InputStream instanceStream = modpackFile.getInputStream(instanceEntry)) {
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
}
};
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.server;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class ServerModpackProvider implements ModpackProvider {
public static final ServerModpackProvider INSTANCE = new ServerModpackProvider();
@Override
public String getName() {
return "Server";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return new ServerModpackCompletionTask(dependencyManager, version);
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof ServerModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ServerModpackLocalInstallTask(dependencyManager, zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name));
}
@Override
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json");
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
return manifest.toModpack(encoding);
}
}