Enhance mod download (#2411)
* Support #2376 * Add necessary @Nullable annotations * Display different types of dependencies in different sections. * Fix checkstyle * Add I18N for different types of dependencies. * Enhance UI * Code cleanup * Enhance UI * Manually sort the result from curseforge when searching mods by name. * Render the search results from remote mod repositories in several pages. * Fix merge * Fix * Add a button which navigates to the modpack download page in the modpack installl page * Fix I18N * Render the mod loaders supported by the version in mod info page. * Fix #2104 * Enhance TwoLineListItem * Render the mod loader supported by this mod file on the ModListPage * Fix chinese searching and curseforge searching * Update I18N * Fix * Fix * Select the specific game version when clicking the 'download' button on ModListPage * Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json * Enhance :HMCL:build.gradle.kts * Revert parse_mcmod_data.py * Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError. FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden. * Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage. * Fix checkstyle * Fix * Fix * Fix * Add license for RemoteResourceManager * Remove TODO * Enhance Chinese searching * Support to decode metadata for local quilt mod. * Enhance ModManager * Fix checkstyle * Refactor * Fix * Fix * Refactor DownloadPage * Fix * Revert "Refactor DownloadPage" This reverts commit 953558da77af5a0fe3153e77cdcb9b6affa30ffa. * Refactor DownloadPage * Refactor * Fix * Fix checkstyle * Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class. * Fix * Fix * Fix * Enhance SimpleMultimap * Revert TwoLineListItem * Fix * Code cleanup * Code cleanup * Fix * Code cleanup * Add license for IModMetadataReader * Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage * Fix #2498 * Update README_cn.md * Opti ModMananger * Log a warning message when 'hmcl.update_source.override' is used. * Fix chinese searching * Enhance chinese searching. * Enhance memory usage * Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty. * Cache builtin images. * Enhance FXUtils (Make tooltip installer faster). * Fix * Fix * Fix #2560 * Fix typo * Fix remote image cache. * Fix javadoc * Fix checkstyle * Optimize FXUtils::shutdown * Fix merge * I have no idea on why the sha1 was matched. * Revert "Enhance FXUtils (Make tooltip installer faster)." This reverts commit 0a49eb2c1204e4be7dc0df3084faa59fdf9b0394. * Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland. * Modify dynamic remote resource urls. * Optimize codes with StringUtils.DynamicCommonSubsequence. * Prevent unofficial HMCL to access HMCL Resource Update URL. * Zip the dynamic-remote-resources json by Gradle automatically. * Remove unnecessary getters. --------- Co-authored-by: Burning_TNT <pangyl08@163.com“>
This commit is contained in:
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.game.VersionProvider;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -161,11 +162,20 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
|| mainClass.startsWith("cpw.mods"));
|
||||
}
|
||||
|
||||
public Set<ModLoaderType> getModLoaders() {
|
||||
return Arrays.stream(LibraryType.values())
|
||||
.filter(LibraryType::isModLoader)
|
||||
.filter(this::has)
|
||||
.map(LibraryType::getModLoaderType)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public enum LibraryType {
|
||||
MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$")),
|
||||
FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader")),
|
||||
FABRIC_API(true, "fabric-api", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-api")),
|
||||
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("(forge|fmlloader)")) {
|
||||
MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$"), null),
|
||||
FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader"), ModLoaderType.FABRIC),
|
||||
FABRIC_API(true, "fabric-api", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-api"), null),
|
||||
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("(forge|fmlloader)"), ModLoaderType.FORGE) {
|
||||
private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
|
||||
|
||||
@Override
|
||||
@@ -177,21 +187,23 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
return super.patchVersion(libraryVersion);
|
||||
}
|
||||
},
|
||||
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader")),
|
||||
OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$")),
|
||||
QUILT(true, "quilt", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-loader")),
|
||||
QUILT_API(true, "quilt-api", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-api")),
|
||||
BOOTSTRAP_LAUNCHER(false, "", Pattern.compile("cpw\\.mods"), Pattern.compile("bootstraplauncher"));
|
||||
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader"), ModLoaderType.LITE_LOADER),
|
||||
OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$"), null),
|
||||
QUILT(true, "quilt", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-loader"), ModLoaderType.QUILT),
|
||||
QUILT_API(true, "quilt-api", Pattern.compile("org\\.quiltmc"), Pattern.compile("quilt-api"), null),
|
||||
BOOTSTRAP_LAUNCHER(false, "", Pattern.compile("cpw\\.mods"), Pattern.compile("bootstraplauncher"), null);
|
||||
|
||||
private final boolean modLoader;
|
||||
private final String patchId;
|
||||
private final Pattern group, artifact;
|
||||
private final ModLoaderType modLoaderType;
|
||||
|
||||
LibraryType(boolean modLoader, String patchId, Pattern group, Pattern artifact) {
|
||||
LibraryType(boolean modLoader, String patchId, Pattern group, Pattern artifact, ModLoaderType modLoaderType) {
|
||||
this.modLoader = modLoader;
|
||||
this.patchId = patchId;
|
||||
this.group = group;
|
||||
this.artifact = artifact;
|
||||
this.modLoaderType = modLoaderType;
|
||||
}
|
||||
|
||||
public boolean isModLoader() {
|
||||
@@ -202,6 +214,10 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
return patchId;
|
||||
}
|
||||
|
||||
public ModLoaderType getModLoaderType() {
|
||||
return modLoaderType;
|
||||
}
|
||||
|
||||
public static LibraryType fromPatchId(String patchId) {
|
||||
for (LibraryType type : values())
|
||||
if (type.getPatchId().equals(patchId))
|
||||
|
||||
@@ -293,7 +293,7 @@ public class MaintainTask extends Task<Version> {
|
||||
public static Version unique(Version version) {
|
||||
List<Library> libraries = new ArrayList<>();
|
||||
|
||||
SimpleMultimap<String, Integer> multimap = new SimpleMultimap<String, Integer>(HashMap::new, ArrayList::new);
|
||||
SimpleMultimap<String, Integer, List<Integer>> multimap = new SimpleMultimap<>(HashMap::new, ArrayList::new);
|
||||
|
||||
for (Library library : version.getLibraries()) {
|
||||
String id = library.getGroupId() + ":" + library.getArtifactId();
|
||||
|
||||
@@ -37,7 +37,7 @@ public abstract class VersionList<T extends RemoteVersion> {
|
||||
* key: game version.
|
||||
* values: corresponding remote versions.
|
||||
*/
|
||||
protected final SimpleMultimap<String, T> versions = new SimpleMultimap<String, T>(HashMap::new, TreeSet::new);
|
||||
protected final SimpleMultimap<String, T, TreeSet<T>> versions = new SimpleMultimap<>(HashMap::new, TreeSet::new);
|
||||
|
||||
/**
|
||||
* True if the version list has been loaded.
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.function.Consumer;
|
||||
*/
|
||||
public final class EventManager<T extends Event> {
|
||||
|
||||
private final SimpleMultimap<EventPriority, Consumer<T>> handlers
|
||||
private final SimpleMultimap<EventPriority, Consumer<T>, CopyOnWriteArraySet<Consumer<T>>> handlers
|
||||
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), CopyOnWriteArraySet::new);
|
||||
|
||||
public Consumer<T> registerWeak(Consumer<T> consumer) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.mod.modinfo.PackMcMeta;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
@@ -18,10 +18,20 @@
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
public enum ModLoaderType {
|
||||
UNKNOWN,
|
||||
FORGE,
|
||||
FABRIC,
|
||||
QUILT,
|
||||
LITE_LOADER,
|
||||
PACK
|
||||
UNKNOWN("Unknown"),
|
||||
FORGE("Forge"),
|
||||
FABRIC("Fabric"),
|
||||
QUILT("Quilt"),
|
||||
LITE_LOADER("LiteLoader"),
|
||||
PACK("Pack");
|
||||
|
||||
private final String loaderName;
|
||||
|
||||
ModLoaderType(String loaderName) {
|
||||
this.loaderName = loaderName;
|
||||
}
|
||||
|
||||
public final String getLoaderName() {
|
||||
return loaderName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.game.GameRepository;
|
||||
import org.jackhuang.hmcl.mod.modinfo.*;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -25,12 +28,32 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeSet;
|
||||
import java.util.*;
|
||||
|
||||
public final class ModManager {
|
||||
@FunctionalInterface
|
||||
private interface ModMetadataReader {
|
||||
LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException;
|
||||
}
|
||||
|
||||
private static final Map<String, Pair<ModMetadataReader[], String>> READERS;
|
||||
|
||||
static {
|
||||
TreeMap<String, Pair<ModMetadataReader[], String>> readers = new TreeMap<>();
|
||||
readers.put("zip", Pair.pair(new ModMetadataReader[]{
|
||||
ForgeOldModMetadata::fromFile,
|
||||
ForgeNewModMetadata::fromFile,
|
||||
FabricModMetadata::fromFile,
|
||||
QuiltModMetadata::fromFile,
|
||||
PackMcMeta::fromFile,
|
||||
}, ""));
|
||||
readers.put("jar", readers.get("zip"));
|
||||
readers.put("litemod", Pair.pair(new ModMetadataReader[]{
|
||||
LiteModMetadata::fromFile
|
||||
}, "LiteLoader Mod"));
|
||||
READERS = Collections.unmodifiableMap(readers);
|
||||
}
|
||||
|
||||
private final GameRepository repository;
|
||||
private final String id;
|
||||
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
|
||||
@@ -71,46 +94,28 @@ public final class ModManager {
|
||||
|
||||
public LocalModFile getModInfo(Path modFile) {
|
||||
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||
String description;
|
||||
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||
try {
|
||||
return ForgeOldModMetadata.fromFile(this, modFile, fs);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return ForgeNewModMetadata.fromFile(this, modFile, fs);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return FabricModMetadata.fromFile(this, modFile, fs);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
return PackMcMeta.fromFile(this, modFile, fs);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
description = "";
|
||||
} else if (fileName.endsWith(".litemod")) {
|
||||
try {
|
||||
return LiteModMetadata.fromFile(this, modFile);
|
||||
} catch (Exception ignore) {
|
||||
description = "LiteLoader Mod";
|
||||
}
|
||||
} else {
|
||||
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
|
||||
Pair<ModMetadataReader[], String> currentReader = READERS.get(extension);
|
||||
if (currentReader == null) {
|
||||
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||
for (ModMetadataReader reader : currentReader.getKey()) {
|
||||
try {
|
||||
return reader.fromFile(this, modFile, fs);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
return new LocalModFile(this,
|
||||
getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN),
|
||||
modFile,
|
||||
FileUtils.getNameWithoutExtension(modFile),
|
||||
new LocalModFile.Description(description));
|
||||
new LocalModFile.Description(currentReader.getValue())
|
||||
);
|
||||
}
|
||||
|
||||
public void refreshMods() throws IOException {
|
||||
@@ -281,6 +286,11 @@ public final class ModManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Files.exists(fs.getPath("quilt.mod.json"))) {
|
||||
// Quilt mod
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Files.exists(fs.getPath("litemod.json"))) {
|
||||
// Liteloader mod
|
||||
return true;
|
||||
|
||||
@@ -101,6 +101,72 @@ public class RemoteMod {
|
||||
Alpha
|
||||
}
|
||||
|
||||
public enum DependencyType {
|
||||
EMBEDDED,
|
||||
OPTIONAL,
|
||||
REQUIRED,
|
||||
TOOL,
|
||||
INCLUDE,
|
||||
INCOMPATIBLE,
|
||||
BROKEN
|
||||
}
|
||||
|
||||
public static final class Dependency {
|
||||
private static Dependency BROKEN_DEPENDENCY = null;
|
||||
|
||||
private final DependencyType type;
|
||||
|
||||
private final RemoteModRepository remoteModRepository;
|
||||
|
||||
private final String id;
|
||||
|
||||
private RemoteMod remoteMod = null;
|
||||
|
||||
private Dependency(DependencyType type, RemoteModRepository remoteModRepository, String modid) {
|
||||
this.type = type;
|
||||
this.remoteModRepository = remoteModRepository;
|
||||
this.id = modid;
|
||||
}
|
||||
|
||||
public static Dependency ofGeneral(DependencyType type, RemoteModRepository remoteModRepository, String modid) {
|
||||
if (type == DependencyType.BROKEN) {
|
||||
return ofBroken();
|
||||
} else {
|
||||
return new Dependency(type, remoteModRepository, modid);
|
||||
}
|
||||
}
|
||||
|
||||
public static Dependency ofBroken() {
|
||||
if (BROKEN_DEPENDENCY == null) {
|
||||
BROKEN_DEPENDENCY = new Dependency(DependencyType.BROKEN, null, null);
|
||||
}
|
||||
return BROKEN_DEPENDENCY;
|
||||
}
|
||||
|
||||
public DependencyType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public RemoteModRepository getRemoteModRepository() {
|
||||
return this.remoteModRepository;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public RemoteMod load() throws IOException {
|
||||
if (this.remoteMod == null) {
|
||||
if (this.type == DependencyType.BROKEN) {
|
||||
this.remoteMod = RemoteMod.getEmptyRemoteMod();
|
||||
} else {
|
||||
this.remoteMod = this.remoteModRepository.getModById(this.id);
|
||||
}
|
||||
}
|
||||
return this.remoteMod;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
CURSEFORGE(CurseForgeRemoteModRepository.MODS),
|
||||
MODRINTH(ModrinthRemoteModRepository.MODS);
|
||||
@@ -135,11 +201,11 @@ public class RemoteMod {
|
||||
private final Date datePublished;
|
||||
private final VersionType versionType;
|
||||
private final File file;
|
||||
private final List<String> dependencies;
|
||||
private final List<Dependency> dependencies;
|
||||
private final List<String> gameVersions;
|
||||
private final List<ModLoaderType> loaders;
|
||||
|
||||
public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List<String> dependencies, List<String> gameVersions, List<ModLoaderType> loaders) {
|
||||
public Version(IVersion self, String modid, String name, String version, String changelog, Date datePublished, VersionType versionType, File file, List<Dependency> dependencies, List<String> gameVersions, List<ModLoaderType> loaders) {
|
||||
this.self = self;
|
||||
this.modid = modid;
|
||||
this.name = name;
|
||||
@@ -185,7 +251,7 @@ public class RemoteMod {
|
||||
return file;
|
||||
}
|
||||
|
||||
public List<String> getDependencies() {
|
||||
public List<Dependency> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,39 @@ public interface RemoteModRepository {
|
||||
DESC
|
||||
}
|
||||
|
||||
Stream<RemoteMod> search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
||||
class SearchResult {
|
||||
private final Stream<RemoteMod> sortedResults;
|
||||
|
||||
private final Stream<RemoteMod> unsortedResults;
|
||||
|
||||
private final int totalPages;
|
||||
|
||||
public SearchResult(Stream<RemoteMod> sortedResults, Stream<RemoteMod> unsortedResults, int totalPages) {
|
||||
this.sortedResults = sortedResults;
|
||||
this.unsortedResults = unsortedResults;
|
||||
this.totalPages = totalPages;
|
||||
}
|
||||
|
||||
public SearchResult(Stream<RemoteMod> sortedResults, int pages) {
|
||||
this.sortedResults = sortedResults;
|
||||
this.unsortedResults = sortedResults;
|
||||
this.totalPages = pages;
|
||||
}
|
||||
|
||||
public Stream<RemoteMod> getResults() {
|
||||
return this.sortedResults;
|
||||
}
|
||||
|
||||
public Stream<RemoteMod> getUnsortedResults() {
|
||||
return this.unsortedResults;
|
||||
}
|
||||
|
||||
public int getTotalPages() {
|
||||
return this.totalPages;
|
||||
}
|
||||
}
|
||||
|
||||
SearchResult search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
||||
throws IOException;
|
||||
|
||||
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
|
||||
|
||||
@@ -21,6 +21,8 @@ import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -30,6 +32,15 @@ import java.util.stream.Stream;
|
||||
|
||||
@Immutable
|
||||
public class CurseAddon implements RemoteMod.IMod {
|
||||
public static final Map<Integer, RemoteMod.DependencyType> RELATION_TYPE = Lang.mapOf(
|
||||
Pair.pair(1, RemoteMod.DependencyType.EMBEDDED),
|
||||
Pair.pair(2, RemoteMod.DependencyType.OPTIONAL),
|
||||
Pair.pair(3, RemoteMod.DependencyType.REQUIRED),
|
||||
Pair.pair(4, RemoteMod.DependencyType.TOOL),
|
||||
Pair.pair(5, RemoteMod.DependencyType.INCOMPATIBLE),
|
||||
Pair.pair(6, RemoteMod.DependencyType.INCLUDE)
|
||||
);
|
||||
|
||||
private final int id;
|
||||
private final int gameId;
|
||||
private final String name;
|
||||
@@ -566,7 +577,12 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
getFileDate(),
|
||||
versionType,
|
||||
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
||||
Collections.emptyList(),
|
||||
dependencies.stream().map(dependency -> {
|
||||
if (!RELATION_TYPE.containsKey(dependency.getRelationType())) {
|
||||
throw new IllegalStateException("Broken datas.");
|
||||
}
|
||||
return RemoteMod.Dependency.ofGeneral(RELATION_TYPE.get(dependency.getRelationType()), CurseForgeRemoteModRepository.MODS, Integer.toString(dependency.getModId()));
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
||||
gameVersions.stream().flatMap(version -> {
|
||||
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
|
||||
|
||||
@@ -22,6 +22,8 @@ import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.MurmurHash2;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -42,6 +44,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
private static final String PREFIX = "https://api.curseforge.com";
|
||||
private static final String apiKey = System.getProperty("hmcl.curseforge.apikey", JarUtils.getManifestAttribute("CurseForge-Api-Key", ""));
|
||||
|
||||
private static final int WORD_PERFECT_MATCH_WEIGHT = 50;
|
||||
|
||||
public static boolean isAvailable() {
|
||||
return !apiKey.isEmpty();
|
||||
}
|
||||
@@ -91,7 +95,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod> search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
int categoryId = 0;
|
||||
if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
||||
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
||||
@@ -102,12 +106,51 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
pair("searchFilter", searchFilter),
|
||||
pair("sortField", Integer.toString(toModsSearchSortField(sortType))),
|
||||
pair("sortOrder", toSortOrder(sortOrder)),
|
||||
pair("index", Integer.toString(pageOffset)),
|
||||
pair("index", Integer.toString(pageOffset * pageSize)),
|
||||
pair("pageSize", Integer.toString(pageSize)))
|
||||
.header("X-API-KEY", apiKey)
|
||||
.getJson(new TypeToken<Response<List<CurseAddon>>>() {
|
||||
}.getType());
|
||||
return response.getData().stream().map(CurseAddon::toMod);
|
||||
Stream<RemoteMod> res = response.getData().stream().map(CurseAddon::toMod);
|
||||
if (sortType != SortType.NAME || searchFilter.length() == 0) {
|
||||
return new SearchResult(res, (int)Math.ceil((double)response.pagination.totalCount / pageSize));
|
||||
}
|
||||
|
||||
// https://github.com/huanghongxun/HMCL/issues/1549
|
||||
String lowerCaseSearchFilter = searchFilter.toLowerCase();
|
||||
Map<String, Integer> searchFilterWords = new HashMap<>();
|
||||
for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) {
|
||||
searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1);
|
||||
}
|
||||
|
||||
return new SearchResult(res.map(remoteMod -> {
|
||||
String lowerCaseResult = remoteMod.getTitle().toLowerCase();
|
||||
int[][] lev = new int[lowerCaseSearchFilter.length() + 1][lowerCaseResult.length() + 1];
|
||||
for (int i = 0; i < lowerCaseResult.length() + 1; i++) {
|
||||
lev[0][i] = i;
|
||||
}
|
||||
for (int i = 0; i < lowerCaseSearchFilter.length() + 1; i++) {
|
||||
lev[i][0] = i;
|
||||
}
|
||||
for (int i = 1; i < lowerCaseSearchFilter.length() + 1; i++) {
|
||||
for (int j = 1; j < lowerCaseResult.length() + 1; j++) {
|
||||
int countByInsert = lev[i][j - 1] + 1;
|
||||
int countByDel = lev[i - 1][j] + 1;
|
||||
int countByReplace = lowerCaseSearchFilter.charAt(i - 1) == lowerCaseResult.charAt(j - 1) ? lev[i - 1][j - 1] : lev[i - 1][j - 1] + 1;
|
||||
lev[i][j] = Math.min(countByInsert, Math.min(countByDel, countByReplace));
|
||||
}
|
||||
}
|
||||
|
||||
int diff = lev[lowerCaseSearchFilter.length()][lowerCaseResult.length()];
|
||||
|
||||
for (String s : StringUtils.tokenize(lowerCaseResult)) {
|
||||
if (searchFilterWords.containsKey(s)) {
|
||||
diff -= WORD_PERFECT_MATCH_WEIGHT * searchFilterWords.get(s) * s.length();
|
||||
}
|
||||
}
|
||||
|
||||
return pair(remoteMod, diff);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), res, response.pagination.totalCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,10 +15,13 @@
|
||||
* 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;
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.jackhuang.hmcl.mod;
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.moandjiezana.toml.Toml;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
@@ -20,7 +23,6 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
@Immutable
|
||||
public final class ForgeNewModMetadata {
|
||||
|
||||
private final String modLoader;
|
||||
|
||||
private final String loaderVersion;
|
||||
@@ -134,7 +136,7 @@ public final class ForgeNewModMetadata {
|
||||
}
|
||||
}
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
|
||||
mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||
mod.getDisplayURL(),
|
||||
metadata.getLogoFile());
|
||||
}
|
||||
@@ -15,11 +15,14 @@
|
||||
* 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;
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -37,7 +40,6 @@ import java.util.List;
|
||||
*/
|
||||
@Immutable
|
||||
public final class ForgeOldModMetadata {
|
||||
|
||||
@SerializedName("modid")
|
||||
private final String modId;
|
||||
private final String name;
|
||||
@@ -15,13 +15,17 @@
|
||||
* 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;
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
@@ -106,7 +110,7 @@ public final class LiteModMetadata {
|
||||
return updateURI;
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||
try (ZipFile zipFile = new ZipFile(modFile.toFile())) {
|
||||
ZipEntry entry = zipFile.getEntry("litemod.json");
|
||||
if (entry == null)
|
||||
@@ -15,11 +15,14 @@
|
||||
* 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;
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
@@ -36,7 +39,6 @@ import java.util.List;
|
||||
|
||||
@Immutable
|
||||
public class PackMcMeta implements Validation {
|
||||
|
||||
@SerializedName("pack")
|
||||
private final PackInfo pack;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Immutable
|
||||
public final class QuiltModMetadata {
|
||||
private static final class QuiltLoader {
|
||||
private static final class Metadata {
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final JsonObject contributors;
|
||||
private final String icon;
|
||||
private final JsonObject contact;
|
||||
|
||||
public Metadata(String name, String description, JsonObject contributors, String icon, JsonObject contact) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.contributors = contributors;
|
||||
this.icon = icon;
|
||||
this.contact = contact;
|
||||
}
|
||||
}
|
||||
|
||||
private final String id;
|
||||
private final String version;
|
||||
private final Metadata metadata;
|
||||
|
||||
public QuiltLoader(String id, String version, Metadata metadata) {
|
||||
this.id = id;
|
||||
this.version = version;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
private final int schema_version;
|
||||
private final QuiltLoader quilt_loader;
|
||||
|
||||
public QuiltModMetadata(int schemaVersion, QuiltLoader quiltLoader) {
|
||||
this.schema_version = schemaVersion;
|
||||
this.quilt_loader = quiltLoader;
|
||||
}
|
||||
|
||||
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||
Path path = fs.getPath("quilt.mod.json");
|
||||
if (Files.notExists(path)) {
|
||||
throw new IOException("File " + modFile + " is not a Quilt mod.");
|
||||
}
|
||||
|
||||
QuiltModMetadata root = JsonUtils.fromNonNullJson(FileUtils.readText(path), QuiltModMetadata.class);
|
||||
if (root.schema_version != 1) {
|
||||
throw new IOException("File " + modFile + " is not a supported Quilt mod.");
|
||||
}
|
||||
|
||||
return new LocalModFile(
|
||||
modManager,
|
||||
modManager.getLocalMod(root.quilt_loader.id, ModLoaderType.QUILT),
|
||||
modFile,
|
||||
root.quilt_loader.metadata.name,
|
||||
new LocalModFile.Description(root.quilt_loader.metadata.description),
|
||||
root.quilt_loader.metadata.contributors.entrySet().stream().map(entry -> String.format("%s (%s)", entry.getKey(), entry.getValue().getAsJsonPrimitive().getAsString())).collect(Collectors.joining(", ")),
|
||||
root.quilt_loader.version,
|
||||
"",
|
||||
Optional.ofNullable(root.quilt_loader.metadata.contact.get("homepage")).map(jsonElement -> jsonElement.getAsJsonPrimitive().getAsString()).orElse(""),
|
||||
root.quilt_loader.metadata.icon
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod> search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
public SearchResult search(String gameVersion, @Nullable RemoteModRepository.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)) {
|
||||
@@ -87,14 +87,14 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
Map<String, String> query = mapOf(
|
||||
pair("query", searchFilter),
|
||||
pair("facets", JsonUtils.UGLY_GSON.toJson(facets)),
|
||||
pair("offset", Integer.toString(pageOffset)),
|
||||
pair("offset", Integer.toString(pageOffset * pageSize)),
|
||||
pair("limit", Integer.toString(pageSize)),
|
||||
pair("index", convertSortType(sort))
|
||||
);
|
||||
Response<ProjectSearchResult> response = HttpRequest.GET(NetworkUtils.withQuery(PREFIX + "/v2/search", query))
|
||||
.getJson(new TypeToken<Response<ProjectSearchResult>>() {
|
||||
}.getType());
|
||||
return response.getHits().stream().map(ProjectSearchResult::toMod);
|
||||
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int)Math.ceil((double)response.totalHits / pageSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -286,17 +286,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
Set<String> dependencies = modRepository.getRemoteVersionsById(getId())
|
||||
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(getId())
|
||||
.flatMap(version -> version.getDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
List<RemoteMod> mods = new ArrayList<>();
|
||||
for (String dependencyId : dependencies) {
|
||||
if (dependencyId == null) {
|
||||
mods.add(RemoteMod.getEmptyRemoteMod());
|
||||
}
|
||||
if (StringUtils.isNotBlank(dependencyId)) {
|
||||
mods.add(modRepository.getModById(dependencyId));
|
||||
}
|
||||
for (RemoteMod.Dependency dependency : dependencies) {
|
||||
mods.add(dependency.load());
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
@@ -313,9 +308,9 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
title,
|
||||
description,
|
||||
categories,
|
||||
null,
|
||||
String.format("https://modrinth.com/%s/%s", projectType, id),
|
||||
iconUrl,
|
||||
(RemoteMod.IMod) this
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -351,6 +346,13 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
public static class ProjectVersion implements RemoteMod.IVersion {
|
||||
private static final Map<String, RemoteMod.@Nullable DependencyType> DEPENDENCY_TYPE = Lang.mapOf(
|
||||
Pair.pair("required", RemoteMod.DependencyType.REQUIRED),
|
||||
Pair.pair("optional", RemoteMod.DependencyType.OPTIONAL),
|
||||
Pair.pair("embedded", RemoteMod.DependencyType.EMBEDDED),
|
||||
Pair.pair("incompatible", RemoteMod.DependencyType.INCOMPATIBLE)
|
||||
);
|
||||
|
||||
private final String name;
|
||||
|
||||
@SerializedName("version_number")
|
||||
@@ -496,7 +498,17 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
datePublished,
|
||||
type,
|
||||
files.get(0).toFile(),
|
||||
dependencies.stream().map(dependency -> dependency.getVersionId() == null ? null : dependency.getProjectId()).collect(Collectors.toList()),
|
||||
dependencies.stream().map(dependency -> {
|
||||
if (dependency.projectId == null) {
|
||||
return RemoteMod.Dependency.ofBroken();
|
||||
}
|
||||
|
||||
if (!DEPENDENCY_TYPE.containsKey(dependency.dependencyType)) {
|
||||
throw new IllegalStateException("Broken datas");
|
||||
}
|
||||
|
||||
return RemoteMod.Dependency.ofGeneral(DEPENDENCY_TYPE.get(dependency.dependencyType), MODS, dependency.projectId);
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
gameVersions,
|
||||
loaders.stream().flatMap(loader -> {
|
||||
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
||||
@@ -651,17 +663,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
Set<String> dependencies = modRepository.getRemoteVersionsById(getProjectId())
|
||||
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(getProjectId())
|
||||
.flatMap(version -> version.getDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
List<RemoteMod> mods = new ArrayList<>();
|
||||
for (String dependencyId : dependencies) {
|
||||
if (dependencyId == null) {
|
||||
mods.add(RemoteMod.getEmptyRemoteMod());
|
||||
}
|
||||
if (StringUtils.isNotBlank(dependencyId)) {
|
||||
mods.add(modRepository.getModById(dependencyId));
|
||||
}
|
||||
for (RemoteMod.Dependency dependency : dependencies) {
|
||||
mods.add(dependency.load());
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ import java.util.function.Supplier;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class SimpleMultimap<K, V> {
|
||||
public final class SimpleMultimap<K, V, M extends Collection<V>> {
|
||||
|
||||
private final Map<K, Collection<V>> map;
|
||||
private final Supplier<Collection<V>> valuer;
|
||||
private final Map<K, M> map;
|
||||
private final Supplier<M> valuer;
|
||||
|
||||
public SimpleMultimap(Supplier<Map<K, Collection<V>>> mapper, Supplier<Collection<V>> valuer) {
|
||||
public SimpleMultimap(Supplier<Map<K, M>> mapper, Supplier<M> valuer) {
|
||||
this.map = mapper.get();
|
||||
this.valuer = valuer;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public final class SimpleMultimap<K, V> {
|
||||
|
||||
public Collection<V> values() {
|
||||
Collection<V> res = valuer.get();
|
||||
for (Map.Entry<K, Collection<V>> entry : map.entrySet())
|
||||
for (Map.Entry<K, M> entry : map.entrySet())
|
||||
res.addAll(entry.getValue());
|
||||
return res;
|
||||
}
|
||||
@@ -61,27 +61,27 @@ public final class SimpleMultimap<K, V> {
|
||||
return map.containsKey(key) && !map.get(key).isEmpty();
|
||||
}
|
||||
|
||||
public Collection<V> get(K key) {
|
||||
public M get(K key) {
|
||||
return map.computeIfAbsent(key, any -> valuer.get());
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
Collection<V> set = get(key);
|
||||
M set = get(key);
|
||||
set.add(value);
|
||||
}
|
||||
|
||||
public void putAll(K key, Collection<? extends V> value) {
|
||||
Collection<V> set = get(key);
|
||||
M set = get(key);
|
||||
set.addAll(value);
|
||||
}
|
||||
|
||||
public Collection<V> removeKey(K key) {
|
||||
public M removeKey(K key) {
|
||||
return map.remove(key);
|
||||
}
|
||||
|
||||
public boolean removeValue(V value) {
|
||||
boolean flag = false;
|
||||
for (Collection<V> c : map.values())
|
||||
for (M c : map.values())
|
||||
flag |= c.remove(value);
|
||||
return flag;
|
||||
}
|
||||
|
||||
@@ -357,6 +357,32 @@ public final class StringUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class DynamicCommonSubsequence {
|
||||
private LongestCommonSubsequence calculator;
|
||||
|
||||
public DynamicCommonSubsequence(int intLengthA, int intLengthB) {
|
||||
if (intLengthA > intLengthB) {
|
||||
calculator = new LongestCommonSubsequence(intLengthA, intLengthB);
|
||||
} else {
|
||||
calculator = new LongestCommonSubsequence(intLengthB, intLengthA);
|
||||
}
|
||||
}
|
||||
|
||||
public int calc(CharSequence a, CharSequence b) {
|
||||
if (a.length() < b.length()) {
|
||||
CharSequence t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
if (calculator.maxLengthA < a.length() || calculator.maxLengthB < b.length()) {
|
||||
calculator = new LongestCommonSubsequence(a.length(), b.length());
|
||||
}
|
||||
|
||||
return calculator.calc(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for computing the longest common subsequence between strings.
|
||||
*/
|
||||
|
||||
@@ -141,7 +141,7 @@ public abstract class HttpRequest {
|
||||
return getStringWithRetry(() -> {
|
||||
HttpURLConnection con = createConnection();
|
||||
con = resolveConnection(con);
|
||||
return IOUtils.readFullyAsString(con.getInputStream());
|
||||
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(con.getInputStream()) : con.getInputStream());
|
||||
}, retryTimes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* This utility class consists of some util methods operating on InputStream/OutputStream.
|
||||
@@ -85,4 +86,8 @@ public final class IOUtils {
|
||||
dest.write(buf, 0, len);
|
||||
}
|
||||
}
|
||||
|
||||
public static InputStream wrapFromGZip(InputStream inputStream) throws IOException {
|
||||
return new GZIPInputStream(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,8 +144,8 @@ public final class NetworkUtils {
|
||||
while (true) {
|
||||
|
||||
conn.setUseCaches(false);
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
conn.setConnectTimeout(8000);
|
||||
conn.setReadTimeout(8000);
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
Map<String, List<String>> properties = conn.getRequestProperties();
|
||||
String method = conn.getRequestMethod();
|
||||
@@ -209,13 +209,13 @@ public final class NetworkUtils {
|
||||
public static String readData(HttpURLConnection con) throws IOException {
|
||||
try {
|
||||
try (InputStream stdout = con.getInputStream()) {
|
||||
return IOUtils.readFullyAsString(stdout);
|
||||
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stdout) : stdout);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
try (InputStream stderr = con.getErrorStream()) {
|
||||
if (stderr == null)
|
||||
throw e;
|
||||
return IOUtils.readFullyAsString(stderr);
|
||||
return IOUtils.readFullyAsString("gzip".equals(con.getContentEncoding()) ? IOUtils.wrapFromGZip(stderr) : stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user