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:
Burning_TNT
2023-12-31 23:15:54 +08:00
committed by GitHub
parent 333fd0ef80
commit 242df8a81a
78 changed files with 1538 additions and 484 deletions

View File

@@ -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))

View File

@@ -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();

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;

View File

@@ -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
);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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.
*/

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}