Compare commits

..
40 Commits
Author SHA1 Message Date
Yuhui Huang 1f9c94dbc4 Merge pull request #616 from machinesmith42/javafx
Update I18N.properties
2019-10-10 16:56:14 +08:00
huanghongxun a2540c2e9b Fix #619 2019-10-10 16:51:01 +08:00
huanghongxun 4737240c91 Add: forgesvc for Curse Mod Detection fallback 2019-10-10 16:46:32 +08:00
huanghongxun 944d88b8af Fix: keep not completed Curse modpack game version 2019-10-10 16:39:27 +08:00
huanghongxun 529c4ed420 Fix: ConcurrentModificationException 2019-10-10 16:03:25 +08:00
huanghongxun e0ebb02530 Fix: java.nio.file.InvalidPathException 2019-10-10 15:53:47 +08:00
huanghongxun be22d55cec Fix: java.lang.IllegalArgumentException: MALFORMED[1] 2019-10-10 15:24:37 +08:00
huanghongxun 993ce660aa Fix: not recognizing java 12 2019-09-29 18:46:03 +08:00
machinesmith42 79e120dd74 Update I18N.properties 2019-09-27 10:51:55 -05:00
huanghongxun d4155bc14d Update Russian and English translations 2019-09-13 13:32:20 +08:00
huanghongxun ba699c4ed5 Fix NPE 2019-09-12 16:38:28 +08:00
huanghongxun bc96afd0cd Fix #605 2019-09-12 15:51:20 +08:00
huanghongxun cf1a66e858 load authlib-injector server from external file 2019-09-09 17:39:49 +08:00
huanghongxun 5eba896e6d Fix forge installer 404 2019-09-09 16:49:36 +08:00
huanghongxun e69d149c34 Fix HMCL import 2019-09-09 15:32:55 +08:00
huanghongxun 2f9955febd No longer report JsonParseException 2019-09-05 19:06:09 +08:00
huanghongxun df39be708f Check if given game version is malformed 2019-09-05 17:49:59 +08:00
huanghongxun ce3b9e3b1f FIx: unable to download Forge installer 2019-09-05 15:49:27 +08:00
huanghongxun a9feb65fc8 Try to fix ConcurrentModificationException 2019-08-25 15:35:17 +08:00
huanghongxun 93837a28fa No longer report imageio exception 2019-08-24 12:06:54 +08:00
huanghongxun 678f3b8eb7 Try to fix NPE 2019-08-24 12:01:04 +08:00
huanghongxun 0833fa9e7d Fix crash when typing wrong path 2019-08-24 11:59:55 +08:00
huanghongxun 369b9bb6cf Fix #603 2019-08-24 11:57:43 +08:00
huanghongxun c9b6f2f62f Collect uncaught RuntimeException thrown in task execution 2019-08-24 11:49:53 +08:00
huanghongxun e2787af044 no longer force using javaw 2019-08-20 00:54:12 +08:00
huanghongxun 30c27bbb67 MultiMC fabric and local libraries 2019-08-20 00:53:59 +08:00
huanghongxun 7aee7e33d0 Making versions whose library being removed pure patched 2019-08-20 00:53:36 +08:00
huanghongxun a26061c388 alter order if game item context menu 2019-08-19 16:08:48 +08:00
huanghongxun 11e66ac13c Support changing game version 2019-08-19 16:05:40 +08:00
huanghongxun a2ac3c1f18 Hide generated resolved patches in InstallerListPage 2019-08-19 15:39:30 +08:00
huanghongxun e7142e4032 Fix displaying "null" in GameItem 2019-08-19 14:57:42 +08:00
huanghongxun 0dd9dc6c17 Now pressing enter when at main page will launch the game 2019-08-19 00:49:48 +08:00
huanghongxun a53bfed060 Add primary and secondary click action on GameListItem 2019-08-19 00:38:31 +08:00
huanghongxun c5919f2719 Fix OptiFine installation 2019-08-19 00:18:34 +08:00
huanghongxun fb24516b90 Show unrecognized patches in InstallerListPage 2019-08-19 00:17:03 +08:00
huanghongxun 60fa4eb0a1 Fix LibrariesUniqueTask changing order of classpath 2019-08-19 00:16:02 +08:00
huanghongxun 536576084d Fix forge local installation 2019-08-19 00:12:28 +08:00
huanghongxun c62cc499c5 Show HMCL version in game main menu 2019-08-18 10:54:05 +08:00
huanghongxun c11bbf8836 Fix failure of auto-installing more than one library 2019-08-18 10:27:48 +08:00
huanghongxun 71c0cede24 Fixed unrecognizing Optifine jar file 2019-08-18 10:27:12 +08:00
91 changed files with 2036 additions and 807 deletions

No files matched your search

