From a3dc7798e78b4e5635aaf8ed918b106242236dbc Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sun, 17 Feb 2019 14:01:05 +0800 Subject: [PATCH] fetch Forge remote version list only from BMCLAPI --- .../hmcl/ui/download/VersionsPage.java | 2 +- .../download/BMCLAPIDownloadProvider.java | 4 +- .../download/DefaultDependencyManager.java | 2 +- .../hmcl/download/MojangDownloadProvider.java | 6 +- .../jackhuang/hmcl/download/VersionList.java | 31 +++ .../download/forge/ForgeBMCLVersionList.java | 185 ++++++++++++++++++ .../hmcl/download/forge/ForgeInstallTask.java | 90 ++++----- .../download/forge/ForgeOldInstallTask.java | 94 +++++++++ .../hmcl/download/forge/ForgeVersionList.java | 3 +- .../jackhuang/hmcl/util/SimpleMultimap.java | 7 + 10 files changed, 362 insertions(+), 62 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index 3da241fd9..bab0e59a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -123,7 +123,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh @Override public void refresh() { getChildren().setAll(spinner); - executor = versionList.refreshAsync(downloadProvider).finalized((variables, isDependentsSucceeded) -> { + executor = versionList.refreshAsync(gameVersion, downloadProvider).finalized((variables, isDependentsSucceeded) -> { if (isDependentsSucceeded) { List items = loadVersions(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java index efe9025e1..123487c55 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/BMCLAPIDownloadProvider.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.download; -import org.jackhuang.hmcl.download.forge.ForgeVersionList; +import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList; import org.jackhuang.hmcl.download.game.GameVersionList; import org.jackhuang.hmcl.download.liteloader.LiteLoaderBMCLVersionList; import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList; @@ -44,7 +44,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider { case "game": return GameVersionList.INSTANCE; case "forge": - return ForgeVersionList.INSTANCE; + return ForgeBMCLVersionList.INSTANCE; case "liteloader": return LiteLoaderBMCLVersionList.INSTANCE; case "optifine": diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java index 7be09a437..d121b4b33 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -90,7 +90,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager { @Override public Task installLibraryAsync(String gameVersion, Version version, String libraryId, String libraryVersion) { VersionList versionList = getVersionList(libraryId); - return versionList.loadAsync(getDownloadProvider()) + return versionList.loadAsync(gameVersion, getDownloadProvider()) .then(variables -> installLibraryAsync(version, versionList.getVersion(gameVersion, libraryVersion) .orElseThrow(() -> new IllegalStateException("Remote library " + libraryId + " has no version " + libraryVersion)))); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java index e4ce8d729..0bc60b0bb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.download; +import org.jackhuang.hmcl.download.forge.ForgeBMCLVersionList; import org.jackhuang.hmcl.download.forge.ForgeVersionList; import org.jackhuang.hmcl.download.game.GameVersionList; import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList; @@ -44,7 +45,7 @@ public class MojangDownloadProvider implements DownloadProvider { case "game": return GameVersionList.INSTANCE; case "forge": - return ForgeVersionList.INSTANCE; + return ForgeBMCLVersionList.INSTANCE; case "liteloader": return LiteLoaderVersionList.INSTANCE; case "optifine": @@ -56,6 +57,7 @@ public class MojangDownloadProvider implements DownloadProvider { @Override public String injectURL(String baseURL) { - return baseURL; + return baseURL + .replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven"); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java index c54426936..e00a8abc5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/VersionList.java @@ -46,6 +46,14 @@ public abstract class VersionList { return !versions.isEmpty(); } + /** + * True if the version list that contains the remote versions which depends on the specific game version has been loaded. + * @param gameVersion the remote version depends on + */ + public boolean isLoaded(String gameVersion) { + return !versions.get(gameVersion).isEmpty(); + } + public abstract boolean hasType(); protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); @@ -56,6 +64,15 @@ public abstract class VersionList { */ public abstract Task refreshAsync(DownloadProvider downloadProvider); + /** + * @param gameVersion the remote version depends on + * @param downloadProvider DownloadProvider + * @return the task to reload the remote version list. + */ + public Task refreshAsync(String gameVersion, DownloadProvider downloadProvider) { + return refreshAsync(downloadProvider); + } + public Task loadAsync(DownloadProvider downloadProvider) { return Task.ofThen(variables -> { lock.readLock().lock(); @@ -70,6 +87,20 @@ public abstract class VersionList { }); } + public Task loadAsync(String gameVersion, DownloadProvider downloadProvider) { + return Task.ofThen(variables -> { + lock.readLock().lock(); + boolean loaded; + + try { + loaded = isLoaded(gameVersion); + } finally { + lock.readLock().unlock(); + } + return loaded ? null : refreshAsync(gameVersion, downloadProvider); + }); + } + protected Collection getVersionsImpl(String gameVersion) { lock.readLock().lock(); try { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java new file mode 100644 index 000000000..fca99043e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeBMCLVersionList.java @@ -0,0 +1,185 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.download.forge; + +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.task.GetTask; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.Immutable; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.gson.Validation; +import org.jackhuang.hmcl.util.io.NetworkUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public final class ForgeBMCLVersionList extends VersionList { + + public static final ForgeBMCLVersionList INSTANCE = new ForgeBMCLVersionList(); + + private ForgeBMCLVersionList() { + } + + @Override + public boolean hasType() { + return false; + } + + @Override + public Task loadAsync(DownloadProvider downloadProvider) { + throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list."); + } + + @Override + public Task refreshAsync(DownloadProvider downloadProvider) { + throw new UnsupportedOperationException("ForgeBMCLVersionList does not support loading the entire Forge remote version list."); + } + + @Override + public Task refreshAsync(String gameVersion, DownloadProvider downloadProvider) { + final GetTask task = new GetTask(NetworkUtils.toURL("https://bmclapi2.bangbang93.com/forge/minecraft/" + gameVersion)); + return new Task() { + @Override + public Collection getDependents() { + return Collections.singleton(task); + } + + @Override + public void execute() { + lock.writeLock().lock(); + + try { + List forgeVersions = JsonUtils.GSON.fromJson(task.getResult(), new TypeToken>() { + }.getType()); + versions.clear(gameVersion); + if (forgeVersions == null) return; + for (ForgeVersion version : forgeVersions) { + if (version == null) + continue; + String jar = null; + for (ForgeVersion.File file : version.getFiles()) + if ("installer".equals(file.getCategory())) { + String classifier = gameVersion + "-" + version.getVersion() + + (StringUtils.isNotBlank(version.getBranch()) ? "-" + version.getBranch() : ""); + String fileName = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat(); + jar = "https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" + classifier + "/" + fileName; + } + + if (jar == null) + continue; + versions.put(gameVersion, new ForgeRemoteVersion( + version.getGameVersion(), version.getVersion(), jar + )); + } + } finally { + lock.writeLock().unlock(); + } + } + }; + } + + @Immutable + public static final class ForgeVersion implements Validation { + + private final String branch; + private final String mcversion; + private final String version; + private final int build; + private final List files; + + /** + * No-arg constructor for Gson. + */ + @SuppressWarnings("unused") + public ForgeVersion() { + this(null, null, null, 0, null); + } + + public ForgeVersion(String branch, String mcversion, String version, int build, List files) { + this.branch = branch; + this.mcversion = mcversion; + this.version = version; + this.build = build; + this.files = files; + } + + public String getBranch() { + return branch; + } + + public String getGameVersion() { + return mcversion; + } + + public String getVersion() { + return version; + } + + public int getBuild() { + return build; + } + + public List getFiles() { + return files; + } + + @Override + public void validate() throws JsonParseException { + if (files == null) + throw new JsonParseException("ForgeVersion files cannot be null"); + if (version == null) + throw new JsonParseException("ForgeVersion version cannot be null"); + if (mcversion == null) + throw new JsonParseException("ForgeVersion mcversion cannot be null"); + } + + @Immutable + public static final class File { + private final String format; + private final String category; + private final String hash; + + public File() { + this("", "", ""); + } + + public File(String format, String category, String hash) { + this.format = format; + this.category = category; + this.hash = hash; + } + + public String getFormat() { + return format; + } + + public String getCategory() { + return category; + } + + public String getHash() { + return hash; + } + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index 7c056d0c6..2843c56b3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -18,24 +18,17 @@ package org.jackhuang.hmcl.download.forge; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.SimpleVersionProvider; import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskResult; -import org.jackhuang.hmcl.util.gson.JsonUtils; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.versioning.VersionNumber; -import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; /** * @@ -45,31 +38,48 @@ public final class ForgeInstallTask extends TaskResult { private final DefaultDependencyManager dependencyManager; private final Version version; - private final File installer = new File("forge-installer.jar").getAbsoluteFile(); + private Path installer; private final ForgeRemoteVersion remote; - private final List dependents = new LinkedList<>(); - private final List dependencies = new LinkedList<>(); - - private Task downloadFileTask() { - return new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer); - } + private Task dependent; + private TaskResult dependency; public ForgeInstallTask(DefaultDependencyManager dependencyManager, Version version, ForgeRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; this.version = version; this.remote = remoteVersion; + } - dependents.add(downloadFileTask()); + @Override + public boolean doPreExecute() { + return true; + } + + @Override + public void preExecute() throws Exception { + installer = Files.createTempFile("forge-installer", ".jar"); + + dependent = new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer.toFile()); + } + + @Override + public boolean doPostExecute() { + return true; + } + + @Override + public void postExecute() throws Exception { + Files.deleteIfExists(installer); + setResult(dependency.getResult()); } @Override public Collection getDependents() { - return dependents; + return Collections.singleton(dependent); } @Override - public List getDependencies() { - return dependencies; + public Collection getDependencies() { + return Collections.singleton(dependency); } @Override @@ -83,38 +93,10 @@ public final class ForgeInstallTask extends TaskResult { } @Override - public void execute() throws Exception { - try (ZipFile zipFile = new ZipFile(installer)) { - InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json")); - if (stream == null) - throw new IOException("Malformed forge installer file, install_profile.json does not exist."); - String json = IOUtils.readFullyAsString(stream); - ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class); - - // unpack the universal jar in the installer file. - Library forgeLibrary = Library.fromName(installProfile.getInstall().getPath()); - File forgeFile = dependencyManager.getGameRepository().getLibraryFile(version, forgeLibrary); - if (!FileUtils.makeFile(forgeFile)) - throw new IOException("Cannot make directory " + forgeFile.getParent()); - - ZipEntry forgeEntry = zipFile.getEntry(installProfile.getInstall().getFilePath()); - try (InputStream is = zipFile.getInputStream(forgeEntry); OutputStream os = new FileOutputStream(forgeFile)) { - IOUtils.copyTo(is, os); - } - - // resolve the version - SimpleVersionProvider provider = new SimpleVersionProvider(); - provider.addVersion(version); - - setResult(installProfile.getVersionInfo() - .setInheritsFrom(version.getId()) - .resolve(provider).setJar(null) - .setId(version.getId()).setLogging(Collections.emptyMap())); - - dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo())); - } - - if (!installer.delete()) - throw new IOException("Unable to delete installer file" + installer); + public void execute() { + if (VersionNumber.VERSION_COMPARATOR.compare("1.13", remote.getGameVersion()) <= 0) + dependency = new ForgeNewInstallTask(dependencyManager, version, installer); + else + dependency = new ForgeOldInstallTask(dependencyManager, version, installer); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java new file mode 100644 index 000000000..fca62abec --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java @@ -0,0 +1,94 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2019 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.download.forge; + +import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.task.TaskResult; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.io.IOUtils; + +import java.io.*; +import java.nio.file.Path; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class ForgeOldInstallTask extends TaskResult { + + private final DefaultDependencyManager dependencyManager; + private final Version version; + private final Path installer; + private final List dependencies = new LinkedList<>(); + + public ForgeOldInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) { + this.dependencyManager = dependencyManager; + this.version = version; + this.installer = installer; + } + + @Override + public List getDependencies() { + return dependencies; + } + + @Override + public String getId() { + return "version"; + } + + @Override + public boolean doPreExecute() { + return true; + } + + @Override + public void execute() throws Exception { + try (ZipFile zipFile = new ZipFile(installer.toFile())) { + InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json")); + if (stream == null) + throw new IOException("Malformed forge installer file, install_profile.json does not exist."); + String json = IOUtils.readFullyAsString(stream); + ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class); + + // unpack the universal jar in the installer file. + Library forgeLibrary = Library.fromName(installProfile.getInstall().getPath()); + File forgeFile = dependencyManager.getGameRepository().getLibraryFile(version, forgeLibrary); + if (!FileUtils.makeFile(forgeFile)) + throw new IOException("Cannot make directory " + forgeFile.getParent()); + + ZipEntry forgeEntry = zipFile.getEntry(installProfile.getInstall().getFilePath()); + try (InputStream is = zipFile.getInputStream(forgeEntry); OutputStream os = new FileOutputStream(forgeFile)) { + IOUtils.copyTo(is, os); + } + + // resolve the version + SimpleVersionProvider provider = new SimpleVersionProvider(); + provider.addVersion(version); + + setResult(installProfile.getVersionInfo() + .setInheritsFrom(version.getId()) + .resolve(provider).setJar(null) + .setId(version.getId()).setLogging(Collections.emptyMap())); + + dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo())); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java index d367ef663..3f9070d08 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeVersionList.java @@ -47,12 +47,11 @@ public final class ForgeVersionList extends VersionList { @Override public Task refreshAsync(DownloadProvider downloadProvider) { final GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(FORGE_LIST))); - final List dependents = Collections.singletonList(task); return new Task() { @Override public Collection getDependents() { - return dependents; + return Collections.singleton(task); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java index eee95919f..be7c2217f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/SimpleMultimap.java @@ -88,4 +88,11 @@ public final class SimpleMultimap { public void clear() { map.clear(); } + + public void clear(K key) { + if (map.containsKey(key)) + map.get(key).clear(); + else + map.put(key, valuer.get()); + } }