Compare commits

..
67 Commits
Author SHA1 Message Date
huanghongxun f4f89fac1f Merge pull request #438 from yushijinhun/force-update
移除「更新一次 HMCL,电脑上所有 HMCL 都得到更新」的功能
2018-08-26 19:51:20 +08:00
yushijinhun 5262150d3c Prevent 2.x being updated to 3.x 2018-08-26 19:34:10 +08:00
yushijinhun b5f56eb2b5 Remove force update feature 2018-08-26 19:02:47 +08:00
huangyuhui 9a375fd0d1 Add hiding versions. Closes #437 2018-08-26 11:50:42 +08:00
huangyuhui 6ca4aeb5a2 Support 1.13 auto-installing 2018-08-26 11:48:27 +08:00
huangyuhui 8ed4a9a5f2 Fix not showing Optifine 1.13 alpha 2018-08-25 19:53:29 +08:00
huangyuhui a4578405cf Remove RandomUserAgent 2018-08-25 19:39:00 +08:00
huangyuhui 73f147b7e2 Update Chinese localization 2018-08-23 21:45:56 +08:00
huangyuhui 5c3d735315 Prevent HMCL 2.x updating 2018-08-23 21:45:47 +08:00
huanghongxun 200621996d Merge pull request #431 from pan93412/javafx
Update Chinese (Taiwan) Translation
2018-08-22 20:56:12 +08:00
byStarTW a912a0a07e Update Chinese (Taiwan) Translation 2018-08-22 20:54:49 +08:00
huangyuhui 4146ceb048 Fix not downloading the correct game version 2018-08-22 04:25:35 +08:00
huangyuhui ad568d2b9f Fix #429 2018-08-22 02:08:49 +08:00
huangyuhui c7bfb9a11d Suggest user to install latest Java 8 to avoid potentional game crashing 2018-08-21 21:12:05 +08:00
huangyuhui c9391181bd Display empty message when loading 1.13 forge version list 2018-08-21 19:31:04 +08:00
huangyuhui b08eb519bf Remove logging support which is meaningless 2018-08-21 11:48:19 +08:00
huangyuhui 47c4fc2f7e Fix #426 2018-08-20 11:53:02 +08:00
huangyuhui dfd177433a Fix not replacing tokens metioned in #426 2018-08-20 11:38:06 +08:00
huanghongxun 27381e9696 Merge pull request #425 from yushijinhun/yushijinhun/javafx
修复 UpdateChecker 潜在问题 & 弃用 ImmediateXXXProperty
2018-08-19 19:47:30 +08:00
yushijinhun 1c7827c53c Deprecate and partially remove ImmediateProperty 2018-08-19 15:55:18 +08:00
yushijinhun 9512bdb5d6 Remove UpdateChecker.updateChannel & Fix concurrent problems in update checking 2018-08-19 15:43:34 +08:00
huangyuhui 33d1f1d692 Not showing 'unknown' in mod info 2018-08-19 12:19:11 +08:00
huangyuhui c6b49545ff Fix #424 2018-08-19 12:13:39 +08:00
huangyuhui a4a833bfdb Fix auto-installing: missing checksums in game json 2018-08-19 11:58:02 +08:00
huangyuhui 2e1b6e4630 Fix not correctly loading remote game version list 2018-08-19 11:56:39 +08:00
huangyuhui 6911cfa387 try to move deleting version to trash 2018-08-19 11:34:07 +08:00
huangyuhui aa29f890de Log memory size and metaspace 2018-08-19 02:03:05 +08:00
huangyuhui 0f4e6a01bc Fix unable to install more than one libraries 2018-08-19 01:18:57 +08:00
huangyuhui 1d3d8d6710 Show OptiFine versions in dividually 2018-08-19 00:39:47 +08:00
huangyuhui 005e06c56f Always download Optifine from BMCLAPI 2018-08-19 00:35:37 +08:00
huangyuhui 0a7c32b3cb Friendly prompt warning user about independent version deletion 2018-08-18 22:40:08 +08:00
huangyuhui 82916a04ea Check if forge libraries' checksums are valid 2018-08-18 22:25:13 +08:00
huangyuhui ba2e419894 Fix #422 2018-08-18 22:09:16 +08:00
huangyuhui 0812c9025d Do not check game asset hash code for every launching 2018-08-18 02:00:00 +08:00
huangyuhui 4c59ed3ccf Fix installing MultiMC modpack 2018-08-18 01:12:22 +08:00
huangyuhui 43d8d96608 Add doublequotes when doublequotes exist in windows batch 2018-08-18 00:43:55 +08:00
huangyuhui 26a6a7d883 Add doublequotes when special characters exist in launch script. Closes #420 2018-08-18 00:32:37 +08:00
huangyuhui 2e9c237367 Use system proxy by default 2018-08-17 23:48:01 +08:00
huangyuhui 99d2816ea7 Suppress RuntimeException since -1 is possible 2018-08-17 22:11:46 +08:00
huangyuhui 7c3853b0e6 Not logging command line in ProcessStoppedEvent 2018-08-17 22:10:11 +08:00
huangyuhui 53aecf38e0 Support displaying Rift mod information 2018-08-17 22:09:37 +08:00
huangyuhui 41fab0a781 Log launching game version name 2018-08-17 21:56:52 +08:00
huangyuhui 3df35aa373 Translate ZipError into ZipException since ZipError is not IOException 2018-08-17 21:53:29 +08:00
huangyuhui 543eaa4f66 Force cancel using custom log4j config 2018-08-17 21:52:35 +08:00
huangyuhui 3a851b7f1a Fix unable to launch 1.13 with Rift 2018-08-17 21:51:25 +08:00
huangyuhui 089ae1e313 Suppress InterruptedException 2018-08-17 21:51:12 +08:00
huangyuhui bfe5802d02 Remove roboto font 2018-08-16 02:46:49 +08:00
huangyuhui 3ec6c97380 Replace 2.x with existent 3.x 2018-08-15 01:40:18 +08:00
huangyuhui e376da208d Fix duplicate JavaVersions in Windows. Closes #419 2018-08-14 10:44:24 +08:00
huangyuhui 3b02c5e4e0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-08-14 00:10:48 +08:00
huangyuhui 1bcc79c415 Fix always trying to close launcher when game version cannot be obtained 2018-08-14 00:09:20 +08:00
huangyuhui 9e2cb25d01 Convert old offline account setings 2018-08-14 00:08:35 +08:00
yushijinhun 7e820ba2a5 Fix closeDialog() throws NPE 2018-08-13 22:18:18 +08:00
huanghongxun b079940cd9 Fix uniquing text2speech and its native library. Closes #418 2018-08-11 09:45:31 +08:00
huanghongxun 593ee24429 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-08-10 19:05:26 +08:00
huanghongxun f7a91bf10c Fix version setting not saved 2018-08-10 19:04:33 +08:00
huanghongxun dad7ac706f Add update channel selection 2018-08-10 12:53:49 +08:00
Haowei Wen 13c48ce78c Merge pull request #417 from tsl0922/osx-maximized
Mac 下恢复使用自定义窗口边框按钮
2018-08-10 08:26:56 +08:00
Shuanglei Tao 8ecbba1003 Remove native window border on osx 2018-08-09 23:41:56 +08:00
huanghongxun 8bf6a9ec9d Rename AdvancedListItem 2018-08-09 20:22:57 +08:00
huanghongxun 64092a4bd4 Merge branch 'yushijinhun-java' into javafx 2018-08-09 13:54:54 +08:00
yushijinhun 8e6bcd969e Refactor JavaVersion 2018-08-09 13:49:16 +08:00
yushijinhun 6777dd7ea7 Fix UnsupportedOperationException in java lookup
Mentioned in #412
2018-08-09 13:22:49 +08:00
yushijinhun f16885568e Make config files hidden 2018-08-09 12:56:03 +08:00
huanghongxun ab109bcfcc Merge pull request #414 from yushijinhun/javafx
Fixes FileSystemNotFoundException in createZipFileSystem
2018-08-09 11:41:27 +08:00
yushijinhun 17f71eaa21 Fixes FileSystemNotFoundException in createZipFileSystem 2018-08-09 11:34:38 +08:00
huanghongxun 3a294e354a ExeLauncher: Search Java in C:/Program Files 2018-08-08 21:16:57 +08:00
107 changed files with 1729 additions and 3543 deletions

No files matched your search