@@ -20,8 +20,12 @@ 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;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.CrashReporter;
@@ -40,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;
@@ -53,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()));
}
@@ -77,6 +90,7 @@ public final class Launcher extends Application {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
TaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
try {
LOG.info("*** " + Metadata.TITLE + " ***");
@@ -142,5 +156,5 @@ public final class Launcher extends Application {
return result;
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
@@ -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)));
}
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.LibrariesUniqueTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
import org.jackhuang.hmcl.launch.PermissionException;
@@ -132,7 +131,7 @@ public final class LauncherHelper {
private void launch0() {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(LibrariesUniqueTask.unique(repository.getResolvedVersion(selectedVersion)));
Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion));
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.runAsync(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
@@ -304,7 +303,7 @@ public final class LauncherHelper {
}
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9_AND_LATER
&& version.getMainClass().contains("launchwrapper")
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
@@ -107,9 +107,6 @@ public final class ModpackHelper {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
} else {
HMCLGameRepository repository = profile.getRepository();
repository.removeVersionFromDisk(name);
}
};
@@ -123,7 +120,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 <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.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");
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
@@ -482,7 +483,7 @@ public final class VersionSetting {
else if (isUsesCustomJavaDir()) {
try {
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
} catch (IOException e) {
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
@@ -537,6 +538,7 @@ public final class VersionSetting {
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
@@ -40,11 +41,16 @@ public class InstallerItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
{
if (version != null) {
TwoLineListItem item = new TwoLineListItem();
item.setTitle(artifact);
item.setSubtitle(i18n("archive.version") + ": " + version);
setCenter(item);
} else {
Label label = new Label(artifact);
label.setStyle("-fx-font-size: 15px;");
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
setCenter(label);
}
{
@@ -23,7 +23,12 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
@@ -113,7 +118,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchButton.setPrefHeight(55);
launchButton.setButtonType(JFXButton.ButtonType.RAISED);
launchButton.getStyleClass().add("jfx-button-raised");
launchButton.setOnMouseClicked(e -> launch());
launchButton.setOnAction(e -> launch());
launchButton.setDefaultButton(true);
launchButton.setClip(new Rectangle(-100, -100, 310, 200));
{
VBox graphic = new VBox();
@@ -26,11 +26,14 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
@@ -86,6 +89,8 @@ public final class TaskListPane extends StackPane {
if (task instanceof GameAssetDownloadTask) {
task.setName(i18n("assets.download_all"));
} else if (task instanceof GameInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
} else if (task instanceof ForgeInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
@@ -107,22 +107,20 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
public void onNavigate(Map<String, Object> settings) {
lblGameVersion.setText(i18n("install.new_game.current_game_version") + ": " + provider.getGameVersion());
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(provider.getVersion().resolve(provider.getProfile().getRepository()));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(provider.getVersion().resolvePreservingPatches(provider.getProfile().getRepository()));
String fabric = analyzer.getVersion(FABRIC).orElse(null);
String forge = analyzer.getVersion(FORGE).orElse(null);
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);
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
@@ -38,7 +39,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
private final String libraryId;
private final String oldLibraryVersion;
public UpdateInstallerWizardProvider(Profile profile, String gameVersion, Version version, String libraryId, String oldLibraryVersion) {
public UpdateInstallerWizardProvider(@NotNull Profile profile, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @NotNull String oldLibraryVersion) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
@@ -57,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());
}
@@ -28,11 +28,13 @@ import javafx.scene.image.Image;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
@@ -57,12 +59,17 @@ public class GameItem extends Control {
CompletableFuture.supplyAsync(() -> GameVersion.minecraftVersion(profile.getRepository().getVersionJar(id)).orElse(i18n("message.unknown")), POOL_VERSION_RESOLVE)
.thenAcceptAsync(game -> {
StringBuilder libraries = new StringBuilder(game);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedVersion(id));
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values())
analyzer.getVersion(type).ifPresent(library ->
libraries
.append(", ").append(i18n("install.installer." + type.name().toLowerCase()))
.append(": ").append(modifyVersion(game, library.replaceAll("(?i)" + type.name().toLowerCase(), ""))));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id));
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)
@@ -22,7 +22,9 @@ import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.SkinBase;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Theme;
@@ -49,22 +51,24 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
chkSelected.setToggleGroup(skinnable.getToggleGroup());
root.setLeft(chkSelected);
root.setCenter(new GameItem(skinnable.getProfile(), skinnable.getVersion()));
GameItem gameItem = new GameItem(skinnable.getProfile(), skinnable.getVersion());
gameItem.setMouseTransparent(true);
root.setCenter(gameItem);
PopupMenu menu = new PopupMenu();
JFXPopup popup = new JFXPopup(menu);
menu.getContent().setAll(
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(skinnable::launch, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(skinnable::generateLaunchScript, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.gear(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(skinnable::modifyGameSettings, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(skinnable::rename, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(skinnable::remove, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(skinnable::export, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(skinnable::browse, popup)),
new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.launch(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch.test"), FXUtils.withJFXPopupClosing(skinnable::launch, popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(skinnable::generateLaunchScript, popup)));
new IconedMenuItem(FXUtils.limitingSize(SVG.folderOpen(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("folder.game"), FXUtils.withJFXPopupClosing(skinnable::browse, popup)));
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
@@ -91,5 +95,16 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);
root.setCursor(Cursor.HAND);
root.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
if (e.getClickCount() == 1) {
skinnable.modifyGameSettings();
}
} else if (e.getButton() == MouseButton.SECONDARY) {
popup.show(root, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
}
});
}
}
@@ -36,6 +36,8 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.ToolbarListPageSkin;
import org.jackhuang.hmcl.ui.download.InstallerWizardProvider;
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
@@ -74,22 +76,29 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
Task.supplyAsync(() -> {
gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse(null);
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedVersion(versionId));
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();
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
String libraryId = type.getPatchId();
analyzer.getVersion(type).ifPresent(libraryVersion -> itemsProperty().add(
new InstallerItem(i18n("install.installer." + libraryId), libraryVersion, () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
}, removeAction.apply(libraryId))));
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)))
itemsProperty().add(
new InstallerItem(title, libraryVersion, () -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
}, action));
else
itemsProperty().add(new InstallerItem(title, libraryVersion, null, action));
}
}).start();
}
@@ -79,7 +79,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void loadVersion(Profile profile, String id) {
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedVersion(id));
libraryAnalyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id));
modded.set(libraryAnalyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
}
@@ -142,6 +142,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
public void removeSelected(ObservableList<TreeItem<ModListPageSkin.ModInfoObject>> selectedItems) {
try {
modManager.removeMods(selectedItems.stream()
.filter(Objects::nonNull)
.map(TreeItem::getValue)
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
@@ -21,7 +21,6 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
@@ -77,8 +76,7 @@ public class Versions {
}
public static void updateGameAssets(Profile profile, String version) {
Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), resolvedVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
.executor();
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
executor.start();
@@ -33,7 +33,9 @@ import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
@@ -117,6 +119,8 @@ public class WorldListPage extends ListPageBase<WorldListItem> {
}, e -> {
if (e instanceof FileAlreadyExistsException)
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
else if (e instanceof IOException && e.getCause() instanceof InvalidPathException)
reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed")));
else
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
}).start();
@@ -55,6 +55,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
put("java.lang.VerifyError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound"));
put("javax.imageio.IIOException", i18n("crash.NoClassDefFound"));
put("netscape.javascript.JSException", i18n("crash.NoClassDefFound"));
put("java.lang.IncompatibleClassChangeError", i18n("crash.NoClassDefFound"));
put("java.lang.ClassFormatError", i18n("crash.NoClassDefFound"));
@@ -86,6 +87,12 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
private static Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
private final boolean showCrashWindow;
public CrashReporter(boolean showCrashWindow) {
this.showCrashWindow = showCrashWindow;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
LOG.log(Level.SEVERE, "Uncaught exception in thread " + t.getName(), e);
@@ -116,7 +123,9 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
LOG.log(Level.SEVERE, text);
if (checkThrowable(e)) {
Platform.runLater(() -> new CrashWindow(text).show());
if (showCrashWindow) {
Platform.runLater(() -> new CrashWindow(text).show());
}
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
reportToServer(text);
}
@@ -51,7 +51,12 @@ public final class Locales {
*/
public static final SupportedLocale ES = new SupportedLocale(new Locale("es"));
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH, ES);
/**
* Russian
*/
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH, ES, RU);
public static SupportedLocale getLocaleByName(String name) {
if (name == null) return DEFAULT;
@@ -60,6 +65,7 @@ public final class Locales {
case "zh": return ZH;
case "zh_cn": return ZH_CN;
case "es": return ES;
case "ru": return RU;
default: return DEFAULT;
}
}
@@ -69,6 +75,7 @@ public final class Locales {
else if (locale == ZH) return "zh";
else if (locale == ZH_CN) return "zh_CN";
else if (locale == ES) return "es";
else if (locale == RU) return "ru";
else if (locale == DEFAULT) return "def";
else throw new IllegalArgumentException("Unknown locale: " + locale);
}
@@ -37,19 +37,19 @@ account.choose=Choose a character
account.create=Create a new account
account.email=Email
account.failed.character_deleted=The character has been deleted.
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your internet connection.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
account.failed.injector_download_failure=Failed to download authlib-injector. Check your network and try switching to another download source.
account.failed.injector_download_failure=Failed to download authlib-injector. Check your network and try switching to a different download source.
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to login.
account.failed.invalid_token=Please log out and re-enter your password to login.
account.failed.no_character=No character in this account.
account.failed.server_response_malformed=Invalid server response. The authentication server may have a failure.
account.failed.server_response_malformed=Invalid server response. The authentication server may have an error.
account.injector.add=Add an authentication server
account.injector.empty=Empty (Click the plus button right to add)
account.injector.empty=Empty (Click the plus button on the right to add)
account.injector.manage=Manage authentication servers
account.injector.manage.title=Authentication servers
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
account.injector.http=Warning: This server uses HTTP so your password will be transmitted in clear text.
account.injector.link.register=Register
account.injector.server=Auth Server
account.injector.server_url=Server URL
@@ -66,11 +66,11 @@ account.username=Name
archive.author=Authors
archive.game_version=Game
archive.name=Name
archive.name=Nickname
archive.version=Version
assets.download=Download assets
assets.download_all=Check the integrity of assets
assets.download=Downloading assets
assets.download_all=Asset Integrity Check
button.cancel=Cancel
button.clear=Clear
@@ -87,16 +87,16 @@ button.yes=Yes
color.recent=Recommended
color.custom=Custom Color
crash.NoClassDefFound=Please check "HMCL" software is complete.
crash.user_fault=Your OS or Java environment may not be properly installed resulting in crashing of this software, please check your Java Environment or your computer!
crash.NoClassDefFound=Please verify that the "Hello Minecraft! Launcher" software is not corrupted.
crash.user_fault=Your OS or Java environment may not be properly installed which may result in a crash, please check your Java Runtime Environment or your computer!
download=Download
download.code.404=File does not found in remote server
download.code.404=File not found on the remote server
download.failed=Failed to download
download.failed.empty=No candidates. Click here to return.
download.failed.refresh=Unable to load version list. Click here to retry.
download.failed.refresh=Unable to download version list. Click here to retry.
download.provider.bmclapi=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.provider.mojang=Mojang (Forge and OptiFine installation being downloaded from BMCLAPI)
download.provider.mojang=Mojang (Forge and OptiFine installation are downloaded from BMCLAPI)
extension.bat=Windows Bat file
extension.mod=Mod file
@@ -104,10 +104,10 @@ extension.png=Image file
extension.sh=Bash shell
fatal.missing_javafx=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Java 8 or 10.\nIf you are using OpenJDK, please ensure OpenJFX is included.
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use HMCL, but HMCL will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause HMCL not to function properly.\nPlease upgrade your Java to 8u101 or later to resolve the problem.
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure HMCL has read and write access to "%s" and the files in it.
fatal.migration_requires_manual_reboot=The upgrade is about to be completed. Please reopen HMCL.
fatal.apply_update_failure=We're sorry that HMCL couldn't finish the upgrade because something went wrong.\nBut you can still manually finish the upgrade by downloading HMCL from %s.\nPlease consider reporting this issue to us.
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use Hello Minecraft! Launcher, but it will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause the launcher not to function properly.\nPlease upgrade your Java Runtime to 8u101 or later to resolve the problem.
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure Hello Minecraft! Launcher has read and write access to "%s" and the files in it.
fatal.migration_requires_manual_reboot=The update is complete. Please reopen Hello Minecraft! Launcher.
fatal.apply_update_failure=We're sorry, Hello Minecraft! Launcher couldn't finish the upgrade because something went wrong.\nBut you can still manually finish the upgrade by downloading Hello Minecraft! Launcher from %s.\nPlease consider reporting this issue to us.
folder.config=Configs
folder.game=Game Directory
@@ -117,83 +117,84 @@ folder.saves=Saves
folder.screenshots=Screenshots
help=Documentations
help.detail=For datapacks, modpacks, etc. makers.
help.detail=For manufacturers of datapacks, modpacks, etc.
input.email=The username must be an e-mail.
input.email=Username must be an email.
input.number=Must be a number.
input.not_empty=Required field
input.url=Must be a valid URL.
install=Install New Game
install.change_version=Change version
install.change_version.confirm=Sure to update %s from verison %s to %s?
install.failed=Failed to install
install.change_version.confirm=Are you sure you want to upgrade %s from verison %s to %s?
install.failed=Version failed to install
install.failed.downloading=Failed to install due to some files not downloaded successfully
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.version_mismatch=The library requires game version %s, but actual version is %s.
install.failed.downloading.timeout=Timed out while downloading the file: %s
install.failed.install_online=Unable to recognize the provided installer file
install.failed.optifine_conflict=Fabric, OptiFine and Forge are installed simultaneously on Minecraft 1.13
install.failed.version_mismatch=The library requires the game version %s, but the actual version is %s.
install.installer.choose=Choose a %s version
install.installer.fabric=Fabric
install.installer.forge=Forge
install.installer.game=Game
install.installer.install=Install %s
install.installer.install_offline=Install/Upgrade from local file
install.installer.install_offline=Install/Upgrade from file
install.installer.install_offline.extension=Forge/OptiFine installer
install.installer.install_offline.tooltip=Support importing Forge/OptiFine installer jar file
install.installer.install_offline.tooltip=Supports import of Forge/OptiFine installation files
install.installer.install_online=Install Online
install.installer.install_online.tooltip=Support Forge, OptiFine, LiteLoader installation.
install.installer.install_online.tooltip=Support installation of Fabric, Forge, OptiFine, LiteLoader.
install.installer.liteloader=LiteLoader
install.installer.not_installed=%s not Installed
install.installer.optifine=OptiFine
install.installer.version=%s Version
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.already_exists=This version already exists.
install.new_game.current_game_version=Current Game Version
install.new_game.malformed=Invalid name
install.select=Select an operation
install.success=Installed successfully
install.success=successfully installed
lang=English
lang.default=Use system language
launch.advice.corrected=We have already corrected JVM selection and game can be launched now. If you do want to keep your JVM choice, you can turn off the Java VM checker in game settings.
launch.advice.uncorrected=If you are sure that game can be launched normally, you can turn off the Java VM checker in game settings.
launch.advice.corrected=We have already fixed the JVM selection. If you want to keep your choice of Java version, you can disable the Java VM check in game settings.
launch.advice.uncorrected=If you are sure that the game can be started normally, you can disable the Java VM check in game settings.
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
launch.advice.forge2760_liteloader=Forge 2760 and above is not compatible with LiteLoader, please consider upgrade Forge to 2773 or later.
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. You are supposed to install the latest version of Java 8.
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
launch.advice.newer_java=Java 8 is suggested, which can make game run more fluently. And many mods and Minecraft 1.12 and newer versions requires Java 8.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game probably crash. The launcher will try to launch it.
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game probably crash. The maximum memory is 1024MB. The launcher will try to launch it.
launch.advice.forge2760_liteloader=Forge 2760 and higher are not compatible with LiteLoader, please consider upgrading Forge to 2773 or later.
launch.advice.java8_1_13=Minecraft 1.13 and later can only run on Java 8 or later.
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. Please install the latest version of Java 8.
launch.advice.java9=You cannot launch Minecraft 1.12 oer earlier with Java 9 or later versions of Java.
launch.advice.newer_java=Java 8 is recommended to make the game faster. For many Minecraft 1.12 of higher, and most mods, Java 8 is required.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game may crash. The launcher attempt to launch.
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. The launcher will attempt to launch.
launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine cannot be created. Java arguments may have problems. You can enable the no args mode in the settings.
launch.failed.creating_process=Failed to create process, maybe your java path is wrong, please modify your java path.
launch.failed.cannot_create_jvm=Java virtual machine could not be created. Java arguments may cause issues. Please restart without JVM arguments.
launch.failed.creating_process=Failed to create process. Check your Java path.
launch.failed.decompressing_natives=Unable to decompress native libraries.
launch.failed.download_library=Unable to download library %s.
launch.failed.executable_permission=Unable to add permission to the launch script
launch.failed.exited_abnormally=Game exited abnormally, please visit the log, or ask someone for help.
launch.failed.exited_abnormally=Game exited abnormally, please check the log, or ask someone for help.
launch.state.dependencies=Dependencies
launch.state.done=Done
launch.state.logging_in=Logging In
launch.state.modpack=Preparing for modpack
launch.state.waiting_launching=Waiting for game launching
launch.wrong_javadir=Incorrect Java directory, will reset to default Java directory.
launch.state.modpack=Loading modpack
launch.state.waiting_launching=Launcing modpack
launch.wrong_javadir=Invalid Java directory, default Java path will be applied.
launcher=Launcher
launcher.background=Background Image
launcher.background.choose=Choose background path.
launcher.background.default=Default
launcher.cache_directory=Download Cache Directory
launcher.cache_directory.choose=Choose download cache directory
launcher.cache_directory.default=Default
launcher.background.choose=Choose a background image file
launcher.background.default=Standard
launcher.cache_directory=Directory for caching
launcher.cache_directory.choose=Choose the directory for caching
launcher.cache_directory.default=Standard
launcher.cache_directory.disabled=Disabled
launcher.cache_directory.invalid=Invalid cache directory. Restoring default settings.
launcher.cache_directory.invalid=Invalid directory. Restoring default settings.
launcher.contact=Contact Us
launcher.crash=Hello Minecraft! Launcher has crashed!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Update it!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Please updater your launcher!
launcher.update_java=Please upgrade your Java.
login.empty_username=You have not set a username!
@@ -210,14 +211,14 @@ message.doing=Please wait
message.downloading=Downloading...
message.error=Error
message.info=Info
message.success=Tasks succeeded
message.success=Job completed successfully
message.unknown=Unknown
message.warning=Warning
modpack=Mod pack
modpack.choose=Choose a modpack zip file which you want to install.
modpack.desc=Describe your modpack, including precautions, changlog, supporting Markdown(also supporting online pictures).
modpack.enter_name=Enter your desired name for this game.
modpack=Modpack
modpack.choose=Choose a modpack zip.
modpack.desc=Describe your modpack, including precautions and changelog. Markdown and online pictures are supported.
modpack.enter_name=Enter a name for this modpack.
modpack.export=Export Modpack
modpack.export.as=Export Modpack As...
modpack.files.blueprints=BuildCraft blueprints
@@ -228,57 +229,58 @@ modpack.files.mods=Mods
modpack.files.mods.voxelmods=VoxelMods (including VoxelMap) options
modpack.files.options_txt=Game options
modpack.files.optionsshaders_txt=Shaders options
modpack.files.resourcepacks=Resource(Texture) packs
modpack.files.resourcepacks=Resource/Texture packs
modpack.files.saves=Saved games
modpack.files.scripts=MineTweaker configuration
modpack.files.servers_dat=Multiplayer servers list
modpack.files.servers_dat=Server list
modpack.install=Install %s modpack
modpack.installing=Installing modpack
modpack.introduction=Curse, MultiMC, HMCL modpacks supprted.
modpack.introduction=Curse, MultiMC, HMCL modpacks are supprted.
modpack.invalid=Invalid modpack file.
modpack.mismatched_type=Mismatched modpack type, your current game is %s modpack, but your update file is %s modpack.
modpack.mismatched_type=Inappropriate modpack type, your current game is a %s modpack, but your update file is a %s modpack.
modpack.name=Modpack Name
modpack.not_a_valid_name=Not a valid modpack name
modpack.not_a_valid_name=Invalid modpack name
modpack.scan=Scanning this modpack
modpack.task.install=Import Modpack
modpack.task.install.error=This modpack file cannot be recognized. Only Curse, MultiMC modpacks are supported.
modpack.task.install.error=This modpack file cannot be recognized. Only Curse and MultiMC modpacks are supported.
modpack.task.install.will=Install the modpack:
modpack.type.curse=Curse
modpack.type.curse.completion=Install files related to Curse modpack
modpack.type.curse.tolerable_error=But we cannot complete downloading all files of this Curse modpack. You can retry downloading when launching corresponding game version. You may retry for a couple of times due to network problems.
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
modpack.type.curse.not_found=Some of required resources are deleted and cannot be downloaded. Please consider the latest version or other modpacks.
modpack.type.hmcl=HMCL
modpack.type.curse.tolerable_error=We cannot complete the download of all files of this Curse modpack. You can retry the download when starting corresponding game version. You may retry for a couple of times due to network problems.
modpack.type.curse.error=Unable to install this Curse modpack. Please retry.
modpack.type.curse.not_found=Some of required resources are missing and thus could not be downloaded. Please consider the latest version or other modpacks.
modpack.type.hmcl=Hello Minecraft! Launcher
modpack.type.hmcl.export=Can be imported by Hello Minecraft! Launcher
modpack.type.multimc=MultiMC
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, Curse modpacks are supported.
modpack.update=Upgrading game
modpack.wizard=Exporting the modpack wizard
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, and Curse modpacks are supported.
modpack.update=Game Update
modpack.wizard=Exporting modpack wizard
modpack.wizard.step.1=Basic options
modpack.wizard.step.1.title=Set the basic options to the modpack.
modpack.wizard.step.2=Files selection
modpack.wizard.step.2.title=Choose the files you want to put into the modpack.
modpack.wizard.step.1.title=Set the basic options for the modpack.
modpack.wizard.step.2=Select files
modpack.wizard.step.2.title=Add files to the modpack.
modpack.wizard.step.3=Modpack Type
modpack.wizard.step.3.title=Choose the format of the modpack.
modpack.wizard.step.initialization.exported_version=The exported game version
modpack.wizard.step.initialization.exported_version=Exported game version
modpack.wizard.step.initialization.include_launcher=Include the launcher
modpack.wizard.step.initialization.save=Choose a path to export the game files to
modpack.wizard.step.initialization.warning=Before making modpack, you should ensure that your game can launch successfully,\nand that your Minecraft is release, not snapshot.\nand that it is not allowed to add mods which is not allowed to distribute to the modpack.
modpack.wizard.step.initialization.save=Export to...
modpack.wizard.step.initialization.warning=Before creating a modpack, you should ensure that the game can launch successfully,\nand that your Minecraft is a release version.\nDo NOT add mods which cannot be redistributed.
mods=Mods
mods.add=Add mods
mods.add.failed=Failed to add mods %s.
mods.add.success=Successfully added mods %s.
mods.add=Install mods
mods.add.failed=Failed to install mods %s.
mods.add.success=Successfully installed mods %s.
mods.choose_mod=Choose your mods
mods.enable=Enable
mods.disable=Disable
mods.name=Name
mods.remove=Remove
mods.not_modded=You should install a mod loader first (Forge, or LiteLoader)
mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader)
datapack=Data packs
datapack.add=Add data pack
datapack.choose_datapack=Choose the datapack zip to be imported
datapack=Datapacks
datapack.add=Install datapack
datapack.choose_datapack=Choose the datapack zip to import
datapack.extension=Datapack
datapack.title=World %s - Datapacks
datapack.remove=Remove
@@ -289,15 +291,15 @@ world.datapack=Manage data packs
world.datapack.1_13=Only Minecraft 1.13 and later versions support data packs.
world.description=%s. Last played time: %s. Game version: %s.
world.export=Export this world
world.export.title=Choose a file location to hold your world
world.export.title=Choose a location to store your world
world.export.location=Export to
world.export.wizard=Export world %s
world.extension=World zip
world.game_version=Game Version
world.import.already_exists=This world already exists.
world.import.choose=Choose the save zip to be imported
world.import.choose=Choose the zip file to import
world.import.failed=Unable to import this world: %s
world.import.invalid=Not a valid world zip
world.import.invalid=Invalid world zip file
world.name=World Name
world.name.enter=Enter the world name
world.reveal=Reveal in Explorer
@@ -305,9 +307,9 @@ world.show_all=Show all
world.time=EEE, MMM d, yyyy HH:mm:ss
profile=Game Directories
profile.already_exists=This name already exists, please consider another name.
profile.already_exists=This name already exists, please use a different name.
profile.default=Current directory
profile.home=User home
profile.home=Standard
profile.instance_directory=Game Directory
profile.instance_directory.choose=Choose Game Directory
profile.manage=Game Directory List
@@ -324,11 +326,11 @@ selector.custom=Custom
settings=Game Settings
settings.advanced=Advanced Settings
settings.advanced.dont_check_game_completeness=Don't check game completeness
settings.advanced.dont_check_game_completeness=Do not scan game files
settings.advanced.dont_check_jvm_validity=Don't check whether JVM can launch the game or not
settings.advanced.game_dir.default=Default (.minecraft/)
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except assets,libraries)
settings.advanced.java_args_default=Default java args: -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.game_dir.default=Standard (.minecraft/)
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except for assets and libraries)
settings.advanced.java_args_default=Stadnard java args: -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=PermGen Space/MB
settings.advanced.jvm_args=Java VM Arguments
settings.advanced.launcher_visibility.close=Close the launcher when the game launched.
@@ -338,9 +340,9 @@ settings.advanced.launcher_visibility.keep=Keep the launcher visible.
settings.advanced.launcher_visible=Launcher Visibility
settings.advanced.minecraft_arguments=Minecraft Arguments
settings.advanced.no_jvm_args=No Default JVM Args
settings.advanced.precall_command=Pre-Launch command(will be executed before game launching)
settings.advanced.server_ip=Server Host
settings.advanced.wrapper_launcher=Wrapper Launcher(i.e. optirun...)
settings.advanced.precall_command=Pre-Launch command (will be executed before game starts)
settings.advanced.server_ip=Server Address
settings.advanced.wrapper_launcher=Wrapper Launcher (i.e. optirun...)
settings.custom=Custom
@@ -358,9 +360,9 @@ settings.game.working_directory.choose=Choose Working Directory
settings.icon=Game Icon
settings.launcher=Settings
settings.launcher.common_path.tooltip=This app will save all game libraries and assets here unless there are existant files in game folder.
settings.launcher.common_path.tooltip=This app will cache all downloads here.
settings.launcher.download_source=Download Source
settings.launcher.enable_game_list=Display game list in main page
settings.launcher.enable_game_list=Show version list in main page
settings.launcher.language=Language
settings.launcher.launcher_log.export=Export launcher logs
settings.launcher.launcher_log.export.failed=Failed to export logs
@@ -388,13 +390,13 @@ settings.type.global.manage=Global Game Settings
settings.type.global.edit=Configure global game settings
settings.type.special.enable=Enable specialized settings for this game
sponsor.bmclapi=To obtain a stable download service, please consider support BMCLAPI. Click here for more information
sponsor.hmcl=HMCL is a free, open source Minecraft launcher the enables users to easily manage multiple, separate installations of Minecraft. We're using Afdian so that we can continue to pay for our hosting and project development. Click here for more information.
sponsor.bmclapi=To obtain a stable download service, please consider supporting BMCLAPI. Click here for more information
sponsor.hmcl=Hello Minecraft! Launcher is a free and open source Minecraft launcher which allows users to easily manage multiple, individual Minecraft installations. We use Afdian to continue to pay for hosting and project development. Click here for more information.
update=Update
update.accept=Update
update.changelog=Changelog
update.channel.dev=Update to development version
update.changelog=Changes
update.channel.dev=Update to beta version
update.channel.stable=Update to stable version
update.checking=Checking for updates
update.failed=Failed to perform upgrade
@@ -402,39 +404,39 @@ update.found=Update Available!
update.newest_version=Latest version: %s
update.bubble.title=Update Available: %s
update.bubble.subtitle=Click here to upgrade
update.note=Development version contains more functionality and bug fixes as well as more possible bugs.
update.latest=This is latest Version.
update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update.
update.note=Warning: Beta versions may have more functionality and bug fixes but also more possible errors.
update.latest=This is the latest version.
update.no_browser=Cannot open a browser. The link has been copied to the clipboard. Paste it into the address bar of browser to update.
update.tooltip=Update
version=Games
version.cannot_read=Unable to gather the game version. Cannot continue auto-installing.
version.cannot_read=Unable to find the game version. Cannot continue automatic installation.
version.empty=No game
version.empty.add=Install game in game list
version.empty.launch=No game to launch, please install new game via game list page.
version.forbidden_name=Forbidden name, do not use this.
version.empty.add=Install new version
version.empty.launch=No version to launch, please install a version via version list page.
version.forbidden_name=Forbidden name please do not use that name.
version.game.old=Old
version.game.release=Release
version.game.snapshot=Snapshot
version.game.release=Releases
version.game.snapshot=Snapshots
version.launch=Play
version.launch.test=Test Game
version.launch_script=Make Launching Script
version.launch_script=Make a Launching Script
version.launch_script.failed=Unable to make launch script.
version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage=Game List
version.manage.clean=Clean game directory
version.manage.clean.tooltip=Clean logs, crash-reports
version.manage.manage=Manage Game
version.launch_script.save=Save launch script
version.launch_script.success=Created script %s.
version.manage=All Versions
version.manage.clean=Clear game directory
version.manage.clean.tooltip=Clear logs, crash-reports
version.manage.manage=Manage Version
version.manage.redownload_assets_index=Update Game Asset Files
version.manage.remove=Delete this game
version.manage.remove.confirm=Sure to remove game %s? You cannot restore this game again!
version.manage.remove.confirm.trash=Sure to remove game %s? You can restore the game with the name of %s in your system trash.
version.manage.remove.confirm.independent=Since this game is in independent mode, deleting this game will result in all saved worlds belonging to this game removed simultaneously. Sure to remove game %s?
version.manage.remove=Delete this version
version.manage.remove.confirm=Are you sure you want to remove this version %s? You cannot restore this version again!
version.manage.remove.confirm.trash=Are you sure you want to remove this version %s? You can restore this version with the name of %s in your system trash.
version.manage.remove.confirm.independent=Since this version is in independent mode, deleting this version will also delete all saved worlds belonging to this version. Do you want to remove this version %s?
version.manage.remove_libraries=Delete library files
version.manage.rename=Rename this game
version.manage.rename.message=Please enter the new name
version.manage.rename.fail=Failed to rename this game.
version.manage.rename=Rename this version
version.manage.rename.message=Please enter the new name for this version
version.manage.rename.fail=Failed to rename the version.
version.settings=Settings
version.update=Update modpack
@@ -16,7 +16,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Contributors: dxNeil
# Contributors: gitfamhub
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2019 huangyuhui.
about.author=Autor
@@ -87,7 +87,7 @@ button.yes=Sí
color.recent=Recomendado
color.custom=Color personalizado
crash.NoClassDefFound=Favor verificar que el software "HMCL" esté completo.
crash.NoClassDefFound=Favor verificar que el software "Hello Minecraft! Launcher" esté completo.
crash.user_fault=Su SO o ambiente de Java podría estar mal instalado resultando en fallos de este software, ¡por favor verifique su ambiente de Java o computadora!
download=Descargar
@@ -104,10 +104,10 @@ extension.png=Archivo de imagen
extension.sh=Bash shell
fatal.missing_javafx=JavaFX no existe.\nSi usas Java 11 o después, por favor descargue Java 8 o 10.\nSi usas OpenJDK, asegúrese que OpenJFX esté incluido.
fatal.missing_dst_root_ca_x3=El certificado DST Root CA X3 no existe en su plataforma Java.\nTodavía podrás usar HMCL, pero HMCL no podrá conectarse a algunos sites (como los que usen certificados de Let's Encrypt), lo cual podrá causar mala función.\nActualize su Java a 8u101 o luego para resolver el problema.
fatal.config_loading_failure=La configuración no es accesible.\nAsegúrese que HMCL tenga permisos read and write a "%s" y los archivos en ello.
fatal.migration_requires_manual_reboot=La actualización está a punto de terminar. Favor reiniciar HMCL.
fatal.apply_update_failure=Lamentamos que HMCL no pudo terminar la actualización por un error.\nPero todavía puedes terminarlo manualmente descargando HMCL por %s.\nConsidere informándonos sobre el error.
fatal.missing_dst_root_ca_x3=El certificado DST Root CA X3 no existe en su plataforma Java.\nTodavía podrás usar Hello Minecraft! Launcher, pero Hello Minecraft! Launcher no podrá conectarse a algunos sites (como los que usen certificados de Let's Encrypt), lo cual podrá causar mala función.\nActualize su Java a 8u101 o luego para resolver el problema.
fatal.config_loading_failure=La configuración no es accesible.\nAsegúrese que Hello Minecraft! Launcher tenga permisos read and write a "%s" y los archivos en ello.
fatal.migration_requires_manual_reboot=La actualización está a punto de terminar. Favor reiniciar Hello Minecraft! Launcher.
fatal.apply_update_failure=Lamentamos que Hello Minecraft! Launcher no pudo terminar la actualización por un error.\nPero todavía puedes terminarlo manualmente descargando Hello Minecraft! Launcher por %s.\nConsidere informándonos sobre el error.
folder.config=Configs
folder.game=Directorio de juego
@@ -271,7 +271,7 @@ mods.enable=Habilitar
mods.disable=Deshabilitar
mods.name=Nombre
mods.remove=Borrar
mods.not_modded=Debes instalar un modloader primero (Forge, or LiteLoader)
mods.not_modded=Debes instalar un modloader primero (Fabric, Forge or LiteLoader)
datapack=Data packs
datapack.add=Añadir data pack
@@ -386,7 +386,7 @@ settings.type.global.edit=Configurar configuración global de juego
settings.type.special.enable=Habilitar configuraciones especializadas para este juego
sponsor.bmclapi=Para obtener un servicio de descargas estable, considere apoyar a BMCLAPI. Clic aquí para más información
sponsor.hmcl=HMCL es un Minecraft launcher gratis y do código abierto que habalita sus usuarios a fácilmente gestionar multiples instalaciones de Minecraft aparte. Usamos Afdian para que podamos continuar pagando por nuestro hosting y desarrollo. Clic aquí para más información.
sponsor.hmcl=Hello Minecraft! Launcher es un Minecraft launcher gratis y do código abierto que habalita sus usuarios a fácilmente gestionar multiples instalaciones de Minecraft aparte. Usamos Afdian para que podamos continuar pagando por nuestro hosting y desarrollo. Clic aquí para más información.
update=Actualizar
update.accept=Actualizar
@@ -0,0 +1,446 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Contributors: Botylyov Yury
about.copyright=Авторское право
about.copyright.statement=Создано (c) 2019 huangyuhui.
about.author=Автор
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Благодарность
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (Default background image)\nAll contributors who participated in this project via issues, pull requests, etc.
about.dependency=Зависимости
# Due to space limitations, only first authors are listed
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)\nConstant Pool Scanner (jenkins-ci, CDDL, GPL 2)\nOpenNBT (Steveice10, BSD 3-Clause)
about.claim=Соглашение
about.claim.statement=Авторские права на Minecraft принадлежат Mojang AB. Мы не несем ответственности за любые нарушения авторских прав, возникающие в результате использования данного программного обеспечения.
about.open_source=Open Source
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=Аккаунты
account.character=герой
account.choose=Выберите героя
account.create=Создать новый аккаунт
account.email=Почта
account.failed.character_deleted=Персонаж был удален.
account.failed.connect_authentication_server=Не удается подключиться к серверу аутентификации. Проверьте подключение к интернету.
account.failed.connect_injector_server=Не удается подключиться к серверу аутентификации. Проверьте свою сеть и убедитесь, что URL-адрес правильный.
account.failed.injector_download_failure=Не удалось загрузить authlib-injector. Проверьте свою сеть и попробуйте переключиться на другой источник загрузки.
account.failed.invalid_credentials=Неверный пароль или вам временно запрещен вход.
account.failed.invalid_password=Неправильный пароль
account.failed.invalid_token=Пожалуйста, выйдите и повторно введите свой пароль для входа.
account.failed.no_character=Нет героев в этом аккаунте.
account.failed.server_response_malformed=Неверный ответ сервера. Возможно, на сервере аутентификации произошел сбой.
account.injector.add=Добавить сервер аутентификации
account.injector.empty=Пусто (нажмите кнопку «плюс» справа, чтобы добавить)
account.injector.manage=Управление серверами аутентификации
account.injector.manage.title=Серверы аутентификации
account.injector.http=Предупреждение\: этот сервер использует HTTP, поэтому ваш пароль будет передан в виде открытого текста.
account.injector.link.register=Зарегистрироваться
account.injector.server=Сервер авторизации
account.injector.server_url=URL сервера
account.injector.server_name=Название сервера
account.manage=Список аккаунтов
account.methods=Тип аккаунта
account.methods.authlib_injector=authlib-injector
account.methods.offline=Пиратка
account.methods.yggdrasil=Лицензия
account.missing=Без аккаунта
account.missing.add=Нажмите сюда, чтобы добавить
account.password=Пароль
account.username=Никнейм
archive.author=Авторы
archive.game_version=Игра
archive.name=Никнейм
archive.version=Версия
assets.download=Загрузка библиотек
assets.download_all=Проверка целостности библиотек
button.cancel=Отмена
button.clear=Очистить
button.delete=Удалить
button.edit=Редактировать
button.install=Установить
button.export=Экспорт
button.no=Нет
button.ok=Ок
button.refresh=Обновить
button.save=Сохранить
button.yes=Да
color.recent=Рекомендуется
color.custom=Свой цвет
crash.NoClassDefFound=Пожалуйста, проверьте, что программное обеспечение "Hello Minecraft! Launcher" не повреждено.
crash.user_fault=Ваша ОС или среда Java могут быть неправильно установлены, что приведет к сбою этого программного обеспечения, пожалуйста, проверьте свою среду Java или свой компьютер\!
download=Загрузка
download.code.404=Файл не найден на удаленном сервере
download.failed=Не удалось загрузить
download.failed.empty=Нет вариантов. Нажмите здесь, чтобы вернуться.
download.failed.refresh=Невозможно загрузить список версий. Нажмите здесь, чтобы повторить попытку.
download.provider.bmclapi=BMCLAPI (Резервный)
download.provider.mojang=Mojang (Официальный)
extension.bat=Windows Bat файл
extension.mod=Mod файл
extension.png=Файл картинки
extension.sh=Командная оболочка bash
fatal.missing_javafx=JavaFX отсутствует.\nЕсли вы используете Java 11 или более позднюю версию, пожалуйста, перейдите на Java 8 или 10.\nЕсли вы используете OpenJDK, убедитесь, что OpenJFX включен.
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use Hello Minecraft! Launcher, but Hello Minecraft! Launcher will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause Hello Minecraft! Launcher not to function properly.\nPlease upgrade your Java to 8u101 or later to resolve the problem.
fatal.config_loading_failure=Конфигурация недоступна.\nУбедитесь, что у Hello Minecraft! Launcher есть права на чтение и запись для "%s" и файлов в нем.
fatal.migration_requires_manual_reboot=Обновление скоро будет завершено. Пожалуйста, перезапустите Hello Minecraft! Launcher.
fatal.apply_update_failure=Мы сожалеем, что Hello Minecraft! Launcher не смог завершить обновление, потому что что-то пошло не так.\nНо вы все же можете вручную завершить обновление, загрузив Hello Minecraft! Launcher с %s.\nПожалуйста, расскажите об этой проблеме нам.
folder.config=Конфиги
folder.game=Директория игры
folder.mod=Мод
folder.resourcepacks=Ресурс паки
folder.saves=Сохранения
folder.screenshots=Скриншоты
help=Документация
help.detail=Для производителей пакетов данных, модпаков и т.д.
input.email=Имя пользователя должно быть электронной почтой.
input.number=Должно быть числом.
input.not_empty=Обязательное поле
input.url=Должен быть действующим URL.
install=Установить версию
install.change_version=Изменить версию
install.change_version.confirm=Уверены, что хотите обновить %s с версии %s до %s?
install.failed=Не удалось установить
install.failed.downloading=Не удалось установить из-за некоторых файлов, которые не были успешно загружены
install.failed.downloading.detail=Не удалось загрузить файл\: %s
install.failed.downloading.timeout=Превышено время ожидания при загрузке файла: %s
install.failed.install_online=Невозможно распознать предоставленный файл установщика
install.failed.optifine_conflict=OptiFine и Forge установлены одновременно на Minecraft 1.13
install.failed.version_mismatch=Для библиотеки требуется версия игры %s, но фактическая версия - %s.
install.installer.choose=Выберите %s версию
install.installer.fabric=Fabric
install.installer.forge=Forge
install.installer.game=Версия
install.installer.install=Установка %s
install.installer.install_offline=Установить/Обновить из файла
install.installer.install_offline.extension=Forge/OptiFine установщик
install.installer.install_offline.tooltip=Моддерживается импорт файлов установки Forge/OptiFine
install.installer.install_online=Установить онлайн
install.installer.install_online.tooltip=Поддерживается установка Forge, OptiFine, LiteLoader.
install.installer.liteloader=LiteLoader
install.installer.not_installed=%s не установлен
install.installer.optifine=OptiFine
install.installer.version=%s Версия
install.modpack=Новый модпак
install.new_game=Новая версия
install.new_game.already_exists=Эта версия уже существует.
install.new_game.current_game_version=Текущая версия игры
install.new_game.malformed=Неверное название
install.select=Выберите операцию
install.success=Успешно установлено
lang=Русский
lang.default=Использовать язык системы
launch.advice.corrected=Мы уже исправили выбор JVM, и теперь игра может быть запущена. Если вы хотите сохранить свой выбор JVM, вы можете отключить проверку Java VM в настройках игры.
launch.advice.uncorrected=Если вы уверены, что игра может быть запущена нормально, вы можете отключить проверку Java VM в настройках игры.
launch.advice.different_platform=Ваша ОС 64-битная, а ваша Java 32-битная. Рекомендуется 64-битная Java.
launch.advice.forge2760_liteloader=Forge 2760 и выше не совместимы с LiteLoader, рассмотрите возможность обновления Forge до 2773 или более поздней.
launch.advice.java8_1_13=Minecraft 1.13 и более поздние версии могут работать только на Java 8 или более поздней версии.
launch.advice.java8_51_1_13=Minecraft 1.13 может сломаться на Java 8 ранее, чем 1.8.0_51. Вы должны установить последнюю версию Java 8.
launch.advice.java9=Вы не можете запустить Minecraft, пока версия игры не станет выше 1.13 с Java 9 или более поздними версиями Java.
launch.advice.newer_java=Предлагается Java 8, которая может сделать игру более стабильной. А для многих модов и Minecraft 1.12 и более новых версий требуется Java 8.
launch.advice.not_enough_space=Вы выделили слишком много памяти, поскольку объем физической памяти составляет %dMB, возможно, ваша игра будет вылетать. Лаунчер попробует это запустить.
launch.advice.too_large_memory_for_32bit=Вы выделили слишком много памяти, из-за вашей 32-битной Java игра, вероятно, вылетает. Максимальный объем памяти составляет 1024 Мб. Лаунчер попробует с таким объемом памяти.
launch.failed=Невозможно запустить
launch.failed.cannot_create_jvm=Виртуальная машина Java не может быть создана. У аргументов Java могут быть проблемы. Вы можете включить режим без аргументов в настройках.
launch.failed.creating_process=Не удалось создать процесс, возможно, ваш путь Java неверен, пожалуйста, измените путь Java.
launch.failed.decompressing_natives=Невозможно распаковать нативные библиотеки.
launch.failed.download_library=Невозможно загрузить библиотеку %s.
launch.failed.executable_permission=Невозможно добавить разрешение в скрипт запуска
launch.failed.exited_abnormally=Игра закрылась ненормально, зайдите в журнал или попросите кого-нибудь помочь.
launch.state.dependencies=Зависимости
launch.state.done=Готово
launch.state.logging_in=Вход в систему
launch.state.modpack=Приготовление к модпаку
launch.state.waiting_launching=В ожидании запуска игры
launch.wrong_javadir=Неверный каталог Java, будет установлен каталог Java по умолчанию.
launcher=Лаунчер
launcher.background=Изображение на заднем плане
launcher.background.choose=Выберите путь до изображения на заднем плане
launcher.background.default=Стандартное
launcher.cache_directory=Каталог для скачки библиотек
launcher.cache_directory.choose=Выбор каталога для скачки кеша
launcher.cache_directory.default=По умолчанию
launcher.cache_directory.disabled=Отключено
launcher.cache_directory.invalid=Неправильный пусть до каталога для скачки кеша. Будет установлен стандартный каталог.
launcher.contact=Связаться с нами
launcher.crash=Hello Minecraft! Launcher вылетел
launcher.crash_out_dated=Hello Minecraft! Launcher вылетел! Ваш лаунчер устарел! Обновите его!
launcher.update_java=Пожалуйста, обновите Java!
login.empty_username=Вы не установили имя пользователя!
login.enter_password=Пожалуйста, введите пароль.
logwindow.show_lines=Показать линии
logwindow.terminate_game=Завершить игру
logwindow.title=Лог
main_page=Главная
message.confirm=Подтвердить
message.doing=Пожалуйста, подождите
message.downloading=Загрузка ...
message.error=Ошибка
message.info=Информация
message.success=Задание успешно завершено
message.unknown=Неизвестно
message.warning=Предупреждение
modpack=Модпак
modpack.choose=Выберите zip-файл модпака, который вы хотите установить.
modpack.desc=Опишите свой модпак, включая меры предосторожности, журнал изменений, поддерживая Markdown (также поддерживая онлайн-изображения).
modpack.enter_name=Введите желаемое имя для этой версии.
modpack.export=Экспортировать модпак
modpack.export.as=Экспорт модпака как...
modpack.files.blueprints=BuildCraft чертежи
modpack.files.config=Настройки мода
modpack.files.dumps=Отладочный вывод NEI
modpack.files.liteconfig=Мод конфигурации
modpack.files.mods=Моды
modpack.files.mods.voxelmods=Опции VoxelMods (включая VoxelMap)
modpack.files.options_txt=Опции игры
modpack.files.optionsshaders_txt=Опции шейдеров
modpack.files.resourcepacks=Ресурсные (текстурные) паки
modpack.files.saves=Сохраненные игры
modpack.files.scripts=Конфигурация MineTweaker
modpack.files.servers_dat=Список серверов
modpack.install=Установка %s модпака
modpack.installing=Установка модпака
modpack.introduction=Curse, MultiMC, HMCL модпаки поддерживаются.
modpack.invalid=Неверный файл модпака.
modpack.mismatched_type=Несоответствующий тип модпака, ваша текущая игра %s модпак, но ваш файл обновления %s модпак.
modpack.name=Название модпака
modpack.not_a_valid_name=Неверное имя модпака
modpack.scan=Сканирование этого модпака
modpack.task.install=Импортировать модпак
modpack.task.install.error=Этот файл мода не может быть распознан. Поддерживаются только Curse, MultiMC модпаки.
modpack.task.install.will=Установите модпак\:
modpack.type.curse=Curse
modpack.type.curse.completion=Установить файлы относительно модпака Curse
modpack.type.curse.tolerable_error=Но мы не можем завершить загрузку всех файлов этого модпака Curse. Вы можете повторить загрузку при запуске соответствующей версии игры. Вы можете повторить попытку несколько раз из-за проблем с сетью.
modpack.type.curse.error=Невозможно завершить этот модпак Curse. Пожалуйста, повторите.
modpack.type.curse.not_found=Некоторые из необходимых ресурсов удалены и не могут быть загружены. Пожалуйста, рассмотрите последнюю версию или другие модпаки.
modpack.type.hmcl=Hello Minecraft! Launcher
modpack.type.hmcl.export=Can be imported by Hello Minecraft! Launcher
modpack.type.multimc=MultiMC
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
modpack.unsupported=Неподдерживаемый модпак, поддерживаются только модпаки HMCL, MultiMC, Curse.
modpack.update=Обновление игры
modpack.wizard=Мастер экспорта модпаков
modpack.wizard.step.1=Основные опции
modpack.wizard.step.1.title=Установите основные параметры для модпака.
modpack.wizard.step.2=Выбор файлов
modpack.wizard.step.2.title=Выберите файлы, которые вы хотите поместить в модпак.
modpack.wizard.step.3=Тип модпака
modpack.wizard.step.3.title=Выберите формат модпака.
modpack.wizard.step.initialization.exported_version=Экспортированная версия игры
modpack.wizard.step.initialization.include_launcher=Включить лаунчер
modpack.wizard.step.initialization.save=Выберите путь для экспорта файлов игры в
modpack.wizard.step.initialization.warning=Перед созданием модпака вы должны убедиться, что ваша игра может успешно запуститься,\nи что ваш Minecraft это релиз, а не снапшот.\nТак же, запрещено добавлять моды, которые нельзя распространять в модпаке.
mods=Моды
mods.add=Установить моды
mods.add.failed=Не удалось установить моды %s.
mods.add.success=Успешно установлены моды %s.
mods.choose_mod=Выберите свои моды
mods.enable=Включить
mods.disable=Отключить
mods.name=Название
mods.remove=Удалить
mods.not_modded=Сначала необходимо установить загрузчик модов (Forge, LiteLoader)
datapack=Пакеты данных
datapack.add=Добавить пакет данных
datapack.choose_datapack=Выберите zip-файл для импорта
datapack.extension=Пакет данных
datapack.title=Мир %s - Пакеты данных
datapack.remove=Удалить
world=Сохранения
world.add=Добавить мир (.zip)
world.datapack=Управление данными
world.datapack.1_13=Только Minecraft 1.13 и более поздние версии поддерживают пакеты данных.
world.description=%s. Время последней игры\: %s. Версия игры\: %s.
world.export=Экспортирт этого мира
world.export.title=Выберите местоположение файла для хранения вашего мира
world.export.location=Экспортировать в
world.export.wizard=Экспорт мира %s
world.extension=Zip файл мира
world.game_version=Версия игры
world.import.already_exists=Этот мир уже существует.
world.import.choose=Выберите zip-файл для импорта
world.import.failed=Невозможно импортировать этот мир\: %s
world.import.invalid=Недопустимый zip мира
world.name=Имя мира
world.name.enter=Введите имя мира
world.reveal=Показать в проводнике
world.show_all=Показать все
world.time=EEE, MMM d, yyyy HH\:mm\:ss
profile=Каталоги игры
profile.already_exists=Это имя уже существует, пожалуйста, рассмотрите другое имя.
profile.default=Текущий каталог
profile.home=Стандартная
profile.instance_directory=Каталог игры
profile.instance_directory.choose=Выберите каталог игры
profile.manage=Список каталогов игры
profile.name=Имя
profile.new=Новый конфиг
profile.title=Каталоги игры
profile.selected=Выбрано
profile.use_relative_path=Используйте относительный путь к каталогу игры, если это возможно
selector.choose=Выбрать
selector.choose_file=Выберите файл
selector.custom=Настраиваемый
settings=Настройки игры
settings.advanced=Расширенные настройки
settings.advanced.dont_check_game_completeness=Не проверять файлы игры
settings.advanced.dont_check_jvm_validity=Не проверять, может ли JVM запустить игру или нет
settings.advanced.game_dir.default=Стандарт (.minecraft/)
settings.advanced.game_dir.independent=Независимая (.minecraft/versions/<версия>/, кроме ассетов и библиотек)
settings.advanced.java_args_default=Стандартные аргументы java\: -XX\:+UseConcMarkSweepGC -XX\:+CMSIncrementalMode -XX\:-UseAdaptiveSizePolicy -XX\:MaxPermSize\=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates\=true -Dfml.ignorePatchDiscrepancies\=true
settings.advanced.java_permanent_generation_space=PermGen Space/MB
settings.advanced.jvm_args=Java VM Аргументы
settings.advanced.launcher_visibility.close=Закрывать лаунчер после запуска игры.
settings.advanced.launcher_visibility.hide=Скрывать лаунчер при запуске игры.
settings.advanced.launcher_visibility.hide_and_reopen=Скрывать лаунчер и снова открывать, когда игра закроется.
settings.advanced.launcher_visibility.keep=Держать лаунчер видимым.
settings.advanced.launcher_visible=Видимость лаунчера
settings.advanced.minecraft_arguments=Аргументы Minecraft
settings.advanced.no_jvm_args=Аргументы JVM по умолчанию отсутствуют
settings.advanced.precall_command=Команда перед запуском (будет выполнена до запуска игры)
settings.advanced.server_ip=Адрес сервера
settings.advanced.wrapper_launcher=Wrapper Launcher(optirun...)
settings.custom=Своя
settings.game=Игры
settings.game.dimension=Размер игрового окна
settings.game.exploration=Изменить
settings.game.fullscreen=На весь экран
settings.game.java_directory=Каталог Java
settings.game.java_directory.bit=, %s-бит
settings.game.java_directory.choose=Выберите каталог Java.
settings.game.management=Управление
settings.game.working_directory=Рабочий каталог
settings.game.working_directory.choose=Выберите рабочий каталог
settings.icon=Иконка игры
settings.launcher=Настройки
settings.launcher.common_path.tooltip=Это приложение сохранит все игровые библиотеки и ресурсы здесь, если в папке с игрой нет существующих файлов.
settings.launcher.download_source=Источник загрузки файлов
settings.launcher.enable_game_list=Показать список версий на главной странице
settings.launcher.language=Язык
settings.launcher.launcher_log.export=Экспорт логов лаунчера
settings.launcher.launcher_log.export.failed=Не удалось экспортировать лог
settings.launcher.launcher_log.export.success=Логи успешно экспортированы в %s
settings.launcher.log=Лог
settings.launcher.log.font=Шрифт логов
settings.launcher.proxy=Прокси
settings.launcher.proxy.authentication=Аутентификация прокси
settings.launcher.proxy.disable=Использовать системные прокси
settings.launcher.proxy.host=Хост
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=Пароль
settings.launcher.proxy.port=Порт
settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=Пользователь
settings.launcher.theme=Тема
settings.max_memory=Макс. Память / МБ
settings.physical_memory=Размер физической памяти
settings.show_log=Показать логи
settings.tabs.installers=Установщики
settings.type=Тип настройки версии
settings.type.global=Общие глобальные настройки(для всех версий)
settings.type.global.manage=Глобальные настройки игры
settings.type.global.edit=Глобальные настройки
settings.type.special.enable=Независимые настройки
sponsor.bmclapi=Для получения стабильной загрузки службы, пожалуйста, рассмотреть возможность поддержки BMCLAPI. Нажмите здесь для получения дополнительной информации
sponsor.hmcl=Hello Minecraft! Launcher - это бесплатная программа Minecraft с открытым исходным кодом, позволяющая пользователям легко управлять несколькими отдельными установками Minecraft. Мы используем Afdian, чтобы продолжать оплачивать хостинг и разработку проектов. Для получения дополнительной информации нажмите здесь.
update=Обновление
update.accept=Обновиться
update.changelog=Изменения
update.channel.dev=Обновляться до бета-версий
update.channel.stable=Обновляться до стабильных версий
update.checking=Проверка обновлений
update.failed=Не удалось выполнить обновление
update.found=Доступно обновление\!
update.newest_version=Последняя версия\: %s
update.bubble.title=Доступно обновление\: %s
update.bubble.subtitle=Нажмите здесь, чтобы обновить
update.note=Бета-версия содержит больше функций и исправлений ошибок, а также больше возможных ошибок.
update.latest=Это последняя версия.
update.no_browser=Не удается открыть любой браузер. Ссылка была скопирована в буфер обмена. Вставьте его в адресную строку браузера для обновления.
update.tooltip=Обновить
version=Версии
version.cannot_read=Невозможно узнать версию игры. Не удается продолжить автоматическую установку.
version.empty=Нет версий
version.empty.add=Установить версию
version.empty.launch=Нет версии для запуска, пожалуйста, установите новую версию через страницу со списком версий.
version.forbidden_name=Запрещенное имя, не используйте его.
version.game.old=Старые
version.game.release=Релизы
version.game.snapshot=Снапшоты
version.launch=Играть
version.launch.test=Тест версии
version.launch_script=Сделать стартовый скрипт
version.launch_script.failed=Невозможно сделать скрипт запуска.
version.launch_script.save=Сохраните скрипт запуска
version.launch_script.success=Завершено создание скрипта, %s.
version.manage=Все версии
version.manage.clean=Очистить каталог версии
version.manage.clean.tooltip=Очистить журналы, отчеты об ошибках
version.manage.manage=Управление версией
version.manage.redownload_assets_index=Перескачать ассеты
version.manage.remove=Удалить эту версию
version.manage.remove.confirm=Обязательно удалить версию %s? Вы не можете восстановить эту версию снова\!
version.manage.remove.confirm.trash=Обязательно удалить версию %s? Вы можете восстановить игру с именем %s в системной корзине.
version.manage.remove.confirm.independent=Поскольку эта версия находится в независимом режиме, удаление этой версии приведет к удалению всех сохраненных миров, принадлежащих этой игре. Намерены удалить версию %s?
version.manage.remove_libraries=Удалить файлы библиотеки
version.manage.rename=Переименовать эту версию
version.manage.rename.message=Пожалуйста, введите имя для версии
version.manage.rename.fail=Не удалось переименовать эту версию.
version.settings=Настройки
version.update=Обновление модпака
wizard.prev=< Пред
wizard.failed=Не удалось
wizard.finish=Закончено
wizard.next=След >
@@ -86,7 +86,7 @@ button.yes=是
color.recent=推薦
color.custom=自訂顏色
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java。
crash.NoClassDefFound=請確認 Hello Minecraft! Launcher 本體是否完整,或更新您的 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
@@ -103,10 +103,10 @@ extension.png=圖片檔案
extension.sh=Bash 腳本
fatal.missing_javafx=找不到 JavaFX。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=目前的 Java 平台缺少 DST Root CA X3 憑證。\n您依然可以使用 HMCL,但將無法連線到部分網站(如使用 Lets Encrypt 證書的站點),這可能會使 HMCL 無法正常運作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
fatal.config_loading_failure=HMCL 無法載入設定檔案。\n請確保 HMCL 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
fatal.migration_requires_manual_reboot=HMCL 即將升級完成,請重新打開 HMCL。
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級程序,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們回報該問題。
fatal.missing_dst_root_ca_x3=目前的 Java 平台缺少 DST Root CA X3 憑證。\n您依然可以使用 Hello Minecraft! Launcher,但將無法連線到部分網站(如使用 Lets Encrypt 證書的站點),這可能會使 Hello Minecraft! Launcher 無法正常運作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 Hello Minecraft! Launcher 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新打開 Hello Minecraft! Launcher。
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程序,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 Hello Minecraft! Launcher 來完成升級。\n請考慮向我們回報該問題。
folder.config=設定資料夾
folder.game=遊戲資料夾
@@ -115,7 +115,7 @@ folder.resourcepacks=資源包資料夾
folder.saves=遊戲存檔資料夾
folder.screenshots=截圖資料夾
help=HMCL 幫助文檔
help=Hello Minecraft! Launcher 幫助文檔
help.detail=可查閱資料包、整合包製作指南等內容。
input.email=「使用者名稱」必須是電子信箱格式
@@ -141,7 +141,7 @@ install.installer.install_offline=從本地檔案安裝/升級
install.installer.install_offline.extension=Forge/OptiFine 安裝器
install.installer.install_offline.tooltip=支持導入已經下載好的 Forge/OptiFine 安裝器
install.installer.install_online=在線安裝
install.installer.install_online.tooltip=支持安裝 Forge、OptiFine、LiteLoader
install.installer.install_online.tooltip=支持安裝 Fabric、Forge、OptiFine、LiteLoader
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫時不安裝 %s,可以點擊此處安裝
install.installer.optifine=OptiFine
@@ -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=安裝成功
@@ -271,8 +272,9 @@ mods.add.success=成功新增模組 %s。
mods.choose_mod=選擇模組
mods.enable=啟用
mods.disable=禁用
mods.name=名稱
mods.remove=刪除
mods.not_modded=你需要先在自動安裝頁面安裝 Forge 或 LiteLoader 才能進行模組管理。
mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge 或 LiteLoader 才能進行模組管理。
datapack=資料包
datapack.add=添加資料包
@@ -387,7 +389,7 @@ settings.type.global.edit=編輯全域遊戲設定
settings.type.special.enable=啟用遊戲特別設定(不影響其他遊戲版本)
sponsor.bmclapi=請贊助 BMCLAPI 從而獲得穩定高速的下載服務,點擊此處查閱詳細訊息。
sponsor.hmcl=HMCL 是一個免費、開源的 Minecraft 啟動器,允許玩家方便快捷地安裝、管理、運行遊戲。您的贊助將幫助 HMCL 獲得更好的發展、支持穩定高速的遊戲安裝與文件下載服務。點擊此處查閱更多詳細訊息。
sponsor.hmcl=Hello Minecraft! Launcher 是一個免費、開源的 Minecraft 啟動器,允許玩家方便快捷地安裝、管理、運行遊戲。您的贊助將幫助 Hello Minecraft! Launcher 獲得更好的發展、支持穩定高速的遊戲安裝與文件下載服務。點擊此處查閱更多詳細訊息。
update=啟動器更新
update.accept=更新
@@ -86,7 +86,7 @@ button.yes=是
color.recent=推荐
color.custom=自定义颜色
crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新您的 Java。
crash.NoClassDefFound=请确认 Hello Minecraft! Launcher 本体是否完整,或更新您的 Java。
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
download=下载
@@ -103,10 +103,10 @@ extension.png=图片文件
extension.sh=Bash 脚本
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Lets Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.config_loading_failure=HMCL 无法加载配置文件。\n请确保 HMCL 对 "%s" 目录及该目录下的文件拥有读写权限。
fatal.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。
fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 HMCL 来完成升级。\n请考虑向我们反馈该问题。
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 Hello Minecraft! Launcher,但将无法连接到部分站点(如使用 Lets Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 Hello Minecraft! Launcher 对 "%s" 目录及该目录下的文件拥有读写权限。
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 Hello Minecraft! Launcher。
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 Hello Minecraft! Launcher 来完成升级。\n请考虑向我们反馈该问题。
folder.config=配置文件夹
folder.game=游戏文件夹
@@ -115,7 +115,7 @@ folder.resourcepacks=资源包文件夹
folder.saves=存档文件夹
folder.screenshots=截图文件夹
help=HMCL 帮助文档
help=Hello Minecraft! Launcher 帮助文档
help.detail=可查阅数据包、整合包制作指南等内容。
input.email=用户名必须是邮箱
@@ -142,7 +142,7 @@ install.installer.install_offline=从本地文件安装/升级
install.installer.install_offline.extension=Forge/OptiFine 安装器
install.installer.install_offline.tooltip=支持导入已经下载好的 Forge/OptiFine 安装器
install.installer.install_online=在线安装
install.installer.install_online.tooltip=支持安装 Forge、OptiFine、LiteLoader
install.installer.install_online.tooltip=支持安装 Fabric、Forge、OptiFine、LiteLoader
install.installer.liteloader=LiteLoader
install.installer.not_installed=暂不安装 %s,可以点击此处安装
install.installer.optifine=OptiFine
@@ -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=安装成功
@@ -272,8 +273,9 @@ mods.add.success=成功添加模组 %s。
mods.choose_mod=选择模组
mods.enable=启用
mods.disable=禁用
mods.name=名称
mods.remove=删除
mods.not_modded=你需要先在自动安装页面安装 Forge 或 LiteLoader 才能进行模组管理。
mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteLoader 才能进行模组管理。
datapack=数据包
datapack.add=添加数据包
@@ -388,7 +390,7 @@ settings.type.global.edit=编辑全局版本设置
settings.type.special.enable=启用游戏特定设置(不影响其他游戏版本)
sponsor.bmclapi=请赞助 BMCLAPI 从而获得稳定高速的下载服务,点击此处查阅详细信息。
sponsor.hmcl=HMCL 是一个免费、开源的 Minecraft 启动器,允许玩家方便快捷地安装、管理、运行游戏。您的赞助将帮助 HMCL 获得更好的发展、支持稳定高速的游戏安装与文件下载服务。点击此处查阅更多详细信息。
sponsor.hmcl=Hello Minecraft! Launcher 是一个免费、开源的 Minecraft 启动器,允许玩家方便快捷地安装、管理、运行游戏。您的赞助将帮助 Hello Minecraft! Launcher 获得更好的发展、支持稳定高速的游戏安装与文件下载服务。点击此处查阅更多详细信息。
update=启动器更新
update.accept=更新
@@ -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;
@@ -23,14 +23,12 @@ import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import java.io.IOException;
import java.nio.file.Path;
import java.util.LinkedList;
/**
* Note: This class has no state.
@@ -70,7 +68,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
}
@Override
public Task<?> checkGameCompletionAsync(Version version) {
public Task<?> checkGameCompletionAsync(Version original) {
Version version = original.resolve(repository);
return Task.allOf(
Task.composeAsync(() -> {
if (!repository.getVersionJar(version).exists())
@@ -85,7 +84,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
@Override
public Task<?> checkLibraryCompletionAsync(Version version) {
return new GameLibrariesTask(this, version);
return new GameLibrariesTask(this, version, version.getLibraries());
}
@Override
@@ -95,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);
}
@@ -127,56 +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);
}
/**
* Remove installed library.
* Will try to remove libraries and patches.
*
* @param versionId version id
* @param libraryId forge/liteloader/optifine
* @return task to remove the specified library
*/
public Task<Version> removeLibraryWithoutSavingAsync(String versionId, String libraryId) {
Version version = repository.getVersion(versionId); // to ensure version is not resolved
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
switch (libraryId) {
case "forge":
analyzer.ifPresent(LibraryAnalyzer.LibraryType.FORGE, (library, libraryVersion) -> newList.remove(library));
version = version.removePatchById(LibraryAnalyzer.LibraryType.FORGE.getPatchId());
break;
case "liteloader":
analyzer.ifPresent(LibraryAnalyzer.LibraryType.LITELOADER, (library, libraryVersion) -> newList.remove(library));
version = version.removePatchById(LibraryAnalyzer.LibraryType.LITELOADER.getPatchId());
break;
case "optifine":
analyzer.ifPresent(LibraryAnalyzer.LibraryType.OPTIFINE, (library, libraryVersion) -> newList.remove(library));
version = version.removePatchById(LibraryAnalyzer.LibraryType.OPTIFINE.getPatchId());
break;
case "fabric":
analyzer.ifPresent(LibraryAnalyzer.LibraryType.FABRIC, (library, libraryVersion) -> newList.remove(library));
version = version.removePatchById(LibraryAnalyzer.LibraryType.FABRIC.getPatchId());
break;
}
return new MaintainTask(version.setLibraries(newList));
public static class UnsupportedLibraryInstallerException extends Exception {
}
/**
* Remove installed library.
* Will try to remove libraries and patches.
*
* @param versionId version id
* @param libraryId forge/liteloader/optifine
* @param version not resolved version
* @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);
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.
if (version.isResolved())
throw new IllegalArgumentException("removeLibraryWithoutSavingAsync requires non-resolved version");
Version independentVersion = version.resolvePreservingPatches(repository);
return Task.supplyAsync(() -> LibraryAnalyzer.analyze(independentVersion).removeLibrary(libraryId).build());
}
}
@@ -17,14 +17,9 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.download.game.VersionJsonDownloadTask;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.util.Map;
@@ -52,24 +47,16 @@ public class DefaultGameBuilder extends GameBuilder {
@Override
public Task<?> buildAsync() {
return new VersionJsonDownloadTask(gameVersion, dependencyManager).thenComposeAsync(rawJson -> {
Version original = JsonUtils.GSON.fromJson(rawJson, Version.class);
Version version = original.setId(name).setJar(null);
Task<?> vanillaTask = downloadGameAsync(gameVersion, version).thenComposeAsync(Task.allOf(
new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY),
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
).withComposeAsync(dependencyManager.getGameRepository().save(version))); // using [with] because download failure here are tolerant.
Task<Version> libraryTask = Task.supplyAsync(() -> new Version(name));
libraryTask = libraryTask.thenComposeAsync(libraryTaskHelper(gameVersion, "game", gameVersion));
Task<Version> libraryTask = vanillaTask.thenSupplyAsync(() -> version);
for (Map.Entry<String, String> entry : toolVersions.entrySet())
libraryTask = libraryTask.thenComposeAsync(libraryTaskHelper(gameVersion, entry.getKey(), entry.getValue()));
for (Map.Entry<String, String> entry : toolVersions.entrySet())
libraryTask = libraryTask.thenComposeAsync(libraryTaskHelper(gameVersion, entry.getKey(), entry.getValue()));
for (RemoteVersion remoteVersion : remoteVersions)
libraryTask = libraryTask.thenComposeAsync(dependencyManager.installLibraryAsync(remoteVersion));
for (RemoteVersion remoteVersion : remoteVersions)
libraryTask = libraryTask.thenComposeAsync(dependencyManager.installLibraryAsync(remoteVersion));
return libraryTask;
}).whenComplete(exception -> {
return libraryTask.whenComplete(exception -> {
if (exception != null)
dependencyManager.getGameRepository().removeVersionFromDisk(name);
});
@@ -78,9 +65,4 @@ public class DefaultGameBuilder extends GameBuilder {
private ExceptionalFunction<Version, Task<Version>, ?> libraryTaskHelper(String gameVersion, String libraryId, String libraryVersion) {
return version -> dependencyManager.installLibraryAsync(gameVersion, version, libraryId, libraryVersion);
}
protected Task<Void> downloadGameAsync(String gameVersion, Version version) {
return new GameDownloadTask(dependencyManager, gameVersion, version);
}
}
@@ -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,11 +42,18 @@ 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"
*
* @param id the id of specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
* @return the version list
* @throws IllegalArgumentException if the version list does not exist
*/
VersionList<?> getVersionListById(String id);
}
@@ -20,70 +20,136 @@ 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.Arrays;
import java.util.EnumMap;
import java.util.Map;
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 {
private final Map<LibraryType, Pair<Library, String>> libraries;
import static org.jackhuang.hmcl.util.Pair.pair;
private LibraryAnalyzer(Map<LibraryType, Pair<Library, String>> libraries) {
public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMark> {
private Version version;
private final Map<String, Pair<Library, String>> libraries;
private LibraryAnalyzer(Version version, Map<String, Pair<Library, String>> libraries) {
this.version = version;
this.libraries = libraries;
}
public Optional<String> getVersion(LibraryType type) {
return getVersion(type.getPatchId());
}
public Optional<String> getVersion(String type) {
return Optional.ofNullable(libraries.get(type)).map(Pair::getValue);
}
public void ifPresent(LibraryType type, BiConsumer<Library, String> consumer) {
if (libraries.containsKey(type)) {
Pair<Library, String> value = libraries.get(type);
consumer.accept(value.getKey(), value.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) {
return has(type.getPatchId());
}
public boolean has(String type) {
return libraries.containsKey(type);
}
public boolean hasModLoader() {
return Arrays.stream(LibraryType.values())
.filter(LibraryType::isModLoader)
.anyMatch(this::has);
return libraries.keySet().stream().map(LibraryType::fromPatchId)
.filter(Objects::nonNull)
.anyMatch(LibraryType::isModLoader);
}
public boolean hasModLauncher() {
final String modLauncher = "cpw.mods.modlauncher.Launcher";
return modLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> modLauncher.equals(patch.getMainClass()));
}
private Version removingMatchedLibrary(Version version, String libraryId) {
LibraryType type = LibraryType.fromPatchId(libraryId);
if (type == null) return version;
List<Library> libraries = new ArrayList<>();
for (Library library : version.getLibraries()) {
String groupId = library.getGroupId();
String artifactId = library.getArtifactId();
if (type.group.matcher(groupId).matches() && type.artifact.matcher(artifactId).matches()) {
// skip
} else {
libraries.add(library);
}
}
return version.setLibraries(libraries);
}
/**
* Remove library by library id
* @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric"
* @return this
*/
public LibraryAnalyzer removeLibrary(String libraryId) {
if (!has(libraryId)) return this;
version = removingMatchedLibrary(version, libraryId)
.setPatches(version.getPatches().stream()
.filter(patch -> !libraryId.equals(patch.getId()))
.map(patch -> removingMatchedLibrary(patch, libraryId))
.collect(Collectors.toList()));
return this;
}
public Version build() {
return version;
}
public static LibraryAnalyzer analyze(Version version) {
Map<LibraryType, Pair<Library, String>> libraries = new EnumMap<>(LibraryType.class);
if (version.getInheritsFrom() != null)
throw new IllegalArgumentException("LibraryAnalyzer can only analyze independent game version");
for (Library library : version.getLibraries()) {
Map<String, Pair<Library, String>> libraries = new HashMap<>();
for (Library library : version.resolve(null).getLibraries()) {
String groupId = library.getGroupId();
String artifactId = library.getArtifactId();
for (LibraryType type : LibraryType.values()) {
if (type.group.matcher(groupId).matches() && type.artifact.matcher(artifactId).matches()) {
libraries.put(type, Pair.pair(library, library.getVersion()));
libraries.put(type.getPatchId(), pair(library, library.getVersion()));
break;
}
}
}
for (Version patch : version.getPatches()) {
for (LibraryType type : LibraryType.values()) {
if (type.patchId.equals(patch.getId())) {
libraries.put(type, Pair.pair(null, patch.getVersion()));
break;
}
}
if (patch.isHidden()) continue;
libraries.put(patch.getId(), pair(null, patch.getVersion()));
}
return new LibraryAnalyzer(libraries);
return new LibraryAnalyzer(version, libraries);
}
public enum LibraryType {
MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$")),
FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader")),
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("forge")),
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader")),
@@ -107,5 +173,32 @@ public final class LibraryAnalyzer {
public String getPatchId() {
return patchId;
}
public static LibraryType fromPatchId(String patchId) {
for (LibraryType type : values())
if (type.getPatchId().equals(patchId))
return type;
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;
}
}
}
@@ -17,37 +17,63 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.CompatibilityRule;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.VersionLibraryBuilder;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
public class MaintainTask extends Task<Version> {
private final GameRepository repository;
private final Version version;
public MaintainTask(Version version) {
public MaintainTask(GameRepository repository, Version version) {
this.repository = repository;
this.version = version;
if (version.getInheritsFrom() != null)
throw new IllegalArgumentException("MaintainTask requires independent game version");
}
@Override
public void execute() {
setResult(maintain(version));
setResult(maintain(repository, version));
}
public static Version maintain(Version version) {
if (version.getMainClass().contains("launchwrapper")) {
return maintainGameWithLaunchWrapper(version);
public static Version maintain(GameRepository repository, Version version) {
if (version.getInheritsFrom() != null)
throw new IllegalArgumentException("MaintainTask requires independent game version");
String mainClass = version.resolve(null).getMainClass();
if (mainClass != null && mainClass.contains("launchwrapper")) {
return maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version)));
} else {
// Vanilla Minecraft does not need maintain
// Forge 1.13 support not implemented, not compatible with OptiFine currently.
// Fabric does not need maintain, nothing compatible with fabric now.
return version;
return maintainOptiFineLibrary(repository, unique(version));
}
}
private static Version maintainGameWithLaunchWrapper(Version version) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
String mainClass = null;
if (!libraryAnalyzer.has(FORGE)) {
builder.removeTweakClass("forge");
@@ -56,7 +82,7 @@ public class MaintainTask extends Task<Version> {
// Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added.
builder.removeTweakClass("liteloader");
if (libraryAnalyzer.has(LITELOADER)) {
if (libraryAnalyzer.has(LITELOADER) && !libraryAnalyzer.hasModLauncher()) {
builder.addArgument("--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
@@ -65,11 +91,101 @@ public class MaintainTask extends Task<Version> {
if (!libraryAnalyzer.has(LITELOADER) && !libraryAnalyzer.has(FORGE)) {
builder.addArgument("--tweakClass", "optifine.OptiFineTweaker");
} else {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
builder.addArgument("--tweakClass", "optifine.OptiFineForgeTweaker");
if (libraryAnalyzer.hasModLauncher()) {
// If ModLauncher installed, we use ModLauncher in place of LaunchWrapper.
mainClass = "cpw.mods.modlauncher.Launcher";
} else {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
builder.addArgument("--tweakClass", "optifine.OptiFineForgeTweaker");
}
}
}
return builder.build();
Version ret = builder.build();
return mainClass == null ? ret : ret.setMainClass(mainClass);
}
private static Version maintainOptiFineLibrary(GameRepository repository, Version version) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
List<Library> libraries = new ArrayList<>(version.getLibraries());
if (libraryAnalyzer.has(OPTIFINE)) {
if (libraryAnalyzer.has(LITELOADER) || libraryAnalyzer.has(FORGE)) {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
// And we should load the installer jar instead of patch jar.
if (repository != null)
for (int i = 0; i < version.getLibraries().size(); ++i) {
Library library = libraries.get(i);
if (library.is("optifine", "OptiFine")) {
Library newLibrary = new Library(new Artifact("optifine", "OptiFine", library.getVersion(), "installer"));
if (repository.getLibraryFile(version, newLibrary).exists()) {
libraries.set(i, null);
// OptiFine should be loaded after Forge in classpath.
// Although we have altered priority of OptiFine higher than Forge,
// there still exists a situation that Forge is installed without patch.
// Here we manually alter the position of OptiFine library in classpath.
libraries.add(newLibrary);
}
}
}
}
}
return version.setLibraries(libraries.stream().filter(Objects::nonNull).collect(Collectors.toList()));
}
public static Version unique(Version version) {
List<Library> libraries = new ArrayList<>();
SimpleMultimap<String, Integer> multimap = new SimpleMultimap<String, Integer>(HashMap::new, LinkedList::new);
for (Library library : version.getLibraries()) {
String id = library.getGroupId() + ":" + library.getArtifactId();
VersionNumber number = VersionNumber.asVersion(library.getVersion());
String serialized = JsonUtils.GSON.toJson(library);
if (multimap.containsKey(id)) {
boolean duplicate = false;
for (int otherLibraryIndex : multimap.get(id)) {
Library otherLibrary = libraries.get(otherLibraryIndex);
VersionNumber otherNumber = VersionNumber.asVersion(otherLibrary.getVersion());
if (CompatibilityRule.equals(library.getRules(), otherLibrary.getRules())) { // rules equal, ignore older version.
boolean flag = true;
if (number.compareTo(otherNumber) > 0) { // if this library is newer
// replace [otherLibrary] with [library]
libraries.set(otherLibraryIndex, library);
} else if (number.compareTo(otherNumber) == 0) { // same library id.
// prevent from duplicated libraries
if (library.equals(otherLibrary)) {
String otherSerialized = JsonUtils.GSON.toJson(otherLibrary);
// A trick, the library that has more information is better, which can be
// considered whose serialized JSON text will be longer.
if (serialized.length() > otherSerialized.length()) {
libraries.set(otherLibraryIndex, library);
}
} else {
// for text2speech, which have same library id as well as version number,
// but its library and native library does not equal
flag = false;
}
}
if (flag) {
duplicate = true;
break;
}
}
}
if (!duplicate) {
multimap.put(id, libraries.size());
libraries.add(library);
}
} else {
multimap.put(id, libraries.size());
libraries.add(library);
}
}
return version.setLibraries(libraries);
}
}
@@ -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;
}
@@ -21,6 +21,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.GetTask;
@@ -64,8 +65,8 @@ public final class FabricInstallTask extends Task<Version> {
}
@Override
public void preExecute() {
if (!Objects.equals("net.minecraft.client.main.Main", version.getMainClass()))
public void preExecute() throws Exception {
if (!Objects.equals("net.minecraft.client.main.Main", version.resolve(dependencyManager.getGameRepository()).getMainClass()))
throw new UnsupportedFabricInstallationException();
}
@@ -121,12 +122,12 @@ public final class FabricInstallTask extends Task<Version> {
}
}
libraries.add(new Library("net.fabricmc", "intermediary", gameVersion, null, "https://maven.fabricmc.net/", null));
libraries.add(new Library("net.fabricmc", "fabric-loader", loaderVersion, null, "https://maven.fabricmc.net/", null));
libraries.add(new Library(new Artifact("net.fabricmc", "intermediary", gameVersion), "https://maven.fabricmc.net/", null));
libraries.add(new Library(new Artifact("net.fabricmc", "fabric-loader", loaderVersion), "https://maven.fabricmc.net/", null));
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;
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.util.Immutable;
/**
@@ -28,7 +29,7 @@ public final class ForgeInstall {
private final String profileName;
private final String target;
private final String path;
private final Artifact path;
private final String version;
private final String filePath;
private final String welcome;
@@ -40,7 +41,7 @@ public final class ForgeInstall {
this(null, null, null, null, null, null, null, null, null);
}
public ForgeInstall(String profileName, String target, String path, String version, String filePath, String welcome, String minecraft, String mirrorList, String logo) {
public ForgeInstall(String profileName, String target, Artifact path, String version, String filePath, String welcome, String minecraft, String mirrorList, String logo) {
this.profileName = profileName;
this.target = target;
this.path = path;
@@ -60,7 +61,7 @@ public final class ForgeInstall {
return target;
}
public String getPath() {
public Artifact getPath() {
return path;
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Version;
@@ -34,10 +33,11 @@ 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;
/**
*
@@ -67,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);
}
@@ -80,10 +84,7 @@ public final class ForgeInstallTask extends Task<Version> {
@Override
public void postExecute() throws Exception {
Files.deleteIfExists(installer);
setResult(dependency.getResult()
.setPriority(30000)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(remote.getSelfVersion()));
setResult(dependency.getResult());
}
@Override
@@ -99,9 +100,9 @@ public final class ForgeInstallTask extends Task<Version> {
@Override
public void execute() {
if (VersionNumber.VERSION_COMPARATOR.compare("1.13", remote.getGameVersion()) <= 0)
dependency = new ForgeNewInstallTask(dependencyManager, version, installer);
dependency = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer);
else
dependency = new ForgeOldInstallTask(dependencyManager, version, installer);
dependency = new ForgeOldInstallTask(dependencyManager, version, remote.getSelfVersion(), installer);
}
/**
@@ -125,15 +126,19 @@ public final class ForgeInstallTask extends Task<Version> {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
if (!gameVersion.get().equals(profile.getMinecraft()))
throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get());
return new ForgeNewInstallTask(dependencyManager, version, installer);
return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getPath().getVersion().replaceAll("(?i)forge", "")), installer);
} else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) {
ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class);
if (!gameVersion.get().equals(profile.getInstall().getMinecraft()))
throw new VersionMismatchException(profile.getInstall().getMinecraft(), gameVersion.get());
return new ForgeOldInstallTask(dependencyManager, version, installer);
return new ForgeOldInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getInstall().getPath().getVersion().replaceAll("(?i)forge", "")), installer);
} else {
throw new IOException();
}
}
}
private static String modifyVersion(String gameVersion, String version) {
return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_");
}
}
@@ -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.game.GameLibrariesTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.Artifact;
@@ -34,11 +35,11 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -70,12 +71,14 @@ public class ForgeNewInstallTask extends Task<Version> {
private ForgeNewInstallProfile profile;
private Version forgeVersion;
private final String selfVersion;
ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) {
ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) {
this.dependencyManager = dependencyManager;
this.gameRepository = dependencyManager.getGameRepository();
this.version = version;
this.installer = installer;
this.selfVersion = selfVersion;
setSignificance(TaskSignificance.MINOR);
}
@@ -86,7 +89,7 @@ public class ForgeNewInstallTask extends Task<Version> {
else if (StringUtils.isSurrounded(literal, "'", "'"))
return StringUtils.removeSurrounding(literal, "'");
else if (StringUtils.isSurrounded(literal, "[", "]"))
return gameRepository.getArtifactFile(version, new Artifact(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
return gameRepository.getArtifactFile(version, Artifact.fromDescriptor(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
else
return plainConverter.apply(literal);
}
@@ -228,25 +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());
Process process = new ProcessBuilder(command).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
for (String line; (line = reader.readLine()) != null;) {
System.out.println(line);
}
}
int exitCode = process.waitFor();
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());
@@ -268,7 +266,10 @@ public class ForgeNewInstallTask extends Task<Version> {
}
}
setResult(forgeVersion);
setResult(forgeVersion
.setPriority(30000)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(selfVersion));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion));
FileUtils.deleteDirectory(temp.toFile());
@@ -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.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
@@ -41,12 +42,14 @@ public class ForgeOldInstallTask extends Task<Version> {
private final DefaultDependencyManager dependencyManager;
private final Version version;
private final Path installer;
private final String selfVersion;
private final List<Task<?>> dependencies = new LinkedList<>();
ForgeOldInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) {
ForgeOldInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) {
this.dependencyManager = dependencyManager;
this.version = version;
this.installer = installer;
this.selfVersion = selfVersion;
setSignificance(TaskSignificance.MINOR);
}
@@ -71,7 +74,7 @@ public class ForgeOldInstallTask extends Task<Version> {
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
// unpack the universal jar in the installer file.
Library forgeLibrary = Library.fromName(installProfile.getInstall().getPath());
Library forgeLibrary = new Library(installProfile.getInstall().getPath());
File forgeFile = dependencyManager.getGameRepository().getLibraryFile(version, forgeLibrary);
if (!FileUtils.makeFile(forgeFile))
throw new IOException("Cannot make directory " + forgeFile.getParent());
@@ -81,7 +84,10 @@ public class ForgeOldInstallTask extends Task<Version> {
IOUtils.copyTo(is, os);
}
setResult(installProfile.getVersionInfo());
setResult(installProfile.getVersionInfo()
.setPriority(30000)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(selfVersion));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo()));
}
}
@@ -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
@@ -51,16 +51,16 @@ public final class GameAssetDownloadTask extends Task<Void> {
* Constructor.
*
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version
* @param version the game version
*/
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloadingIndex) {
this.dependencyManager = dependencyManager;
this.version = version;
this.assetIndexInfo = version.getAssetIndex();
this.version = version.resolve(dependencyManager.getGameRepository());
this.assetIndexInfo = this.version.getAssetIndex();
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
if (!assetIndexFile.exists() || forceDownloadingIndex)
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, version));
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, this.version));
}
@Override
@@ -43,7 +43,7 @@ public final class GameDownloadTask extends Task<Void> {
public GameDownloadTask(DefaultDependencyManager dependencyManager, String gameVersion, Version version) {
this.dependencyManager = dependencyManager;
this.gameVersion = gameVersion;
this.version = version;
this.version = version.resolve(dependencyManager.getGameRepository());
setSignificance(TaskSignificance.MODERATE);
}
@@ -0,0 +1,79 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
public class GameInstallTask extends Task<Version> {
private final DefaultGameRepository gameRepository;
private final DefaultDependencyManager dependencyManager;
private final Version version;
private final GameRemoteVersion remote;
private final VersionJsonDownloadTask downloadTask;
private final List<Task<?>> dependencies = new LinkedList<>();
public GameInstallTask(DefaultDependencyManager dependencyManager, Version version, GameRemoteVersion remoteVersion) {
this.dependencyManager = dependencyManager;
this.gameRepository = dependencyManager.getGameRepository();
this.version = version;
this.remote = remoteVersion;
this.downloadTask = new VersionJsonDownloadTask(remoteVersion.getGameVersion(), dependencyManager);
}
@Override
public Collection<Task<?>> getDependents() {
return Collections.singleton(downloadTask);
}
@Override
public Collection<Task<?>> getDependencies() {
return dependencies;
}
@Override
public boolean isRelyingOnDependencies() {
return false;
}
@Override
public void execute() throws Exception {
Version patch = JsonUtils.fromNonNullJson(downloadTask.getResult(), Version.class)
.setId(MINECRAFT.getPatchId()).setVersion(remote.getGameVersion()).setJar(null).setPriority(0);
setResult(patch);
Version version = new Version(this.version.getId()).addPatch(patch);
dependencies.add(new GameDownloadTask(dependencyManager, remote.getGameVersion(), version)
.thenComposeAsync(Task.allOf(
new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY),
new GameLibrariesTask(dependencyManager, version)
).withComposeAsync(gameRepository.save(version))));
}
}
@@ -43,17 +43,17 @@ public final class GameLibrariesTask extends Task<Void> {
* Constructor.
*
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version
* @param version the game version
*/
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
this(dependencyManager, version, version.getLibraries());
this(dependencyManager, version, version.resolve(dependencyManager.getGameRepository()).getLibraries());
}
/**
* Constructor.
*
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version
* @param version the game version
*/
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, List<Library> libraries) {
this.dependencyManager = dependencyManager;
@@ -17,8 +17,12 @@
*/
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;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Date;
@@ -34,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;
}
@@ -47,6 +51,11 @@ public final class GameRemoteVersion extends RemoteVersion {
return type;
}
@Override
public Task<Version> getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) {
return new GameInstallTask(dependencyManager, baseVersion, this);
}
@Override
public int compareTo(RemoteVersion o) {
if (!(o instanceof GameRemoteVersion))
@@ -21,7 +21,6 @@ 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.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -48,7 +47,7 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
protected Collection<GameRemoteVersion> getVersionsImpl(String gameVersion) {
lock.readLock().lock();
try {
return StringUtils.isBlank(gameVersion) ? versions.values() : versions.get(gameVersion);
return versions.values();
} finally {
lock.readLock().unlock();
}
@@ -1,100 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.game.CompatibilityRule;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
public class LibrariesUniqueTask extends Task<Version> {
private final Version version;
public LibrariesUniqueTask(Version version) {
this.version = version;
}
@Override
public void execute() {
setResult(unique(version));
}
public static Version unique(Version version) {
List<Library> libraries = new ArrayList<>(version.getLibraries());
SimpleMultimap<String, Library> multimap = new SimpleMultimap<String, Library>(HashMap::new, LinkedList::new);
for (Library library : libraries) {
String id = library.getGroupId() + ":" + library.getArtifactId();
VersionNumber number = VersionNumber.asVersion(library.getVersion());
String serialized = JsonUtils.GSON.toJson(library);
if (multimap.containsKey(id)) {
boolean duplicate = false;
for (Library otherLibrary : multimap.get(id)) {
VersionNumber otherNumber = VersionNumber.asVersion(otherLibrary.getVersion());
if (CompatibilityRule.equals(library.getRules(), otherLibrary.getRules())) { // rules equal, ignore older version.
boolean flag = true;
if (number.compareTo(otherNumber) > 0) { // if this library is newer
multimap.removeValue(otherLibrary);
multimap.put(id, library);
break;
} else if (number.compareTo(otherNumber) == 0) { // same library id.
// prevent from duplicated libraries
if (library.equals(otherLibrary)) {
String otherSerialized = JsonUtils.GSON.toJson(otherLibrary);
// A trick, the library that has more information is better, which can be
// considered whose serialized JSON text will be longer.
if (serialized.length() > otherSerialized.length()) {
multimap.removeValue(id, otherLibrary);
multimap.put(id, library);
break;
}
} else {
// for text2speech, which have same library id as well as version number,
// but its library and native library does not equal
flag = false;
}
}
if (flag) {
duplicate = true;
break;
}
}
}
if (!duplicate) {
multimap.put(id, library);
}
} else {
multimap.put(id, library);
}
}
return version.setLibraries(multimap.values().stream().sorted().collect(Collectors.toList()));
}
}
@@ -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() {
RemoteVersion remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
.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));
public void execute() throws IOException {
RemoteVersion remoteVersion = gameVersionList.getVersion(gameVersion, gameVersion)
.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));
}
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
@@ -64,9 +65,9 @@ public final class LiteLoaderInstallTask extends Task<Version> {
@Override
public void execute() {
Library library = new Library(
"com.mumfrey", "liteloader", remote.getSelfVersion(), null,
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(),
@@ -78,7 +79,7 @@ public final class LiteLoaderInstallTask extends Task<Version> {
.setLogging(Collections.emptyMap()) // Mods may log in malformed format, causing XML parser to crash. So we suppress using official log4j configuration
);
dependencies.add(dependencyManager.checkLibraryCompletionAsync(version.setLibraries(getResult().getLibraries())));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult()));
}
}
@@ -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;
@@ -56,23 +56,29 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
@Override
public void execute() {
versions.clear();
Set<String> duplicates = new HashSet<>();
List<OptiFineVersion> root = JsonUtils.GSON.fromJson(task.getResult(), new TypeToken<List<OptiFineVersion>>() {
}.getType());
for (OptiFineVersion element : root) {
String version = element.getType() + "_" + element.getPatch();
String mirror = "http://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
if (!duplicates.add(mirror))
continue;
lock.writeLock().lock();
boolean isPre = element.getPatch() != null && (element.getPatch().startsWith("pre") || element.getPatch().startsWith("alpha"));
try {
versions.clear();
Set<String> duplicates = new HashSet<>();
List<OptiFineVersion> root = JsonUtils.GSON.fromJson(task.getResult(), new TypeToken<List<OptiFineVersion>>() {
}.getType());
for (OptiFineVersion element : root) {
String version = element.getType() + "_" + element.getPatch();
String mirror = "http://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
if (!duplicates.add(mirror))
continue;
if (StringUtils.isBlank(element.getGameVersion()))
continue;
boolean isPre = element.getPatch() != null && (element.getPatch().startsWith("pre") || element.getPatch().startsWith("alpha"));
String gameVersion = VersionNumber.normalize(element.getGameVersion());
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre));
if (StringUtils.isBlank(element.getGameVersion()))
continue;
String gameVersion = VersionNumber.normalize(element.getGameVersion());
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, mirror, isPre));
}
} finally {
lock.writeLock().unlock();
}
}
};
@@ -21,6 +21,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
@@ -31,6 +33,8 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jenkinsci.constant_pool_scanner.ConstantPool;
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
import org.jenkinsci.constant_pool_scanner.ConstantType;
@@ -41,12 +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.Arrays;
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;
@@ -57,15 +57,17 @@ import static org.jackhuang.hmcl.util.Lang.getOrDefault;
*/
public final class OptiFineInstallTask extends Task<Version> {
private final DefaultGameRepository gameRepository;
private final DefaultDependencyManager dependencyManager;
private final Version version;
private final OptiFineRemoteVersion remote;
private final Path installer;
private final List<Task<?>> dependents = new LinkedList<>();
private final List<Task<?>> dependencies = new LinkedList<>();
private final File dest;
private Path dest;
private final Library optiFineLibrary;
private final Library optiFineInstallerLibrary;
public OptiFineInstallTask(DefaultDependencyManager dependencyManager, Version version, OptiFineRemoteVersion remoteVersion) {
this(dependencyManager, version, remoteVersion, null);
@@ -73,20 +75,21 @@ public final class OptiFineInstallTask extends Task<Version> {
public OptiFineInstallTask(DefaultDependencyManager dependencyManager, Version version, OptiFineRemoteVersion remoteVersion, Path installer) {
this.dependencyManager = dependencyManager;
this.gameRepository = dependencyManager.getGameRepository();
this.version = version;
this.remote = remoteVersion;
this.installer = installer;
String mavenVersion = remote.getGameVersion() + "_" + remote.getSelfVersion();
optiFineLibrary = new Library(
"optifine", "OptiFine", mavenVersion, null, null,
new LibrariesDownloadInfo(new LibraryDownloadInfo(
"optifine/OptiFine/" + mavenVersion + "/OptiFine-" + mavenVersion + ".jar",
remote.getUrl()))
);
optiFineLibrary = new Library(new Artifact("optifine", "OptiFine", mavenVersion));
dest = dependencyManager.getGameRepository().getLibraryFile(version, optiFineLibrary);
optiFineInstallerLibrary = new Library(
new Artifact("optifine", "OptiFine", mavenVersion, "installer"), null,
new LibrariesDownloadInfo(new LibraryDownloadInfo(
"optifine/OptiFine/" + mavenVersion + "/OptiFine-" + mavenVersion + "-installer.jar",
remote.getUrl()[0]))
);
}
@Override
@@ -96,18 +99,16 @@ public final class OptiFineInstallTask extends Task<Version> {
@Override
public void preExecute() throws Exception {
if (!Arrays.asList("net.minecraft.client.main.Main",
"net.minecraft.launchwrapper.Launch")
.contains(version.resolve(dependencyManager.getGameRepository()).getMainClass()))
throw new UnsupportedOptiFineInstallationException();
dest = Files.createTempFile("optifine-installer", ".jar");
if (installer == null) {
dependents.add(new FileDownloadTask(NetworkUtils.toURL(remote.getUrl()), dest)
dependents.add(new FileDownloadTask(
Arrays.stream(remote.getUrl()).map(NetworkUtils::toURL).collect(Collectors.toList()),
dest.toFile(), null)
.setCacheRepository(dependencyManager.getCacheRepository())
.setCaching(true));
} else {
FileUtils.copyFile(installer, dest.toPath());
FileUtils.copyFile(installer, dest);
}
}
@@ -127,48 +128,66 @@ public final class OptiFineInstallTask extends Task<Version> {
}
@Override
public void execute() throws IOException {
public void execute() throws Exception {
List<Library> libraries = new LinkedList<>();
libraries.add(optiFineLibrary);
FileUtils.copyFile(dest, gameRepository.getLibraryFile(version, optiFineInstallerLibrary).toPath());
// Install launch wrapper modified by OptiFine
boolean hasLaunchWrapper = false;
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(dest.toPath())) {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(dest)) {
if (Files.exists(fs.getPath("optifine/Patcher.class"))) {
int exitCode = SystemUtils.callExternalProcess(
JavaVersion.fromCurrentEnvironment().getBinary().toString(),
"-cp",
dest.toString(),
"optifine.Patcher",
gameRepository.getVersionJar(version).getAbsolutePath(),
dest.toString(),
gameRepository.getLibraryFile(version, optiFineLibrary).toString()
);
if (exitCode != 0)
throw new IOException("OptiFine patcher failed");
} else {
FileUtils.copyFile(dest, gameRepository.getLibraryFile(version, optiFineLibrary).toPath());
}
Path launchWrapperVersionText = fs.getPath("launchwrapper-of.txt");
if (Files.exists(launchWrapperVersionText)) {
String launchWrapperVersion = FileUtils.readText(launchWrapperVersionText).trim();
Path launchWrapperJar = fs.getPath("launchwrapper-of-" + launchWrapperVersion + ".jar");
Library launchWrapper = new Library("optifine", "launchwrapper-of", launchWrapperVersion);
Library launchWrapper = new Library(new Artifact("optifine", "launchwrapper-of", launchWrapperVersion));
if (Files.exists(launchWrapperJar)) {
File launchWrapperFile = dependencyManager.getGameRepository().getLibraryFile(version, launchWrapper);
File launchWrapperFile = gameRepository.getLibraryFile(version, launchWrapper);
FileUtils.makeDirectory(launchWrapperFile.getAbsoluteFile().getParentFile());
FileUtils.copyFile(launchWrapperJar, launchWrapperFile.toPath());
hasLaunchWrapper = true;
libraries.add(0, launchWrapper);
libraries.add(launchWrapper);
}
}
}
if (!hasLaunchWrapper) {
libraries.add(0, new Library("net.minecraft", "launchwrapper", "1.12"));
libraries.add(new Library(new Artifact("net.minecraft", "launchwrapper", "1.12")));
}
setResult(new Version(
LibraryAnalyzer.LibraryType.OPTIFINE.getPatchId(),
remote.getSelfVersion(),
90000,
10000,
new Arguments().addGameArguments("--tweakClass", "optifine.OptiFineTweaker"),
"net.minecraft.launchwrapper.Launch",
libraries
));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(version.setLibraries(libraries)));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult()));
}
public static class UnsupportedOptiFineInstallationException extends UnsupportedOperationException {
public static class UnsupportedOptiFineInstallationException extends Exception {
}
/**
@@ -186,7 +205,10 @@ public final class OptiFineInstallTask extends Task<Version> {
Optional<String> gameVersion = GameVersion.minecraftVersion(jar);
if (!gameVersion.isPresent()) throw new IOException();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(fs.getPath("Config.class")), ConstantType.UTF8);
Path configClass = fs.getPath("Config.class");
if (!Files.exists(configClass)) configClass = fs.getPath("net/optifine/Config.class");
if (!Files.exists(configClass)) throw new IOException("Unrecognized installer");
ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(configClass), ConstantType.UTF8);
List<String> constants = new ArrayList<>();
pool.list(Utf8Constant.class).forEach(utf8 -> constants.add(utf8.get()));
String mcVersion = getOrDefault(constants, constants.indexOf("MC_VERSION") + 1, null);
@@ -200,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;
@@ -17,7 +17,14 @@
*/
package org.jackhuang.hmcl.game;
import com.google.gson.*;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.Immutable;
@@ -38,9 +45,34 @@ public final class Artifact {
private final String fileName;
private final String path;
public Artifact(String descriptor) {
this.descriptor = descriptor;
public Artifact(String group, String name, String version) {
this(group, name, version, null);
}
public Artifact(String group, String name, String version, String classifier) {
this(group, name, version, classifier, null);
}
public Artifact(String group, String name, String version, String classifier, String extension) {
this.group = group;
this.name = name;
this.version = version;
this.classifier = classifier;
this.extension = extension == null ? "jar" : extension;
String fileName = this.name + "-" + this.version;
if (classifier != null) fileName += "-" + this.classifier;
this.fileName = fileName + "." + this.extension;
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName);
// group:name:version:classifier@extension
String descriptor = String.format("%s:%s:%s", group, name, version);
if (classifier != null) descriptor += ":" + classifier;
if (!"jar".equals(this.extension)) descriptor += "@" + this.extension;
this.descriptor = descriptor;
}
public static Artifact fromDescriptor(String descriptor) {
String[] arr = descriptor.split(":", 4);
if (arr.length != 3 && arr.length != 4)
throw new IllegalArgumentException("Artifact name is malformed");
@@ -55,16 +87,7 @@ public final class Artifact {
throw new IllegalArgumentException("Artifact name is malformed");
}
this.group = arr[0].replace("\\", "/");
this.name = arr[1];
this.version = arr[2];
this.classifier = arr.length >= 4 ? arr[3] : null;
this.extension = ext == null ? "jar" : ext;
String fileName = this.name + "-" + this.version;
if (classifier != null) fileName += "-" + this.classifier;
this.fileName = fileName + "." + this.extension;
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName);
return new Artifact(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, ext);
}
public String getGroup() {
@@ -83,6 +106,10 @@ public final class Artifact {
return classifier;
}
public Artifact setClassifier(String classifier) {
return new Artifact(group, name, version, classifier, extension);
}
public String getExtension() {
return extension;
}
@@ -91,6 +118,8 @@ public final class Artifact {
return fileName;
}
public String getPath() { return path; }
public Path getPath(Path root) {
return root.resolve(path);
}
@@ -108,7 +137,7 @@ public final class Artifact {
@Override
public Artifact deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return json.isJsonPrimitive() ? new Artifact(json.getAsJsonPrimitive().getAsString()) : null;
return json.isJsonPrimitive() ? fromDescriptor(json.getAsJsonPrimitive().getAsString()) : null;
}
}
}
@@ -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.");
}
}
@@ -32,15 +32,15 @@ public class ClassicVersion extends Version {
super(true, "Classic", null, null, "${auth_player_name} ${auth_session} --workDir ${game_directory}",
null, "net.minecraft.client.Minecraft", null, null, null, null,
Arrays.asList(new ClassicLibrary("lwjgl"), new ClassicLibrary("jinput"), new ClassicLibrary("lwjgl_util")),
null, null, null, ReleaseType.UNKNOWN, new Date(), new Date(), 0, false, null);
null, null, null, ReleaseType.UNKNOWN, new Date(), new Date(), 0, false, false, null);
}
private static class ClassicLibrary extends Library {
public ClassicLibrary(String name) {
super("", "", "", null, null,
super(new Artifact("", "", ""), null,
new LibrariesDownloadInfo(new LibraryDownloadInfo("bin/" + name + ".jar"), null),
null, null, null, null);
null, null, null, null, null, null);
}
}
@@ -94,7 +94,10 @@ public class DefaultGameRepository implements GameRepository {
@Override
public File getLibraryFile(Version version, Library lib) {
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
if ("local".equals(lib.getHint()))
return new File(getVersionRoot(version.getId()), "libraries/" + lib.getFileName());
else
return new File(getBaseDirectory(), "libraries/" + lib.getPath());
}
public Path getArtifactFile(Version version, Artifact artifact) {
@@ -160,7 +163,7 @@ public class DefaultGameRepository implements GameRepository {
}
if (fromVersion.getId().equals(fromVersion.getJar()))
fromVersion = fromVersion.setJar(to);
fromVersion = fromVersion.setJar(null);
FileUtils.writeText(toJson, JsonUtils.GSON.toJson(fromVersion.setId(to)));
return true;
} catch (IOException | JsonParseException | VersionNotFoundException e) {
@@ -24,5 +24,7 @@ package org.jackhuang.hmcl.game;
public enum DownloadType {
CLIENT,
SERVER,
WINDOWS_SERVER
WINDOWS_SERVER,
CLIENT_MAPPINGS,
SERVER_MAPPINGS
}
@@ -56,6 +56,10 @@ public interface GameRepository extends VersionProvider {
return getVersion(id).resolve(this);
}
default Version getResolvedPreservingPatchesVersion(String id) throws VersionNotFoundException {
return getVersion(id).resolvePreservingPatches(this);
}
/**
* How many version are there?
*/
@@ -17,11 +17,11 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.File;
import java.io.Serializable;
import org.jackhuang.hmcl.util.platform.JavaVersion;
/**
*
* @author huangyuhui
@@ -31,6 +31,7 @@ public class LaunchOptions implements Serializable {
private File gameDir;
private JavaVersion java;
private String versionName;
private String versionType;
private String profileName;
private String minecraftArgs;
private String javaArgs;
@@ -71,6 +72,14 @@ public class LaunchOptions implements Serializable {
return versionName;
}
/**
* Will shown in the left bottom corner of the main menu of Minecraft.
* null if use Version.versionType.
*/
public String getVersionType() {
return versionType;
}
/**
* Don't know what the hell this is.
*/
@@ -215,6 +224,11 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setVersionType(String versionType) {
options.versionType = versionType;
return this;
}
public Builder setProfileName(String profileName) {
options.profileName = profileName;
return this;
@@ -34,7 +34,7 @@ public final class LibrariesDownloadInfo {
private final Map<String, LibraryDownloadInfo> classifiers;
public LibrariesDownloadInfo(LibraryDownloadInfo artifact) {
this(artifact, Collections.emptyMap());
this(artifact, null);
}
public LibrariesDownloadInfo(LibraryDownloadInfo artifact, Map<String, LibraryDownloadInfo> classifiers) {
@@ -17,15 +17,16 @@
*/
package org.jackhuang.hmcl.game;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -36,83 +37,68 @@ import java.util.Optional;
*
* @author huangyuhui
*/
@JsonAdapter(Library.Serializer.class)
public class Library implements Comparable<Library> {
@Immutable
public class Library implements Comparable<Library>, Validation {
private final String groupId;
private final String artifactId;
private final String version;
private final String classifier;
@SerializedName("name")
private final Artifact artifact;
private final String url;
private final LibrariesDownloadInfo downloads;
private transient final LibraryDownloadInfo download;
private final ExtractRules extract;
private final Map<OperatingSystem, String> natives;
private final List<CompatibilityRule> rules;
private final List<String> checksums;
private transient final String path;
@SerializedName(value = "hint", alternate = {"MMC-hint"})
private final String hint;
public Library(String groupId, String artifactId, String version) {
this(groupId, artifactId, version, null, null, null);
@SerializedName(value = "filename", alternate = {"MMC-filename"})
private final String fileName;
public Library(Artifact artifact) {
this(artifact, null, null);
}
public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads) {
this(groupId, artifactId, version, classifier, url, downloads, null, null, null, null);
public Library(Artifact artifact, String url, LibrariesDownloadInfo downloads) {
this(artifact, url, downloads, null, null, null, null, null, null);
}
public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads, List<String> checksums, ExtractRules extract, Map<OperatingSystem, String> natives, List<CompatibilityRule> rules) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
if (classifier == null)
if (natives != null && natives.containsKey(OperatingSystem.CURRENT_OS))
this.classifier = natives.get(OperatingSystem.CURRENT_OS).replace("${arch}", Platform.PLATFORM.getBit());
else
this.classifier = null;
else
this.classifier = classifier;
public Library(Artifact artifact, String url, LibrariesDownloadInfo downloads, List<String> checksums, ExtractRules extract, Map<OperatingSystem, String> natives, List<CompatibilityRule> rules, String hint, String filename) {
this.artifact = artifact;
this.url = url;
this.downloads = downloads;
this.extract = extract;
this.natives = natives;
this.rules = rules;
this.checksums = checksums;
LibraryDownloadInfo temp = null;
if (downloads != null)
if (isNative())
temp = downloads.getClassifiers().get(this.classifier);
else
temp = downloads.getArtifact();
if (temp != null && temp.getPath() != null)
path = temp.getPath();
else
path = String.format("%s/%s/%s/%s-%s", groupId.replace(".", "/"), artifactId, version, artifactId, version)
+ (this.classifier == null ? "" : "-" + this.classifier) + ".jar";
download = new LibraryDownloadInfo(path,
Optional.ofNullable(temp).map(LibraryDownloadInfo::getUrl).orElse(Optional.ofNullable(url).orElse(Constants.DEFAULT_LIBRARY_URL) + path),
temp != null ? temp.getSha1() : null,
temp != null ? temp.getSize() : 0
);
this.hint = hint;
this.fileName = filename;
}
public String getGroupId() {
return groupId;
return artifact.getGroup();
}
public String getArtifactId() {
return artifactId;
return artifact.getName();
}
public String getName() {
return groupId + ":" + artifactId + ":" + version;
return artifact.toString();
}
public String getVersion() {
return version;
return artifact.getVersion();
}
public String getClassifier() {
if (artifact.getClassifier() == null)
if (natives != null && natives.containsKey(OperatingSystem.CURRENT_OS))
return natives.get(OperatingSystem.CURRENT_OS).replace("${arch}", Platform.PLATFORM.getBit());
else
return null;
else
return artifact.getClassifier();
}
public ExtractRules getExtract() {
@@ -127,12 +113,33 @@ public class Library implements Comparable<Library> {
return natives != null && appliesToCurrentEnvironment();
}
protected LibraryDownloadInfo getRawDownloadInfo() {
if (downloads != null) {
if (isNative())
return downloads.getClassifiers().get(getClassifier());
else
return downloads.getArtifact();
} else {
return null;
}
}
public String getPath() {
return path;
LibraryDownloadInfo temp = getRawDownloadInfo();
if (temp != null && temp.getPath() != null)
return temp.getPath();
else
return artifact.setClassifier(getClassifier()).getPath();
}
public LibraryDownloadInfo getDownload() {
return download;
LibraryDownloadInfo temp = getRawDownloadInfo();
String path = getPath();
return new LibraryDownloadInfo(path,
Optional.ofNullable(temp).map(LibraryDownloadInfo::getUrl).orElse(Optional.ofNullable(url).orElse(Constants.DEFAULT_LIBRARY_URL) + path),
temp != null ? temp.getSha1() : null,
temp != null ? temp.getSize() : 0
);
}
public List<String> getChecksums() {
@@ -143,8 +150,24 @@ public class Library implements Comparable<Library> {
return rules;
}
/**
* Hint for how to locate the library file.
* @return null for default, "local" for location in version/&lt;version&gt;/libraries/filename
*/
public String getHint() {
return hint;
}
/**
* Available when hint is "local"
* @return the filename of the local library in version/&lt;version&gt;/libraries/$filename
*/
public String getFileName() {
return fileName;
}
public boolean is(String groupId, String artifactId) {
return this.groupId.equals(groupId) && this.artifactId.equals(artifactId);
return getGroupId().equals(groupId) && getArtifactId().equals(artifactId);
}
@Override
@@ -175,58 +198,12 @@ public class Library implements Comparable<Library> {
}
public Library setClassifier(String classifier) {
return new Library(groupId, artifactId, version, classifier, url, downloads, checksums, extract, natives, rules);
return new Library(artifact.setClassifier(classifier), url, downloads, checksums, extract, natives, rules, hint, fileName);
}
public static Library fromName(String name) {
return fromName(name, null, null, null, null, null, null);
}
public static Library fromName(String name, String url, LibrariesDownloadInfo downloads, List<String> checksums, ExtractRules extract, Map<OperatingSystem, String> natives, List<CompatibilityRule> rules) {
String[] arr = name.split(":", 4);
if (arr.length != 3 && arr.length != 4)
throw new IllegalArgumentException("Library name is malformed. Correct example: group:artifact:version(:classifier).");
return new Library(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, url, downloads, checksums, extract, natives, rules);
}
public static class Serializer implements JsonDeserializer<Library>, JsonSerializer<Library> {
@Override
public Library deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
if (json == null || json == JsonNull.INSTANCE)
return null;
JsonObject jsonObject = json.getAsJsonObject();
if (!jsonObject.has("name"))
throw new JsonParseException("Library name not found.");
return fromName(
jsonObject.get("name").getAsString(),
jsonObject.has("url") ? jsonObject.get("url").getAsString() : null,
context.deserialize(jsonObject.get("downloads"), LibrariesDownloadInfo.class),
context.deserialize(jsonObject.get("checksums"), new TypeToken<List<String>>() {
}.getType()),
context.deserialize(jsonObject.get("extract"), ExtractRules.class),
context.deserialize(jsonObject.get("natives"), new TypeToken<Map<OperatingSystem, String>>() {
}.getType()),
context.deserialize(jsonObject.get("rules"), new TypeToken<List<CompatibilityRule>>() {
}.getType()));
}
@Override
public JsonElement serialize(Library src, Type type, JsonSerializationContext context) {
if (src == null)
return JsonNull.INSTANCE;
JsonObject obj = new JsonObject();
obj.addProperty("name", src.getName());
obj.addProperty("url", src.url);
obj.add("downloads", context.serialize(src.downloads));
obj.add("checksums", context.serialize(src.getChecksums()));
obj.add("extract", context.serialize(src.extract));
obj.add("natives", context.serialize(src.natives, new TypeToken<Map<OperatingSystem, String>>() {
}.getType()));
obj.add("rules", context.serialize(src.rules, new TypeToken<List<CompatibilityRule>>() {
}.getType()));
return obj;
}
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (artifact == null)
throw new JsonParseException("Library.name cannot be null");
}
}
@@ -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,15 +24,16 @@ 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;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -60,22 +61,36 @@ 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;
private final Integer minimumLauncherVersion;
private final Boolean root;
private final Boolean hidden;
private final List<Version> patches;
private transient final boolean resolved;
public Version(String id, String version, int priority, Arguments arguments, String mainClass, List<Library> libraries) {
this(false, id, version, priority, null, arguments, mainClass, null, null, null, null, libraries, null, null, null, null, null, null, null, null, null);
public Version(String id) {
this(false, id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, true, null);
}
public Version(boolean resolved, String id, String version, Integer priority, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, List<Library> libraries, List<CompatibilityRule> compatibilityRules, Map<DownloadType, DownloadInfo> downloads, Map<DownloadType, LoggingInfo> logging, ReleaseType type, Date time, Date releaseTime, Integer minimumLauncherVersion, Boolean hidden, List<Version> patches) {
/**
* Constructor for patch
* @param id patch id
* @param version patch version
* @param priority patch priority
* @param arguments patch additional arguments
* @param mainClass main class to override
* @param libraries additional libraries
*/
public Version(String id, String version, int priority, Arguments arguments, String mainClass, List<Library> libraries) {
this(false, id, version, priority, null, arguments, mainClass, null, null, null, null, libraries, null, null, null, null, null, null, null, null, null, null);
}
public Version(boolean resolved, String id, String version, Integer priority, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, List<Library> libraries, List<CompatibilityRule> compatibilityRules, Map<DownloadType, DownloadInfo> downloads, Map<DownloadType, LoggingInfo> logging, ReleaseType type, Date time, Date releaseTime, Integer minimumLauncherVersion, Boolean hidden, Boolean root, List<Version> patches) {
this.resolved = resolved;
this.id = id;
this.version = version;
@@ -87,16 +102,17 @@ public class Version implements Comparable<Version>, Validation {
this.jar = jar;
this.assetIndex = assetIndex;
this.assets = assets;
this.libraries = libraries == null ? Collections.emptyList() : new LinkedList<>(libraries);
this.compatibilityRules = compatibilityRules == null ? null : new LinkedList<>(compatibilityRules);
this.downloads = downloads == null ? null : new HashMap<>(downloads);
this.logging = logging == null ? null : new HashMap<>(logging);
this.libraries = Lang.copyList(libraries);
this.compatibilityRules = Lang.copyList(compatibilityRules);
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();
this.minimumLauncherVersion = minimumLauncherVersion;
this.hidden = hidden;
this.patches = patches == null ? null : patches.isEmpty() ? null : new LinkedList<>(patches);
this.root = root;
this.patches = Lang.copyList(patches);
}
public Optional<String> getMinecraftArguments() {
@@ -157,10 +173,16 @@ public class Version implements Comparable<Version>, Validation {
return hidden == null ? false : hidden;
}
public boolean isRoot() { return root == null ? false : root; }
public boolean isResolved() {
return resolved;
}
public boolean isResolvedPreservingPatches() {
return inheritsFrom == null && !resolved;
}
public List<Version> getPatches() {
return patches == null ? Collections.emptyList() : patches;
}
@@ -202,7 +224,7 @@ public class Version implements Comparable<Version>, Validation {
return resolve(provider, new HashSet<>()).setResolved();
}
protected Version merge(Version parent) {
protected Version merge(Version parent, boolean isPatch) {
return new Version(
true,
id,
@@ -224,7 +246,8 @@ public class Version implements Comparable<Version>, Validation {
releaseTime,
Lang.merge(minimumLauncherVersion, parent.minimumLauncherVersion, Math::max),
hidden,
Lang.merge(parent.patches, patches));
false,
isPatch ? parent.patches : Lang.merge(Lang.merge(parent.patches, Collections.singleton(toPatch())), patches));
}
protected Version resolve(VersionProvider provider, Set<String> resolvedSoFar) throws VersionNotFoundException {
@@ -239,7 +262,7 @@ public class Version implements Comparable<Version>, Validation {
thisVersion = this.jar == null ? this.setJar(id) : this;
} else {
// It is supposed to auto install an version in getVersion.
thisVersion = merge(provider.getVersion(inheritsFrom).resolve(provider, resolvedSoFar));
thisVersion = merge(provider.getVersion(inheritsFrom).resolve(provider, resolvedSoFar), false);
}
}
@@ -249,7 +272,41 @@ public class Version implements Comparable<Version>, Validation {
.sorted(Comparator.comparing(Version::getPriority))
.collect(Collectors.toList());
for (Version patch : sortedPatches) {
thisVersion = patch.setJar(null).merge(thisVersion);
thisVersion = patch.setJar(null).merge(thisVersion, true);
}
}
return thisVersion.setId(id);
}
private Version toPatch() {
return this.clearPatches().setHidden(true).setId("resolved." + getId());
}
/**
* Resolve the version preserving all dependencies and patches.
*/
public Version resolvePreservingPatches(VersionProvider provider) throws VersionNotFoundException {
return resolvePreservingPatches(provider, new HashSet<>());
}
protected Version mergePreservingPatches(Version parent) {
return parent.addPatch(toPatch()).addPatches(patches);
}
protected Version resolvePreservingPatches(VersionProvider provider, Set<String> resolvedSoFar) throws VersionNotFoundException {
Version thisVersion = isRoot() ? this : new Version(id).addPatch(toPatch()).addPatches(getPatches());
if (inheritsFrom == null) {
// keep thisVersion
} else {
// To maximize the compatibility.
if (!resolvedSoFar.add(id)) {
Logging.LOG.log(Level.WARNING, "Found circular dependency versions: " + resolvedSoFar);
// keep thisVersion
} else {
// It is supposed to auto install an version in getVersion.
thisVersion = mergePreservingPatches(provider.getVersion(inheritsFrom).resolvePreservingPatches(provider, resolvedSoFar));
}
}
@@ -257,58 +314,78 @@ public class Version implements Comparable<Version>, Validation {
}
private Version setResolved() {
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
private Version setHidden(Boolean hidden) {
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setId(String id) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setVersion(String version) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setPriority(Integer priority) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setMinecraftArguments(String minecraftArguments) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setArguments(Arguments arguments) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setMainClass(String mainClass) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setInheritsFrom(String inheritsFrom) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setJar(String jar) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setLibraries(List<Library> libraries) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version setLogging(Map<DownloadType, LoggingInfo> logging) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, patches);
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version addPatch(Version patch) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, Lang.merge(patches, Collections.singleton(patch)));
public Version setPatches(List<Version> patches) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
}
public Version addPatch(Version... additional) {
return addPatches(Arrays.asList(additional));
}
public Version addPatches(List<Version> additional) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, Lang.merge(patches, additional));
}
public Version clearPatches() {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root,null);
}
public Version removePatchById(String patchId) {
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden,
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root,
patches == null ? null : patches.stream().filter(patch -> !patchId.equals(patch.getId())).collect(Collectors.toList()));
}
public boolean hasPatch(String patchId) {
return patches != null && patches.stream().anyMatch(patch -> patchId.equals(patch.getId()));
}
@Override
public int hashCode() {
return id.hashCode();
@@ -32,10 +32,7 @@ import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.*;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -91,7 +88,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 +113,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
@@ -154,7 +154,13 @@ public class World {
}
public void install(Path savesDir, String name) throws IOException {
Path worldDir = savesDir.resolve(name);
Path worldDir;
try {
worldDir = savesDir.resolve(name);
} catch (InvalidPathException e) {
throw new IOException(e);
}
if (Files.isDirectory(worldDir)) {
throw new FileAlreadyExistsException("World already exists");
}
@@ -209,7 +215,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();
}
@@ -206,7 +206,7 @@ public class DefaultLauncher extends Launcher {
}
private final Map<String, Supplier<Boolean>> forbiddens = mapOf(
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9)
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9_AND_LATER)
);
protected Map<String, Supplier<Boolean>> getForbiddens() {
@@ -259,7 +259,7 @@ public class DefaultLauncher extends Launcher {
pair("${auth_uuid}", UUIDTypeAdapter.fromUUID(authInfo.getUUID())),
pair("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
pair("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
pair("${version_type}", version.getType().getId()),
pair("${version_type}", Optional.ofNullable(options.getVersionType()).orElse(version.getType().getId())),
pair("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
pair("${user_type}", "mojang"),
pair("${assets_index_name}", version.getAssetIndex().getId()),
@@ -72,6 +72,8 @@ public class ModpackUpdateTask extends Task<Void> {
repository.removeVersionFromDisk(id);
FileUtils.copyDirectory(backupFolder, repository.getVersionRoot(id).toPath());
repository.refreshVersionsAsync().start();
}
}
}
@@ -23,7 +23,8 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -124,9 +125,15 @@ public final class CurseCompletionTask extends Task<Void> {
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
} catch (IOException | JsonParseException e2) {
Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), e2);
notFound.set(true);
return file;
try {
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
} catch (IOException | JsonParseException e3) {
Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), e2);
notFound.set(true);
return file;
}
}
} catch (IOException ioe) {
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
@@ -84,7 +85,12 @@ public final class CurseInstallTask extends Task<Void> {
dependents.add(builder.buildAsync());
onDone().register(event -> {
if (event.isFailed()) repository.removeVersionFromDisk(name);
Exception ex = event.getTask().getException();
if (event.isFailed()) {
if (!(ex instanceof CurseCompletionException) || ex.getCause() instanceof FileNotFoundException) {
repository.removeVersionFromDisk(name);
}
}
});
ModpackConfiguration<CurseManifest> config = null;
@@ -20,18 +20,23 @@ package org.jackhuang.hmcl.mod.curse;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable;
/**
* CurseMetaMod is JSON structure for
* https://cursemeta.dries007.net/&lt;projectID&gt;/&lt;fileID&gt;.json
* https://addons-ecs.forgesvc.net/api/v2/addon/&lt;projectID&gt;/file/<fileID&gt;
*/
@Immutable
public final class CurseMetaMod {
@SerializedName("Id")
@SerializedName(value = "Id", alternate = "id")
private final int id;
@SerializedName("FileName")
@SerializedName(value = "FileName", alternate = "fileName")
private final String fileName;
@SerializedName("FileNameOnDisk")
@SerializedName(value = "FileNameOnDisk")
private final String fileNameOnDisk;
@SerializedName("DownloadURL")
@SerializedName(value = "DownloadURL", alternate = "downloadUrl")
private final String downloadURL;
public CurseMetaMod() {
@@ -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;
@@ -78,9 +79,9 @@ public class MultiMCModpackExportTask extends Task<Void> {
return false;
});
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedVersion(versionId));
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 ->
@@ -21,16 +21,14 @@ import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.VersionLibraryBuilder;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -41,6 +39,7 @@ 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.LinkedList;
import java.util.List;
import java.util.Optional;
@@ -84,6 +83,12 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
if (c.getVersion() != null)
builder.version("liteloader", c.getVersion());
});
Optional<MultiMCManifest.MultiMCManifestComponent> fabric = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.fabricmc.fabric-loader")).findAny();
fabric.ifPresent(c -> {
if (c.getVersion() != null)
builder.version("fabric", c.getVersion());
});
}
dependents.add(builder.buildAsync());
@@ -137,7 +142,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
public void execute() throws Exception {
Version version = repository.readVersionJson(name);
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) {
Path root = MultiMCInstanceConfiguration.getRootPath(fs.getPath("/"));
Path patches = root.resolve("patches");
@@ -145,20 +150,29 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
for (Path patchJson : Files.newDirectoryStream(patches)) {
if (patchJson.toString().endsWith(".json")) {
// If json is malformed, we should stop installing this modpack instead of skipping it.
MultiMCInstancePatch patch = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(patchJson)), MultiMCInstancePatch.class);
MultiMCInstancePatch multiMCPatch = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(patchJson)), MultiMCInstancePatch.class);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
for (String arg : patch.getTweakers())
builder.addArgument("--tweakClass", arg);
List<String> arguments = new ArrayList<>();
for (String arg : multiMCPatch.getTweakers()) {
arguments.add("--tweakClass");
arguments.add(arg);
}
version = builder.build()
.setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries()))
.setMainClass(patch.getMainClass());
Version patch = new Version(multiMCPatch.getName(), multiMCPatch.getVersion(), 1, new Arguments().addGameArguments(arguments), multiMCPatch.getMainClass(), multiMCPatch.getLibraries());
version = version.addPatch(patch);
}
}
Path libraries = root.resolve("libraries");
if (Files.exists(libraries))
FileUtils.copyDirectory(libraries, repository.getVersionRoot(name).toPath().resolve("libraries"));
Path jarmods = root.resolve("jarmods");
if (Files.exists(jarmods))
FileUtils.copyDirectory(jarmods, repository.getVersionRoot(name).toPath().resolve("jarmods"));
}
dependencies.add(new MaintainTask(version).thenComposeAsync(repository::save));
dependencies.add(repository.save(version));
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/" + manifest.getName() + "/minecraft", manifest, MODPACK_TYPE, repository.getModpackConfiguration(name)));
}
@@ -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);
}
}
@@ -190,6 +190,7 @@ public abstract class Task<T> {
*/
public Task<T> storeTo(Consumer<T> action) {
this.resultConsumer = action;
action.accept(getResult());
return this;
}
@@ -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;
@@ -62,6 +63,15 @@ public final class TaskExecutor {
if (!success) {
// We log exception stacktrace because some of exceptions occurred because of bugs.
Logging.LOG.log(Level.WARNING, "An exception occurred in task execution", exception);
Throwable resolvedException = resolveException(exception);
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);
}
}
taskListeners.forEach(it -> it.onStop(success, this));
@@ -253,4 +263,10 @@ public final class TaskExecutor {
}
};
}
private static Thread.UncaughtExceptionHandler uncaughtExceptionHandler = null;
public static void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler uncaughtExceptionHandler) {
TaskExecutor.uncaughtExceptionHandler = uncaughtExceptionHandler;
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
import java.util.*;
@@ -57,6 +58,15 @@ public final class Lang {
return Collections.unmodifiableList(Arrays.asList(elements));
}
public static boolean test(ExceptionalRunnable<?> r) {
try {
r.run();
return true;
} catch (Exception e) {
return false;
}
}
public static <E extends Exception> boolean test(ExceptionalSupplier<Boolean, E> r) {
try {
return r.get();
@@ -119,6 +129,10 @@ public final class Lang {
return result;
}
public static <T> List<T> copyList(List<T> list) {
return list == null ? null : list.isEmpty() ? null : new ArrayList<>(list);
}
public static void executeDelayed(Runnable runnable, TimeUnit timeUnit, long timeout, boolean isDaemon) {
thread(() -> {
try {
@@ -0,0 +1,51 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.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");
}
}
@@ -199,7 +199,7 @@ public final class FileUtils {
Files.walkFileTree(src, new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path destFile = dest.resolve(src.relativize(file));
Path destFile = dest.resolve(src.relativize(file).toString());
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
@@ -207,7 +207,7 @@ public final class FileUtils {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path destDir = dest.resolve(src.relativize(dir));
Path destDir = dest.resolve(src.relativize(dir).toString());
Files.createDirectories(destDir);
return FileVisitResult.CONTINUE;
@@ -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;
@@ -176,7 +178,7 @@ public final class NetworkUtils {
public static String detectFileName(URL url) throws IOException {
HttpURLConnection conn = resolveConnection(createConnection(url));
int code = conn.getResponseCode();
if (code == 404)
if (code / 100 == 4)
throw new FileNotFoundException();
if (code / 100 != 2)
throw new IOException(url + ": response code " + conn.getResponseCode());
@@ -17,9 +17,9 @@
*/
package org.jackhuang.hmcl.util.platform;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.util.Logging.LOG;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.BufferedReader;
import java.io.IOException;
@@ -35,8 +35,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* Represents a Java installation.
@@ -76,9 +77,6 @@ public final class JavaVersion {
/**
* The major version of Java installation.
*
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_11
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_10
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_9
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_8
* @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_7
* @see org.jackhuang.hmcl.util.platform.JavaVersion#UNKNOWN
@@ -88,22 +86,20 @@ public final class JavaVersion {
}
private static final Pattern REGEX = Pattern.compile("version \"(?<version>(.*?))\"");
private static final Pattern VERSION = Pattern.compile("^(?<version>[0-9]+)");
public static final int UNKNOWN = -1;
public static final int JAVA_7 = 70;
public static final int JAVA_8 = 80;
public static final int JAVA_9 = 90;
public static final int JAVA_10 = 100;
public static final int JAVA_11 = 110;
public static final int JAVA_9_AND_LATER = 90;
private static int parseVersion(String version) {
if (version.startsWith("11"))
return JAVA_11;
else if (version.startsWith("10"))
return JAVA_10;
else if (version.startsWith("9"))
return JAVA_9;
else if (version.contains("1.8"))
Matcher matcher = VERSION.matcher(version);
if (matcher.find()) {
int head = Lang.parseInt(matcher.group(), -1);
if (head > 1) return JAVA_9_AND_LATER;
}
if (version.contains("1.8"))
return JAVA_8;
else if (version.contains("1.7"))
return JAVA_7;
@@ -115,11 +111,6 @@ public final class JavaVersion {
Platform platform = Platform.BIT_32;
String version = null;
// javaw is only used on windows
if ("javaw.exe".equalsIgnoreCase(executable.getFileName().toString())) {
executable = executable.resolveSibling("java.exe");
}
executable = executable.toRealPath();
Process process = new ProcessBuilder(executable.toString(), "-version").start();
@@ -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;
}
}
@@ -0,0 +1,42 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
public final class SystemUtils {
private SystemUtils() {}
public static int callExternalProcess(String... command) throws IOException, InterruptedException {
return callExternalProcess(Arrays.asList(command));
}
public static int callExternalProcess(List<String> command) throws IOException, InterruptedException {
Process process = new ProcessBuilder(command).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
for (String line; (line = reader.readLine()) != null;) {
System.out.println(line);
}
}
return process.waitFor();
}
}