Compare commits

...
9 Commits
51 changed files with 515 additions and 166 deletions

No files matched your search

@@ -20,6 +20,9 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.AuthlibInjectorServers;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -41,6 +44,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -54,6 +58,14 @@ public final class Launcher extends Application {
try {
try {
ConfigHolder.init();
AuthlibInjectorServers.init();
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
AuthlibInjectorServers.getConfigInstance().getUrls().stream()
.map(AuthlibInjectorServer::new)
.forEach(config().getAuthlibInjectorServers()::add);
}
} catch (IOException e) {
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -73,9 +74,9 @@ public class HMCLModpackExportTask extends Task<Void> {
return false;
});
Version mv = repository.getResolvedVersion(version);
Version mv = repository.getResolvedPreservingPatchesVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + version));
.orElseThrow(() -> new IOException("Cannot parse the version of " + version));
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
@@ -39,12 +40,13 @@ public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
private final String name;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
private final Modpack modpack;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
DependencyManager dependency = profile.getDependency();
dependency = profile.getDependency();
repository = profile.getRepository();
this.zipFile = zipFile;
this.name = name;
@@ -88,8 +90,18 @@ public final class HMCLModpackInstallTask extends Task<Void> {
@Override
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
dependencies.add(repository.save(version));
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
// reinstall libraries
// libraries of Forge and OptiFine should be obtained by installation.
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
if (LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(mark.getLibraryId()))
continue;
libraryTask = libraryTask.thenComposeAsync(version -> dependency.installLibraryAsync(modpack.getGameVersion(), version, mark.getLibraryId(), mark.getLibraryVersion()));
}
dependencies.add(libraryTask.thenComposeAsync(repository::save));
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
}
@@ -123,7 +123,7 @@ public final class ModpackHelper {
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack);
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
@@ -121,6 +121,9 @@ public final class TexturesLoader {
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
}
if (img == null)
throw new IOException("Texture is malformed");
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
metadata = emptyMap();
@@ -0,0 +1,78 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
private final List<String> urls;
public AuthlibInjectorServers(List<String> urls) {
this.urls = urls;
}
public List<String> getUrls() {
return urls;
}
@Override
public void validate() throws JsonParseException {
if (urls == null)
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
}
private static final Path configLocation = Paths.get(CONFIG_FILENAME);
private static AuthlibInjectorServers configInstance;
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
}
configInstance = new AuthlibInjectorServers(Collections.emptyList());
if (Files.exists(configLocation)) {
try {
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
}
}
}
public static AuthlibInjectorServers getConfigInstance() {
return configInstance;
}
}
@@ -52,6 +52,10 @@ public final class ConfigHolder {
return configInstance;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -113,16 +113,14 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
String liteLoader = analyzer.getVersion(LITELOADER).orElse(null);
String optiFine = analyzer.getVersion(OPTIFINE).orElse(null);
JFXButton[] buttons = new JFXButton[]{btnFabric, btnForge, btnLiteLoader, btnOptiFine};
Label[] labels = new Label[]{lblFabric, lblForge, lblLiteLoader, lblOptiFine};
String[] libraryIds = new String[]{"fabric", "forge", "liteloader", "optifine"};
String[] versions = new String[]{fabric, forge, liteLoader, optiFine};
for (int i = 0; i < libraryIds.length; ++i) {
String libraryId = libraryIds[i];
buttons[i].setDisable(versions[i] != null);
if (versions[i] != null || controller.getSettings().containsKey(libraryId))
labels[i].setText(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + Lang.nonNull(versions[i], getVersion(libraryId)));
labels[i].setText(i18n("install.installer.version", i18n("install.installer." + libraryId)) + ": " + Lang.nonNull(getVersion(libraryId), versions[i]));
else
labels[i].setText(i18n("install.installer.not_installed", i18n("install.installer." + libraryId)));
}
@@ -18,9 +18,11 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Version;
@@ -114,9 +116,10 @@ public final class InstallerWizardProvider implements WizardProvider {
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", ((DownloadException) exception).getUrl()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next);
}
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException) {
} else if (exception instanceof OptiFineInstallTask.UnsupportedOptiFineInstallationException ||
exception instanceof FabricInstallTask.UnsupportedFabricInstallationException) {
Controllers.dialog(i18n("install.failed.optifine_conflict"), i18n("install.failed"), MessageType.ERROR, next);
} else if (exception instanceof UnsupportedOperationException) {
} else if (exception instanceof DefaultDependencyManager.UnsupportedLibraryInstallerException) {
Controllers.dialog(i18n("install.failed.install_online"), i18n("install.failed"), MessageType.ERROR, next);
} else if (exception instanceof VersionMismatchException) {
VersionMismatchException e = ((VersionMismatchException) exception);
@@ -30,8 +30,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Paths;
import java.util.Map;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -83,7 +86,9 @@ public class InstallersPage extends StackPane implements WizardPage {
String gameVersion = ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(i18n("install.new_game.already_exists"));
txtName.getValidators().add(hasVersion);
Validator nameValidator = new Validator(OperatingSystem::isNameValid);
nameValidator.setMessage(i18n("install.new_game.malformed"));
txtName.getValidators().addAll(hasVersion, nameValidator);
txtName.textProperty().addListener(e -> btnInstall.setDisable(!txtName.validate()));
txtName.setText(gameVersion);
@@ -58,8 +58,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
// We remove library but not save it,
// so if installation failed will not break down current version.
return profile.getDependency().removeLibraryWithoutSavingAsync(version.getId(), libraryId)
.thenComposeAsync(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get(libraryId)))
return profile.getDependency().installLibraryAsync(version, (RemoteVersion) settings.get(libraryId))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
}
@@ -60,14 +60,16 @@ public class GameItem extends Control {
.thenAcceptAsync(game -> {
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id));
analyzer.forEachLibrary((libraryId, libraryVersion) -> {
if (libraryId.equals(MINECRAFT.getPatchId())) return;
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
if (libraryId.equals(MINECRAFT.getPatchId())) continue;
if (I18n.hasKey("install.installer." + libraryId)) {
libraries.append(", ").append(i18n("install.installer." + libraryId));
if (libraryVersion != null)
libraries.append(": ").append(modifyVersion("", libraryVersion.replaceAll("(?i)" + libraryId, "")));
}
});
}
subtitle.set(libraries.toString());
}, Platform::runLater)
@@ -79,14 +79,17 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId));
}).thenAcceptAsync(Schedulers.javafx(), analyzer -> {
Function<String, Consumer<InstallerItem>> removeAction = libraryId -> x -> {
profile.getDependency().removeLibraryAsync(version.getId(), libraryId)
profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::save)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
.start();
};
itemsProperty().clear();
analyzer.forEachLibrary((libraryId, libraryVersion) -> {
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
String title = I18n.hasKey("install.installer." + libraryId) ? i18n("install.installer." + libraryId) : libraryId;
Consumer<InstallerItem> action = "game".equals(libraryId) ? null : removeAction.apply(libraryId);
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId)))
@@ -96,7 +99,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
}, action));
else
itemsProperty().add(new InstallerItem(title, libraryVersion, null, action));
});
}
}).start();
}
@@ -132,7 +132,7 @@ install.failed.downloading=Failed to install due to some files not downloaded su
install.failed.downloading.detail=Failed to download file: %s
install.failed.downloading.timeout=Download timed out: %s
install.failed.install_online=Unable to recognize what you provided installer file is
install.failed.optifine_conflict=OptiFine and Forge are both installed simultaneously on Minecraft 1.13
install.failed.optifine_conflict=Fabric, OptiFine and Forge are both installed simultaneously on Minecraft 1.13
install.failed.version_mismatch=The library requires game version %s, but actual version is %s.
install.installer.choose=Choose a %s version
install.installer.fabric=Fabric
@@ -152,6 +152,7 @@ install.modpack=Install a modpack
install.new_game=Install a New Game
install.new_game.already_exists=This version has already been existing.
install.new_game.current_game_version=Current Game Version
install.new_game.malformed=Name malformed
install.select=Select an operation
install.success=Installed successfully
@@ -150,6 +150,7 @@ install.modpack=安裝整合包
install.new_game=安裝新遊戲版本
install.new_game.already_exists=此版本已經存在,請換一個名字
install.new_game.current_game_version=目前遊戲版本
install.new_game.malformed=名字不合法
install.select=請選擇安裝方式
install.success=安裝成功
@@ -151,6 +151,7 @@ install.modpack=安装整合包
install.new_game=安装新游戏版本
install.new_game.already_exists=此版本已经存在,请换一个名字
install.new_game.current_game_version=当前游戏版本
install.new_game.malformed=名字不合法
install.select=请选择安装方式
install.success=安装成功
@@ -23,8 +23,11 @@ import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
@@ -60,7 +63,7 @@ public class DefaultCacheRepository extends CacheRepository {
lock.writeLock().lock();
try {
if (Files.isRegularFile(indexFile))
index = JsonUtils.GSON.fromJson(FileUtils.readText(indexFile.toFile()), Index.class);
index = JsonUtils.fromNonNullJson(FileUtils.readText(indexFile.toFile()), Index.class);
else
index = new Index();
} catch (IOException | JsonParseException e) {
@@ -214,7 +217,7 @@ public class DefaultCacheRepository extends CacheRepository {
* // assets and versions will not be included in index.
* }
*/
private class Index {
private class Index implements Validation {
private final Set<LibraryIndex> libraries;
public Index() {
@@ -222,21 +225,28 @@ public class DefaultCacheRepository extends CacheRepository {
}
public Index(Set<LibraryIndex> libraries) {
this.libraries = libraries;
this.libraries = Objects.requireNonNull(libraries);
}
@NotNull
public Set<LibraryIndex> getLibraries() {
return libraries;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (libraries == null)
throw new JsonParseException("Index.libraries cannot be null");
}
}
private class LibraryIndex {
private class LibraryIndex implements Validation {
private final String name;
private final String hash;
private final String type;
public LibraryIndex() {
this(null, null, null);
this("", "", "");
}
public LibraryIndex(String name, String hash, String type) {
@@ -245,18 +255,27 @@ public class DefaultCacheRepository extends CacheRepository {
this.type = type;
}
@NotNull
public String getName() {
return name;
}
@NotNull
public String getHash() {
return hash;
}
@NotNull
public String getType() {
return type;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (name == null || hash == null || type == null)
throw new JsonParseException("Index.LibraryIndex.* cannot be null");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -94,19 +94,19 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
VersionList<?> versionList = getVersionList(libraryId);
return versionList.loadAsync(gameVersion, getDownloadProvider())
.thenComposeAsync(() -> installLibraryAsync(baseVersion, versionList.getVersion(gameVersion, libraryVersion)
.orElseThrow(() -> new IllegalStateException("Remote library " + libraryId + " has no version " + libraryVersion))));
.orElseThrow(() -> new IOException("Remote library " + libraryId + " has no version " + libraryVersion))));
}
@Override
public Task<Version> installLibraryAsync(Version baseVersion, RemoteVersion libraryVersion) {
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
return libraryVersion.getInstallTask(this, baseVersion)
return removeLibraryAsync(baseVersion.resolvePreservingPatches(repository), libraryVersion.getLibraryId())
.thenComposeAsync(version -> libraryVersion.getInstallTask(this, version))
.thenApplyAsync(baseVersion::addPatch)
.thenComposeAsync(repository::save);
}
public ExceptionalFunction<Version, Task<Version>, ?> installLibraryAsync(RemoteVersion libraryVersion) {
return version -> installLibraryAsync(version, libraryVersion);
}
@@ -126,38 +126,32 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
} catch (IOException ignore) {
}
throw new UnsupportedOperationException("Library cannot be recognized");
throw new UnsupportedLibraryInstallerException();
})
.thenApplyAsync(oldVersion::addPatch)
.thenComposeAsync(repository::save);
}
public static class UnsupportedLibraryInstallerException extends Exception {
}
/**
* Remove installed library.
* Will try to remove libraries and patches.
*
* @param versionId version id
* @param version not resolved version
* @param libraryId forge/liteloader/optifine/fabric
* @return task to remove the specified library
*/
public Task<Version> removeLibraryWithoutSavingAsync(String versionId, String libraryId) {
public Task<Version> removeLibraryAsync(Version version, String libraryId) {
// MaintainTask requires version that does not inherits from any version.
// If we want to remove a library in dependent version, we should keep the dependents not changed
// So resolving this game version to preserve all information in this version.json is necessary.
Version version = repository.getResolvedPreservingPatchesVersion(versionId);
if (version.isResolved())
throw new IllegalArgumentException("removeLibraryWithoutSavingAsync requires non-resolved version");
Version independentVersion = version.resolvePreservingPatches(repository);
return Task.supplyAsync(() -> LibraryAnalyzer.analyze(version).removeLibrary(libraryId).build());
return Task.supplyAsync(() -> LibraryAnalyzer.analyze(independentVersion).removeLibrary(libraryId).build());
}
/**
* Remove installed library.
* Will try to remove libraries and patches.
*
* @param versionId version id
* @param libraryId forge/liteloader/optifine/fabric
* @return task to remove the specified library
*/
public Task<Version> removeLibraryAsync(String versionId, String libraryId) {
return removeLibraryWithoutSavingAsync(versionId, libraryId).thenComposeAsync(repository::save);
}
}
@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.download;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* The service provider that provides Minecraft online file downloads.
*
@@ -39,6 +42,12 @@ public interface DownloadProvider {
*/
String injectURL(String baseURL);
default Stream<String> injectURLs(String[] baseURLs) {
Stream<String> urls = Arrays.stream(baseURLs);
Stream<String> jsonURLs = Arrays.stream(baseURLs).map(this::injectURL);
return Stream.concat(jsonURLs, urls);
}
/**
* the specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
*
@@ -20,19 +20,17 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public final class LibraryAnalyzer {
import static org.jackhuang.hmcl.util.Pair.pair;
public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMark> {
private Version version;
private final Map<String, Pair<Library, String>> libraries;
@@ -49,9 +47,23 @@ public final class LibraryAnalyzer {
return Optional.ofNullable(libraries.get(type)).map(Pair::getValue);
}
public void forEachLibrary(BiConsumer<String, @Nullable String> callback) {
for (Map.Entry<String, Pair<Library, String>> entry : libraries.entrySet())
callback.accept(entry.getKey(), entry.getValue().getValue());
@NotNull
@Override
public Iterator<LibraryMark> iterator() {
return new Iterator<LibraryMark>() {
Iterator<Map.Entry<String, Pair<Library, String>>> impl = libraries.entrySet().iterator();
@Override
public boolean hasNext() {
return impl.hasNext();
}
@Override
public LibraryMark next() {
Map.Entry<String, Pair<Library, String>> entry = impl.next();
return new LibraryMark(entry.getKey(), entry.getValue().getValue());
}
};
}
public boolean has(LibraryType type) {
@@ -122,7 +134,7 @@ public final class LibraryAnalyzer {
for (LibraryType type : LibraryType.values()) {
if (type.group.matcher(groupId).matches() && type.artifact.matcher(artifactId).matches()) {
libraries.put(type.getPatchId(), Pair.pair(library, library.getVersion()));
libraries.put(type.getPatchId(), pair(library, library.getVersion()));
break;
}
}
@@ -130,7 +142,7 @@ public final class LibraryAnalyzer {
for (Version patch : version.getPatches()) {
if (patch.isHidden()) continue;
libraries.put(patch.getId(), Pair.pair(null, patch.getVersion()));
libraries.put(patch.getId(), pair(null, patch.getVersion()));
}
return new LibraryAnalyzer(version, libraries);
@@ -169,4 +181,24 @@ public final class LibraryAnalyzer {
return null;
}
}
public static class LibraryMark {
private final String libraryId;
private final String libraryVersion;
public LibraryMark(@NotNull String libraryId, @Nullable String libraryVersion) {
this.libraryId = libraryId;
this.libraryVersion = libraryVersion;
}
@NotNull
public String getLibraryId() {
return libraryId;
}
@Nullable
public String getLibraryVersion() {
return libraryVersion;
}
}
}
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.List;
import java.util.Objects;
/**
@@ -31,9 +32,10 @@ import java.util.Objects;
*/
public class RemoteVersion implements Comparable<RemoteVersion> {
private final String libraryId;
private final String gameVersion;
private final String selfVersion;
private final String url;
private final String[] url;
private final Type type;
/**
@@ -43,8 +45,8 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
*/
public RemoteVersion(String gameVersion, String selfVersion, String url) {
this(gameVersion, selfVersion, url, Type.UNCATEGORIZED);
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, String... url) {
this(libraryId, gameVersion, selfVersion, Type.UNCATEGORIZED, url);
}
/**
@@ -54,13 +56,18 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
*/
public RemoteVersion(String gameVersion, String selfVersion, String url, Type type) {
public RemoteVersion(String libraryId, String gameVersion, String selfVersion, Type type, String... url) {
this.libraryId = Objects.requireNonNull(libraryId);
this.gameVersion = Objects.requireNonNull(gameVersion);
this.selfVersion = Objects.requireNonNull(selfVersion);
this.url = Objects.requireNonNull(url);
this.type = Objects.requireNonNull(type);
}
public String getLibraryId() {
return libraryId;
}
public String getGameVersion() {
return gameVersion;
}
@@ -69,7 +76,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
return selfVersion;
}
public String getUrl() {
public String[] getUrl() {
return url;
}
@@ -65,7 +65,7 @@ public final class FabricInstallTask extends Task<Version> {
}
@Override
public void preExecute() {
public void preExecute() throws Exception {
if (!Objects.equals("net.minecraft.client.main.Main", version.resolve(dependencyManager.getGameRepository()).getMainClass()))
throw new UnsupportedFabricInstallationException();
}
@@ -128,6 +128,6 @@ public final class FabricInstallTask extends Task<Version> {
return new Version("net.fabricmc", loaderVersion, 30000, arguments, mainClass, libraries);
}
public static class UnsupportedFabricInstallationException extends UnsupportedOperationException {
public static class UnsupportedFabricInstallationException extends Exception {
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.download.fabric;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
@@ -31,7 +32,7 @@ public class FabricRemoteVersion extends RemoteVersion {
* @param url the installer or universal jar URL.
*/
FabricRemoteVersion(String gameVersion, String selfVersion, String url) {
super(gameVersion, selfVersion, url);
super(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), gameVersion, selfVersion, url);
}
@Override
@@ -28,11 +28,17 @@ 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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion> {
public static final ForgeBMCLVersionList INSTANCE = new ForgeBMCLVersionList();
@@ -76,19 +82,28 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
for (ForgeVersion version : forgeVersions) {
if (version == null)
continue;
String jar = null;
List<String> urls = new ArrayList<>();
for (ForgeVersion.File file : version.getFiles())
if ("installer".equals(file.getCategory()) && "jar".equals(file.getFormat())) {
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;
String fileName1 = "forge-" + classifier + "-" + file.getCategory() + "." + file.getFormat();
String fileName2 = "forge-" + classifier + "-" + gameVersion + "-" + file.getCategory() + "." + file.getFormat();
urls.add(NetworkUtils.withQuery("https://bmclapi2.bangbang93.com/forge/download", mapOf(
pair("mcversion", version.getGameVersion()),
pair("version", version.getVersion()),
pair("branch", version.getBranch()),
pair("category", file.getCategory()),
pair("format", file.getFormat())
)));
urls.add("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" + classifier + "/" + fileName1);
urls.add("https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" + classifier + "-" + gameVersion + "/" + fileName2);
}
if (jar == null)
if (urls.isEmpty())
continue;
versions.put(gameVersion, new ForgeRemoteVersion(
version.getGameVersion(), version.getVersion(), jar
version.getGameVersion(), version.getVersion(), urls.toArray(new String[0])
));
}
} finally {
@@ -121,18 +136,22 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
this.files = files;
}
@Nullable
public String getBranch() {
return branch;
}
@NotNull
public String getGameVersion() {
return mcversion;
}
@NotNull
public String getVersion() {
return version;
}
@NotNull
public List<File> getFiles() {
return files;
}
@@ -33,10 +33,8 @@ import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
@@ -69,7 +67,11 @@ public final class ForgeInstallTask extends Task<Version> {
public void preExecute() throws Exception {
installer = Files.createTempFile("forge-installer", ".jar");
dependent = new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), installer.toFile())
dependent = new FileDownloadTask(
Arrays.stream(remote.getUrl())
.map(NetworkUtils::toURL)
.collect(Collectors.toList()),
installer.toFile(), null)
.setCacheRepository(dependencyManager.getCacheRepository())
.setCaching(true);
}
@@ -38,6 +38,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
@@ -230,19 +231,20 @@ public class ForgeNewInstallTask extends Task<Version> {
command.add(mainClass);
List<String> args = processor.getArgs().stream().map(arg -> {
List<String> args = new ArrayList<>(processor.getArgs().size());
for (String arg : processor.getArgs()) {
String parsed = parseLiteral(arg, data, ExceptionalFunction.identity());
if (parsed == null)
throw new IllegalStateException("Invalid forge installation configuration");
return parsed;
}).collect(Collectors.toList());
throw new IOException("Invalid forge installation configuration");
args.add(parsed);
}
command.addAll(args);
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
int exitCode = SystemUtils.callExternalProcess(command);
if (exitCode != 0)
throw new IllegalStateException("Game processor exited abnormally");
throw new IOException("Game processor exited abnormally");
for (Map.Entry<String, String> entry : outputs.entrySet()) {
Path artifact = Paths.get(entry.getKey());
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
@@ -30,8 +31,8 @@ public class ForgeRemoteVersion extends RemoteVersion {
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
*/
public ForgeRemoteVersion(String gameVersion, String selfVersion, String url) {
super(gameVersion, selfVersion, url);
public ForgeRemoteVersion(String gameVersion, String selfVersion, String... url) {
super(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), gameVersion, selfVersion, url);
}
@Override
@@ -64,7 +64,7 @@ public class GameInstallTask extends Task<Version> {
@Override
public void execute() throws Exception {
Version patch = JsonUtils.GSON.fromJson(downloadTask.getResult(), Version.class)
Version patch = JsonUtils.fromNonNullJson(downloadTask.getResult(), Version.class)
.setId(MINECRAFT.getPatchId()).setVersion(remote.getGameVersion()).setJar(null).setPriority(0);
setResult(patch);
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.game.Version;
@@ -37,7 +38,7 @@ public final class GameRemoteVersion extends RemoteVersion {
private final Date time;
public GameRemoteVersion(String gameVersion, String selfVersion, String url, ReleaseType type, Date time) {
super(gameVersion, selfVersion, url, getReleaseType(type));
super(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion, selfVersion, getReleaseType(type), url);
this.type = type;
this.time = time;
}
@@ -24,9 +24,15 @@ import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
@@ -61,10 +67,12 @@ public final class VersionJsonDownloadTask extends Task<String> {
}
@Override
public void execute() {
public void execute() throws IOException {
RemoteVersion remoteVersion = gameVersionList.getVersion(gameVersion, gameVersion)
.orElseThrow(() -> new IllegalStateException("Cannot find specific version " + gameVersion + " in remote repository"));
String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl());
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL)).storeTo(this::setResult));
.orElseThrow(() -> new IOException("Cannot find specific version " + gameVersion + " in remote repository"));
dependencies.add(new GetTask(
dependencyManager.getDownloadProvider().injectURLs(remoteVersion.getUrl())
.map(NetworkUtils::toURL).collect(Collectors.toList()),
UTF_8).storeTo(this::setResult));
}
}
@@ -67,7 +67,7 @@ public final class LiteLoaderInstallTask extends Task<Version> {
Library library = new Library(
new Artifact("com.mumfrey", "liteloader", remote.getSelfVersion()),
"http://dl.liteloader.com/versions/",
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()))
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()[0]))
);
setResult(new Version(LibraryAnalyzer.LibraryType.LITELOADER.getPatchId(),
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
@@ -36,7 +37,7 @@ public class LiteLoaderRemoteVersion extends RemoteVersion {
* @param url the installer or universal jar URL.
*/
LiteLoaderRemoteVersion(String gameVersion, String selfVersion, String url, String tweakClass, Collection<Library> libraries) {
super(gameVersion, selfVersion, url);
super(LibraryAnalyzer.LibraryType.LITELOADER.getPatchId(), gameVersion, selfVersion, url);
this.tweakClass = tweakClass;
this.libraries = libraries;
@@ -72,7 +72,7 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
continue;
String gameVersion = VersionNumber.normalize(element.getGameVersion());
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre));
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, mirror, isPre));
}
}
};
@@ -45,11 +45,8 @@ import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.getOrDefault;
@@ -91,7 +88,7 @@ public final class OptiFineInstallTask extends Task<Version> {
new Artifact("optifine", "OptiFine", mavenVersion, "installer"), null,
new LibrariesDownloadInfo(new LibraryDownloadInfo(
"optifine/OptiFine/" + mavenVersion + "/OptiFine-" + mavenVersion + "-installer.jar",
remote.getUrl()))
remote.getUrl()[0]))
);
}
@@ -105,7 +102,9 @@ public final class OptiFineInstallTask extends Task<Version> {
dest = Files.createTempFile("optifine-installer", ".jar");
if (installer == null) {
dependents.add(new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), dest.toFile())
dependents.add(new FileDownloadTask(
Arrays.stream(remote.getUrl()).map(NetworkUtils::toURL).collect(Collectors.toList()),
dest.toFile(), null)
.setCacheRepository(dependencyManager.getCacheRepository())
.setCaching(true));
} else {
@@ -149,7 +148,7 @@ public final class OptiFineInstallTask extends Task<Version> {
gameRepository.getLibraryFile(version, optiFineLibrary).toString()
);
if (exitCode != 0)
throw new IllegalStateException("OptiFine patcher failed");
throw new IOException("OptiFine patcher failed");
} else {
FileUtils.copyFile(dest, gameRepository.getLibraryFile(version, optiFineLibrary).toPath());
}
@@ -188,7 +187,7 @@ public final class OptiFineInstallTask extends Task<Version> {
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult()));
}
public static class UnsupportedOptiFineInstallationException extends UnsupportedOperationException {
public static class UnsupportedOptiFineInstallationException extends Exception {
}
/**
@@ -223,7 +222,7 @@ public final class OptiFineInstallTask extends Task<Version> {
throw new VersionMismatchException(mcVersion, gameVersion.get());
return new OptiFineInstallTask(dependencyManager, version,
new OptiFineRemoteVersion(mcVersion, ofEdition + "_" + ofRelease, () -> null, false), installer);
new OptiFineRemoteVersion(mcVersion, ofEdition + "_" + ofRelease, "", false), installer);
}
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.download.optifine;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
@@ -25,17 +26,9 @@ import org.jackhuang.hmcl.task.Task;
import java.util.function.Supplier;
public class OptiFineRemoteVersion extends RemoteVersion {
private final Supplier<String> url;
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url, boolean snapshot) {
super(gameVersion, selfVersion, "", snapshot ? Type.SNAPSHOT : Type.RELEASE);
this.url = url;
}
@Override
public String getUrl() {
return url.get();
public OptiFineRemoteVersion(String gameVersion, String selfVersion, String url, boolean snapshot) {
super(LibraryAnalyzer.LibraryType.OPTIFINE.getPatchId(), gameVersion, selfVersion, snapshot ? Type.SNAPSHOT : Type.RELEASE, url);
}
@Override
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.util.Logging;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
*
@@ -27,12 +28,11 @@ import java.util.HashMap;
*/
public final class EventBus {
private final HashMap<Class<?>, EventManager<?>> events = new HashMap<>();
private final ConcurrentHashMap<Class<?>, EventManager<?>> events = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
if (!events.containsKey(clazz))
events.put(clazz, new EventManager<>());
events.putIfAbsent(clazz, new EventManager<>());
return (EventManager<T>) events.get(clazz);
}
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.util.SimpleMultimap;
import java.lang.ref.WeakReference;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
/**
@@ -31,7 +32,7 @@ import java.util.function.Consumer;
public final class EventManager<T extends Event> {
private final SimpleMultimap<EventPriority, Consumer<T>> handlers
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new);
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), CopyOnWriteArraySet::new);
public Consumer<T> registerWeak(Consumer<T> consumer) {
register(new WeakListener(consumer));
@@ -23,11 +23,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -99,7 +95,7 @@ public final class Arguments {
}
public static List<String> parseStringArguments(List<String> arguments, Map<String, String> keys) {
return arguments.stream().flatMap(str -> new StringArgument(str).toString(keys, Collections.emptyMap()).stream()).collect(Collectors.toList());
return arguments.stream().filter(Objects::nonNull).flatMap(str -> new StringArgument(str).toString(keys, Collections.emptyMap()).stream()).collect(Collectors.toList());
}
public static List<String> parseArguments(List<Argument> arguments, Map<String, String> keys) {
@@ -107,7 +103,7 @@ public final class Arguments {
}
public static List<String> parseArguments(List<Argument> arguments, Map<String, String> keys, Map<String, Boolean> features) {
return arguments.stream().flatMap(arg -> arg.toString(keys, features).stream()).collect(Collectors.toList());
return arguments.stream().filter(Objects::nonNull).flatMap(arg -> arg.toString(keys, features).stream()).collect(Collectors.toList());
}
public static final List<Argument> DEFAULT_JVM_ARGUMENTS;
@@ -54,6 +54,6 @@ public final class AssetObject implements Validation {
@Override
public void validate() throws JsonParseException {
if (StringUtils.isBlank(hash) || hash.length() < 2)
throw new IllegalStateException("AssetObject hash cannot be blank.");
throw new JsonParseException("AssetObject hash cannot be blank.");
}
}
@@ -24,5 +24,7 @@ package org.jackhuang.hmcl.game;
public enum DownloadType {
CLIENT,
SERVER,
WINDOWS_SERVER
WINDOWS_SERVER,
CLIENT_MAPPINGS,
SERVER_MAPPINGS
}
@@ -23,10 +23,7 @@ import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.util.Immutable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -69,6 +66,7 @@ public class RuledArgument implements Argument {
public List<String> toString(Map<String, String> keys, Map<String, Boolean> features) {
if (CompatibilityRule.appliesToCurrentEnvironment(rules, features) && value != null)
return value.stream()
.filter(Objects::nonNull)
.map(StringArgument::new)
.map(str -> str.toString(keys, features).get(0))
.collect(Collectors.toList());
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.JsonMap;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable;
@@ -60,8 +61,8 @@ public class Version implements Comparable<Version>, Validation {
private final String assets;
private final List<Library> libraries;
private final List<CompatibilityRule> compatibilityRules;
private final Map<DownloadType, DownloadInfo> downloads;
private final Map<DownloadType, LoggingInfo> logging;
private final JsonMap<DownloadType, DownloadInfo> downloads;
private final JsonMap<DownloadType, LoggingInfo> logging;
private final ReleaseType type;
private final Date time;
private final Date releaseTime;
@@ -103,8 +104,8 @@ public class Version implements Comparable<Version>, Validation {
this.assets = assets;
this.libraries = Lang.copyList(libraries);
this.compatibilityRules = Lang.copyList(compatibilityRules);
this.downloads = downloads == null ? null : new HashMap<>(downloads);
this.logging = logging == null ? null : new HashMap<>(logging);
this.downloads = downloads == null ? null : new JsonMap<>(downloads);
this.logging = logging == null ? null : new JsonMap<>(logging);
this.type = type;
this.time = time == null ? null : (Date) time.clone();
this.releaseTime = releaseTime == null ? null : (Date) releaseTime.clone();
@@ -91,7 +91,7 @@ public class World {
private void loadFromZipImpl(Path root) throws IOException {
Path levelDat = root.resolve("level.dat");
if (!Files.exists(levelDat))
throw new IllegalArgumentException("Not a valid world zip file since level.dat cannot be found.");
throw new IOException("Not a valid world zip file since level.dat cannot be found.");
getWorldName(levelDat);
}
@@ -116,6 +116,9 @@ public class World {
CompoundTag nbt = parseLevelDat(levelDat);
CompoundTag data = nbt.get("Data");
if (data == null)
throw new IOException("level.dat missing Data");
if (data.get("LevelName") instanceof StringTag)
worldName = data.<StringTag>get("LevelName").getValue();
else
@@ -209,7 +212,7 @@ public class World {
return Files.list(savesDir).flatMap(world -> {
try {
return Stream.of(new World(world));
} catch (IOException | IllegalArgumentException e) {
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read world " + world, e);
return Stream.empty();
}
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
@@ -80,7 +81,7 @@ public class MultiMCModpackExportTask extends Task<Void> {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(versionId));
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(versionId))
.orElseThrow(() -> new IllegalStateException("Cannot parse the version of " + versionId));
.orElseThrow(() -> new IOException("Cannot parse the version of " + versionId));
List<MultiMCManifest.MultiMCManifestComponent> components = new ArrayList<>();
components.add(new MultiMCManifest.MultiMCManifestComponent(true, false, "net.minecraft", gameVersion));
analyzer.getVersion(FORGE).ifPresent(forgeVersion ->
@@ -33,6 +33,9 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
@@ -80,11 +83,10 @@ public class FileDownloadTask extends Task<Void> {
}
}
private final URL url;
private final List<URL> urls;
private final File file;
private final IntegrityCheck integrityCheck;
private final int retry;
private final EventManager<FailedEvent<URL>> onFailed = new EventManager<>();
private Path candidate;
private boolean caching;
private CacheRepository repository = CacheRepository.getInstance();
@@ -115,7 +117,7 @@ public class FileDownloadTask extends Task<Void> {
* @param retry the times for retrying if downloading fails.
*/
public FileDownloadTask(URL url, File file, IntegrityCheck integrityCheck, int retry) {
this.url = url;
this.urls = Collections.singletonList(url);
this.file = file;
this.integrityCheck = integrityCheck;
this.retry = retry;
@@ -124,6 +126,25 @@ public class FileDownloadTask extends Task<Void> {
setExecutor(Schedulers.io());
}
/**
* Constructor.
* @param urls urls of remote file, will be attempted in order.
* @param file the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
*/
public FileDownloadTask(List<URL> urls, File file, IntegrityCheck integrityCheck) {
if (urls == null || urls.isEmpty())
throw new IllegalArgumentException("At least one URL is required");
this.urls = new ArrayList<>(urls);
this.file = file;
this.integrityCheck = integrityCheck;
this.retry = urls.size();
setName(file.getName());
setExecutor(Schedulers.io());
}
private void closeFiles() {
if (rFile != null)
try {
@@ -143,14 +164,6 @@ public class FileDownloadTask extends Task<Void> {
stream = null;
}
public EventManager<FailedEvent<URL>> getOnFailed() {
return onFailed;
}
public URL getUrl() {
return url;
}
public File getFile() {
return file;
}
@@ -172,8 +185,6 @@ public class FileDownloadTask extends Task<Void> {
@Override
public void execute() throws Exception {
URL currentURL = url;
boolean checkETag;
// Check cache
if (integrityCheck != null && caching) {
@@ -182,7 +193,7 @@ public class FileDownloadTask extends Task<Void> {
if (cache.isPresent()) {
try {
FileUtils.copyFile(cache.get().toFile(), file);
Logging.LOG.log(Level.FINER, "Successfully verified file " + file + " from " + currentURL);
Logging.LOG.log(Level.FINER, "Successfully verified file " + file + " from " + urls.get(0));
return;
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to copy cache files", e);
@@ -192,15 +203,11 @@ public class FileDownloadTask extends Task<Void> {
checkETag = true;
}
Logging.LOG.log(Level.FINER, "Downloading " + currentURL + " to " + file);
Logging.LOG.log(Level.FINER, "Downloading " + urls.get(0) + " to " + file);
Exception exception = null;
for (int repeat = 0; repeat < retry; repeat++) {
if (repeat > 0) {
FailedEvent<URL> event = new FailedEvent<>(this, repeat, currentURL);
onFailed.fireEvent(event);
currentURL = event.getNewResult();
}
URL url = urls.get(repeat % urls.size());
if (Thread.interrupted()) {
Thread.currentThread().interrupt();
break;
@@ -226,7 +233,7 @@ public class FileDownloadTask extends Task<Void> {
repository.removeRemoteEntry(con);
}
} else if (con.getResponseCode() / 100 != 2) {
throw new ResponseCodeException(currentURL, con.getResponseCode());
throw new ResponseCodeException(url, con.getResponseCode());
}
int contentLength = con.getContentLength();
@@ -292,7 +299,7 @@ public class FileDownloadTask extends Task<Void> {
}
if (downloaded != contentLength)
throw new IllegalStateException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
throw new IOException("Unexpected file size: " + downloaded + ", expected: " + contentLength);
// Integrity check
if (integrityCheck != null) {
@@ -312,17 +319,18 @@ public class FileDownloadTask extends Task<Void> {
}
return;
} catch (IOException | IllegalStateException e) {
} catch (IOException e) {
if (temp != null)
temp.toFile().delete();
exception = e;
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + repeat + 1, e);
} finally {
closeFiles();
}
}
if (exception != null)
throw new DownloadException(currentURL, exception);
throw new DownloadException(urls.get(0), exception);
}
}
@@ -30,6 +30,9 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -40,7 +43,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/
public final class GetTask extends Task<String> {
private final URL url;
private final List<URL> urls;
private final Charset charset;
private final int retry;
private CacheRepository repository = CacheRepository.getInstance();
@@ -54,7 +57,7 @@ public final class GetTask extends Task<String> {
}
public GetTask(URL url, Charset charset, int retry) {
this.url = url;
this.urls = Collections.singletonList(url);
this.charset = charset;
this.retry = retry;
@@ -62,6 +65,15 @@ public final class GetTask extends Task<String> {
setExecutor(Schedulers.io());
}
public GetTask(List<URL> urls, Charset charset) {
this.urls = new ArrayList<>(urls);
this.charset = charset;
this.retry = urls.size();
setName(urls.get(0).toString());
setExecutor(Schedulers.io());
}
public GetTask setCacheRepository(CacheRepository repository) {
this.repository = repository;
return this;
@@ -72,8 +84,7 @@ public final class GetTask extends Task<String> {
Exception exception = null;
boolean checkETag = true;
for (int time = 0; time < retry; ++time) {
if (time > 0)
Logging.LOG.log(Level.WARNING, "Failed to download, repeat times: " + time);
URL url = urls.get(time % urls.size());
try {
updateProgress(0);
HttpURLConnection conn = NetworkUtils.createConnection(url);
@@ -122,10 +133,11 @@ public final class GetTask extends Task<String> {
return;
} catch (IOException ex) {
exception = ex;
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + time + 1, ex);
}
}
if (exception != null)
throw new DownloadException(url, exception);
throw new DownloadException(urls.get(0), exception);
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.task;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
@@ -64,7 +65,9 @@ public final class TaskExecutor {
Logging.LOG.log(Level.WARNING, "An exception occurred in task execution", exception);
Throwable resolvedException = resolveException(exception);
if (resolvedException instanceof RuntimeException) {
if (resolvedException instanceof RuntimeException &&
!(resolvedException instanceof CancellationException) &&
!(resolvedException instanceof JsonParseException)) {
// Track uncaught RuntimeException which are thrown mostly by our mistake
if (uncaughtExceptionHandler != null)
uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), resolvedException);
@@ -0,0 +1,51 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.gson;
import java.util.HashMap;
import java.util.Map;
/**
* To resolve JsonParseException: duplicate key: null
* By skipping inserting data with key null
* @param <K>
* @param <V>
*/
public class JsonMap<K, V> extends HashMap<K, V> {
public JsonMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}
public JsonMap(int initialCapacity) {
super(initialCapacity);
}
public JsonMap() {
super();
}
public JsonMap(Map<? extends K, ? extends V> m) {
super(m);
}
@Override
public V put(K key, V value) {
if (key == null) return null;
return super.put(key, value);
}
}
@@ -179,6 +179,8 @@ public final class CompressingUtils {
throw new ZipException(error.getMessage());
} catch (UnsupportedOperationException ex) {
throw new IOException("Not a zip file", ex);
} catch (FileSystemNotFoundException ex) {
throw new IOException("Java Environment is broken");
}
}
@@ -40,6 +40,8 @@ public final class NetworkUtils {
StringBuilder sb = new StringBuilder(baseUrl);
boolean first = true;
for (Entry<String, String> param : params.entrySet()) {
if (param.getValue() == null)
continue;
if (first) {
sb.append('?');
first = false;
@@ -28,8 +28,10 @@ import java.lang.management.ManagementFactory;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* Represents the operating system.
@@ -98,6 +100,10 @@ public enum OperatingSystem {
*/
public static final String SYSTEM_ARCHITECTURE;
public static final Pattern INVALID_RESOURCE_CHARACTERS;
private static final String[] INVALID_RESOURCE_BASENAMES;
private static final String[] INVALID_RESOURCE_FULLNAMES;
static {
String name = System.getProperty("os.name").toLowerCase(Locale.US);
if (name.contains("win"))
@@ -119,6 +125,24 @@ public enum OperatingSystem {
if (arch == null)
arch = System.getProperty("os.arch");
SYSTEM_ARCHITECTURE = arch;
// setup the invalid names
if (CURRENT_OS == WINDOWS) {
// valid names and characters taken from http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/naming_a_file.asp
INVALID_RESOURCE_CHARACTERS = Pattern.compile("[/\"<>|?*:\\\\]");
INVALID_RESOURCE_BASENAMES = new String[]{"aux", "com1", "com2", "com3", "com4", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
"com5", "com6", "com7", "com8", "com9", "con", "lpt1", "lpt2", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
"lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "nul", "prn"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$
Arrays.sort(INVALID_RESOURCE_BASENAMES);
//CLOCK$ may be used if an extension is provided
INVALID_RESOURCE_FULLNAMES = new String[]{"clock$"}; //$NON-NLS-1$
} else {
//only front slash and null char are invalid on UNIXes
//taken from http://www.faqs.org/faqs/unix-faq/faq/part2/section-2.html
INVALID_RESOURCE_CHARACTERS = null;
INVALID_RESOURCE_BASENAMES = null;
INVALID_RESOURCE_FULLNAMES = null;
}
}
private static Optional<Long> getTotalPhysicalMemorySize() {
@@ -154,4 +178,38 @@ public enum OperatingSystem {
return Paths.get(home, folder);
}
}
/**
* Returns true if the given name is a valid resource name on this operating system,
* and false otherwise.
*/
public static boolean isNameValid(String name) {
//. and .. have special meaning on all platforms
if (name.equals(".") || name.equals("..") || name.indexOf('/') == 0 || name.indexOf('\0') >= 0) //$NON-NLS-1$ //$NON-NLS-2$
return false;
if (CURRENT_OS == WINDOWS) {
//empty names are not valid
final int length = name.length();
if (length == 0)
return false;
final char lastChar = name.charAt(length - 1);
// filenames ending in dot are not valid
if (lastChar == '.')
return false;
// file names ending with whitespace are truncated (bug 118997)
if (Character.isWhitespace(lastChar))
return false;
int dot = name.indexOf('.');
// on windows, filename suffixes are not relevant to name validity
String basename = dot == -1 ? name : name.substring(0, dot);
if (Arrays.binarySearch(INVALID_RESOURCE_BASENAMES, basename.toLowerCase()) >= 0)
return false;
if (Arrays.binarySearch(INVALID_RESOURCE_FULLNAMES, name.toLowerCase()) >= 0)
return false;
if (INVALID_RESOURCE_CHARACTERS != null && INVALID_RESOURCE_CHARACTERS.matcher(name).find())
return false;
}
return true;
}
}