@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import com.jfoenix.concurrency.JFXUtilities;
@@ -31,7 +30,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
@@ -39,7 +38,6 @@ import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public final class Launcher extends Application {
@@ -55,13 +53,7 @@ public final class Launcher extends Application {
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
thread(() -> {
try {
UpdateChecker.checkUpdate();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check for update", e);
}
});
UpdateChecker.init();
primaryStage.show();
} catch (Throwable e) {
@@ -88,6 +80,9 @@ public final class Launcher extends Application {
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
+10 -20
View File
@@ -21,8 +21,8 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -40,16 +40,22 @@ import org.jackhuang.hmcl.upgrade.UpdateHandler;
public final class Main {
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
checkConfigPermission();
if (UpdateHandler.processArguments(args)) {
return;
}
ConfigHolder.init();
try {
ConfigHolder.init();
} catch (IOException e) {
showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
Launcher.main(args);
}
@@ -92,22 +98,6 @@ public final class Main {
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
}
private static void checkConfigPermission() {
Path config = ConfigHolder.CONFIG_PATH;
if (Files.exists(config)) {
if (Files.isReadable(config) && Files.isWritable(config)) {
// we are able to read & write the existent config
return;
}
} else {
if (Files.isWritable(config.getParent())) {
// we are able to create a new config
return;
}
}
showErrorAndExit(i18n("fatal.config_access_denied", config));
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
@@ -65,6 +65,8 @@ public final class LauncherHelper {
if (account == null)
throw new IllegalArgumentException("No account");
Logging.LOG.info("Launching game version: " + selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
@@ -225,29 +227,50 @@ public final class LauncherHelper {
flag = true;
}
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8) {
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
}
flag = true;
}
flag = true;
}
// LaunchWrapper will crash because of assuming the system class loader is an instance of URLClassLoader.
// cpw has claimed that he will make MinecraftForge of 1.13 and later versions able to run on Java 9.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.13")) < 0 && version.getMainClass().contains("launchwrapper")) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
} else {
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
flag = true;
}
}
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0.51");
if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
flag = true;
}
flag = true;
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
@@ -255,6 +278,7 @@ public final class LauncherHelper {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
@@ -262,6 +286,7 @@ public final class LauncherHelper {
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
@@ -388,15 +413,18 @@ public final class LauncherHelper {
logs.removeFirst();
if (setting.isShowLogs()) {
Lang.invoke(() -> {
try {
latch.await();
logWindow.waitForLoaded();
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
Platform.runLater(() -> logWindow.logLine(log, level));
}
if (!lwjgl && log.contains("LWJGL Version: ") || !detectWindow) {
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
lwjgl = true;
switch (visibility) {
case HIDE_AND_REOPEN:
@@ -87,7 +87,7 @@ public final class Accounts {
return type2factory.get(accountType(account));
}
private static String accountId(Account account) {
static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
@@ -138,20 +138,19 @@ public final class Accounts {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
}
private static void updateAccountStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(
accounts.stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
})
.collect(toList()));
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
}
/**
@@ -30,6 +30,7 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.FileTypeAdapter;
import org.jackhuang.hmcl.util.ObservableHelper;
@@ -149,6 +150,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@SerializedName("updateChannel")
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -435,4 +439,15 @@ public final class Config implements Cloneable, Observable {
return authlibInjectorServers;
}
public UpdateChannel getUpdateChannel() {
return updateChannel.get();
}
public ObjectProperty<UpdateChannel> updateChannelProperty() {
return updateChannel;
}
public void setUpdateChannel(UpdateChannel updateChannel) {
this.updateChannel.set(updateChannel);
}
}
@@ -29,15 +29,17 @@ import java.util.logging.Level;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.OperatingSystem;
public final class ConfigHolder {
private ConfigHolder() {}
public static final String CONFIG_FILENAME = "hmcl.json";
public static final Path CONFIG_PATH = Paths.get(CONFIG_FILENAME).toAbsolutePath();
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
private static Path configLocation;
private static Config configInstance;
private static boolean newlyCreated;
@@ -55,35 +57,64 @@ public final class ConfigHolder {
return newlyCreated;
}
public synchronized static void init() {
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configLocation = locateConfig();
configInstance = loadConfig();
configInstance.addListener(source -> saveConfig());
configInstance.addListener(source -> markConfigDirty());
Settings.init();
if (newlyCreated) {
saveConfig();
// hide the config file on windows
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
}
}
}
if (!Files.isWritable(configLocation)) {
// the config cannot be saved
// throw up the error now to prevent further data loss
throw new IOException("Config at " + configLocation + " is not writable");
}
}
private static Config loadConfig() {
if (Files.exists(CONFIG_PATH)) {
private static Path locateConfig() {
Path config = Paths.get(CONFIG_FILENAME);
if (Files.isRegularFile(config))
return config;
Path dotConfig = Paths.get(CONFIG_FILENAME_LINUX);
if (Files.isRegularFile(dotConfig))
return dotConfig;
// create new
return OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? config : dotConfig;
}
private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) {
try {
String content = new String(Files.readAllBytes(CONFIG_PATH), UTF_8);
String content = new String(Files.readAllBytes(configLocation), UTF_8);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
upgradeConfig(deserialized, raw);
ConfigUpgrader.upgradeConfig(deserialized, raw);
return deserialized;
}
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Something went wrong when loading config.", e);
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
}
}
@@ -92,30 +123,22 @@ public final class ConfigHolder {
return new Config();
}
static void saveConfig() {
private static void saveConfig() throws IOException {
LOG.info("Saving config");
try {
Files.write(CONFIG_PATH, configInstance.toJson().getBytes(UTF_8));
} catch (IOException ex) {
LOG.log(Level.SEVERE, "Failed to save config", ex);
Files.write(configLocation, configInstance.toJson().getBytes(UTF_8));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
throw e;
}
}
/**
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
* @return {@code deserialized}
*/
private static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
// Following is for the compatibility with HMCL 2.x
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
static void markConfigDirty() {
// TODO: change this to async
try {
saveConfig();
} catch (IOException ignored) {
// ignore it as it has been logged
}
}
}
@@ -0,0 +1,90 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
final class ConfigUpgrader {
private ConfigUpgrader() {
}
/**
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
upgradeV2(deserialized, rawJson);
upgradeV3(deserialized, rawJson);
}
/**
* Upgrade configuration of HMCL 2.x
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
// Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
List<OfflineAccount> accounts = new LinkedList<>();
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
OfflineAccount account = OfflineAccountFactory.INSTANCE.create((String) key, UUIDTypeAdapter.fromString((String) value));
accounts.add(account);
deserialized.getAccountStorages().add(Accounts.getAccountStorage(account));
});
});
tryCast(offline.get("IAuthenticator_UserName"), String.class).ifPresent(selected -> {
if (!rawJson.containsKey("selectedAccount"))
accounts.stream().filter(account -> selected.equals(account.getUsername())).findAny().ifPresent(account ->
deserialized.setSelectedAccount(Accounts.accountId(account)));
});
});
});
}
/**
* Upgrade configuration of HMCL earlier than 3.1.70
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
}
}
@@ -66,7 +66,7 @@ public final class Profile {
return globalProperty.get();
}
public void setGlobal(VersionSetting global) {
private void setGlobal(VersionSetting global) {
if (global == null)
global = new VersionSetting();
globalProperty.set(global);
@@ -175,6 +175,7 @@ public final class Profile {
globalProperty.addListener(listener);
gameDirProperty.addListener(listener);
useRelativePathProperty.addListener(listener);
globalProperty.get().addPropertyChangedListener(listener);
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@@ -58,7 +58,7 @@ public class Settings {
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
profileEntry.getValue().setName(profileEntry.getKey());
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.saveConfig());
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.markConfigDirty());
}
}
@@ -88,8 +88,6 @@ public class Theme {
}
return new String[]{
Theme.class.getResource("/css/jfoenix-fonts.css").toExternalForm(),
Theme.class.getResource("/css/jfoenix-design.css").toExternalForm(),
css,
Theme.class.getResource("/assets/css/root.css").toExternalForm()
};
@@ -38,6 +38,7 @@ import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
@@ -49,7 +50,7 @@ import java.util.Optional;
public class AccountPage extends StackPane implements DecoratorPage {
private final StringProperty title;
private final ObjectProperty<Runnable> onDelete = new SimpleObjectProperty<>(this, "onDelete");
private final VersionListItem item;
private final AdvancedListItem item;
private final Account account;
@FXML
@@ -73,7 +74,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
@FXML
private JFXProgressBar progressBar;
public AccountPage(Account account, VersionListItem item) {
public AccountPage(Account account, AdvancedListItem item) {
this.account = account;
this.item = item;
@@ -27,7 +27,6 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -163,15 +162,6 @@ public final class Controllers {
return pane;
}
/**
* Use {@link DialogCloseEvent}
*/
public static void closeDialog(Region content) {
if (stage == null) // shut down
return;
decorator.closeDialog(content);
}
public static void navigate(Node node) {
if (decorator.getNowPage() == node)
decorator.showPage(null);
@@ -171,14 +171,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
onCloseButtonAction = new SimpleObjectProperty<>(this, "onCloseButtonAction", Launcher::stopApplication);
switch (OperatingSystem.CURRENT_OS) {
case OSX:
titleContainer.setRight(null);
break;
default:
primaryStage.initStyle(StageStyle.UNDECORATED);
break;
}
primaryStage.initStyle(StageStyle.UNDECORATED);
btnClose.setGraphic(close);
btnMin.setGraphic(minus);
btnMax.setGraphic(resizeMax);
@@ -310,11 +303,22 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
return Optional.empty();
}
private boolean isMaximized() {
switch (OperatingSystem.CURRENT_OS) {
case OSX:
Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
return primaryStage.getWidth() >= bounds.getWidth() && primaryStage.getHeight() >= bounds.getHeight();
default:
return primaryStage.isMaximized();
}
}
// ====
@FXML
private void onMouseMoved(MouseEvent mouseEvent) {
if (!primaryStage.isMaximized() && !primaryStage.isFullScreen() && !maximized) {
if (!isMaximized() && !primaryStage.isFullScreen() && !maximized) {
if (!primaryStage.isResizable())
updateInitMouseValues(mouseEvent);
else {
@@ -363,7 +367,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private void onMouseDragged(MouseEvent mouseEvent) {
this.isDragging = true;
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !isMaximized() && !this.maximized) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltaX = this.newX - this.initX;
@@ -611,18 +615,20 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
@SuppressWarnings("unchecked")
public void closeDialog(Node node) {
private void closeDialog(Node node) {
FXUtils.checkFxUserThread();
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
dialogPane.pop(node);
if (dialog != null) {
dialogPane.pop(node);
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
dialogPane = null;
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
dialogPane = null;
}
}
}
@@ -21,6 +21,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.game.GameVersion;
@@ -35,6 +36,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.LinkedList;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
public class InstallerController {
private Profile profile;
@@ -43,9 +45,6 @@ public class InstallerController {
@FXML
private ScrollPane scrollPane;
@FXML private VBox contentPane;
private String forge;
private String liteLoader;
private String optiFine;
@FXML
private void initialize() {
@@ -58,32 +57,22 @@ public class InstallerController {
this.version = profile.getRepository().getResolvedVersion(versionId);
contentPane.getChildren().clear();
forge = liteLoader = optiFine = null;
for (Library library : version.getLibraries()) {
Consumer<InstallerItem> removeAction = x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction));
forge = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction));
liteLoader = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction));
optiFine = library.getVersion();
}
}
Function<Library, Consumer<InstallerItem>> removeAction = library -> x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
analyzer.getForge().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction.apply(library))));
analyzer.getLiteLoader().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction.apply(library))));
analyzer.getOptiFine().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction.apply(library))));
}
@FXML
@@ -93,6 +82,6 @@ public class InstallerController {
if (!gameVersion.isPresent())
Controllers.dialog(i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version));
}
}
@@ -49,11 +49,7 @@ import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.MappedObservableList;
@@ -127,7 +123,7 @@ public final class LeftPaneController {
// ==== Accounts ====
// Missing account item
VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
AdvancedListItem missingAccountItem = new AdvancedListItem(i18n("account.missing"), i18n("message.unknown"));
RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
missingAccountRippler.setOnMouseClicked(e -> addNewAccount());
@@ -162,7 +158,7 @@ public final class LeftPaneController {
}
private RipplerContainer createAccountItem(Account account) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountSubtitle(account));
AdvancedListItem item = new AdvancedListItem(account.getCharacter(), accountSubtitle(account));
RipplerContainer rippler = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
@@ -219,7 +215,7 @@ public final class LeftPaneController {
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof String) {
boolean current = Objects.equals(node.getProperties().get("profile"), profile.getName());
((RipplerContainer) node).setSelected(current);
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? i18n("profile.selected") : "");
((AdvancedListItem) ((RipplerContainer) node).getContainer()).setSubtitle(current ? i18n("profile.selected") : "");
}
}
});
@@ -228,7 +224,7 @@ public final class LeftPaneController {
private void onProfilesLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
for (Profile profile : Settings.instance().getProfiles()) {
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
AdvancedListItem item = new AdvancedListItem(Profiles.getProfileDisplayName(profile));
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
ripplerContainer.setOnMouseClicked(e -> Settings.instance().setSelectedProfile(profile));
@@ -30,6 +30,7 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -119,17 +120,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
analyzer.getForge().ifPresent(library -> libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n"));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n"));
analyzer.getOptiFine().ifPresent(library -> libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n"));
item.setLibraries(libraries.toString());
});
@@ -236,6 +230,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
@@ -26,6 +26,8 @@ import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.function.Consumer;
@@ -54,7 +56,14 @@ public final class ModItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
modItem.setTitle(info.getFileName());
modItem.setSubtitle(info.getName() + ", " + i18n("archive.version") + ": " + info.getVersion() + ", " + i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + i18n("archive.author") + ": " + info.getAuthors());
StringBuilder message = new StringBuilder(info.getName());
if (StringUtils.isNotBlank(info.getVersion()))
message.append(", ").append(i18n("archive.version")).append(": ").append(info.getVersion());
if (StringUtils.isNotBlank(info.getGameVersion()))
message.append(", ").append(i18n("archive.game_version")).append(": ").append(info.getGameVersion());
if (StringUtils.isNotBlank(info.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(info.getAuthors());
modItem.setSubtitle(message.toString());
chkEnabled.setSelected(info.isActive());
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
info.activeProperty().set(newValue));
@@ -39,12 +39,14 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.FontComboBox;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
@@ -88,6 +90,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private Label lblUpdateSub;
@FXML
private Text lblUpdateNote;
@FXML
private JFXRadioButton chkUpdateStable;
@FXML
private JFXRadioButton chkUpdateDev;
@FXML
private JFXButton btnUpdate;
@FXML
private ScrollPane scroll;
@@ -96,7 +104,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private StackPane themeColorPickerContainer;
@FXML
private JFXCheckBox chkEnableProxy;
private JFXCheckBox chkDisableProxy;
@FXML
private JFXRadioButton chkProxyHttp;
@FXML
@@ -153,10 +161,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
proxyPane.disableProperty().bind(chkEnableProxy.selectedProperty().not());
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkEnableProxy.selectedProperty().bindBidirectional(config().hasProxyProperty());
chkDisableProxy.setSelected(!config().hasProxy());
chkDisableProxy.selectedProperty().addListener(o -> config().setHasProxy(!chkDisableProxy.isSelected()));
config().hasProxyProperty().addListener(o -> chkDisableProxy.setSelected(!config().hasProxy()));
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
@@ -212,6 +222,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
@@ -222,7 +238,32 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
lblUpdateNote.setWrappingWidth(470);
ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<UpdateChannel>() {
@Override
protected void invalidated() {
UpdateChannel updateChannel = Objects.requireNonNull(get());
chkUpdateDev.setSelected(updateChannel == UpdateChannel.DEVELOPMENT);
chkUpdateStable.setSelected(updateChannel == UpdateChannel.STABLE);
}
};
ToggleGroup updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
updateChannelGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
updateChannel.set((UpdateChannel) toggle.getUserData());
}
}));
updateChannel.bindBidirectional(ConfigHolder.config().updateChannelProperty());
// ====
// ==== Background ====
@@ -26,16 +26,17 @@ import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", null);
@@ -147,7 +148,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
sub = "saves";
break;
default:
throw new RuntimeException();
return;
}
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
}
@@ -186,7 +187,12 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void deleteVersion(Profile profile, String version) {
Controllers.confirmDialog(i18n("version.manage.remove.confirm", version), i18n("message.confirm"), () -> {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == EnumGameDirectory.VERSION_FOLDER;
boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported();
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
i18n("version.manage.remove.confirm", version);
Controllers.confirmDialog(message, i18n("message.confirm"), () -> {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.event.EventHandler;
@@ -26,27 +26,28 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
public final class VersionListItem extends StackPane {
public final class AdvancedListItem extends StackPane {
@FXML
private StackPane imageViewContainer;
@FXML
private Label lblVersionName;
private Label lblTitle;
@FXML
private Label lblGameVersion;
private Label lblSubtitle;
@FXML
private ImageView imageView;
@FXML private JFXButton btnSettings;
public VersionListItem(String versionName) {
this(versionName, "");
public AdvancedListItem(String title) {
this(title, "");
}
public VersionListItem(String versionName, String gameVersion) {
FXUtils.loadFXML(this, "/assets/fxml/version-list-item.fxml");
public AdvancedListItem(String title, String subtitle) {
FXUtils.loadFXML(this, "/assets/fxml/advanced-list-item.fxml");
lblVersionName.setText(versionName);
lblGameVersion.setText(gameVersion);
lblTitle.setText(title);
lblSubtitle.setText(subtitle);
FXUtils.limitSize(imageView, 32, 32);
}
@@ -55,12 +56,12 @@ public final class VersionListItem extends StackPane {
btnSettings.setOnMouseClicked(handler);
}
public void setVersionName(String versionName) {
lblVersionName.setText(versionName);
public void setTitle(String title) {
lblTitle.setText(title);
}
public void setGameVersion(String gameVersion) {
lblGameVersion.setText(gameVersion);
public void setSubtitle(String subtitle) {
lblSubtitle.setText(subtitle);
}
public void setImage(Image image, Rectangle2D viewport) {
@@ -50,10 +50,6 @@ public class ComponentList extends StackPane {
}
public void addChildren(Node node) {
if (node instanceof ComponentList) {
node.getProperties().put("title", ((ComponentList) node).getTitle());
node.getProperties().put("subtitle", ((ComponentList) node).getSubtitle());
}
StackPane child = new StackPane();
child.getChildren().add(new ComponentListCell(node));
if (vbox.getChildren().isEmpty())
@@ -27,6 +27,8 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
@@ -75,31 +77,47 @@ public class ComponentListCell extends StackPane {
content.getStyleClass().remove("options-list");
content.getStyleClass().add("options-sublist");
StackPane groupNode = new StackPane();
BorderPane groupNode = new BorderPane();
groupNode.getStyleClass().add("options-list-item-header");
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
StackPane.setAlignment(expandButton, Pos.CENTER_RIGHT);
VBox labelVBox = new VBox();
Label label = new Label();
label.textProperty().bind(list.titleProperty());
label.setMouseTransparent(true);
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
if (list instanceof ComponentSublist) {
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
if (leftNode != null)
labelVBox.getChildren().setAll(leftNode);
} else {
Label label = new Label();
label.textProperty().bind(list.titleProperty());
label.setMouseTransparent(true);
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
}
}
StackPane.setAlignment(labelVBox, Pos.CENTER_LEFT);
groupNode.getChildren().setAll(labelVBox, expandButton);
groupNode.setLeft(labelVBox);
HBox right = new HBox();
if (list instanceof ComponentSublist) {
Node rightNode = ((ComponentSublist) list).getHeaderRight();
if (rightNode != null)
right.getChildren().add(rightNode);
}
right.getChildren().add(expandButton);
groupNode.setRight(right);
labelVBox.setAlignment(Pos.CENTER_LEFT);
right.setAlignment(Pos.CENTER_RIGHT);
VBox container = new VBox();
container.setStyle("-fx-padding: 8 0 0 0;");
@@ -0,0 +1,41 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
@DefaultProperty("content")
public final class ComponentSublist extends ComponentList {
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
public ComponentSublist() {
super();
}
public Node getHeaderLeft() {
return headerLeft.get();
}
public ObjectProperty<Node> headerLeftProperty() {
return headerLeft;
}
public void setHeaderLeft(Node headerLeft) {
this.headerLeft.set(headerLeft);
}
public Node getHeaderRight() {
return headerRight.get();
}
public ObjectProperty<Node> headerRightProperty() {
return headerRight;
}
public void setHeaderRight(Node headerRight) {
this.headerRight.set(headerRight);
}
}
@@ -103,7 +103,7 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
}
private String getVersion(String id) {
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion<?>) it).map(RemoteVersion::getSelfVersion).orElse(null);
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion) it).map(RemoteVersion::getSelfVersion).orElse(null);
}
@Override
@@ -49,16 +49,16 @@ public final class DownloadWizardProvider implements WizardProvider {
GameBuilder builder = profile.getDependency().gameBuilder();
builder.name((String) settings.get("name"));
builder.gameVersion(((RemoteVersion<?>) settings.get("game")).getGameVersion());
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
if (settings.containsKey("forge"))
builder.version((RemoteVersion<?>) settings.get("forge"));
builder.version((RemoteVersion) settings.get("forge"));
if (settings.containsKey("liteloader"))
builder.version((RemoteVersion<?>) settings.get("liteloader"));
builder.version((RemoteVersion) settings.get("liteloader"));
if (settings.containsKey("optifine"))
builder.version((RemoteVersion<?>) settings.get("optifine"));
builder.version((RemoteVersion) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions());
}
@@ -21,7 +21,9 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
@@ -40,16 +42,14 @@ public final class InstallerWizardProvider implements WizardProvider {
private final String optiFine;
public InstallerWizardProvider(Profile profile, String gameVersion, Version version) {
this(profile, gameVersion, version, null, null, null);
}
public InstallerWizardProvider(Profile profile, String gameVersion, Version version, String forge, String liteLoader, String optiFine) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
this.forge = forge;
this.liteLoader = liteLoader;
this.optiFine = optiFine;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
forge = analyzer.getForge().map(Library::getVersion).orElse(null);
liteLoader = analyzer.getLiteLoader().map(Library::getVersion).orElse(null);
optiFine = analyzer.getOptiFine().map(Library::getVersion).orElse(null);
}
public Profile getProfile() {
@@ -85,16 +85,16 @@ public final class InstallerWizardProvider implements WizardProvider {
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
Task ret = Task.empty();
Task ret = Task.ofResult("version", () -> version);
if (settings.containsKey("forge"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("forge")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
if (settings.containsKey("liteloader"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("liteloader")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("optifine")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
return ret.then(profile.getRepository().refreshVersionsAsync());
}
@@ -74,7 +74,7 @@ public class InstallersPage extends StackPane implements WizardPage {
FXUtils.loadFXML(this, "/assets/fxml/download/installers.fxml");
String gameVersion = ((RemoteVersion<?>) controller.getSettings().get("game")).getGameVersion();
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);
@@ -103,7 +103,7 @@ public class InstallersPage extends StackPane implements WizardPage {
}
private String getVersion(String id) {
return ((RemoteVersion<?>) controller.getSettings().get(id)).getSelfVersion();
return ((RemoteVersion) controller.getSettings().get(id)).getSelfVersion();
}
@Override
@@ -27,9 +27,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.game.GameVersionList;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -48,6 +47,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private final DownloadProvider downloadProvider;
private final String libraryId;
private final String title;
private final WizardController controller;
@FXML
private JFXListView<VersionsPageItem> list;
@@ -56,6 +56,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@FXML
private StackPane failedPane;
@FXML
private StackPane emptyPane;
@FXML
private JFXCheckBox chkRelease;
@FXML
private JFXCheckBox chkSnapshot;
@@ -75,11 +77,12 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
this.gameVersion = gameVersion;
this.downloadProvider = downloadProvider;
this.libraryId = libraryId;
this.controller = controller;
this.versionList = downloadProvider.getVersionListById(libraryId);
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
if (versionList instanceof GameVersionList) {
if (versionList.hasType()) {
centrePane.getChildren().setAll(checkPane, list);
} else
centrePane.getChildren().setAll(list);
@@ -99,21 +102,19 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
}
private List<VersionsPageItem> loadVersions() {
boolean isGameVersionList = versionList instanceof GameVersionList;
return versionList.getVersions(gameVersion).stream()
.filter(it -> {
if (isGameVersionList)
switch (((GameRemoteVersionTag) it.getTag()).getType()) {
case RELEASE:
return chkRelease.isSelected();
case SNAPSHOT:
return chkSnapshot.isSelected();
default:
return chkOld.isSelected();
}
else return true;
switch (it.getVersionType()) {
case RELEASE:
return chkRelease.isSelected();
case SNAPSHOT:
return chkSnapshot.isSelected();
case OLD:
return chkOld.isSelected();
default:
return true;
}
})
.filter(Objects::nonNull)
.sorted()
.map(VersionsPageItem::new).collect(Collectors.toList());
}
@@ -126,8 +127,18 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
List<VersionsPageItem> items = loadVersions();
Platform.runLater(() -> {
list.getItems().setAll(items);
transitionHandler.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer());
if (versionList.getVersions(gameVersion).isEmpty()) {
transitionHandler.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
if (items.isEmpty()) {
chkRelease.setSelected(true);
chkSnapshot.setSelected(true);
chkOld.setSelected(true);
} else {
list.getItems().setAll(items);
}
transitionHandler.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer());
}
});
} else {
Platform.runLater(() -> {
@@ -153,4 +164,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private void onRefresh() {
refresh();
}
@FXML
private void onBack() { controller.onPrev(true); }
}
@@ -25,8 +25,9 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersionTag;
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.ui.FXUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -37,7 +38,7 @@ import java.util.Objects;
* @author huangyuhui
*/
public final class VersionsPageItem extends StackPane {
private final RemoteVersion<?> remoteVersion;
private final RemoteVersion remoteVersion;
@FXML
private Label lblSelfVersion;
@FXML
@@ -49,14 +50,14 @@ public final class VersionsPageItem extends StackPane {
@FXML
private StackPane imageViewContainer;
public VersionsPageItem(RemoteVersion<?> remoteVersion) {
public VersionsPageItem(RemoteVersion remoteVersion) {
this.remoteVersion = Objects.requireNonNull(remoteVersion);
FXUtils.loadFXML(this, "/assets/fxml/download/versions-list-item.fxml");
lblSelfVersion.setText(remoteVersion.getSelfVersion());
if (remoteVersion.getTag() instanceof GameRemoteVersionTag) {
switch (((GameRemoteVersionTag) remoteVersion.getTag()).getType()) {
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
lblGameVersion.setText(i18n("version.game.release"));
imageView.setImage(new Image("/assets/img/icon.png", 32, 32, false, true));
@@ -70,13 +71,13 @@ public final class VersionsPageItem extends StackPane {
imageView.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
}
} else if (remoteVersion.getTag() instanceof LiteLoaderRemoteVersionTag) {
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
imageView.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
lblGameVersion.setText(remoteVersion.getGameVersion());
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
// optifine has no icon.
lblGameVersion.setText(remoteVersion.getGameVersion());
} else { // forge
} else if (remoteVersion instanceof ForgeRemoteVersion) {
imageView.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
lblGameVersion.setText(remoteVersion.getGameVersion());
}
@@ -84,7 +85,7 @@ public final class VersionsPageItem extends StackPane {
leftPane.getChildren().remove(imageViewContainer);
}
public RemoteVersion<?> getRemoteVersion() {
public RemoteVersion getRemoteVersion() {
return remoteVersion;
}
}
@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.upgrade;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.tukaani.xz.XZInputStream;
class HMCLDownloadTask extends FileDownloadTask {
private RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) {
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
archiveFormat = version.getType();
}
@Override
public void execute() throws Exception {
super.execute();
try {
Path target = getFile().toPath();
switch (archiveFormat) {
case JAR:
break;
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200.newUnpacker().unpack(in, out);
}
break;
default:
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
} catch (Throwable e) {
getFile().delete();
throw e;
}
}
}
@@ -37,6 +37,7 @@ import java.util.zip.ZipFile;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.JarUtils;
/**
* A class that checks the integrity of HMCL.
@@ -123,8 +124,7 @@ public final class IntegrityChecker {
}
private static void verifySelf() throws IOException {
Path self = LocalVersion.current().orElseThrow(() -> new IOException("Failed to find myself"))
.getLocation();
Path self = JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location"));
requireVerifiedJar(self);
}
}
@@ -1,149 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.JarUtils;
import org.tukaani.xz.XZInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* A class used to manage the local HMCL repository.
*
* @author yushijinhun
*/
final class LocalRepository {
private LocalRepository() {}
private static Path localStorage = Launcher.HMCL_DIRECTORY.toPath().resolve("hmcl.jar");
/**
* Gets the current stored executable in local repository.
*/
public static Optional<LocalVersion> getStored() {
if (!Files.isRegularFile(localStorage)) {
return Optional.empty();
}
return Optional.of(localStorage)
.flatMap(JarUtils::getImplementationVersion)
.map(version -> new LocalVersion(version, localStorage));
}
private static void writeToStorage(Path source, boolean checkHeaders) throws IOException {
IntegrityChecker.requireVerifiedJar(source);
Files.createDirectories(localStorage.getParent());
if (checkHeaders) {
ExecutableHeaderHelper.copyWithoutHeader(source, localStorage);
} else {
Files.copy(source, localStorage, StandardCopyOption.REPLACE_EXISTING);
}
}
/**
* Creates a task that downloads the given version to local repository.
*/
public static FileDownloadTask downloadFromRemote(RemoteVersion version) throws IOException {
Path downloaded = Files.createTempFile("hmcl-update-", null);
return new FileDownloadTask(new URL(version.getUrl()), downloaded.toFile(), version.getIntegrityCheck()) {
@Override
public void execute() throws Exception {
super.execute();
try {
switch (version.getType()) {
case JAR:
writeToStorage(downloaded, false);
break;
case PACK_XZ:
Path unpacked = Files.createTempFile("hmcl-update-unpack-", null);
try {
try (InputStream in = new XZInputStream(Files.newInputStream(downloaded));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(unpacked))) {
Pack200.newUnpacker().unpack(in, out);
}
writeToStorage(unpacked, false);
} finally {
Files.deleteIfExists(unpacked);
}
break;
default:
throw new IllegalArgumentException("Unknown type: " + version.getType());
}
} finally {
Files.deleteIfExists(downloaded);
}
}
};
}
/**
* Copies the current HMCL executable to local repository.
*/
public static void downloadFromCurrent() {
Optional<LocalVersion> current = LocalVersion.current();
if (current.isPresent()) {
Path currentPath = current.get().getLocation();
if (!Files.isRegularFile(currentPath)) {
LOG.warning("Failed to download " + current.get() + ", it isn't a file");
return;
}
if (isSameAsLocalStorage(currentPath)) {
LOG.warning("Trying to download from self, ignored");
return;
}
LOG.info("Downloading " + current.get());
try {
writeToStorage(current.get().getLocation(), true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to download " + current.get(), e);
}
}
}
/**
* Writes the executable stored in local repository to the given location.
*/
public static void applyTo(Path target) throws IOException {
if (isSameAsLocalStorage(target)) {
throw new IOException("Cannot apply update to self");
}
LOG.info("Applying update to " + target);
IntegrityChecker.requireVerifiedJar(localStorage);
ExecutableHeaderHelper.copyWithHeader(localStorage, target);
}
private static boolean isSameAsLocalStorage(Path path) {
return path.toAbsolutePath().equals(localStorage.toAbsolutePath());
}
}
@@ -17,36 +17,13 @@
*/
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.JarUtils;
public enum UpdateChannel {
STABLE("stable"),
DEVELOPMENT("dev");
import java.nio.file.Path;
import java.util.Optional;
public final String channelName;
class LocalVersion {
public static Optional<LocalVersion> current() {
return JarUtils.thisJar().map(path -> new LocalVersion(Metadata.VERSION, path));
}
private String version;
private Path location;
public LocalVersion(String version, Path location) {
this.version = version;
this.location = location;
}
public String getVersion() {
return version;
}
public Path getLocation() {
return location;
}
@Override
public String toString() {
return "[" + version + " at " + location + "]";
UpdateChannel(String channelName) {
this.channelName = channelName;
}
}
@@ -17,33 +17,30 @@
*/
package org.jackhuang.hmcl.upgrade;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.VersionNumber.asVersion;
import java.io.IOException;
import java.util.logging.Level;
import javafx.beans.property.*;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.util.NetworkUtils;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
public final class UpdateChecker {
private UpdateChecker() {}
public static final String CHANNEL_STABLE = "stable";
public static final String CHANNEL_DEV = "dev";
private static StringProperty updateChannel = new SimpleStringProperty(CHANNEL_STABLE);
private static ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
private static BooleanBinding outdated = Bindings.createBooleanBinding(
() -> {
@@ -55,17 +52,11 @@ public final class UpdateChecker {
}
},
latestVersion);
private static ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
public static String getUpdateChannel() {
return updateChannel.get();
}
public static void setUpdateChannel(String updateChannel) {
UpdateChecker.updateChannel.set(updateChannel);
}
public static StringProperty updateChannelProperty() {
return updateChannel;
public static void init() {
ConfigHolder.config().updateChannelProperty().addListener(onInvalidating(UpdateChecker::requestCheckUpdate));
requestCheckUpdate();
}
public static RemoteVersion getLatestVersion() {
@@ -84,26 +75,61 @@ public final class UpdateChecker {
return outdated;
}
public static void checkUpdate() throws IOException {
public static boolean isCheckingUpdate() {
return checkingUpdate.get();
}
public static ReadOnlyBooleanProperty checkingUpdateProperty() {
return checkingUpdate.getReadOnlyProperty();
}
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
if (!IntegrityChecker.isSelfVerified()) {
return;
throw new IOException("Self verification failed");
}
String channel = getUpdateChannel();
String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf(
pair("version", Metadata.VERSION),
pair("channel", channel)));
pair("channel", channel.channelName)));
RemoteVersion fetched = RemoteVersion.fetch(url);
Platform.runLater(() -> {
if (channel.equals(getUpdateChannel())) {
latestVersion.set(fetched);
}
});
return RemoteVersion.fetch(url);
}
private static boolean isDevelopmentVersion(String version) {
return version.contains("@") || // eg. @develop@
version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT
}
public static void requestCheckUpdate() {
Platform.runLater(() -> {
if (isCheckingUpdate())
return;
checkingUpdate.set(true);
UpdateChannel channel = config().getUpdateChannel();
thread(() -> {
RemoteVersion result = null;
try {
result = checkUpdate(channel);
LOG.info("Latest version (" + channel + ") is " + result);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check for update", e);
}
RemoteVersion finalResult = result;
Platform.runLater(() -> {
checkingUpdate.set(false);
if (finalResult != null) {
if (channel.equals(config().getUpdateChannel())) {
latestVersion.set(finalResult);
} else {
// the channel has been changed during the period
// check update again
requestCheckUpdate();
}
}
});
});
});
}
}
@@ -17,11 +17,10 @@
*/
package org.jackhuang.hmcl.upgrade;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
import static org.jackhuang.hmcl.util.IntVersionNumber.isIntVersionNumber;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.VersionNumber.asVersion;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.IOException;
@@ -30,6 +29,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.regex.Matcher;
@@ -45,9 +45,12 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.JarUtils;
import org.jackhuang.hmcl.util.JavaVersion;
import org.jackhuang.hmcl.util.StringUtils;
import com.google.gson.Gson;
import javafx.application.Platform;
import javafx.scene.layout.Region;
@@ -58,9 +61,7 @@ public final class UpdateHandler {
* @return whether to exit
*/
public static boolean processArguments(String[] args) {
if (!isIntVersionNumber(Metadata.VERSION)) {
return false;
}
breakForceUpdateFeature();
if (isNestedApplication()) {
// updated from old versions
@@ -88,33 +89,69 @@ public final class UpdateHandler {
return true;
}
Optional<LocalVersion> local = LocalRepository.getStored();
if (local.isPresent()) {
int difference = asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION));
return false;
}
if (difference < 0) {
LocalRepository.downloadFromCurrent();
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();
} else if (difference > 0) {
Optional<LocalVersion> current = LocalVersion.current();
if (current.isPresent() && IntegrityChecker.isSelfVerified()) {
try {
requestUpdate(local.get().getLocation(), current.get().getLocation());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to update from local repository", e);
return false;
}
return true;
} else {
return false;
}
}
} else {
LocalRepository.downloadFromCurrent();
Path downloaded;
try {
downloaded = Files.createTempFile("hmcl-update-", ".jar");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create temp file", e);
return;
}
return false;
Task task = new HMCLDownloadTask(version, downloaded);
TaskExecutor executor = task.executor();
Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null);
thread(() -> {
boolean success = executor.test();
Platform.runLater(() -> dialog.fireEvent(new DialogCloseEvent()));
if (success) {
try {
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
}
requestUpdate(downloaded, getCurrentLocation());
System.exit(0);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
return;
}
} else {
Throwable e = task.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
}
});
}
private static void applyUpdate(Path target) throws IOException {
LOG.info("Applying update to " + target);
Path self = getCurrentLocation();
IntegrityChecker.requireVerifiedJar(self);
ExecutableHeaderHelper.copyWithHeader(self, target);
Optional<Path> newFilename = tryRename(target, Metadata.VERSION);
if (newFilename.isPresent()) {
LOG.info("Move " + target + " to " + newFilename.get());
try {
Files.move(target, newFilename.get());
target = newFilename.get();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to move target", e);
}
}
startJava(target);
}
private static void requestUpdate(Path updateTo, Path self) throws IOException {
@@ -122,28 +159,6 @@ public final class UpdateHandler {
startJava(updateTo, "--apply-to", self.toString());
}
private static void applyUpdate(Path target) throws IOException {
LocalRepository.applyTo(target);
Optional<String> newVersion = LocalRepository.getStored().map(LocalVersion::getVersion);
if (newVersion.isPresent()) {
Optional<Path> newFilename = tryRename(target, newVersion.get());
if (newFilename.isPresent()) {
LOG.info("Move " + target + " to " + newFilename.get());
try {
Files.move(target, newFilename.get());
target = newFilename.get();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to move target", e);
}
}
} else {
LOG.warning("Failed to find local repository");
}
startJava(target);
}
private static void startJava(Path jar, String... appArgs) throws IOException {
List<String> commandline = new ArrayList<>();
commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath());
@@ -159,49 +174,6 @@ public final class UpdateHandler {
.start();
}
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();
Task task;
try {
task = LocalRepository.downloadFromRemote(version);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create upgrade download task", e);
return;
}
TaskExecutor executor = task.executor();
Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null);
thread(() -> {
boolean success = executor.test();
Platform.runLater(() -> dialog.fireEvent(new DialogCloseEvent()));
if (success) {
try {
Optional<LocalVersion> current = LocalVersion.current();
Optional<LocalVersion> stored = LocalRepository.getStored();
if (!current.isPresent()) {
throw new IOException("Failed to find current HMCL location");
}
if (!stored.isPresent()) {
throw new IOException("Failed to find local repository, this shouldn't happen");
}
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
}
requestUpdate(stored.get().getLocation(), current.get().getLocation());
System.exit(0);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
return;
}
} else {
Throwable e = task.getLastException();
LOG.log(Level.WARNING, "Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
}
});
}
private static Optional<Path> tryRename(Path path, String newVersion) {
String filename = path.getFileName().toString();
Matcher matcher = Pattern.compile("^(?<prefix>[hH][mM][cC][lL][.-])(?<version>\\d+(?:\\.\\d+)*)(?<suffix>\\.[^.]+)$").matcher(filename);
@@ -214,6 +186,10 @@ public final class UpdateHandler {
return Optional.empty();
}
private static Path getCurrentLocation() throws IOException {
return JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location"));
}
// ==== support for old versions ===
private static void performMigration() throws IOException {
LOG.info("Migrating from old versions");
@@ -221,17 +197,7 @@ public final class UpdateHandler {
Path location = getParentApplicationLocation()
.orElseThrow(() -> new IOException("Failed to get parent application location"));
Optional<LocalVersion> local = LocalRepository.getStored();
if (!local.isPresent() ||
asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION)) < 0) {
LocalRepository.downloadFromCurrent();
}
local = LocalRepository.getStored();
if (!local.isPresent()) {
throw new IOException("Failed to find local repository");
}
requestUpdate(local.get().getLocation(), location);
requestUpdate(getCurrentLocation(), location);
}
/**
@@ -265,7 +231,7 @@ public final class UpdateHandler {
}
private static boolean isFirstLaunchAfterUpgrade() {
Optional<Path> currentPath = LocalVersion.current().map(LocalVersion::getLocation);
Optional<Path> currentPath = JarUtils.thisJar();
if (currentPath.isPresent()) {
Path updated = Launcher.HMCL_DIRECTORY.toPath().resolve("HMCL-" + Metadata.VERSION + ".jar");
if (currentPath.get().toAbsolutePath().equals(updated.toAbsolutePath())) {
@@ -274,5 +240,21 @@ public final class UpdateHandler {
}
return false;
}
private static void breakForceUpdateFeature() {
try {
Path hmclVersionJson = Launcher.HMCL_DIRECTORY.toPath().resolve("hmclver.json");
if (Files.isRegularFile(hmclVersionJson)) {
Map<?, ?> content = new Gson().fromJson(new String(Files.readAllBytes(hmclVersionJson), UTF_8), Map.class);
Object ver = content.get("ver");
if (ver instanceof String && ((String) ver).startsWith("3.")) {
Files.delete(hmclVersionJson);
LOG.info("Successfully broke the force update feature");
}
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to break the force update feature", e);
}
}
// ====
}
Binary file not shown.
@@ -1,11 +1,4 @@
/*
* Apply google Roboto font to all UI components
* #5264AE
*/
.root {
-fx-base-font-family: "微软雅黑", "Microsoft YaHei", "Roboto";
-fx-font-family: -fx-base-font-family;
}
.disabled Label {
@@ -21,10 +21,10 @@
</StackPane>
<BorderPane style="-fx-padding: 0 0 0 10;">
<top>
<Label fx:id="lblVersionName" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
<Label fx:id="lblTitle" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
</top>
<bottom>
<Label fx:id="lblGameVersion" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
<Label fx:id="lblSubtitle" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
</bottom>
</BorderPane>
</HBox>
@@ -20,4 +20,7 @@
<StackPane fx:id="failedPane">
<Label onMouseClicked="#onRefresh" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.refresh" />
</StackPane>
<StackPane fx:id="emptyPane">
<Label onMouseClicked="#onBack" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.empty" />
</StackPane>
</fx:root>
@@ -6,6 +6,7 @@
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.*?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import javafx.scene.text.Text?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
@@ -15,21 +16,28 @@
<VBox fx:id="rootPane" style="-fx-padding: 20;">
<ComponentList fx:id="settingsPane">
<BorderPane> <!-- Update -->
<left>
<ComponentSublist fx:id="updatePane" title="%update" hasSubtitle="true">
<headerLeft>
<VBox>
<Label fx:id="lblUpdate" text="%update" BorderPane.alignment="CENTER_LEFT"/>
<Label fx:id="lblUpdateSub" styleClass="subtitle-label"/>
</VBox>
</left>
<right>
</headerLeft>
<headerRight>
<JFXButton fx:id="btnUpdate" onMouseClicked="#onUpdate" styleClass="toggle-icon4">
<graphic>
<fx:include source="/assets/svg/update.fxml" />
</graphic>
</JFXButton>
</right>
</BorderPane>
</headerRight>
<VBox spacing="8">
<JFXRadioButton fx:id="chkUpdateStable" text="%update.channel.stable" />
<JFXRadioButton fx:id="chkUpdateDev" text="%update.channel.dev" />
<VBox style="-fx-padding: 10 0 0 0;">
<Text fx:id="lblUpdateNote" text="%update.note" />
</VBox>
</VBox>
</ComponentSublist>
<MultiFileItem fx:id="fileCommonLocation" title="%launcher.common_directory" directory="true" chooserTitle="%launcher.common_directory.choose" hasSubtitle="true" customText="%settings.custom" />
@@ -55,7 +63,7 @@
<ComponentList title="%settings.launcher.proxy"> <!-- proxy -->
<VBox spacing="10">
<JFXCheckBox fx:id="chkEnableProxy" text="%settings.launcher.proxy.enable" />
<JFXCheckBox fx:id="chkDisableProxy" text="%settings.launcher.proxy.disable" />
<VBox fx:id="proxyPane" style="-fx-padding: 0 0 0 30;">
<HBox>
<JFXRadioButton fx:id="chkProxyHttp" text="%settings.launcher.proxy.http" />
@@ -83,6 +83,7 @@ crash.user_fault=Your OS or Java environment may not be properly installed resul
download=Download
download.BMCL=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
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.mojang=Mojang
@@ -93,7 +94,7 @@ 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_access_denied=The configuration at "%s" is not readable or writable.\nPlease grant HMCL read and write access to this file and its parent directory.
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.
@@ -134,6 +135,7 @@ lang.default=Use system language
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
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.
@@ -293,7 +295,7 @@ settings.launcher.language=Language
settings.launcher.log_font=Log Font
settings.launcher.proxy=Proxy
settings.launcher.proxy.authentication=Proxy Authentication
settings.launcher.proxy.enable=Enable Proxy
settings.launcher.proxy.disable=Use system proxies
settings.launcher.proxy.host=Host
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=Password
@@ -311,9 +313,13 @@ settings.type=Version setting type
settings.type.special=Specialized version settings(will not affect other versions)
update=Update
update.channel.dev=Update to development version
update.channel.stable=Update to stable version
update.checking=Checking for updates
update.failed=Failed to perform upgrade
update.found=Update Available!
update.newest_version=Latest version: %s
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.tooltip=Update
@@ -330,7 +336,9 @@ version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage.redownload_assets_index=Redownload Assets Index
version.manage.remove=Delete this game
version.manage.remove.confirm=Sure to remove version %s?
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_libraries=Delete library files
version.manage.rename=Rename this game
version.manage.rename.message=Please enter the new name
@@ -14,328 +14,336 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see {http://www.gnu.org/licenses/}.
#
#author: huangyuhui
# author: huangyuhui
about.copyright=版權
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鳴謝
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默認背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (預設背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.dependency=依賴
# 由於篇幅限制,僅列出第一作者
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)
about.claim=免責聲明
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟件產生的版權問題,軟件製作方概不負責。請支持正版。
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟體產生的版權問題,軟體製作方概不負責。請支援正版。
about.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=賬戶
account=帳戶
account.character=角色
account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.create=建立帳戶
account.email=電子信箱
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的使用者名稱或密碼錯誤,或者登入次數過多被暫時禁止登入,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.invalid_token=請嘗試登出並重新輸入密碼登入
account.failed.no_character=該帳號沒有角色
account.failed.server_response_malformed=這個服務器不是合法的登錄驗證服務器
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
account.injector.manage.title=認證服務器
account.injector.http=警告:此服務器使用不安全的 HTTP 協議,您的密碼在登錄時會被明文傳輸。
account.injector.server=認證服務器
account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.failed.server_response_malformed=這個伺服器不是合法的登入驗證伺服器
account.injector.add=新增認證伺服器
account.injector.manage=管理認證伺服器
account.injector.manage.title=認證伺服器
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
account.injector.server=認證伺服器
account.injector.server_url=伺服器位址
account.injector.server_name=伺服器名稱
account.methods=登入方式
account.methods.authlib_injector=authlib-injector 登入
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
account.methods.yggdrasil=正版登入
account.missing=沒有帳戶
account.password=密碼
account.username=用戶名
account.username=使用者名稱
archive.author=作者
archive.game_version=遊戲版本
archive.version=版本
assets.download=下載資源
assets.download_all=檢查資源文件完整性
assets.download_all=驗證資源檔案完整性
button.cancel=取消
button.clear=清除
button.delete=刪除
button.edit=修改
button.edit=編輯
button.install=安裝
button.no=否
button.ok=確定
button.refresh=刷新
button.save=保存
button.refresh=重新整理
button.save=儲存
button.yes=是
color.recent=推薦
color.custom=自定義顏色
color.custom=自訂顏色
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟件崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下載失敗
download.failed.refresh=加載版本列表失敗,點擊此處重試。
download.mojang=官方
download.failed.empty=沒有可供安裝的版本,點擊此處返回。
download.failed.refresh=載入版本列表失敗,點擊此處重試。
download.mojang=官方伺服器
extension.bat=Windows 腳本
extension.mod=模組文件
extension.png=圖片文件
extension.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=當前 Java 平台缺少 DST Root CA X3 證書。\n您依然可以使用 HMCL,但將無法連接到部分站點(如使用 Let's Encrypt 證書的站點),這可能會使 HMCL 無法正常工作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
fatal.config_access_denied=HMCL 無法讀取或寫入位於 "%s" 的配置文件。\n請授予 HMCL 對該文件及其父目錄的讀寫權限。
fatal.migration_requires_manual_reboot=HMCL 即將完成升級,請重新打開 HMCL。
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們反饋該問題。
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請考慮向我們回報該問題。
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
folder.game=遊戲文件夾
folder.mod=MOD文件夾
folder.resourcepacks=資源包文件夾
folder.saves=存檔文件夾
folder.screenshots=截圖文件夾
folder.config=設定資料夾
folder.coremod=核心 MOD 模組資料夾
folder.game=遊戲資料夾
folder.mod=MOD 模組資料夾
folder.resourcepacks=資源包資料夾
folder.saves=遊戲存檔資料夾
folder.screenshots=截圖資料夾
general.prompt.empty=(無)
general.prompt.empty=(無)
input.email=用戶名必須是郵箱
input.email=「使用者名稱」必須是電子信箱格式
input.number=必須是數字
input.not_empty=必填項
input.url=必須是合法的鏈接
input.not_empty=必填
input.url=必須為有效連結
install=添加遊戲
install=新增遊戲
install.failed=安裝失敗
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
install.installer.install=安裝 %s
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫不安裝 %s,可以點擊此處安裝
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.current_game_version=目前遊戲版本
install.select=請選擇安裝方式
install.success=安裝成功
lang=繁體中文
lang=正體中文
lang.default=跟隨系統語言
launch.advice.different_platform=您的系統是 64 位的但是 Java 是 32 位的,推薦您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java9=不高於 1.13 的安裝了 Mod 的 Minecraft 不支持 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=檢測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更流暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要需要 Java 8。
launch.advice.not_enough_space=您設置的內存大小過大,由於超過了系統內存大小 %dMB,所以可能影響遊戲體驗或無法啓動遊戲,啓動器仍會嘗試啓動。
launch.advice.too_large_memory_for_32bit=您設置的內存大小過大,由於可能超過了 32 位 Java 的內存分配限制,所以可能無法啓動遊戲,請將內存調至 1024MB 或更小,啓動器仍會嘗試啓動。
launch.failed=啓動失敗
launch.failed.cannot_create_jvm=截獲到無法創建 Java 虛擬機,可能是 Java 參數有問題,可以在設置中開啓無參數模式啓動。
launch.failed.creating_process=啓動失敗,在創建新進程時發生錯誤,可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=未能解壓遊戲本地庫。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能爲啓動文件添加執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看日志文件,或聯繫他人尋求幫助。
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod Minecraft 版本 不支援 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=偵測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更順暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要 Java 8 版本。
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。啟動器仍會嘗試啟動。
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。啟動器仍會嘗試啟動。
launch.failed=啟動失敗
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
launch.failed.creating_process=啟動失敗,在建立新執行程序時發生錯誤。可能是 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 路徑。
launch.state.done=啟動完成
launch.state.logging_in=登入中
launch.state.modpack=正在下載必要檔案
launch.state.waiting_launching=等待遊戲啟動
launch.wrong_javadir=錯誤的 Java 路徑,將自動重設為預設 Java 路徑。
launcher=啓動器
launcher.background=背景地址
launcher=啟動器
launcher.background=背景位址
launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
launcher.common_directory.default=默認
launcher.common_directory.disabled=禁用
launcher.contact=聯繫我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並通過 MCBBS、貼吧、GitHub 或 Minecraft Forum 反饋 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已檢測到您的啓動器不是最新版本,請更新後再試!
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
launcher.common_directory=公共資料夾
launcher.common_directory.choose=選擇公共資料夾
launcher.common_directory.default=預設
launcher.common_directory.disabled=停用
launcher.contact=聯絡我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並透過 MCBBS、貼吧、GitHub 或 Minecraft Forum 回報 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已偵測到您的啟動器不是最新版本,請更新後重試!
launcher.update_java=請更新您的 Java
login.empty_username=你還未設置用戶名!
login.empty_username=你還未設定使用者名稱!
login.enter_password=請輸入您的密碼
logwindow.show_lines=顯示行數
logwindow.terminate_game=結束遊戲進程
logwindow.title=日誌
logwindow.terminate_game=結束遊戲執行程序
logwindow.title=記錄
main_page=主頁
main_page=首頁
message.confirm=提示
message.doing=請耐心等待
message.downloading=正在下載...
message.downloading=正在下載…
message.error=錯誤
message.info=提示
message.success=已完成
message.info=資訊
message.success=完成
message.unknown=未知
message.warning=警告
modpack=整合包
modpack.choose=選擇要安裝的遊戲整合包文件
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支持 Markdown(圖片請用網絡圖)。
modpack.enter_name=給遊戲起個你喜歡的名字
modpack.export=導出整合包
modpack.choose=選擇要安裝的遊戲整合包檔案
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支援 Markdown(圖片請上傳至網路)。
modpack.enter_name=給遊戲取個你喜歡的名字
modpack.export=匯出整合包
modpack.files.blueprints=BuildCraft 藍圖
modpack.files.config=Mod 配置文件
modpack.files.dumps=NEI 調試輸出
modpack.files.liteconfig=Mod 配置文件
modpack.files.mods=Mod
modpack.files.mods.voxelmods=VoxelMods 配置,如小地圖
modpack.files.config=Mod 模組設定檔案
modpack.files.dumps=NEI 調校輸出
modpack.files.liteconfig=Mod 模組設定檔案
modpack.files.mods=Mod 模組
modpack.files.mods.voxelmods=VoxelMods 設定,如小地圖
modpack.files.options_txt=遊戲設定
modpack.files.optionsshaders_txt=光影設定
modpack.files.resourcepacks=資源包(材質包)
modpack.files.resourcepacks=資源包 (材質包)
modpack.files.saves=遊戲存檔
modpack.files.scripts=MineTweaker 配置
modpack.files.servers_dat=多人遊戲服務器列表
modpack.install=安裝%s整合包
modpack.files.scripts=MineTweaker 設定
modpack.files.servers_dat=多人遊戲伺服器列表
modpack.install=安裝 %s 整合包
modpack.installing=正在安裝整合包
modpack.introduction=支持 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級文件,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,當前遊戲是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.introduction=支援 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級檔案,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,目前遊戲是 %s 整合包,但是提供的整合包更新檔案是 %s 整合包。
modpack.name=整合包名稱
modpack.not_a_valid_name=不是一個有效的整合包名稱
modpack.not_a_valid_name=整合包名稱無效
modpack.scan=解析整合包
modpack.task.install=導入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式不正確或操作文件失敗。
modpack.task.install=匯入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式錯誤或操作檔案失敗。
modpack.task.install.will=將會安裝整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 整合包相關文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.completion=下載 Curse 整合包相關檔案
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=該整合包不被支持。僅 HMCL、MultiMC、Curse 整合包受支持。
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=匯出整合包引導
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.initialization.exported_version=要導出的遊戲版本
modpack.wizard.step.initialization.include_launcher=包含啓動器
modpack.wizard.step.initialization.save=選擇要導出到的遊戲整合包位置
modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啓動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應當將不允許非官方途徑傳播的 Mod、材質包等納入整合包。\n整合包會保存您目前的下載源設置
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而且不應將不允許非官方途徑傳播的 Mod 模組、材質包等納入整合包。\n整合包會儲存您目前的下載來源設定
mods=模組管理
mods.add=添加模組
mods.add.failed=添加模組 %s 失敗。
mods.add.success=成功添加模組 %s。
mods.add=新增模組
mods.add.failed=新增模組 %s 失敗。
mods.add.success=成功新增模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
profile=配置
profile.default=當前目錄
profile.home=主文件夾
profile=設定
profile.default=目前目錄
profile.home=主資料夾
profile.instance_directory=遊戲路徑
profile.instance_directory.choose=選擇遊戲路徑
profile.new=新建配置
profile.new=建立設定
profile.title=遊戲目錄
profile.selected=已選中
profile.use_relative_path=若可能,遊戲目錄使用相對路徑
profile.selected=已選取
profile.use_relative_path=若可能,則在遊戲目錄使用相對路徑
selector.choose=選擇
selector.choose_file=選擇文件
selector.custom=自定義
selector.choose_file=選擇檔案
selector.custom=自訂
settings=普通設置
settings=普通設定
settings.advanced=高級設置
settings.advanced=進階設定
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
settings.advanced.game_dir.default=默認(.minecraft/)
settings.advanced.game_dir.default=預設(.minecraft/)
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=啓動器默認添加的參數(請不要重複添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=內存永久保存區域(不必填寫,MB)
settings.advanced.java_args_default=啟動器預設新增的參數(請勿重複新增):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(不必填寫,格式:MB)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.launcher_visibility.close=遊戲啓動後結束啓動器
settings.advanced.launcher_visibility.hide=遊戲啓動後隱藏啓動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啓動器並在遊戲結束後重新打開
settings.advanced.launcher_visibility.keep=保持啓動器可見
settings.advanced.launcher_visible=啓動器可見性
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啟動器並在遊戲結束後重新開啟
settings.advanced.launcher_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
settings.advanced.no_common=不使用公共路徑
settings.advanced.no_jvm_args=不添加默認的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啓動前執行命令(不必填寫,將在遊戲啓動前調用)
settings.advanced.server_ip=直入服務器 IP 地址(不必填寫,啓動遊戲後直接進入對應服務器)
settings.advanced.no_jvm_args=不新增預設的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啟動前執行命令(不必填寫,將在遊戲啟動前呼叫使用)
settings.advanced.server_ip=直接進入伺服器 IP 位址(不必填寫,啟動遊戲後直接進入對應伺服器)
settings.advanced.wrapper_launcher=前置指令(不必填寫,如 optirun)
settings.custom=自定義
settings.custom=自訂
settings.game=遊戲設置
settings.game.dimension=遊戲窗口分辨率
settings.game=遊戲設定
settings.game.dimension=遊戲介面解析度大小
settings.game.exploration=瀏覽
settings.game.fullscreen=全屏
settings.game.fullscreen=全螢幕
settings.game.game_directory=遊戲路徑(版本隔離)
settings.game.game_directory.choose=選擇遊戲路徑
settings.game.java_directory=Java 路徑
settings.game.java_directory.choose=選擇 Java 路徑
settings.game.management=管理
settings.icon=遊戲圖標
settings.icon=遊戲圖示
settings.launcher=啓動器設置
settings.launcher.common_path.tooltip=啓動器將所有遊戲資源及依賴庫文件放於此集中管理,如果遊戲文件夾內有現成的將不會使用公共庫文件
settings.launcher.download_source=下載源
settings.launcher=啟動器設定
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及依賴庫檔案放於此集中管理,如果遊戲資料夾內有現成的將不會使用公共庫檔案
settings.launcher.download_source=下載來源
settings.launcher.language=語言
settings.launcher.log_font=日誌字體
settings.launcher.log_font=記錄字體
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=身份驗證
settings.launcher.proxy.enable=启用代理
settings.launcher.proxy.disable=使用系統代理
settings.launcher.proxy.host=主機
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密碼
settings.launcher.proxy.port=端口
settings.launcher.proxy.port=埠 (Port)
settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=賬戶
settings.launcher.proxy.username=帳戶
settings.launcher.theme=主題
settings.max_memory=最大內存(MB)
settings.physical_memory=物理內存大小
settings.show_log=查看日誌
settings.max_memory=最大記憶體(MB)
settings.physical_memory=實體記憶體大小
settings.show_log=查看記錄
settings.tabs.installers=自動安裝
settings.type.global=全局版本設置(使用該設置的版本共用一套設定)
settings.type=版本設置類型
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
settings.type.global=全域版本設定(使用該設定的版本共用一套設定)
settings.type=版本設定類型
settings.type.special=單獨版本設定(不會影響到其他版本的設定)
update=啓動器更新
update=啟動器更新
update.channel.dev=更新到開發版
update.channel.stable=更新到推薦版本
update.checking=正在檢查更新
update.failed=更新失敗
update.found=發現更新
update.newest_version=最新版本爲:%s
update.latest=當前版本爲最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼板了,您可以手動粘貼網址打開頁面
update.found=發現到更新
update.newest_version=最新版本為:%s
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.latest=目前版本為最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼簿了,您可以手動複製網址打開頁面
update.tooltip=更新
version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝
version.forbidden_name=此版本名稱不受支持,請換一個名字
version.game.old=遠古版
version.game.release=穩定版
version.game.snapshot=測試版
version.launch=啓動遊戲
version.launch_script=生成啓動腳本
version.launch_script.failed=生成啓動腳本失敗
version.launch_script.save=保存啓動腳本
version.launch_script.success=啓動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源配置(assets_index.json)
version.forbidden_name=此版本名稱不受支援,請換一個名字
version.game.old=老舊版本
version.game.release=穩定版本
version.game.snapshot=測試版本
version.launch=啟動遊戲
version.launch_script=生成啟動腳本
version.launch_script.failed=生成啟動腳本失敗
version.launch_script.save=儲存啟動腳本
version.launch_script.success=啟動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源設定(assets_index.json)
version.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫文件
version.manage.rename=重命名該版本
version.manage.rename.message=請輸入要改成的名字
version.manage.rename.fail=重命名版本失敗,可能文件被佔用或者名字有特殊字符
version.settings=遊戲設置
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=< 上一步
@@ -83,6 +83,7 @@ crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩
download=下载
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下载失败
download.failed.empty=没有可供安装的版本,点击此处返回。
download.failed.refresh=加载版本列表失败,点击此处重试。
download.mojang=官方
@@ -92,13 +93,13 @@ 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,但将无法连接到部分站点(如使用 Let's Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.config_access_denied=HMCL 无法读取或写入位于 "%s" 的配置文件。\n请授予 HMCL 对该文件及其父目录的读写权限。
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请考虑向我们反馈该问题。
folder.config=配置文件夹
folder.coremod=核心MOD文件夹
folder.coremod=核心 MOD 文件夹
folder.game=游戏文件夹
folder.mod=MOD文件夹
folder.resourcepacks=资源包文件夹
@@ -132,10 +133,11 @@ install.success=安装成功
lang=简体中文
lang.default=跟随系统语言
launch.advice.different_platform=您的系统是 64 位的但是 Java 是 32 位的,推荐您安装 64 位 Java。
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
launch.advice.java9=不高于 1.13 的安装了 Mod 的 Minecraft 不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要需要 Java 8。
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅,而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要 Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小,启动器仍会尝试启动。
launch.failed=启动失败
@@ -258,7 +260,7 @@ settings.advanced.dont_check_game_completeness=不检查游戏完整性
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=启动器默认添加的参数(请不要重复添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,MB)
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,单位 MB)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
@@ -293,7 +295,7 @@ settings.launcher.language=语言
settings.launcher.log_font=日志字体
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=身份验证
settings.launcher.proxy.enable=启用代理
settings.launcher.proxy.disable=使用系统代理
settings.launcher.proxy.host=主机
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密码
@@ -311,9 +313,13 @@ settings.type=版本设置类型
settings.type.special=单独版本设置(不会影响到其他版本的设定)
update=启动器更新
update.channel.dev=更新到开发版
update.channel.stable=更新到推荐版本
update.checking=正在检查更新
update.failed=更新失败
update.found=发现更新
update.newest_version=最新版本为:%s
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
update.tooltip=更新
@@ -330,7 +336,9 @@ version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕:%s
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.remove=删除该版本
version.manage.remove.confirm=真的要删除版本 %s 吗?
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=请输入要改成的名字
@@ -37,6 +37,10 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
private OfflineAccountFactory() {
}
public OfflineAccount create(String username, UUID uuid) {
return new OfflineAccount(username, uuid);
}
@Override
public OfflineAccount create(CharacterSelector selector, String username, String password, Object additionalData) {
return new OfflineAccount(username, getUUIDFromUserName(username));
@@ -64,8 +64,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
return baseURL
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven")
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven")
.replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");
@@ -28,6 +28,8 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.ParallelTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.ExceptionalFunction;
/**
* Note: This class has no state.
@@ -63,7 +65,6 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
public Task checkGameCompletionAsync(Version version) {
return new ParallelTask(
new GameAssetDownloadTask(this, version),
new GameLoggingDownloadTask(this, version),
new GameLibrariesTask(this, version)
);
}
@@ -77,7 +78,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
}
@Override
public Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion) {
public Task installLibraryAsync(Version version, RemoteVersion libraryVersion) {
if (libraryVersion instanceof ForgeRemoteVersion)
return new ForgeInstallTask(this, version, (ForgeRemoteVersion) libraryVersion)
.then(variables -> new LibrariesUniqueTask(variables.get("version")))
@@ -97,4 +98,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
throw new IllegalArgumentException("Remote library " + libraryVersion + " is unrecognized.");
}
public ExceptionalFunction<AutoTypingMap<String>, Task, ?> installLibraryAsync(RemoteVersion libraryVersion) {
return var -> installLibraryAsync(var.get("version"), libraryVersion);
}
}
@@ -55,7 +55,6 @@ public class DefaultGameBuilder extends GameBuilder {
variables.set("version", version);
Task result = new ParallelTask(
new GameAssetDownloadTask(dependencyManager, version),
new GameLoggingDownloadTask(dependencyManager, version),
downloadGameAsync(gameVersion, version),
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version)); // using [with] because download failure here are tolerant.
@@ -67,8 +66,8 @@ public class DefaultGameBuilder extends GameBuilder {
if (toolVersions.containsKey("optifine"))
result = result.then(libraryTaskHelper(gameVersion, "optifine"));
for (RemoteVersion<?> remoteVersion : remoteVersions)
result = result.then(var -> dependencyManager.installLibraryAsync(var.get("version"), remoteVersion));
for (RemoteVersion remoteVersion : remoteVersions)
result = result.then(dependencyManager.installLibraryAsync(remoteVersion));
return result;
}).finalized((variables, isDependentsSucceeded) -> {
@@ -67,7 +67,7 @@ public interface DependencyManager {
* @param libraryVersion the remote version of being installed library.
* @return the task to install the specific library.
*/
Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion);
Task installLibraryAsync(Version version, RemoteVersion libraryVersion);
/**
* Get registered version list.
@@ -31,7 +31,7 @@ public abstract class GameBuilder {
protected String name = "";
protected String gameVersion = "";
protected final Map<String, String> toolVersions = new HashMap<>();
protected final Set<RemoteVersion<?>> remoteVersions = new HashSet<>();
protected final Set<RemoteVersion> remoteVersions = new HashSet<>();
public String getName() {
return name;
@@ -64,7 +64,7 @@ public abstract class GameBuilder {
return this;
}
public GameBuilder version(RemoteVersion<?> remoteVersion) {
public GameBuilder version(RemoteVersion remoteVersion) {
remoteVersions.add(remoteVersion);
return this;
}
@@ -0,0 +1,78 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import java.util.Optional;
public final class LibraryAnalyzer {
private final Library forge;
private final Library liteLoader;
private final Library optiFine;
public LibraryAnalyzer(Library forge, Library liteLoader, Library optiFine) {
this.forge = forge;
this.liteLoader = liteLoader;
this.optiFine = optiFine;
}
public Optional<Library> getForge() {
return Optional.ofNullable(forge);
}
public boolean hasForge() {
return forge != null;
}
public Optional<Library> getLiteLoader() {
return Optional.ofNullable(liteLoader);
}
public boolean hasLiteLoader() {
return liteLoader != null;
}
public Optional<Library> getOptiFine() {
return Optional.ofNullable(optiFine);
}
public boolean hasOptiFine() {
return optiFine != null;
}
public static LibraryAnalyzer analyze(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
for (Library library : version.getLibraries()) {
String groupId = library.getGroupId();
String artifactId = library.getArtifactId();
if (groupId.equalsIgnoreCase("net.minecraftforge") && artifactId.equalsIgnoreCase("forge"))
forge = library;
if (groupId.equalsIgnoreCase("com.mumfrey") && artifactId.equalsIgnoreCase("liteloader"))
liteLoader = library;
if ((groupId.equalsIgnoreCase("optifine") || groupId.equalsIgnoreCase("net.optifine")) && artifactId.equalsIgnoreCase("optifine"))
optiFine = library;
}
return new LibraryAnalyzer(forge, liteLoader, optiFine);
}
}
@@ -17,14 +17,12 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.CommandBuilder;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
public class MaintainTask extends TaskResult<Version> {
@@ -46,55 +44,41 @@ public class MaintainTask extends TaskResult<Version> {
}
public static Version maintain(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
List<String> args = new ArrayList<>(StringUtils.tokenize(version.getMinecraftArguments().orElse("")));
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
List<String> mcArgs = version.getMinecraftArguments().map(StringUtils::tokenize).map(ArrayList::new).orElse(null);
List<Argument> game = version.getArguments().map(Arguments::getGame).map(ArrayList::new).orElseGet(ArrayList::new);
boolean useMcArgs = mcArgs != null;
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge"))
forge = library;
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader"))
liteLoader = library;
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine"))
optiFine = library;
}
if (forge == null) {
removeTweakClass(args, "forge");
if (!libraryAnalyzer.hasForge()) {
removeTweakClass(useMcArgs, mcArgs, game, "forge");
}
// Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added.
if (liteLoader == null) {
removeTweakClass(args, "liteloader");
removeTweakClass(useMcArgs, mcArgs, game, "liteloader");
if (libraryAnalyzer.hasLiteLoader()) {
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
removeTweakClass(useMcArgs, mcArgs, game, "optifine");
if (libraryAnalyzer.hasOptiFine()) {
if (!libraryAnalyzer.hasLiteLoader() && !libraryAnalyzer.hasForge()) {
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineTweaker");
} else {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineForgeTweaker");
}
}
Version result;
if (useMcArgs) {
// Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping,
// so we regenerate the minecraftArgument without escaping.
result = version.setMinecraftArguments(new CommandBuilder().addAllWithoutParsing(mcArgs).toString());
} else {
if (!StringUtils.containsOne(args, "LiteLoaderTweaker")) {
args.add("--tweakClass");
args.add("com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
result = version.setArguments(version.getArguments().map(args -> args.withGame(game)).orElse(new Arguments(game, Collections.emptyList())));
}
if (optiFine == null) {
removeTweakClass(args, "optifine");
}
if (liteLoader == null && forge == null && optiFine != null) {
if (!StringUtils.containsOne(args, "optifine.OptiFineTweaker")) {
args.add("--tweakClass");
args.add("optifine.OptiFineTweaker");
}
}
if ((liteLoader != null || forge != null) && optiFine != null) {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
removeTweakClass(args, "optifine");
args.add("--tweakClass");
args.add("optifine.OptiFineForgeTweaker");
}
return version.setMinecraftArguments(new CommandBuilder().addAll(args).toString());
return result;
}
@Override
@@ -102,17 +86,43 @@ public class MaintainTask extends TaskResult<Version> {
return id;
}
private static void removeTweakClass(List<String> args, String target) {
for (int i = 0; i < args.size(); ++i) {
if (args.get(i).toLowerCase().contains(target)) {
if (i > 0 && args.get(i - 1).equals("--tweakClass")) {
args.remove(i - 1);
args.remove(i - 1);
i -= 2;
private static void removeTweakClass(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String target) {
if (useMcArgs) {
for (int i = 0; i + 1 < mcArgs.size(); ++i) {
String arg0Str = mcArgs.get(i);
String arg1Str = mcArgs.get(i + 1);
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
mcArgs.remove(i);
mcArgs.remove(i);
--i;
}
}
} else {
for (int i = 0; i + 1 < game.size(); ++i) {
Argument arg0 = game.get(i);
Argument arg1 = game.get(i + 1);
if (arg0 instanceof StringArgument && arg1 instanceof StringArgument) {
// We need to preserve the tokens
String arg0Str = arg0.toString();
String arg1Str = arg1.toString();
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
game.remove(i);
game.remove(i);
--i;
}
}
}
}
}
private static void addArgument(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String... args) {
if (useMcArgs) {
mcArgs.addAll(Arrays.asList(args));
} else {
for (String arg : args)
game.add(new StringArgument(arg));
}
}
public static final String ID = "version";
}
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
import org.jackhuang.hmcl.download.game.GameVersionList;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
/**
* @see <a href="http://wiki.vg">http://wiki,vg</a>
@@ -48,7 +48,7 @@ public class MojangDownloadProvider implements DownloadProvider {
case "liteloader":
return LiteLoaderVersionList.INSTANCE;
case "optifine":
return OptiFineVersionList.INSTANCE;
return OptiFineBMCLVersionList.INSTANCE;
default:
throw new IllegalArgumentException("Unrecognized version list id: " + id);
}
@@ -27,26 +27,36 @@ import java.util.Objects;
*
* @author huangyuhui
*/
public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
public class RemoteVersion implements Comparable<RemoteVersion> {
private final String gameVersion;
private final String selfVersion;
private final String url;
private final T tag;
private final Type type;
/**
* Constructor.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param tag some necessary information for Installer Task.
* @param url the installer or universal jar URL.
*/
public RemoteVersion(String gameVersion, String selfVersion, String url, T tag) {
public RemoteVersion(String gameVersion, String selfVersion, String url) {
this(gameVersion, selfVersion, url, Type.UNCATEGORIZED);
}
/**
* Constructor.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @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) {
this.gameVersion = Objects.requireNonNull(gameVersion);
this.selfVersion = Objects.requireNonNull(selfVersion);
this.url = Objects.requireNonNull(url);
this.tag = tag;
this.type = Objects.requireNonNull(type);
}
public String getGameVersion() {
@@ -57,14 +67,14 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
return selfVersion;
}
public T getTag() {
return tag;
}
public String getUrl() {
return url;
}
public Type getVersionType() {
return type;
}
@Override
public boolean equals(Object obj) {
return obj instanceof RemoteVersion && Objects.equals(selfVersion, ((RemoteVersion) obj).selfVersion);
@@ -80,13 +90,19 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
return new ToStringBuilder(this)
.append("selfVersion", selfVersion)
.append("gameVersion", gameVersion)
.append("tag", tag)
.toString();
}
@Override
public int compareTo(RemoteVersion<T> o) {
public int compareTo(RemoteVersion o) {
// newer versions are smaller than older versions
return VersionNumber.asVersion(o.selfVersion).compareTo(VersionNumber.asVersion(selfVersion));
}
public enum Type {
UNCATEGORIZED,
RELEASE,
SNAPSHOT,
OLD
}
}
@@ -26,18 +26,18 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The remote version list.
*
* @param <T> The type of {@code RemoteVersion<T>}, the type of tags.
* @param <T> The subclass of {@code RemoteVersion}, the type of RemoteVersion.
*
* @author huangyuhui
*/
public abstract class VersionList<T> {
public abstract class VersionList<T extends RemoteVersion> {
/**
* the remote version list.
* key: game version.
* values: corresponding remote versions.
*/
protected final SimpleMultimap<String, RemoteVersion<T>> versions = new SimpleMultimap<String, RemoteVersion<T>>(HashMap::new, TreeSet::new);
protected final SimpleMultimap<String, T> versions = new SimpleMultimap<String, T>(HashMap::new, TreeSet::new);
/**
* True if the version list has been loaded.
@@ -46,6 +46,8 @@ public abstract class VersionList<T> {
return !versions.isEmpty();
}
public abstract boolean hasType();
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
@@ -68,11 +70,10 @@ public abstract class VersionList<T> {
});
}
private Collection<RemoteVersion<T>> getVersionsImpl(String gameVersion) {
protected Collection<T> getVersionsImpl(String gameVersion) {
lock.readLock().lock();
try {
Collection<RemoteVersion<T>> ans = versions.get(gameVersion);
return ans.isEmpty() ? versions.values() : ans;
return versions.get(gameVersion);
} finally {
lock.readLock().unlock();
}
@@ -84,7 +85,7 @@ public abstract class VersionList<T> {
* @param gameVersion the Minecraft version that remote versions belong to
* @return the collection of specific remote versions
*/
public final Collection<RemoteVersion<T>> getVersions(String gameVersion) {
public final Collection<T> getVersions(String gameVersion) {
return Collections.unmodifiableCollection(getVersionsImpl(gameVersion));
}
@@ -95,11 +96,11 @@ public abstract class VersionList<T> {
* @param remoteVersion the version of the remote version.
* @return the specific remote version, null if it is not found.
*/
public final Optional<RemoteVersion<T>> getVersion(String gameVersion, String remoteVersion) {
public final Optional<T> getVersion(String gameVersion, String remoteVersion) {
lock.readLock().lock();
try {
RemoteVersion<T> result = null;
for (RemoteVersion<T> it : versions.get(gameVersion))
T result = null;
for (T it : versions.get(gameVersion))
if (remoteVersion.equals(it.getSelfVersion()))
result = it;
return Optional.ofNullable(result);
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.RemoteVersion;
public class ForgeRemoteVersion extends RemoteVersion<Void> {
public class ForgeRemoteVersion extends RemoteVersion {
/**
* Constructor.
*
@@ -28,6 +28,6 @@ public class ForgeRemoteVersion extends RemoteVersion<Void> {
* @param url the installer or universal jar URL.
*/
public ForgeRemoteVersion(String gameVersion, String selfVersion, String url) {
super(gameVersion, selfVersion, url, null);
super(gameVersion, selfVersion, url);
}
}
@@ -32,13 +32,18 @@ import java.util.*;
*
* @author huangyuhui
*/
public final class ForgeVersionList extends VersionList<Void> {
public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
public static final ForgeVersionList INSTANCE = new ForgeVersionList();
private ForgeVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
final GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(FORGE_LIST)));
@@ -75,8 +75,6 @@ public final class GameAssetDownloadTask extends Task {
@Override
public void execute() throws Exception {
int size = refreshTask.getResult().size();
int downloaded = 0;
for (Map.Entry<File, AssetObject> entry : refreshTask.getResult()) {
if (Thread.interrupted())
throw new InterruptedException();
@@ -88,29 +86,11 @@ public final class GameAssetDownloadTask extends Task {
Logging.LOG.log(Level.SEVERE, "Unable to create new file " + file + ", because parent directory cannot be created");
continue;
}
if (file.isDirectory())
if (file.isDirectory() || file.isFile() && file.exists())
continue;
boolean flag = true;
try {
// check the checksum of file to ensure that the file is not need to re-download.
if (file.exists()) {
String sha1 = encodeHex(digest("SHA-1", FileUtils.readBytes(file)));
if (sha1.equals(assetObject.getHash())) {
++downloaded;
Logging.LOG.finest("File $file has been downloaded successfully, skipped downloading");
updateProgress(downloaded, size);
continue;
}
}
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to get hash code of file " + file, e);
flag = !file.exists();
}
if (flag) {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
dependencies.add(task);
}
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
dependencies.add(task);
}
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.game.DownloadType;
import org.jackhuang.hmcl.game.LoggingInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
/**
* This task is to download log4j configuration file provided in minecraft.json.
*
* @author huangyuhui
*/
public final class GameLoggingDownloadTask extends Task {
private final DependencyManager dependencyManager;
private final Version version;
private final List<Task> dependencies = new LinkedList<>();
/**
* Constructor.
*
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version
*/
public GameLoggingDownloadTask(DependencyManager dependencyManager, Version version) {
this.dependencyManager = dependencyManager;
this.version = version;
setSignificance(TaskSignificance.MODERATE);
}
@Override
public Collection<Task> getDependencies() {
return dependencies;
}
@Override
public void execute() {
if (version.getLogging() == null || !version.getLogging().containsKey(DownloadType.CLIENT))
return;
LoggingInfo logging = version.getLogging().get(DownloadType.CLIENT);
File file = dependencyManager.getGameRepository().getLoggingObject(version.getId(), version.getAssetIndex().getId(), logging);
if (!file.exists())
dependencies.add(new FileDownloadTask(NetworkUtils.toURL(logging.getFile().getUrl()), file));
}
}
@@ -17,12 +17,9 @@
*/
package org.jackhuang.hmcl.download.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.Validation;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Date;
@@ -30,64 +27,45 @@ import java.util.Date;
*
* @author huangyuhui
*/
public final class GameRemoteVersion implements Validation {
@Immutable
public final class GameRemoteVersion extends RemoteVersion {
@SerializedName("id")
private final String gameVersion;
@SerializedName("time")
private final ReleaseType type;
private final Date time;
@SerializedName("releaseTime")
private final Date releaseTime;
@SerializedName("type")
private final ReleaseType type;
@SerializedName("url")
private final String url;
public GameRemoteVersion() {
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
}
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
}
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
this.gameVersion = gameVersion;
this.time = time;
this.releaseTime = releaseTime;
public GameRemoteVersion(String gameVersion, String selfVersion, String url, ReleaseType type, Date time) {
super(gameVersion, selfVersion, url, getReleaseType(type));
this.type = type;
this.url = url;
}
public String getGameVersion() {
return gameVersion;
this.time = time;
}
public Date getTime() {
return time;
}
public Date getReleaseTime() {
return releaseTime;
}
public ReleaseType getType() {
return type;
}
public String getUrl() {
return url;
}
@Override
public void validate() throws JsonParseException {
if (StringUtils.isBlank(gameVersion))
throw new JsonParseException("GameRemoteVersion id cannot be blank");
if (StringUtils.isBlank(url))
throw new JsonParseException("GameRemoteVersion url cannot be blank");
public int compareTo(RemoteVersion o) {
if (!(o instanceof GameRemoteVersion))
return 0;
return ((GameRemoteVersion) o).getTime().compareTo(getTime());
}
private static Type getReleaseType(ReleaseType type) {
if (type == null) return Type.UNCATEGORIZED;
switch (type) {
case RELEASE:
return Type.RELEASE;
case SNAPSHOT:
return Type.SNAPSHOT;
case UNKNOWN:
return Type.UNCATEGORIZED;
default:
return Type.OLD;
}
}
}
@@ -0,0 +1,93 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.Validation;
import java.util.Date;
/**
*
* @author huangyuhui
*/
public final class GameRemoteVersionInfo implements Validation {
@SerializedName("id")
private final String gameVersion;
@SerializedName("time")
private final Date time;
@SerializedName("releaseTime")
private final Date releaseTime;
@SerializedName("type")
private final ReleaseType type;
@SerializedName("url")
private final String url;
public GameRemoteVersionInfo() {
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
}
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
}
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
this.gameVersion = gameVersion;
this.time = time;
this.releaseTime = releaseTime;
this.type = type;
this.url = url;
}
public String getGameVersion() {
return gameVersion;
}
public Date getTime() {
return time;
}
public Date getReleaseTime() {
return releaseTime;
}
public ReleaseType getType() {
return type;
}
public String getUrl() {
return url;
}
@Override
public void validate() throws JsonParseException {
if (StringUtils.isBlank(gameVersion))
throw new JsonParseException("GameRemoteVersion id cannot be blank");
if (StringUtils.isBlank(url))
throw new JsonParseException("GameRemoteVersion url cannot be blank");
}
}
@@ -1,55 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Date;
/**
*
* @author huangyuhui
*/
@Immutable
public final class GameRemoteVersionTag {
private final ReleaseType type;
private final Date time;
/**
* No-arg constructor for Gson.
*/
@SuppressWarnings("unused")
public GameRemoteVersionTag() {
this(ReleaseType.UNKNOWN, new Date());
}
public GameRemoteVersionTag(ReleaseType type, Date time) {
this.type = type;
this.time = time;
}
public Date getTime() {
return time;
}
public ReleaseType getType() {
return type;
}
}
@@ -31,7 +31,7 @@ import java.util.List;
public final class GameRemoteVersions {
@SerializedName("versions")
private final List<GameRemoteVersion> versions;
private final List<GameRemoteVersionInfo> versions;
@SerializedName("latest")
private final GameRemoteLatestVersions latest;
@@ -44,7 +44,7 @@ public final class GameRemoteVersions {
this(Collections.emptyList(), null);
}
public GameRemoteVersions(List<GameRemoteVersion> versions, GameRemoteLatestVersions latest) {
public GameRemoteVersions(List<GameRemoteVersionInfo> versions, GameRemoteLatestVersions latest) {
this.versions = versions;
this.latest = latest;
}
@@ -53,7 +53,7 @@ public final class GameRemoteVersions {
return latest;
}
public List<GameRemoteVersion> getVersions() {
public List<GameRemoteVersionInfo> getVersions() {
return versions;
}
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Collection;
import java.util.Collections;
@@ -32,13 +33,28 @@ import java.util.Collections;
*
* @author huangyuhui
*/
public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
public final class GameVersionList extends VersionList<GameRemoteVersion> {
public static final GameVersionList INSTANCE = new GameVersionList();
private GameVersionList() {
}
@Override
public boolean hasType() {
return true;
}
@Override
protected Collection<GameRemoteVersion> getVersionsImpl(String gameVersion) {
lock.readLock().lock();
try {
return StringUtils.isBlank(gameVersion) ? versions.values() : versions.get(gameVersion);
} finally {
lock.readLock().unlock();
}
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.getVersionListURL()));
@@ -56,12 +72,12 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
versions.clear();
GameRemoteVersions root = Constants.GSON.fromJson(task.getResult(), GameRemoteVersions.class);
for (GameRemoteVersion remoteVersion : root.getVersions()) {
versions.put(remoteVersion.getGameVersion(), new RemoteVersionGame(
for (GameRemoteVersionInfo remoteVersion : root.getVersions()) {
versions.put(remoteVersion.getGameVersion(), new GameRemoteVersion(
remoteVersion.getGameVersion(),
remoteVersion.getGameVersion(),
remoteVersion.getUrl(),
new GameRemoteVersionTag(remoteVersion.getType(), remoteVersion.getReleaseTime()))
remoteVersion.getType(), remoteVersion.getReleaseTime())
);
}
} finally {
@@ -70,15 +86,4 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
}
};
}
private static class RemoteVersionGame extends RemoteVersion<GameRemoteVersionTag> {
public RemoteVersionGame(String gameVersion, String selfVersion, String url, GameRemoteVersionTag tag) {
super(gameVersion, selfVersion, url, tag);
}
@Override
public int compareTo(RemoteVersion<GameRemoteVersionTag> o) {
return o.getTag().getTime().compareTo(getTag().getTime());
}
}
}
@@ -56,11 +56,11 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
String serialized = Constants.GSON.toJson(library);
if (multimap.containsKey(id)) {
boolean hasEqualRule = false;
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.
hasEqualRule = true;
boolean flag = true;
if (number.compareTo(otherNumber) > 0) { // if this library is newer
multimap.removeValue(otherLibrary);
multimap.put(id, library);
@@ -76,12 +76,20 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
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 (!hasEqualRule) {
if (!duplicate) {
multimap.put(id, library);
}
} else {
@@ -12,7 +12,10 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
@@ -23,15 +26,15 @@ import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public final class LibraryDownloadTask extends Task {
private final FileDownloadTask xzTask;
private final FileDownloadTask task;
private final LibraryDownloadTaskHelper helperTask;
private final File jar;
private final File xzFile;
private final Library library;
private boolean downloaded = false;
private boolean xz;
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
setSignificance(TaskSignificance.MODERATE);
if (library.is("net.minecraftforge", "forge"))
library = library.setClassifier("universal");
@@ -42,20 +45,12 @@ public final class LibraryDownloadTask extends Task {
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
xzTask = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"),
xzFile, null, 1);
xzTask.setSignificance(TaskSignificance.MINOR);
setSignificance(TaskSignificance.MODERATE);
task = new FileDownloadTask(NetworkUtils.toURL(url),
file,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
helperTask = new LibraryDownloadTaskHelper(file, url);
}
@Override
public Collection<? extends Task> getDependents() {
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.singleton(task);
return Collections.singleton(helperTask);
}
@Override
@@ -66,32 +61,75 @@ public final class LibraryDownloadTask extends Task {
@Override
public void execute() throws Exception {
if (!isDependentsSucceeded()) {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = library.getChecksums() != null ? xzTask.getLastException() : task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
if (helperTask.task == null) {
// NetworkUtils.URLExists failed.
throw new LibraryDownloadException(library, helperTask.getLastException());
} else {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = helperTask.task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
}
} else {
if (xz) unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums())) {
jar.delete();
throw new IOException("Checksum failed for " + library);
}
}
}
private class LibraryDownloadTaskHelper extends Task {
private FileDownloadTask task;
private final File file;
private final String url;
public LibraryDownloadTaskHelper(File file, String url) {
this.file = file;
this.url = url;
setName(library.getName());
}
if (library.getChecksums() != null) {
unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums()))
throw new IOException("Checksum failed for " + library);
@Override
public boolean isRelyingOnDependencies() {
return true;
}
@Override
public Collection<? extends Task> getDependencies() {
return Collections.singleton(task);
}
@Override
public void execute() throws Exception {
URL packXz = NetworkUtils.toURL(url + ".pack.xz");
if (NetworkUtils.URLExists(packXz)) {
task = new FileDownloadTask(packXz, xzFile, null);
xz = true;
} else {
task = new FileDownloadTask(NetworkUtils.toURL(url),
file,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
xz = false;
}
}
}
private static boolean checksumValid(File libPath, List<String> checksums) {
try {
if ((checksums == null) || (checksums.isEmpty())) {
if (checksums == null || checksums.isEmpty()) {
return true;
}
byte[] fileData = FileUtils.readBytes(libPath);
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
if ((!valid) && (libPath.getName().endsWith(".jar"))) {
if (!valid && libPath.getName().endsWith(".jar")) {
valid = validateJar(fileData, checksums);
}
return validateJar(fileData, checksums);
return valid;
} catch (IOException e) {
e.printStackTrace();
}
@@ -104,7 +142,7 @@ public final class LibraryDownloadTask extends Task {
JarInputStream jar = new JarInputStream(new ByteArrayInputStream(data));
JarEntry entry = jar.getNextJarEntry();
while (entry != null) {
byte[] eData = IOUtils.readFullyAsByteArray(jar);
byte[] eData = IOUtils.readFullyWithoutClosing(jar);
if (entry.getName().equals("checksums.sha1")) {
hashes = new String(eData, Charset.forName("UTF-8")).split("\n");
}
@@ -115,8 +153,8 @@ public final class LibraryDownloadTask extends Task {
}
jar.close();
if (hashes != null) {
boolean passed = !checksums.contains(files.get("checksums.sha1"));
if (passed) {
boolean failed = !checksums.contains(files.get("checksums.sha1"));
if (!failed) {
for (String hash : hashes) {
if ((!hash.trim().equals("")) && (hash.contains(" "))) {
String[] e = hash.split(" ");
@@ -125,17 +163,17 @@ public final class LibraryDownloadTask extends Task {
String checksum = files.get(target);
if ((!files.containsKey(target)) || (checksum == null)) {
Logging.LOG.warning(" " + target + " : missing");
passed = false;
failed = true;
break;
} else if (!checksum.equals(validChecksum)) {
Logging.LOG.warning(" " + target + " : failed (" + checksum + ", " + validChecksum + ")");
passed = false;
failed = true;
break;
}
}
}
}
return passed;
return !failed;
}
return false;
}
@@ -62,7 +62,7 @@ public final class VersionJsonDownloadTask extends Task {
@Override
public void execute() {
RemoteVersion<?> remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
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), ID));
@@ -39,13 +39,18 @@ import java.util.Optional;
*
* @author huangyuhui
*/
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersion> {
public static final LiteLoaderBMCLVersionList INSTANCE = new LiteLoaderBMCLVersionList();
private LiteLoaderBMCLVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Lang;
@@ -74,18 +71,12 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()))
);
Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Collections.singleton(library)));
String mcArg = version.getMinecraftArguments().orElse("");
if (mcArg.contains("--tweakClass optifine.OptiFineTweaker"))
mcArg = mcArg.replace("--tweakClass optifine.OptiFineTweaker", "");
Version tempVersion = version.setLibraries(Lang.merge(remote.getLibraries(), Collections.singleton(library)));
setResult(version
.setMainClass("net.minecraft.launchwrapper.Launch")
.setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries()))
.setLogging(Collections.emptyMap())
.setMinecraftArguments(mcArg + " --tweakClass " + remote.getTag().getTweakClass())
//.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass()))
);
dependencies.add(new GameLibrariesTask(dependencyManager, tempVersion));
@@ -22,7 +22,9 @@ import org.jackhuang.hmcl.game.Library;
import java.util.Collection;
public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersionTag> {
public class LiteLoaderRemoteVersion extends RemoteVersion {
private final String tweakClass;
private final Collection<Library> libraries;
/**
* Constructor.
*
@@ -31,6 +33,18 @@ public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersi
* @param url the installer or universal jar URL.
*/
LiteLoaderRemoteVersion(String gameVersion, String selfVersion, String url, String tweakClass, Collection<Library> libraries) {
super(gameVersion, selfVersion, url, new LiteLoaderRemoteVersionTag(tweakClass, libraries));
super(gameVersion, selfVersion, url);
this.tweakClass = tweakClass;
this.libraries = libraries;
}
public Collection<Library> getLibraries() {
return libraries;
}
public String getTweakClass() {
return tweakClass;
}
}
@@ -1,54 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.game.Library;
import java.util.Collection;
import java.util.Collections;
/**
*
* @author huangyuhui
*/
public final class LiteLoaderRemoteVersionTag {
private final String tweakClass;
private final Collection<Library> libraries;
/**
* No-arg constructor for Gson.
*/
@SuppressWarnings("unused")
public LiteLoaderRemoteVersionTag() {
this("", Collections.emptySet());
}
public LiteLoaderRemoteVersionTag(String tweakClass, Collection<Library> libraries) {
this.tweakClass = tweakClass;
this.libraries = libraries;
}
public Collection<Library> getLibraries() {
return libraries;
}
public String getTweakClass() {
return tweakClass;
}
}
@@ -39,13 +39,18 @@ import java.util.Optional;
*
* @author huangyuhui
*/
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersion> {
public static final LiteLoaderVersionList INSTANCE = new LiteLoaderVersionList();
private LiteLoaderVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
@@ -33,13 +33,18 @@ import java.util.*;
*
* @author huangyuhui
*/
public final class OptiFineBMCLVersionList extends VersionList<Void> {
public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVersion> {
public static final OptiFineBMCLVersionList INSTANCE = new OptiFineBMCLVersionList();
private OptiFineBMCLVersionList() {
}
@Override
public boolean hasType() {
return true;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("http://bmclapi2.bangbang93.com/optifine/versionlist"));
@@ -57,16 +62,16 @@ public final class OptiFineBMCLVersionList extends VersionList<Void> {
}.getType());
for (OptiFineVersion element : root) {
String version = element.getType() + "_" + element.getPatch();
if (element.getType() == null || "pre".equals(element.getPatch()))
continue;
String mirror = "http://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
if (!duplicates.add(mirror))
continue;
boolean isPre = element.getPatch() != null && (element.getPatch().equals("pre") || element.getPatch().startsWith("alpha"));
if (StringUtils.isBlank(element.getGameVersion()))
continue;
VersionNumber.parseVersion(element.getGameVersion())
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror)));
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre)));
}
}
};
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Lang;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -70,46 +69,24 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
@Override
public void execute() {
String remoteVersion = remote.getGameVersion() + "-" + remote.getSelfVersion();
String remoteVersion = remote.getGameVersion() + "_" + remote.getSelfVersion();
Library library = new Library(
"net.optifine", "optifine", remoteVersion, null, null,
"optifine", "OptiFine", remoteVersion, null, null,
new LibrariesDownloadInfo(new LibraryDownloadInfo(
"net/optifine/optifine/" + remoteVersion + "/optifine-" + remoteVersion + ".jar",
"optifine/OptiFine/" + remoteVersion + "/OptiFine-" + remoteVersion + ".jar",
remote.getUrl()))
);
List<Library> libraries = new LinkedList<>();
libraries.add(library);
boolean hasFMLTweaker = false;
if (version.getMinecraftArguments().isPresent() && version.getMinecraftArguments().get().contains("FMLTweaker"))
hasFMLTweaker = true;
if (version.getArguments().isPresent()) {
List<Argument> game = version.getArguments().get().getGame();
if (game.stream().anyMatch(arg -> arg.toString(Collections.emptyMap(), Collections.emptyMap()).contains("FMLTweaker")))
hasFMLTweaker = true;
}
/*Arguments arguments = Lang.get(version.getArguments());
if (!hasFMLTweaker)
arguments = Arguments.addGameArguments(arguments, "--tweakClass", "optifine.OptiFineTweaker");
*/
String minecraftArguments = version.getMinecraftArguments().orElse("");
if (!hasFMLTweaker)
minecraftArguments = minecraftArguments + " --tweakClass optifine.OptiFineTweaker";
else
minecraftArguments = minecraftArguments + " --tweakClass optifine.OptiFineForgeTweaker";
if (version.getMainClass() == null || !version.getMainClass().startsWith("net.minecraft.launchwrapper."))
libraries.add(0, new Library("net.minecraft", "launchwrapper", "1.12"));
setResult(version
.setLibraries(Lang.merge(version.getLibraries(), libraries))
.setMainClass("net.minecraft.launchwrapper.Launch")
.setMinecraftArguments(minecraftArguments)
//.setArguments(arguments)
.setLibraries(Lang.merge(version.getLibraries(), libraries))
.setMainClass("net.minecraft.launchwrapper.Launch")
);
dependencies.add(new GameLibrariesTask(dependencyManager, version.setLibraries(libraries)));
@@ -21,11 +21,11 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import java.util.function.Supplier;
public class OptiFineRemoteVersion extends RemoteVersion<Void> {
public class OptiFineRemoteVersion extends RemoteVersion {
private final Supplier<String> url;
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url) {
super(gameVersion, selfVersion, "", null);
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url, boolean snapshot) {
super(gameVersion, selfVersion, "", snapshot ? Type.SNAPSHOT : Type.RELEASE);
this.url = url;
}
@@ -1,121 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.optifine;
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.Lang;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author huangyuhui
*/
public final class OptiFineVersionList extends VersionList<Void> {
private static final Pattern PATTERN = Pattern.compile("OptiFine (.*?) ");
private static final Pattern LINK_PATTERN = Pattern.compile("\"downloadx\\?f=OptiFine(.*)\"");
public static final OptiFineVersionList INSTANCE = new OptiFineVersionList();
private OptiFineVersionList() {
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("https://optifine.net/downloads"));
return new Task() {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(task);
}
@Override
public void execute() throws Exception {
lock.writeLock().lock();
try {
versions.clear();
String html = task.getResult().replace("&nbsp;", " ").replace("&gt;", ">").replace("&lt;", "<").replace("<br>", "<br />");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder db = factory.newDocumentBuilder();
Document doc = db.parse(new ByteArrayInputStream(html.getBytes("UTF-8")));
Element r = doc.getDocumentElement();
NodeList tables = r.getElementsByTagName("table");
for (int i = 0; i < tables.getLength(); i++) {
Element e = (Element) tables.item(i);
if ("downloadTable".equals(e.getAttribute("class"))) {
NodeList tr = e.getElementsByTagName("tr");
for (int k = 0; k < tr.getLength(); k++) {
NodeList downloadLine = ((Element) tr.item(k)).getElementsByTagName("td");
String url = null, version = null, gameVersion = null;
for (int j = 0; j < downloadLine.getLength(); j++) {
Element td = (Element) downloadLine.item(j);
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineMirror"))
url = ((Element) td.getElementsByTagName("a").item(0)).getAttribute("href");
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile"))
version = td.getTextContent();
}
if (version == null || url == null)
continue;
Matcher matcher = PATTERN.matcher(version);
while (matcher.find())
gameVersion = matcher.group(1);
if (gameVersion == null)
continue;
String finalURL = url;
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, Lang.hideException(() -> getLink(finalURL))));
}
}
}
} finally {
lock.writeLock().unlock();
}
}
};
}
private static String getLink(String url) throws IOException {
String result = null;
String content = NetworkUtils.doGet(NetworkUtils.toURL(url));
Matcher m = LINK_PATTERN.matcher(content);
while (m.find())
result = m.group(1);
if (result == null)
throw new IllegalStateException("Cannot find version in " + content);
return "https://optifine.net/downloadx?f=OptiFine" + result;
}
}
@@ -45,12 +45,4 @@ public class ProcessStoppedEvent extends Event {
public ManagedProcess getProcess() {
return process;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("process", process)
.toString();
}
}
@@ -50,10 +50,18 @@ public final class Arguments {
return game == null ? Collections.emptyList() : Collections.unmodifiableList(game);
}
public Arguments withGame(List<Argument> game) {
return new Arguments(game, jvm);
}
public List<Argument> getJvm() {
return jvm == null ? Collections.emptyList() : Collections.unmodifiableList(jvm);
}
public Arguments withJvm(List<Argument> jvm) {
return new Arguments(game, jvm);
}
public Arguments addGameArguments(String... gameArguments) {
return addGameArguments(Arrays.asList(gameArguments));
}
@@ -82,7 +90,7 @@ public final class Arguments {
}
public static List<String> parseStringArguments(List<String> arguments, Map<String, String> keys) {
return arguments.stream().map(str -> keys.getOrDefault(str, str)).collect(Collectors.toList());
return arguments.stream().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) {
@@ -32,7 +32,7 @@ public class ClassicVersion extends Version {
super(true, "Classic", "${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);
null, null, null, ReleaseType.UNKNOWN, new Date(), new Date(), 0, false);
}
private static class ClassicLibrary extends Library {
@@ -160,14 +160,18 @@ public class DefaultGameRepository implements GameRepository {
if (!file.renameTo(removedFile))
return false;
versions.remove(id);
if (FileUtils.isMovingToTrashSupported()) {
return FileUtils.moveToTrash(removedFile);
}
// remove json files first to ensure HMCL will not recognize this folder as a valid version.
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
jsons.forEach(f -> {
if (!f.delete())
Logging.LOG.warning("Unable to delete file " + f);
});
versions.remove(id);
// remove the version from version list regardless of whether the directory was removed successfully or not.
try {
FileUtils.deleteDirectory(removedFile);
@@ -223,6 +223,7 @@ public class Library implements Comparable<Library> {
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()));
@@ -65,7 +65,7 @@ public class RuledArgument implements Argument {
@Override
public List<String> toString(Map<String, String> keys, Map<String, Boolean> features) {
if (CompatibilityRule.appliesToCurrentEnvironment(rules) && value != null)
if (CompatibilityRule.appliesToCurrentEnvironment(rules, features) && value != null)
return value.stream()
.map(StringArgument::new)
.map(str -> str.toString(keys, features).get(0))
@@ -54,4 +54,8 @@ public final class StringArgument implements Argument {
return Collections.singletonList(res);
}
@Override
public String toString() {
return argument;
}
}
@@ -46,10 +46,11 @@ public class Version implements Comparable<Version>, Validation {
private final Date time;
private final Date releaseTime;
private final int minimumLauncherVersion;
private final boolean hidden;
private transient final boolean resolved;
public Version(boolean resolved, String id, 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, int minimumLauncherVersion) {
public Version(boolean resolved, String id, 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, int minimumLauncherVersion, boolean hidden) {
this.resolved = resolved;
this.id = id;
this.minecraftArguments = minecraftArguments;
@@ -67,6 +68,7 @@ public class Version implements Comparable<Version>, Validation {
this.time = time == null ? new Date() : (Date) time.clone();
this.releaseTime = releaseTime == null ? new Date() : (Date) releaseTime.clone();
this.minimumLauncherVersion = minimumLauncherVersion;
this.hidden = hidden;
}
public Optional<String> getMinecraftArguments() {
@@ -109,6 +111,10 @@ public class Version implements Comparable<Version>, Validation {
return minimumLauncherVersion;
}
public boolean isHidden() {
return hidden;
}
public Map<DownloadType, LoggingInfo> getLogging() {
return logging == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(logging);
}
@@ -139,10 +145,6 @@ public class Version implements Comparable<Version>, Validation {
return CompatibilityRule.appliesToCurrentEnvironment(compatibilityRules);
}
public boolean install(String id) {
return false;
}
/**
* Resolve given version
*/
@@ -151,8 +153,9 @@ public class Version implements Comparable<Version>, Validation {
}
protected Version resolve(VersionProvider provider, Set<String> resolvedSoFar) throws VersionNotFoundException {
if (inheritsFrom == null)
return this;
if (inheritsFrom == null) {
return this.jar == null ? this.setJar(id) : this;
}
// To maximize the compatibility.
if (!resolvedSoFar.add(id)) {
@@ -179,43 +182,44 @@ public class Version implements Comparable<Version>, Validation {
type,
time,
releaseTime,
Math.max(minimumLauncherVersion, parent.minimumLauncherVersion));
Math.max(minimumLauncherVersion, parent.minimumLauncherVersion),
hidden);
}
private Version setResolved() {
return new Version(true, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(true, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setId(String id) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setMinecraftArguments(String minecraftArguments) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setArguments(Arguments arguments) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setMainClass(String mainClass) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setInheritsFrom(String inheritsFrom) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setJar(String jar) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setLibraries(List<Library> libraries) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
public Version setLogging(Map<DownloadType, LoggingInfo> logging) {
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion);
return new Version(resolved, id, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden);
}
@Override
@@ -47,7 +47,7 @@ public class DefaultLauncher extends Launcher {
super(repository, versionId, authInfo, options, listener, daemon);
}
private CommandBuilder generateCommandLine(File nativeFolder, boolean enableLoggingInfo) throws IOException {
private CommandBuilder generateCommandLine(File nativeFolder) throws IOException {
CommandBuilder res = new CommandBuilder();
// Executable
@@ -70,16 +70,6 @@ public class DefaultLauncher extends Launcher {
res.add("-Xdock:icon=" + repository.getAssetObject(version.getId(), version.getAssetIndex().getId(), "icons/minecraft.icns").getAbsolutePath());
}
Map<DownloadType, LoggingInfo> logging = version.getLogging();
if (logging != null && enableLoggingInfo) {
LoggingInfo loggingInfo = logging.get(DownloadType.CLIENT);
if (loggingInfo != null) {
File loggingFile = repository.getLoggingObject(version.getId(), version.getAssetIndex().getId(), loggingInfo);
if (loggingFile.exists())
res.add(loggingInfo.getArgument().replace("${path}", loggingFile.getAbsolutePath()));
}
}
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
res.add("-Duser.home=" + options.getGameDir().getParent());
@@ -153,14 +143,6 @@ public class DefaultLauncher extends Launcher {
res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(LinkedList::new), configuration));
// Optional Minecraft arguments
if (options.getHeight() != null && options.getHeight() != 0 && options.getWidth() != null && options.getWidth() != 0) {
res.add("--height");
res.add(options.getHeight().toString());
res.add("--width");
res.add(options.getWidth().toString());
}
if (StringUtils.isNotBlank(options.getServerIp())) {
String[] args = options.getServerIp().split(":");
res.add("--server");
@@ -249,7 +231,9 @@ public class DefaultLauncher extends Launcher {
pair("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
pair("${user_type}", "mojang"),
pair("${assets_index_name}", version.getAssetIndex().getId()),
pair("${user_properties}", authInfo.getUserProperties())
pair("${user_properties}", authInfo.getUserProperties()),
pair("${resolution_width}", options.getWidth().toString()),
pair("${resolution_height}", options.getHeight().toString())
);
}
@@ -258,7 +242,7 @@ public class DefaultLauncher extends Launcher {
File nativeFolder = Files.createTempDirectory("minecraft").toFile();
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = generateCommandLine(nativeFolder, true).asList();
List<String> rawCommandLine = generateCommandLine(nativeFolder).asList();
decompressNatives(nativeFolder);
@@ -310,7 +294,7 @@ public class DefaultLauncher extends Launcher {
writer.write(options.getPreLaunchCommand());
writer.newLine();
}
writer.write(generateCommandLine(nativeFolder, false).toString());
writer.write(generateCommandLine(nativeFolder).toString());
}
if (!scriptFile.setExecutable(true))
throw new PermissionException();
@@ -321,36 +305,6 @@ public class DefaultLauncher extends Launcher {
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
boolean enablesLoggingInfo = version.getLogging() != null && version.getLogging().containsKey(DownloadType.CLIENT)
&& !"net.minecraft.launchwrapper.Launch".equals(version.getMainClass());
if (enablesLoggingInfo)
startMonitorsWithLoggingInfo(managedProcess, processListener, isDaemon);
else
startMonitorsWithoutLoggingInfo(managedProcess, processListener, isDaemon);
}
private void startMonitorsWithLoggingInfo(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
processListener.setProcess(managedProcess);
Log4jHandler logHandler = new Log4jHandler((line, level) -> {
processListener.onLog(line, level);
managedProcess.addLine(line);
});
logHandler.start();
managedProcess.addRelatedThread(logHandler);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), logHandler::newLine), "stdout-pump", isDaemon);
managedProcess.addRelatedThread(stdout);
Thread stderr = Lang.thread(new StreamPump(managedProcess.getProcess().getErrorStream(), it -> {
processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Log4jLevel.ERROR);
managedProcess.addLine(it);
}), "stderr-pump", isDaemon);
managedProcess.addRelatedThread(stderr);
managedProcess.addRelatedThread(Lang.thread(new ExitWaiter(managedProcess, Arrays.asList(stdout, stderr), (exitCode, exitType) -> {
logHandler.onStopped();
processListener.onExit(exitCode, exitType);
}), "exit-waiter", isDaemon));
}
private void startMonitorsWithoutLoggingInfo(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
processListener.setProcess(managedProcess);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Optional.ofNullable(Log4jLevel.guessLevel(it)).orElse(Log4jLevel.INFO));
@@ -1,180 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.launch;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.logging.Level;
/**
* This class is to parse log4j classic XML layout logging,
* since only vanilla Minecraft will enable this layout.
*
* Also supports plain logs.
*
* @author huangyuhui
*/
final class Log4jHandler extends Thread {
private final XMLReader reader;
private final BiConsumer<String, Log4jLevel> callback;
private final PipedOutputStream outputStream = new PipedOutputStream();
private final PipedInputStream inputStream = Lang.invoke(() -> new PipedInputStream(outputStream));
private final AtomicBoolean interrupted = new AtomicBoolean(false);
private final List<String> logs = new LinkedList<>();
private boolean broken = false;
public Log4jHandler(BiConsumer<String, Log4jLevel> callback) {
this.callback = callback;
newLine("<output>");
reader = Lang.invoke((ExceptionalSupplier<XMLReader, SAXException>) XMLReaderFactory::createXMLReader);
reader.setContentHandler(new Log4jHandlerImpl());
}
@Override
public void run() {
setName("log4j-handler");
try {
InputSource inputSource = new InputSource(inputStream);
reader.parse(inputSource);
} catch (InterruptedIOException e) {
// Game has been interrupted.
interrupted.set(true);
} catch (SAXException | IOException e) {
Logging.LOG.log(Level.WARNING, "An error occurred when reading console lines", e);
}
}
public void onStopped() {
if (interrupted.get())
return;
Lang.invoke(() -> Schedulers.newThread().schedule(() -> {
if (!interrupted.get()) {
newLine("</output>").get();
outputStream.close();
join();
}
}).get());
}
public Future<?> newLine(String log) {
return Schedulers.computation().schedule(() -> {
try {
// Prevent from namespace parsing
String line = log.replace("log4j:", "log4j_");
logs.add(line);
if (broken)
System.out.println(line);
byte[] bytes = (line + OperatingSystem.LINE_SEPARATOR).getBytes(Constants.SYSTEM_CHARSET);
outputStream.write(bytes);
outputStream.flush();
} catch (IOException e) {
// Ignoring IOException, including read end dead.
if (!broken) {
Logging.LOG.log(Level.WARNING, "An error occurred when writing console lines", e);
logs.forEach(System.out::println);
broken = true;
} else {
// Output plain XML to user
callback.accept(log, Log4jLevel.INFO);
}
}
});
}
private class Log4jHandlerImpl extends DefaultHandler {
private final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
private String date = "", thread = "", logger = "";
private StringBuilder message;
private Log4jLevel level;
private boolean readingMessage = false;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (localName) {
case "log4j_Event":
message = new StringBuilder();
Date d = new Date(Long.parseLong(attributes.getValue("timestamp")));
date = format.format(d);
try {
level = Log4jLevel.valueOf(attributes.getValue("level"));
} catch (IllegalArgumentException e) {
level = Log4jLevel.INFO;
}
thread = attributes.getValue("thread");
logger = attributes.getValue("logger");
if ("STDERR".equals(logger))
level = Log4jLevel.ERROR;
break;
case "log4j_Message":
readingMessage = true;
break;
case "log4j_Throwable":
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
switch (localName) {
case "log4j_Event":
callback.accept("[" + date + "] [" + thread + "/" + level.name() + "] [" + logger + "] " + message.toString(), level);
break;
case "log4j_Message":
readingMessage = false;
break;
}
}
@Override
public void characters(char[] ch, int start, int length) {
String line = new String(ch, start, length);
if (line.trim().isEmpty())
return;
if (readingMessage)
message.append(line).append(OperatingSystem.LINE_SEPARATOR);
else
callback.accept(line, Optional.ofNullable(Log4jLevel.guessLevel(line)).orElse(Log4jLevel.INFO));
}
}
}
@@ -46,7 +46,7 @@ public final class ModInfo implements Comparable<ModInfo> {
}
public ModInfo(File file, String name, String description) {
this(file, name, description, "unknown", "unknown", "unknown", "");
this(file, name, description, "", "", "", "");
}
public ModInfo(File file, String name, String description, String authors, String version, String gameVersion, String url) {
@@ -152,14 +152,20 @@ public final class ModInfo implements Comparable<ModInfo> {
try {
return ForgeModMetadata.fromFile(modFile);
} catch (Exception ignore) {
description = "May be Forge mod";
}
try {
return RiftModMetadata.fromFile(modFile);
} catch (Exception ignore) {
}
description = "";
break;
case "litemod":
try {
return LiteModMetadata.fromFile(modFile);
} catch (Exception ignore) {
description = "May be LiteLoader mod";
description = "LiteLoader Mod";
}
break;
default:
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
@@ -268,7 +269,7 @@ public final class MultiMCInstanceConfiguration {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modpackFile.toPath())) {
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
String name = root.normalize().getFileName().toString();
String name = StringUtils.removeSuffix(root.normalize().getFileName().toString(), "/");
Path instancePath = root.resolve("instance.cfg");
if (Files.notExists(instancePath))
@@ -120,7 +120,7 @@ public final class MultiMCModpackInstallTask extends Task {
if (Files.exists(patches))
for (Path patchJson : Files.newDirectoryStream(patches)) {
if (patchJson.endsWith(".json")) {
if (patchJson.toString().endsWith(".json")) {
MultiMCInstancePatch patch = Constants.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(patchJson)), MultiMCInstancePatch.class);
List<String> newArguments = new LinkedList<>();
for (String arg : patch.getTweakers()) {
@@ -0,0 +1,70 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
@Immutable
public final class RiftModMetadata {
private final String id;
private final String name;
private final List<String> authors;
public RiftModMetadata() {
this("", "", Collections.emptyList());
}
public RiftModMetadata(String id, String name, List<String> authors) {
this.id = id;
this.name = name;
this.authors = authors;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public List<String> getAuthors() {
return authors;
}
public static ModInfo fromFile(File modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
Path mcmod = fs.getPath("riftmod.json");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod.");
RiftModMetadata metadata = JsonUtils.fromNonNullJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)), RiftModMetadata.class);
String authors = metadata.getAuthors() == null ? "" : String.join(", ", metadata.getAuthors());
return new ModInfo(modFile, metadata.getName(), "",
authors, "", "", "");
}
}
}
@@ -108,7 +108,7 @@ public abstract class Task {
return true;
}
private String name = getClass().toString();
private String name = getClass().getName();
public String getName() {
return name;
@@ -41,6 +41,12 @@ public final class CommandBuilder {
}
}
/**
* Parsing will ignore your manual escaping
*
* @param args commands
* @return this
*/
public CommandBuilder add(String... args) {
for (String s : args)
raw.add(new Item(s, true));
@@ -89,35 +95,27 @@ public final class CommandBuilder {
}
private static String parseWindows(String s) {
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0)
if (s.charAt(0) != '"') {
// The argument has not been quoted, add quotes.
return '"' + s.replace("\\", "\\\\").replace("\"", "\"\"") + '"';
} else if (s.endsWith("\"")) {
// The argument has already been quoted.
return s;
} else {
// Unmatched quote for the argument.
throw new IllegalArgumentException();
}
String escape = " \t\"^&<>|";
if (StringUtils.containsOne(s, escape.toCharArray()))
// The argument has not been quoted, add quotes.
return '"' + s
.replace("\\", "\\\\")
.replace("\"", "\"\"")
+ '"';
else {
return s;
}
}
private static String parseBash(String s) {
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0)
if (s.charAt(0) != '"') {
// The argument has not been quoted, add quotes.
return '"' + s.replace("\"", "\\\"") + '"';
} else if (s.endsWith("\"")) {
// The argument has already been quoted.
return s;
} else {
// Unmatched quote for the argument.
throw new IllegalArgumentException();
}
else
String escaping = " \t\"!#$&'()*,;<=>?[\\]^`{|}~";
String escaped = "\"$&`";
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0 || StringUtils.containsOne(s, escaping.toCharArray())) {
// The argument has not been quoted, add quotes.
for (char ch : escaped.toCharArray())
s = s.replace("" + ch, "\\" + ch);
return '"' + s + '"';
} else
return s;
}
}
@@ -19,14 +19,15 @@ package org.jackhuang.hmcl.util;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.spi.FileSystemProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.zip.ZipError;
import java.util.zip.ZipException;
/**
* Utilities of compressing
@@ -35,6 +36,11 @@ import java.util.Optional;
*/
public final class CompressingUtils {
private static final FileSystemProvider ZIPFS_PROVIDER = FileSystemProvider.installedProviders().stream()
.filter(it -> "jar".equalsIgnoreCase(it.getScheme()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Zipfs not supported"));
private CompressingUtils() {
}
@@ -62,7 +68,12 @@ public final class CompressingUtils {
env.put("encoding", encoding);
if (useTempFile)
env.put("useTempFile", true);
return FileSystems.newFileSystem(URI.create("jar:" + zipFile.toUri()), env);
try {
return ZIPFS_PROVIDER.newFileSystem(zipFile, env);
} catch (ZipError error) {
// Since Java 8 throws ZipError stupidly
throw new ZipException(error.getMessage());
}
}
/**
@@ -20,8 +20,10 @@ package org.jackhuang.hmcl.util;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
@@ -95,6 +97,30 @@ public final class FileUtils {
return Lang.test(() -> deleteDirectory(directory));
}
public static boolean moveToTrash(File file) {
try {
java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
Method moveToTrash = desktop.getClass().getMethod("moveToTrash", File.class);
moveToTrash.invoke(desktop, file);
return true;
} catch (Exception e) {
return false;
}
}
/**
* Check if {@code java.awt.Desktop.moveToTrash} exists.
* @return true if the method exists.
*/
public static boolean isMovingToTrashSupported() {
try {
java.awt.Desktop.class.getMethod("moveToTrash", File.class);
return true;
} catch (ReflectiveOperationException e) {
return false;
}
}
public static void cleanDirectory(File directory)
throws IOException {
if (!directory.exists()) {
@@ -31,6 +31,12 @@ public final class IOUtils {
public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
copyTo(stream, result);
return result.toByteArray();
}
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
try (InputStream is = stream) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
@@ -21,13 +21,11 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleBooleanProperty instead
*/
@Deprecated
public class ImmediateBooleanProperty extends SimpleBooleanProperty {
@Override
@@ -48,31 +46,9 @@ public class ImmediateBooleanProperty extends SimpleBooleanProperty {
super.unbind();
}
private Consumer<Boolean> consumer = null;
private ChangeListener<Boolean> listener = null;
public void setChangedListener(Consumer<Boolean> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<Boolean> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<Boolean> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateBooleanProperty(Object bean, String name, boolean initialValue) {
super(bean, name, initialValue);
ChangeListener<Boolean> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue);
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
@@ -1,81 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
*/
public class ImmediateDoubleProperty extends SimpleDoubleProperty {
@Override
public void set(double newValue) {
super.get();
super.set(newValue);
}
@Override
public void bind(ObservableValue<? extends Number> newObservable) {
super.get();
super.bind(newObservable);
}
@Override
public void unbind() {
super.get();
super.unbind();
}
private Consumer<Double> consumer = null;
private ChangeListener<Number> listener = null;
public void setChangedListener(Consumer<Double> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<Number> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<Double> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateDoubleProperty(Object bean, String name, double initialValue) {
super(bean, name, initialValue);
ChangeListener<Number> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue.doubleValue());
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
}
@@ -21,13 +21,11 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleIntegerProperty instead
*/
@Deprecated
public class ImmediateIntegerProperty extends SimpleIntegerProperty {
@Override
@@ -48,31 +46,9 @@ public class ImmediateIntegerProperty extends SimpleIntegerProperty {
super.unbind();
}
private Consumer<Integer> consumer = null;
private ChangeListener<Number> listener = null;
public void setChangedListener(Consumer<Integer> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<Number> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<Integer> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateIntegerProperty(Object bean, String name, int initialValue) {
super(bean, name, initialValue);
ChangeListener<Number> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue.intValue());
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
@@ -21,13 +21,11 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleObjectProperty instead
*/
@Deprecated
public class ImmediateObjectProperty<T> extends SimpleObjectProperty<T> {
@Override
@@ -48,31 +46,9 @@ public class ImmediateObjectProperty<T> extends SimpleObjectProperty<T> {
super.unbind();
}
private Consumer<T> consumer = null;
private ChangeListener<T> listener = null;
public void setChangedListener(Consumer<T> consumer) {
this.consumer = Objects.requireNonNull(consumer);
this.listener = null;
}
public void setChangedListener(ChangeListener<T> listener) {
this.consumer = null;
this.listener = Objects.requireNonNull(listener);
}
public void setChangedListenerAndOperate(Consumer<T> listener) {
setChangedListener(listener);
listener.accept(get());
}
public ImmediateObjectProperty(Object bean, String name, T initialValue) {
super(bean, name, initialValue);
ChangeListener<T> changeListener = (a, b, newValue) -> {
if (consumer != null)
consumer.accept(newValue);
if (listener != null)
listener.changed(a, b, newValue);
};
addListener(changeListener);
}
@@ -25,9 +25,10 @@ import java.util.Objects;
import java.util.function.Consumer;
/**
*
* @author huangyuhui
* @deprecated Use SimpleStringProperty instead
*/
@Deprecated
public class ImmediateStringProperty extends SimpleStringProperty {
@Override
@@ -22,10 +22,9 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.regex.Matcher;
@@ -38,7 +37,7 @@ import java.util.stream.Stream;
*
* @author huangyuhui
*/
public final class JavaVersion implements Serializable {
public final class JavaVersion {
private final File binary;
private final String longVersion;
@@ -64,15 +63,18 @@ public final class JavaVersion implements Serializable {
return platform;
}
public VersionNumber getVersionNumber() {
return VersionNumber.asVersion(longVersion.replace('_', '.'));
}
/**
* The major version of Java installation.
*
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_X
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_11
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_10
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_9
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_8
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_7
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_6
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_5
* @see org.jackhuang.hmcl.util.JavaVersion#UNKNOWN
*/
public int getParsedVersion() {
@@ -82,52 +84,44 @@ public final class JavaVersion implements Serializable {
private static final Pattern REGEX = Pattern.compile("version \"(?<version>(.*?))\"");
public static final int UNKNOWN = -1;
public static final int JAVA_5 = 50;
public static final int JAVA_6 = 60;
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_X = 100;
public static final int JAVA_10 = 100;
public static final int JAVA_11 = 110;
private static int parseVersion(String version) {
if (version.startsWith("10") || version.startsWith("X"))
return JAVA_X;
else if (version.contains("1.9.") || version.startsWith("9"))
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"))
return JAVA_8;
else if (version.contains("1.7"))
return JAVA_7;
else if (version.contains("1.6"))
return JAVA_6;
else if (version.contains("1.5"))
return JAVA_5;
else
return UNKNOWN;
}
public static JavaVersion fromExecutable(File executable) throws IOException {
File actualFile = executable;
Platform platform = Platform.BIT_32;
String version = null;
if ("javaw".equals(FileUtils.getNameWithoutExtension(actualFile)))
actualFile = new File(actualFile.getAbsoluteFile().getParentFile(), "java");
// javaw is only used on windows
if ("javaw.exe".equalsIgnoreCase(executable.getName()))
executable = new File(executable.getAbsoluteFile().getParentFile(), "java.exe");
try {
Process process = new ProcessBuilder(actualFile.getAbsolutePath(), "-version").start();
process.waitFor();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
for (String line; (line = reader.readLine()) != null; ) {
Matcher m = REGEX.matcher(line);
if (m.find())
version = m.group("version");
if (line.contains("64-Bit"))
platform = Platform.BIT_64;
}
Process process = new ProcessBuilder(executable.getAbsolutePath(), "-version").start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
for (String line; (line = reader.readLine()) != null;) {
Matcher m = REGEX.matcher(line);
if (m.find())
version = m.group("version");
if (line.contains("64-Bit"))
platform = Platform.BIT_64;
}
} catch (InterruptedException e) {
throw new IOException("Interrupted scanning the java version.", e);
}
if (version == null)
@@ -135,24 +129,20 @@ public final class JavaVersion implements Serializable {
if (parseVersion(version) == UNKNOWN)
throw new IOException("Unrecognized Java version " + version);
return new JavaVersion(actualFile, version, platform);
}
private static JavaVersion fromKnownExecutable(File file, String version) {
return new JavaVersion(file, version, Platform.UNKNOWN);
return new JavaVersion(executable, version, platform);
}
public static JavaVersion fromJavaHome(File home) throws IOException {
return fromExecutable(getJavaFile(home));
return fromExecutable(getExecutable(home));
}
private static File getJavaFile(File home) {
File path = new File(home, "bin");
File javaw = new File(path, "javae.exe");
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && javaw.isFile())
return javaw;
else
return new File(path, "java"); // Both linux and windows allow this.
private static File getExecutable(File javaHome) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
return new File(javaHome, "bin/java.exe");
} else {
return new File(javaHome, "bin/java");
}
}
public static JavaVersion fromCurrentEnvironment() {
@@ -160,7 +150,7 @@ public final class JavaVersion implements Serializable {
}
public static final JavaVersion THIS_JAVA = new JavaVersion(
getJavaFile(new File(System.getProperty("java.home"))),
getExecutable(new File(System.getProperty("java.home"))),
System.getProperty("java.version"),
Platform.PLATFORM
);
@@ -168,10 +158,6 @@ public final class JavaVersion implements Serializable {
private static List<JavaVersion> JAVAS;
private static final CountDownLatch LATCH = new CountDownLatch(1);
public static Optional<List<JavaVersion>> getJREsImmediately() {
return Optional.ofNullable(JAVAS);
}
public static List<JavaVersion> getJREs() throws InterruptedException {
if (JAVAS != null)
return JAVAS;
@@ -194,7 +180,7 @@ public final class JavaVersion implements Serializable {
javaVersions = queryMacintosh();
break;
default:
javaVersions = Collections.emptyList();
javaVersions = new ArrayList<>();
break;
}
@@ -214,6 +200,7 @@ public final class JavaVersion implements Serializable {
LATCH.countDown();
}
// ==== Linux ====
private static List<JavaVersion> queryLinux() throws IOException {
Path jvmDir = Paths.get("/usr/lib/jvm");
if (Files.isDirectory(jvmDir)) {
@@ -234,9 +221,11 @@ public final class JavaVersion implements Serializable {
return Collections.emptyList();
}
}
// ====
// ==== OSX ====
private static List<JavaVersion> queryMacintosh() throws IOException {
LinkedList<JavaVersion> res = new LinkedList<>();
List<JavaVersion> res = new ArrayList<>();
File currentJRE = new File("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home");
if (currentJRE.exists())
@@ -248,18 +237,32 @@ public final class JavaVersion implements Serializable {
return res;
}
// ====
// ==== Windows ====
private static List<JavaVersion> queryWindows() {
LinkedList<JavaVersion> res = new LinkedList<>();
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\")));
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\")));
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\")));
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\")));
List<JavaVersion> res = new ArrayList<>();
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\")));
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\")));
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\")));
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\")));
return res;
}
private static List<JavaVersion> queryJavaInRegistryKey(String location) throws IOException, InterruptedException {
List<JavaVersion> res = new ArrayList<>();
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI")) continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null)
res.add(fromJavaHome(new File(home)));
}
return res;
}
// Registry utilities
private static List<String> querySubFolders(String location) throws IOException, InterruptedException {
List<String> res = new LinkedList<>();
List<String> res = new ArrayList<>();
String[] cmd = new String[] { "cmd", "/c", "reg", "query", location };
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
@@ -272,16 +275,6 @@ public final class JavaVersion implements Serializable {
return res;
}
private static List<JavaVersion> queryRegisterKey(String location) throws IOException, InterruptedException {
List<JavaVersion> res = new LinkedList<>();
for (String java : querySubFolders(location)) {
String home = queryRegisterValue(java, "JavaHome");
if (home != null)
res.add(fromJavaHome(new File(home)));
}
return res;
}
private static String queryRegisterValue(String location, String name) throws IOException, InterruptedException {
String[] cmd = new String[] { "cmd", "/c", "reg", "query", location, "/v", name };
boolean last = false;
@@ -306,4 +299,5 @@ public final class JavaVersion implements Serializable {
}
return null;
}
// ====
}
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more