Compare commits

..
75 Commits
Author SHA1 Message Date
huangyuhui 630f164877 Continue load old version if the newer version cannot launch 2018-06-10 00:10:17 +08:00
huangyuhui 38c6bce2c0 Change log folder to ~/.hmcl 2018-06-09 17:37:43 +08:00
huanghongxun 034734ec66 Merge pull request #348 from yushijinhun/fxgson
使用 FxGson
2018-06-09 01:12:50 +08:00
yushijinhun d875eeb22a 将Config中属性替换为JavaFX Property 2018-06-09 00:32:33 +08:00
yushijinhun 61db6b8fb2 添加FxGson依赖 2018-06-09 00:28:23 +08:00
huanghongxun 945d4cf20d Merge pull request #347 from yushijinhun/merge-head
重构
2018-06-08 21:34:36 +08:00
yushijinhun 631337c451 YggdrasilAccount记录角色uuid而不是名称
注:旧版本无法读取此版本配置(移除了character)。
2018-06-08 21:13:33 +08:00
yushijinhun 4f51e377ae 移除canPlayOffline() 2018-06-08 21:09:35 +08:00
yushijinhun e71c06a0d0 Arguments.addXXX改为链式调用 2018-06-08 21:09:35 +08:00
yushijinhun 13aabde5d9 完善Account文档 2018-06-08 21:09:35 +08:00
huanghongxun e3f371f457 Merge pull request #344 from yushijinhun/javafx
修复YggdrasilAccount读取到了错误的character
2018-06-08 16:36:50 +08:00
yushijinhun 6ee99b5736 修复YggdrasilAccount读取到了错误的character
引入:https://github.com/huanghongxun/HMCL/commit/6c4231a6b242c2a11af9768e424593cc6cc14bc5#diff-839dd42e51cd641828b00c5302c02b12R50
2018-06-08 16:31:56 +08:00
huangyuhui 895188b25f Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-06-08 16:06:31 +08:00
huanghongxun 9fba923a51 Merge pull request #343 from yushijinhun/javafx
修复 defaultJavaPath 不能保存的问题
2018-06-08 16:06:01 +08:00
huangyuhui 37a39098f8 Try to fix VersionNotFoundException. Closes #298 2018-06-08 16:05:33 +08:00
yushijinhun 6fb548e6c3 修复defaultJavaPath不能保存的问题(未添加监听器) 2018-06-08 15:19:47 +08:00
huangyuhui 110b7cb06f Should fix 'Never get to here' 2018-06-08 13:55:38 +08:00
huangyuhui e9070a77c3 Fixed java.lang.IllegalStateException: stream has already been operated upon or closed 2018-06-08 13:40:12 +08:00
huangyuhui 475e6e5511 Remove redundant URLs that download providers provide. Mentioned in #242 2018-06-08 13:13:42 +08:00
huangyuhui eed4c7c11a Load skin when added new account 2018-06-08 12:58:01 +08:00
huangyuhui d986d7ac03 Apply spinner instead of intermediate progress bar 2018-06-08 12:47:46 +08:00
huangyuhui f640922a50 Optimize imports 2018-06-08 12:47:34 +08:00
huangyuhui 118a6cf0d1 Fasten version item construction 2018-06-08 12:45:44 +08:00
huanghongxun 72871e6d9a Merge pull request #342 from yushijinhun/refactor-skin
Yggdrasil 部分重构及 bug 修复
2018-06-08 10:41:46 +08:00
yushijinhun 61acbcec0d 移除GameProfile类的TypeAdapter 2018-06-07 22:28:17 +08:00
yushijinhun 06af79e2b4 移除对legcay账户的支持 2018-06-07 22:25:31 +08:00
yushijinhun 6782602141 重构YggdrasilService相关代码 2018-06-07 21:12:38 +08:00
yushijinhun 8df17ae38a 使用静态方法pair()替代new Pair<>() 2018-06-07 21:08:25 +08:00
huanghongxun 1645a93adf Merge pull request #341 from yushijinhun/cleanup
清理冗余代码
2018-06-07 12:41:51 +08:00
yushijinhun 6a04ab37d7 清理Hex 2018-06-07 12:17:58 +08:00
yushijinhun 27c9abd9ed 清理DigestUtils 2018-06-07 12:16:34 +08:00
yushijinhun a08be2e11d 移除Charsets类 2018-06-07 11:47:36 +08:00
huanghongxun 17615b1791 Merge pull request #340 from yushijinhun/javafx
修复 Java 选择相关 bug
2018-06-07 01:15:44 +08:00
yushijinhun d75c3b584a 修复加载JREs时可能的并发修改异常 2018-06-07 01:06:13 +08:00
huangyuhui 20f919b93e Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-06-07 00:53:56 +08:00
huangyuhui 20244fe5cc Fasten versions loading 2018-06-07 00:50:15 +08:00
yushijinhun 6a89f0a661 将当前Java版本包含在getJREs()中 2018-06-07 00:28:33 +08:00
huanghongxun 3a9efdb03e Merge pull request #339 from yushijinhun/multiple-java
Linux下自动检测Java + 允许多个相同版本的JRE
2018-06-06 23:57:24 +08:00
huangyuhui 2ae00db94b Removed MultiFileItem tooltips 2018-06-06 23:54:19 +08:00
huangyuhui a9261b759b Shutdown Now to prevent NPE when task executed after app closed 2018-06-06 23:52:23 +08:00
yushijinhun 6b02dc3cd5 允许多个相同版本的Java共存 2018-06-06 23:43:58 +08:00
yushijinhun afa37c0d2f 自动检测Linux下的Java 2018-06-06 23:01:27 +08:00
huangyuhui 2575f10497 Fixed #336 2018-06-06 19:45:39 +08:00
huanghongxun 27c4a68a13 Merge pull request #338 from yushijinhun/openjdk
对 OpenJDK 更好的支持
2018-06-06 14:02:14 +08:00
yushijinhun f09cb54894 当JavaFX缺失时弹窗提示 2018-06-06 13:52:23 +08:00
yushijinhun 5431cfef79 对OpenJDK上出现的错误同样进行上报 2018-06-06 13:31:53 +08:00
huanghongxun 35c0f3cddc Merge pull request #337 from yushijinhun/javafx
完善 authlib-injector 功能
2018-06-03 03:12:19 +08:00
yushijinhun efe22ca613 防止执行平移动画时组件溢出面板 2018-06-02 20:09:36 +08:00
yushijinhun 3408a8ffcf 更改措辞:添加认证服务器->管理认证服务器 2018-06-02 19:59:00 +08:00
yushijinhun 60a140efb3 修复删除验证服务器时可能出现ConcurrentModificationException
初始化块执行时字段并不一定都完成了初始化,应当避免使用。
利用ConcurrentHashMap的弱一致性防止迭代中删除账户造成ConcurrentModificationException。
2018-06-02 19:38:03 +08:00
yushijinhun bbe584bd91 修复无法显示未设置皮肤的Yggdrasil账户的皮肤的问题 2018-06-02 19:21:41 +08:00
yushijinhun 3866377a28 使用UUID而不是角色名来标识皮肤 2018-06-02 18:15:41 +08:00
yushijinhun ebc0b4b2b0 移除AccountHelper未使用的单例 2018-06-02 18:06:32 +08:00
huanghongxun d6c65a320d Merge pull request #334 from yushijinhun/java9
移除对Unsafe的使用
2018-05-29 22:47:35 +08:00
yushijinhun 2e46d76165 移除Lang.get(Map,Object,Class) 2018-05-27 12:40:45 +08:00
yushijinhun 23037da406 移除对Unsafe的使用 2018-05-27 12:13:27 +08:00
yushijinhun 617261c74d 不使用ReflectionHelper来安装Tooltip 2018-05-27 12:09:04 +08:00
yushijinhun 6c0d6ee0cf 通过非反射方法获取总物理内存 2018-05-27 11:04:39 +08:00
huanghongxun 663e1ef7ab Merge pull request #332 from xfl03/javafx
Fix #325 #296
2018-05-20 19:40:39 +08:00
Alexander Xia 1a47d355f0 Fix #325 #296 2018-05-20 19:12:12 +08:00
huangyuhui e407994103 Fixed #331 2018-05-17 16:46:16 +08:00
huangyuhui bda2149c02 Fix #326 2018-04-30 01:59:33 +08:00
huangyuhui b925423d61 Fixded #324 2018-04-26 22:15:27 +08:00
huangyuhui f3906a37bf Fixed #321 2018-04-26 13:14:14 +08:00
huangyuhui a1e115ea6b Ignore some exceptions 2018-03-09 16:54:28 +08:00
huangyuhui 03383510ef Disable accept button in AddAccountPage 2018-03-09 16:45:12 +08:00
huangyuhui e8cf9644eb Fix IllegalArgumentException: Invalid UUID String 2018-03-09 16:31:39 +08:00
huangyuhui dbfcf1facc Try to fix empty stack of StackContainerPane 2018-03-09 16:24:02 +08:00
huangyuhui 44aff94937 Remove error 2018-03-06 11:25:15 +08:00
huangyuhui fb1fc92b7c Fix optifine 2018-03-06 11:19:21 +08:00
huangyuhui 71cfa64b16 Fix StackContainerPane 2018-03-06 10:19:37 +08:00
huangyuhui b200105a85 Fix Liteloader installing of BMCLAPI 2018-03-06 10:13:48 +08:00
huanghongxun ce83f3c5e1 Merge pull request #297 from andylizi/switch-to-https
Switch to Https for most links.
2018-03-05 20:57:30 +08:00
huanghongxun 57d3275856 Merge branch 'javafx' into switch-to-https 2018-03-05 20:56:26 +08:00
andylizi 76c254ee98 Switch the download links to HTTPS 2018-02-28 17:23:49 +08:00
111 changed files with 1835 additions and 2078 deletions

No files matched your search

+5 -5
View File
@@ -48,16 +48,16 @@ jar {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
manifest {
attributes 'Created-By' : 'Copyright(c) 2013-2017 huangyuhui.',
attributes 'Created-By' : 'Copyright(c) 2013-2018 huangyuhui.',
'Main-Class' : mainClass,
'Multi-Release': "true"
}
doLast {
new File("build/signed").mkdirs()
ant.signjar(signedjar: archivePath, jar: archivePath,
keystore: "HMCL.keystore", storepass: "123456",
alias: "HMCL")
//new File("build/signed").mkdirs()
//ant.signjar(signedjar: archivePath, jar: archivePath,
// keystore: "HMCL.keystore", storepass: "123456",
// alias: "HMCL")
def messageDigest = MessageDigest.getInstance("SHA1")
archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
@@ -31,7 +31,12 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@@ -66,8 +71,11 @@ public final class Launcher extends Application {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
if (!FileUtils.makeDirectory(LOG_DIRECTORY))
System.out.println("Unable to create log directory " + LOG_DIRECTORY + ", log files cannot be generated.");
try {
Logging.start();
Logging.start(LOG_DIRECTORY);
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
@@ -108,19 +116,23 @@ public final class Launcher extends Application {
});
}
public static File getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return new File(home, "." + folder + "/");
case WINDOWS:
String appdata = System.getenv("APPDATA");
return new File(Lang.nonNull(appdata, home), "." + folder + "/");
case OSX:
return new File(home, "Library/Application Support/" + folder);
default:
return new File(home, folder + "/");
public static List<File> getCurrentJarFiles() {
List<File> result = new LinkedList<>();
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) Launcher.class.getClassLoader()).getURLs();
for (URL u : urls)
try {
File f = new File(u.toURI());
if (f.isFile() && (f.getName().endsWith(".exe") || f.getName().endsWith(".jar")))
result.add(f);
} catch (URISyntaxException e) {
return null;
}
}
if (result.isEmpty())
return null;
else
return result;
}
public static String i18n(String key) {
@@ -136,8 +148,9 @@ public final class Launcher extends Application {
return String.format(i18n(key), formatArgs);
}
public static final File MINECRAFT_DIRECTORY = getWorkingDirectory("minecraft");
public static final File HMCL_DIRECTORY = getWorkingDirectory("hmcl");
public static final File MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final File HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
public static final File LOG_DIRECTORY = new File(Launcher.HMCL_DIRECTORY, "logs");
public static final String VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@";
public static final String NAME = "HMCL";
@@ -147,6 +160,7 @@ public final class Launcher extends Application {
public static final IUpgrader UPGRADER = new AppDataUpgrader();
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final String CONTACT = "http://huangyuhui.duapp.com/hmcl.php";
public static final String UPDATE_SERVER = "http://47.101.47.110";
public static final String CONTACT = UPDATE_SERVER + "/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -17,23 +17,41 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.Logging;
import javax.swing.*;
import java.io.File;
public final class Main {
public static void main(String[] args) {
checkJavaFX();
checkDirectoryPath();
Launcher.main(args);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
System.err.println("Exclamation mark(!) is not allowed in the path where HMCL is in. Forcibly exit.");
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
String message = "Exclamation mark(!) is not allowed in the path where HMCL is in.\nThe path is " + currentDirectory;
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
} else
Launcher.main(args);
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDirectory);
}
}
private static void checkJavaFX() {
try {
Class.forName("javafx.application.Application");
} catch (ClassNotFoundException e) {
showErrorAndExit("JavaFX is missing.\n"
+ "If you are using Java 11 or above, please downgrade to Java 8 or 10.\n"
+ "If you are using OpenJDK, please ensure OpenJFX is included.");
}
}
private static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
}
@@ -0,0 +1,57 @@
/*
* 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.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.util.ToStringBuilder;
/**
* This event gets fired when added accounts.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class AccountAddedEvent extends Event {
private final Account account;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.setting.Settings}
*/
public AccountAddedEvent(Object source, Account account) {
super(source);
this.account = account;
}
public Account getAccount() {
return account;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("account", account)
.toString();
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Collection;
@@ -34,13 +34,10 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.net.Proxy;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.*;
public final class AccountHelper {
public static final AccountHelper INSTANCE = new AccountHelper();
private AccountHelper() {}
public static final File SKIN_DIR = new File(Launcher.HMCL_DIRECTORY, "skins");
@@ -73,8 +70,8 @@ public final class AccountHelper {
return new SkinLoadTask(account, proxy, true);
}
private static File getSkinFile(String name) {
return new File(SKIN_DIR, name + ".png");
private static File getSkinFile(UUID uuid) {
return new File(SKIN_DIR, uuid + ".png");
}
public static Image getSkin(YggdrasilAccount account) {
@@ -82,9 +79,11 @@ public final class AccountHelper {
}
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
if (account.getCharacter() == null)
return getDefaultSkin(account, scaleRatio);
File file = getSkinFile(account.getCharacter());
UUID uuid = account.getUUID();
if (uuid == null)
return getSteveSkin(scaleRatio);
File file = getSkinFile(uuid);
if (file.exists()) {
Image original = new Image("file:" + file.getAbsolutePath());
return new Image("file:" + file.getAbsolutePath(),
@@ -92,15 +91,14 @@ public final class AccountHelper {
original.getHeight() * scaleRatio,
false, false);
}
return getDefaultSkin(account, scaleRatio);
return getDefaultSkin(uuid, scaleRatio);
}
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio, Proxy proxy) throws Exception {
String name = profile.getName();
File file = getSkinFile(name);
File file = getSkinFile(profile.getId());
downloadSkin(account, profile, true, proxy);
if (!file.exists())
return getDefaultSkin(account, scaleRatio);
return getDefaultSkin(profile.getId(), scaleRatio);
String url = "file:" + file.getAbsolutePath();
return scale(url, scaleRatio);
@@ -152,7 +150,7 @@ public final class AccountHelper {
Optional<Texture> texture = account.getSkin(profile);
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(profile.getName());
File file = getSkinFile(profile.getId());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
@@ -165,7 +163,7 @@ public final class AccountHelper {
Optional<Texture> texture = account.getSkin();
if (!texture.isPresent()) return;
String url = texture.get().getUrl();
File file = getSkinFile(account.getCharacter());
File file = getSkinFile(account.getUUID());
if (!refresh && file.exists())
return;
new FileDownloadTask(NetworkUtils.toURL(url), file, proxy).run();
@@ -187,11 +185,8 @@ public final class AccountHelper {
return scale("/assets/img/alex.png", 4);
}
public static Image getDefaultSkin(Account account, double scaleRatio) {
if (account == null)
return getSteveSkin(scaleRatio);
int type = account.getUUID().hashCode() & 1;
public static Image getDefaultSkin(UUID uuid, double scaleRatio) {
int type = uuid.hashCode() & 1;
if (type == 1)
return getAlexSkin(scaleRatio);
else
@@ -27,7 +27,6 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import java.io.File;
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
@@ -25,6 +24,7 @@ import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.setting.LauncherVisibility;
@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.LogWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
@@ -46,12 +45,15 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class LauncherHelper {
public static final LauncherHelper INSTANCE = new LauncherHelper();
private LauncherHelper(){}
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(() -> {});
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
public void launch(Profile profile, Account account, String selectedVersion, File scriptFile) {
if (account == null)
@@ -64,7 +66,10 @@ public final class LauncherHelper {
Platform.runLater(() -> {
try {
checkGameState(profile, setting, version, () -> Schedulers.newThread().schedule(() -> launch0(profile, account, selectedVersion, scriptFile)));
checkGameState(profile, setting, version, () -> {
Controllers.dialog(launchingStepsPane);
Schedulers.newThread().schedule(() -> launch0(profile, account, selectedVersion, scriptFile));
});
} catch (InterruptedException ignore) {
}
});
@@ -73,12 +78,11 @@ public final class LauncherHelper {
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
GameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = repository.getResolvedVersion(selectedVersion);
Version version = MaintainTask.maintain(repository.getResolvedVersion(selectedVersion));
VersionSetting setting = profile.getVersionSetting(selectedVersion);
Optional<String> gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version));
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> Controllers.dialog(launchingStepsPane))
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES)))
TaskExecutor executor = Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.DEPENDENCIES))
.then(variables -> {
if (setting.isNotCheckGame())
return null;
@@ -93,10 +97,8 @@ public final class LauncherHelper {
try {
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
if (account.canPlayOffline())
variables.set("account", account.playOffline());
else
throw e;
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
} catch (AuthenticationException e) {
variables.set("account", DialogController.logIn(account));
@@ -105,7 +107,13 @@ public final class LauncherHelper {
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LAUNCHING)))
.then(Task.of(variables -> {
variables.set("launcher", new HMCLGameLauncher(
repository, selectedVersion, variables.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
repository,
selectedVersion,
variables.get("account"),
setting.toLaunchOptions(profile.getGameDir()),
setting.getLauncherVisibility() == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(variables.get("account"), setting, gameVersion.isPresent())
));
}))
.then(variables -> {
@@ -126,13 +134,13 @@ public final class LauncherHelper {
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
launchingStepsPane.setCancel(() -> {
launchingStepsPane.setCancel(it -> {
process.stop();
Controllers.closeDialog();
Controllers.closeDialog(it);
});
} else
Platform.runLater(() -> {
Controllers.closeDialog();
Controllers.closeDialog(launchingStepsPane);
Controllers.dialog(Launcher.i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
@@ -154,11 +162,11 @@ public final class LauncherHelper {
public void onStop(boolean success, TaskExecutor executor) {
if (!success) {
Platform.runLater(() -> {
Controllers.closeDialog();
Controllers.closeDialog(launchingStepsPane);
if (executor.getLastException() != null)
Controllers.dialog(I18nException.getStackTrace(executor.getLastException()),
scriptFile == null ? Launcher.i18n("launch.failed") : Launcher.i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE, Controllers::closeDialog);
MessageBox.ERROR_MESSAGE);
});
}
launchingStepsPane.setExecutor(null);
@@ -169,13 +177,14 @@ public final class LauncherHelper {
}
private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException {
boolean flag = false, suggest = true;
boolean flag = false;
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
JavaVersion java = setting.getJavaVersion();
if (java == null) {
Controllers.dialog(Launcher.i18n("launch.wrong_javadir"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
flag = true;
}
@@ -187,7 +196,6 @@ public final class LauncherHelper {
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
Controllers.dialog(Launcher.i18n("launch.advice.java9"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
suggest = false;
flag = true;
}
@@ -206,10 +214,7 @@ public final class LauncherHelper {
flag = true;
}
if (flag) {
if (suggest && Controllers.getDialogContent() instanceof MessageDialogPane)
((MessageDialogPane) Controllers.getDialogContent()).disableClosingDialog();
} else
if (!flag)
onAccept.run();
}
@@ -219,6 +224,9 @@ public final class LauncherHelper {
}
public void emitStatus(LoadingState state) {
if (state == LoadingState.DONE)
Controllers.closeDialog(launchingStepsPane);
launchingStepsPane.setTitle(state.getLocalizedMessage());
launchingStepsPane.setSubtitle((state.ordinal() + 1) + " / " + LoadingState.values().length);
}
@@ -296,10 +304,10 @@ public final class LauncherHelper {
if (authInfo == null)
forbiddenTokens = Collections.emptyMap();
else
forbiddenTokens = Lang.mapOf(
new Pair<>(authInfo.getAccessToken(), "<access token>"),
new Pair<>(authInfo.getUserId(), "<uuid>"),
new Pair<>(authInfo.getUsername(), "<player>")
forbiddenTokens = mapOf(
pair(authInfo.getAccessToken(), "<access token>"),
pair(UUIDTypeAdapter.fromUUID(authInfo.getUUID()), "<uuid>"),
pair(authInfo.getUsername(), "<player>")
);
visibility = setting.getLauncherVisibility();
@@ -329,7 +337,7 @@ public final class LauncherHelper {
else
System.out.print(log);
logs.add(new Pair<>(log, level));
logs.add(pair(log, level));
if (logs.size() > Settings.INSTANCE.getLogLines())
logs.removeFirst();
@@ -355,7 +363,9 @@ public final class LauncherHelper {
// Never come to here.
break;
case KEEP:
// No operations here
Platform.runLater(() -> {
emitStatus(LoadingState.DONE);
});
break;
case HIDE:
Platform.runLater(() -> {
@@ -32,7 +32,9 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.io.IOException;
@@ -40,6 +42,9 @@ import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* @author huangyuhui
*/
@@ -50,10 +55,10 @@ public final class Accounts {
public static final String YGGDRASIL_ACCOUNT_KEY = "yggdrasil";
public static final String AUTHLIB_INJECTOR_ACCOUNT_KEY = "authlibInjector";
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = Lang.mapOf(
new Pair<>(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
new Pair<>(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
new Pair<>(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector))
public static final Map<String, AccountFactory<?>> ACCOUNT_FACTORY = mapOf(
pair(OFFLINE_ACCOUNT_KEY, OfflineAccountFactory.INSTANCE),
pair(YGGDRASIL_ACCOUNT_KEY, new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE)),
pair(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector))
);
private static final Map<String, String> AUTHLIB_INJECTOR_SERVER_NAMES = new HashMap<>();
@@ -17,274 +17,93 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.SerializedName;
import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.util.JavaVersion;
import com.google.gson.annotations.SerializedName;
import java.util.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
final class Config {
public final class Config implements Cloneable {
@SerializedName("last")
private String selectedProfile = "";
public final StringProperty selectedProfile = new SimpleStringProperty("");
@SerializedName("backgroundType")
private int backgroundImageType = 0;
public final IntegerProperty backgroundImageType = new SimpleIntegerProperty(0);
@SerializedName("bgpath")
private String backgroundImage = null;
public final StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("commonpath")
private String commonDirectory = Launcher.MINECRAFT_DIRECTORY.getAbsolutePath();
public final StringProperty commonDirectory = new SimpleStringProperty(Launcher.MINECRAFT_DIRECTORY.getAbsolutePath());
@SerializedName("hasProxy")
private boolean hasProxy = false;
public final BooleanProperty hasProxy = new SimpleBooleanProperty();
@SerializedName("hasProxyAuth")
private boolean hasProxyAuth = false;
public final BooleanProperty hasProxyAuth = new SimpleBooleanProperty();
@SerializedName("proxyType")
private int proxyType = 0;
public final IntegerProperty proxyType = new SimpleIntegerProperty();
@SerializedName("proxyHost")
private String proxyHost = null;
public final StringProperty proxyHost = new SimpleStringProperty();
@SerializedName("proxyPort")
private String proxyPort = null;
public final StringProperty proxyPort = new SimpleStringProperty();
@SerializedName("proxyUserName")
private String proxyUser = null;
public final StringProperty proxyUser = new SimpleStringProperty();
@SerializedName("proxyPassword")
private String proxyPass = null;
public final StringProperty proxyPass = new SimpleStringProperty();
@SerializedName("theme")
private String theme = null;
@SerializedName("java")
private List<JavaVersion> java = null;
public final StringProperty theme = new SimpleStringProperty();
@SerializedName("localization")
private String localization;
public final StringProperty localization = new SimpleStringProperty();
@SerializedName("downloadtype")
private int downloadType = 0;
public final IntegerProperty downloadType = new SimpleIntegerProperty();
@SerializedName("configurations")
private Map<String, Profile> configurations = new TreeMap<>();
public final ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
@SerializedName("accounts")
private List<Map<Object, Object>> accounts = new LinkedList<>();
public final ObservableList<Map<Object, Object>> accounts = FXCollections.observableArrayList();
@SerializedName("selectedAccount")
private String selectedAccount = "";
public final StringProperty selectedAccount = new SimpleStringProperty("");
@SerializedName("fontFamily")
private String fontFamily = "Consolas";
public final StringProperty fontFamily = new SimpleStringProperty("Consolas");
@SerializedName("fontSize")
private double fontSize = 12;
public final DoubleProperty fontSize = new SimpleDoubleProperty(12);
@SerializedName("logLines")
private int logLines = 100;
public final IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("authlibInjectorServerURLs")
private Set<String> authlibInjectorServerURLs = new HashSet<>();
public final ObservableSet<String> authlibInjectorServerURLs = FXCollections.observableSet(new HashSet<>());
public String getSelectedProfile() {
return selectedProfile;
}
public void setSelectedProfile(String selectedProfile) {
this.selectedProfile = selectedProfile;
Settings.INSTANCE.save();
}
public String getBackgroundImage() {
return backgroundImage;
}
public void setBackgroundImage(String backgroundImage) {
this.backgroundImage = backgroundImage;
Settings.INSTANCE.save();
}
public int getBackgroundImageType() {
return backgroundImageType;
}
public void setBackgroundImageType(int backgroundImageType) {
this.backgroundImageType = backgroundImageType;
Settings.INSTANCE.save();
}
public String getCommonDirectory() {
return commonDirectory;
}
public void setCommonDirectory(String commonDirectory) {
this.commonDirectory = commonDirectory;
Settings.INSTANCE.save();
}
public boolean isHasProxy() {
return hasProxy;
}
public void setHasProxy(boolean hasProxy) {
this.hasProxy = hasProxy;
Settings.INSTANCE.save();
}
public boolean isHasProxyAuth() {
return hasProxyAuth;
}
public void setHasProxyAuth(boolean hasProxyAuth) {
this.hasProxyAuth = hasProxyAuth;
Settings.INSTANCE.save();
}
public int getProxyType() {
return proxyType;
}
public void setProxyType(int proxyType) {
this.proxyType = proxyType;
Settings.INSTANCE.save();
}
public String getProxyHost() {
return proxyHost;
}
public void setProxyHost(String proxyHost) {
this.proxyHost = proxyHost;
Settings.INSTANCE.save();
}
public String getProxyPort() {
return proxyPort;
}
public void setProxyPort(String proxyPort) {
this.proxyPort = proxyPort;
Settings.INSTANCE.save();
}
public String getProxyUser() {
return proxyUser;
}
public void setProxyUser(String proxyUser) {
this.proxyUser = proxyUser;
Settings.INSTANCE.save();
}
public String getProxyPass() {
return proxyPass;
}
public void setProxyPass(String proxyPass) {
this.proxyPass = proxyPass;
Settings.INSTANCE.save();
}
public String getTheme() {
return theme;
}
public void setTheme(String theme) {
this.theme = theme;
Settings.INSTANCE.save();
}
public List<JavaVersion> getJava() {
return java;
}
public void setJava(List<JavaVersion> java) {
this.java = java;
Settings.INSTANCE.save();
}
public String getLocalization() {
return localization;
}
public void setLocalization(String localization) {
this.localization = localization;
Settings.INSTANCE.save();
}
public int getDownloadType() {
return downloadType;
}
public void setDownloadType(int downloadType) {
this.downloadType = downloadType;
Settings.INSTANCE.save();
}
public Map<String, Profile> getConfigurations() {
return configurations;
}
public void setConfigurations(Map<String, Profile> configurations) {
this.configurations = configurations;
Settings.INSTANCE.save();
}
public List<Map<Object, Object>> getAccounts() {
return accounts;
}
public void setAccounts(List<Map<Object, Object>> accounts) {
this.accounts = accounts;
Settings.INSTANCE.save();
}
public String getSelectedAccount() {
return selectedAccount;
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount = selectedAccount;
Settings.INSTANCE.save();
}
public String getFontFamily() {
return fontFamily;
}
public void setFontFamily(String fontFamily) {
this.fontFamily = fontFamily;
Settings.INSTANCE.save();
}
public double getFontSize() {
return fontSize;
}
public void setFontSize(double fontSize) {
this.fontSize = fontSize;
Settings.INSTANCE.save();
}
public int getLogLines() {
return logLines;
}
public void setLogLines(int logLines) {
this.logLines = logLines;
}
public Set<String> getAuthlibInjectorServerURLs() {
return authlibInjectorServerURLs;
}
public void setAuthlibInjectorServerURLs(Set<String> authlibInjectorServerURLs) {
this.authlibInjectorServerURLs = authlibInjectorServerURLs;
Settings.INSTANCE.save();
@Override
public Config clone() {
return Settings.GSON.fromJson(Settings.GSON.toJson(this), Config.class);
}
}
@@ -28,7 +28,7 @@ import java.util.List;
public final class DownloadProviders {
private DownloadProviders() {}
public static final List<DownloadProvider> DOWNLOAD_PROVIDERS = Lang.immutableListOf(new MojangDownloadProvider(Settings.INSTANCE.getLocale() == Locales.ZH_CN), BMCLAPIDownloadProvider.INSTANCE, CurseCDNDownloadProvider.INSTANCE);
public static final List<DownloadProvider> DOWNLOAD_PROVIDERS = Lang.immutableListOf(new MojangDownloadProvider(), BMCLAPIDownloadProvider.INSTANCE, CurseCDNDownloadProvider.INSTANCE);
public static DownloadProvider getDownloadProvider(int index) {
return Lang.get(DOWNLOAD_PROVIDERS, index).orElse(DOWNLOAD_PROVIDERS.get(0));
@@ -22,16 +22,21 @@ import com.google.gson.GsonBuilder;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.ObservableSet;
import javafx.scene.text.Font;
import org.hildan.fxgson.creators.ObservableListCreator;
import org.hildan.fxgson.creators.ObservableMapCreator;
import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.AccountLoadingEvent;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.ProfileLoadingEvent;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
@@ -42,35 +47,42 @@ import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class Settings {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
.registerTypeAdapter(Profile.class, Profile.Serializer.INSTANCE)
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.setPrettyPrinting().create();
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting()
.create();
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
public static final File SETTINGS_FILE = new File("hmcl.json").getAbsoluteFile();
public static final String SETTINGS_FILE_NAME = "hmcl.json";
public static final File SETTINGS_FILE = new File(SETTINGS_FILE_NAME).getAbsoluteFile();
public static final Settings INSTANCE = new Settings();
private Settings() {}
private final Config SETTINGS = initSettings();
private final Map<String, Account> accounts = new HashMap<>();
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
{
private Settings() {
loadProxy();
for (Iterator<Map<Object, Object>> iterator = SETTINGS.getAccounts().iterator(); iterator.hasNext(); ) {
for (Iterator<Map<Object, Object>> iterator = SETTINGS.accounts.iterator(); iterator.hasNext();) {
Map<Object, Object> settings = iterator.next();
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(Lang.get(settings, "type", String.class).orElse(""));
AccountFactory<?> factory = Accounts.ACCOUNT_FACTORY.get(tryCast(settings.get("type"), String.class).orElse(""));
if (factory == null) {
// unrecognized account type, so remove it.
iterator.remove();
@@ -89,7 +101,6 @@ public class Settings {
accounts.put(Accounts.getAccountId(account), account);
}
checkAuthlibInjectorServerURLs();
checkAuthlibInjectorAccounts();
checkProfileMap();
@@ -125,11 +136,11 @@ public class Settings {
public void save() {
try {
SETTINGS.getAccounts().clear();
SETTINGS.accounts.clear();
for (Account account : accounts.values()) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", Accounts.getAccountType(account));
SETTINGS.getAccounts().add(storage);
SETTINGS.accounts.add(storage);
}
FileUtils.writeText(SETTINGS_FILE, GSON.toJson(SETTINGS));
@@ -138,12 +149,13 @@ public class Settings {
}
}
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.getCommonDirectory()) {
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.commonDirectory.get()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setCommonDirectory(get());
SETTINGS.commonDirectory.set(get());
save();
}
};
@@ -159,7 +171,7 @@ public class Settings {
this.commonPath.set(commonPath);
}
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.getLocalization());
private Locales.SupportedLocale locale = Locales.getLocaleByName(SETTINGS.localization.get());
public Locales.SupportedLocale getLocale() {
return locale;
@@ -167,7 +179,8 @@ public class Settings {
public void setLocale(Locales.SupportedLocale locale) {
this.locale = locale;
SETTINGS.setLocalization(Locales.getNameByLocale(locale));
SETTINGS.localization.set(Locales.getNameByLocale(locale));
save();
}
private Proxy proxy = Proxy.NO_PROXY;
@@ -176,7 +189,7 @@ public class Settings {
return proxy;
}
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.getProxyType());
private Proxy.Type proxyType = Proxies.getProxyType(SETTINGS.proxyType.get());
public Proxy.Type getProxyType() {
return proxyType;
@@ -184,49 +197,64 @@ public class Settings {
public void setProxyType(Proxy.Type proxyType) {
this.proxyType = proxyType;
SETTINGS.setProxyType(Proxies.PROXIES.indexOf(proxyType));
SETTINGS.proxyType.set(Proxies.PROXIES.indexOf(proxyType));
save();
loadProxy();
}
public String getProxyHost() {
return SETTINGS.getProxyHost();
return SETTINGS.proxyHost.get();
}
public void setProxyHost(String proxyHost) {
SETTINGS.setProxyHost(proxyHost);
SETTINGS.proxyHost.set(proxyHost);
save();
}
public String getProxyPort() {
return SETTINGS.getProxyPort();
return SETTINGS.proxyPort.get();
}
public void setProxyPort(String proxyPort) {
SETTINGS.setProxyPort(proxyPort);
SETTINGS.proxyPort.set(proxyPort);
save();
}
public String getProxyUser() {
return SETTINGS.getProxyUser();
return SETTINGS.proxyUser.get();
}
public void setProxyUser(String proxyUser) {
SETTINGS.setProxyUser(proxyUser);
SETTINGS.proxyUser.set(proxyUser);
save();
}
public String getProxyPass() {
return SETTINGS.getProxyPass();
return SETTINGS.proxyPass.get();
}
public void setProxyPass(String proxyPass) {
SETTINGS.setProxyPass(proxyPass);
SETTINGS.proxyPass.set(proxyPass);
save();
}
public boolean hasProxy() { return SETTINGS.isHasProxy(); }
public boolean hasProxy() {
return SETTINGS.hasProxy.get();
}
public void setHasProxy(boolean hasProxy) { SETTINGS.setHasProxy(hasProxy); }
public void setHasProxy(boolean hasProxy) {
SETTINGS.hasProxy.set(hasProxy);
save();
}
public boolean hasProxyAuth() { return SETTINGS.isHasProxyAuth(); }
public boolean hasProxyAuth() {
return SETTINGS.hasProxyAuth.get();
}
public void setHasProxyAuth(boolean hasProxyAuth) { SETTINGS.setHasProxyAuth(hasProxyAuth); }
public void setHasProxyAuth(boolean hasProxyAuth) {
SETTINGS.hasProxyAuth.set(hasProxyAuth);
save();
}
private void loadProxy() {
String host = getProxyHost();
@@ -255,20 +283,22 @@ public class Settings {
}
public Font getFont() {
return Font.font(SETTINGS.getFontFamily(), SETTINGS.getFontSize());
return Font.font(SETTINGS.fontFamily.get(), SETTINGS.fontSize.get());
}
public void setFont(Font font) {
SETTINGS.setFontFamily(font.getFamily());
SETTINGS.setFontSize(font.getSize());
SETTINGS.fontFamily.set(font.getFamily());
SETTINGS.fontSize.set(font.getSize());
save();
}
public int getLogLines() {
return Math.max(SETTINGS.getLogLines(), 100);
return Math.max(SETTINGS.logLines.get(), 100);
}
public void setLogLines(int logLines) {
SETTINGS.setLogLines(logLines);
SETTINGS.logLines.set(logLines);
save();
}
/****************************************
@@ -276,35 +306,26 @@ public class Settings {
****************************************/
public Set<String> getAuthlibInjectorServerURLs() {
return SETTINGS.getAuthlibInjectorServerURLs();
return SETTINGS.authlibInjectorServerURLs;
}
public void removeAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().remove(serverURL);
SETTINGS.authlibInjectorServerURLs.remove(serverURL);
checkAuthlibInjectorAccounts();
save();
}
public void addAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().add(serverURL);
SETTINGS.authlibInjectorServerURLs.add(serverURL);
save();
}
private void checkAuthlibInjectorServerURLs() {
if (SETTINGS.getAuthlibInjectorServerURLs() == null)
SETTINGS.setAuthlibInjectorServerURLs(new HashSet<>());
}
private void checkAuthlibInjectorAccounts() {
for (Account account : getAccounts()) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount injectorAccount = (AuthlibInjectorAccount) account;
if (!SETTINGS.getAuthlibInjectorServerURLs().contains(injectorAccount.getServerBaseURL()))
if (!SETTINGS.authlibInjectorServerURLs.contains(injectorAccount.getServerBaseURL()))
deleteAccount(account);
}
}
@@ -315,21 +336,22 @@ public class Settings {
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(SETTINGS.getDownloadType());
return DownloadProviders.getDownloadProvider(SETTINGS.downloadType.get());
}
public void setDownloadProvider(DownloadProvider downloadProvider) {
int index = DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(downloadProvider);
if (index == -1)
throw new IllegalArgumentException("Unknown download provider: " + downloadProvider);
SETTINGS.setDownloadType(index);
SETTINGS.downloadType.set(index);
save();
}
/****************************************
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.getSelectedAccount())) {
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(SETTINGS.selectedAccount.get())) {
@Override
public Account get() {
Account a = super.get();
@@ -351,7 +373,8 @@ public class Settings {
public void invalidated() {
super.invalidated();
SETTINGS.setSelectedAccount(getValue() == null ? "" : Accounts.getAccountId(getValue()));
SETTINGS.selectedAccount.set(getValue() == null ? "" : Accounts.getAccountId(getValue()));
save();
}
};
@@ -370,6 +393,8 @@ public class Settings {
public void addAccount(Account account) {
accounts.put(Accounts.getAccountId(account), account);
onAccountLoading();
EventBus.EVENT_BUS.fireEvent(new AccountAddedEvent(this, account));
}
public Account getAccount(String name, String character) {
@@ -398,12 +423,13 @@ public class Settings {
* BACKGROUND *
****************************************/
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.getBackgroundImage()) {
private final ImmediateStringProperty backgroundImage = new ImmediateStringProperty(this, "backgroundImage", SETTINGS.backgroundImage.get()) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setBackgroundImage(get());
SETTINGS.backgroundImage.set(get());
save();
}
};
@@ -419,12 +445,13 @@ public class Settings {
this.backgroundImage.set(backgroundImage);
}
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.getBackgroundImageType())) {
private final ImmediateObjectProperty<EnumBackgroundImage> backgroundImageType = new ImmediateObjectProperty<EnumBackgroundImage>(this, "backgroundImageType", EnumBackgroundImage.indexOf(SETTINGS.backgroundImageType.get())) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setBackgroundImageType(get().ordinal());
SETTINGS.backgroundImageType.set(get().ordinal());
save();
}
};
@@ -444,12 +471,13 @@ public class Settings {
* THEME *
****************************************/
private final ImmediateObjectProperty<Theme> theme = new ImmediateObjectProperty<Theme>(this, "theme", Theme.getTheme(SETTINGS.getTheme()).orElse(Theme.BLUE)) {
private final ImmediateObjectProperty<Theme> theme = new ImmediateObjectProperty<Theme>(this, "theme", Theme.getTheme(SETTINGS.theme.get()).orElse(Theme.BLUE)) {
@Override
public void invalidated() {
super.invalidated();
SETTINGS.setTheme(get().getName().toLowerCase());
SETTINGS.theme.set(get().getName().toLowerCase());
save();
}
};
@@ -472,16 +500,20 @@ public class Settings {
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(SETTINGS.getSelectedProfile())) {
getProfileMap().keySet().stream().findFirst().ifPresent(SETTINGS::setSelectedProfile);
if (!hasProfile(SETTINGS.selectedProfile.get())) {
getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> {
SETTINGS.selectedProfile.set(selectedProfile);
save();
});
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(SETTINGS.getSelectedProfile());
return getProfile(SETTINGS.selectedProfile.get());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.getSelectedProfile())) {
SETTINGS.setSelectedProfile(selectedProfile.getName());
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), SETTINGS.selectedProfile.get())) {
SETTINGS.selectedProfile.set(selectedProfile.getName());
save();
Schedulers.computation().schedule(this::onProfileChanged);
}
}
@@ -498,7 +530,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return SETTINGS.getConfigurations();
return SETTINGS.configurations;
}
public Collection<Profile> getProfiles() {
@@ -555,4 +587,8 @@ public class Settings {
public void onAccountLoading() {
EventBus.EVENT_BUS.fireEvent(new AccountLoadingEvent(this, getAccounts()));
}
public Config getRawConfig() {
return SETTINGS.clone();
}
}
@@ -26,7 +26,9 @@ import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
*
@@ -87,6 +89,20 @@ public final class VersionSetting {
javaProperty.set(java);
}
private final ImmediateStringProperty defaultJavaPathProperty = new ImmediateStringProperty(this, "defaultJavaPath", "");
/**
* Path to Java executable, or null if user customizes java directory.
* It's used to determine which JRE to use when multiple JREs match the selected Java version.
*/
public String getDefaultJavaPath() {
return defaultJavaPathProperty.get();
}
public void setDefaultJavaPath(String defaultJavaPath) {
defaultJavaPathProperty.set(defaultJavaPath);
}
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
public ImmediateStringProperty javaDirProperty() {
@@ -444,12 +460,18 @@ public final class VersionSetting {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
JavaVersion c = JavaVersion.getJREs().get(getJava());
if (c == null) {
List<JavaVersion> matchedJava = JavaVersion.getJREs().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
setJava("Default");
return JavaVersion.fromCurrentEnvironment();
} else
return c;
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
}
@@ -475,6 +497,7 @@ public final class VersionSetting {
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
}
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
@@ -535,6 +558,7 @@ public final class VersionSetting {
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
return obj;
}
@@ -571,6 +595,7 @@ public final class VersionSetting {
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
return vs;
}
@@ -17,11 +17,11 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
@@ -35,18 +35,19 @@ public class AccountLoginPane extends StackPane {
private final Account oldAccount;
private final Consumer<AuthInfo> success;
private final Runnable failed;
private final Consumer<Region> closeConsumer;
@FXML
private Label lblUsername;
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar;
private JFXDialog dialog;
public AccountLoginPane(Account oldAccount, Consumer<AuthInfo> success, Runnable failed) {
public AccountLoginPane(Account oldAccount, Consumer<Region> closeConsumer, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount;
this.success = success;
this.failed = failed;
this.closeConsumer = closeConsumer;
FXUtils.loadFXML(this, "/assets/fxml/account-login.fxml");
@@ -69,9 +70,9 @@ public class AccountLoginPane extends StackPane {
Object account = variable.get("login");
if (account instanceof AuthInfo) {
success.accept(((AuthInfo) account));
dialog.close();
closeConsumer.accept(this);
} else if (account instanceof NoSelectedCharacterException) {
dialog.close();
closeConsumer.accept(this);
} else if (account instanceof Exception) {
lblCreationWarning.setText(AddAccountPane.accountException((Exception) account));
}
@@ -83,10 +84,6 @@ public class AccountLoginPane extends StackPane {
@FXML
private void onCancel() {
failed.run();
dialog.close();
}
public void setDialog(JFXDialog dialog) {
this.dialog = dialog;
closeConsumer.accept(this);
}
}
@@ -28,6 +28,7 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
@@ -40,6 +41,8 @@ import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -49,6 +52,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -61,17 +65,23 @@ public class AddAccountPane extends StackPane {
@FXML private Label lblPassword;
@FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<TwoLineListItem> cboServers;
@FXML private JFXProgressBar progressBar;
@FXML private Label lblAddInjectorServer;
@FXML private Hyperlink linkAddInjectorServer;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
private final Runnable finalization;
@FXML private JFXButton btnAccept;
@FXML private StackPane acceptPane;
@FXML private JFXSpinner spinnerAccept;
private final Consumer<Region> finalization;
private final TransitionHandler transitionHandler;
public AddAccountPane(Runnable finalization) {
public AddAccountPane(Consumer<Region> finalization) {
this.finalization = finalization;
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
transitionHandler = new TransitionHandler(acceptPane);
acceptPane.getChildren().setAll(btnAccept);
loadServers();
cboType.getItems().setAll(Launcher.i18n("account.methods.offline"), Launcher.i18n("account.methods.yggdrasil"), Launcher.i18n("account.methods.authlib_injector"));
@@ -79,8 +89,9 @@ public class AddAccountPane extends StackPane {
txtPassword.setVisible(newValue.intValue() != 0);
lblPassword.setVisible(newValue.intValue() != 0);
cboServers.setVisible(newValue.intValue() == 2);
linkAddInjectorServer.setVisible(newValue.intValue() == 2);
lblAddInjectorServer.setVisible(newValue.intValue() == 2);
linkManageInjectorServers.setVisible(newValue.intValue() == 2);
lblInjectorServer.setVisible(newValue.intValue() == 2);
validateAcceptButton();
});
cboType.getSelectionModel().select(0);
@@ -92,6 +103,12 @@ public class AddAccountPane extends StackPane {
txtUsername.setOnAction(e -> onCreationAccept());
txtUsername.getValidators().add(new Validator(Launcher.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
txtUsername.textProperty().addListener(it -> validateAcceptButton());
txtPassword.textProperty().addListener(it -> validateAcceptButton());
}
private void validateAcceptButton() {
btnAccept.setDisable(!txtUsername.validate() || (cboType.getSelectionModel().getSelectedIndex() != 0 && !txtPassword.validate()));
}
private void loadServers() {
@@ -112,13 +129,21 @@ public class AddAccountPane extends StackPane {
}));
}
private void showSpinner() {
transitionHandler.setContent(spinnerAccept, ContainerAnimations.FADE.getAnimationProducer());
}
private void hideSpinner() {
transitionHandler.setContent(btnAccept, ContainerAnimations.FADE.getAnimationProducer());
}
@FXML
private void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex();
String username = txtUsername.getText();
String password = txtPassword.getText();
String apiRoot = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem()).map(TwoLineListItem::getSubtitle).orElse(null);
progressBar.setVisible(true);
showSpinner();
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> {
AccountFactory<?> factory;
@@ -132,26 +157,26 @@ public class AddAccountPane extends StackPane {
return factory.create(new Selector(), username, password, apiRoot, Settings.INSTANCE.getProxy());
}).finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
progressBar.setVisible(false);
finalization.run();
hideSpinner();
finalization.accept(this);
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
finalization.run();
finalization.accept(this);
} else {
lblCreationWarning.setText(accountException(exception));
}
progressBar.setVisible(false);
hideSpinner();
}).start();
}
@FXML
private void onCreationCancel() {
finalization.run();
finalization.accept(this);
}
@FXML
private void onAddInjecterServer() {
finalization.run();
private void onManageInjecterServers() {
finalization.accept(this);
Controllers.navigate(Controllers.getServersPage());
}
@@ -198,6 +223,7 @@ public class AddAccountPane extends StackPane {
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = FXUtils.DEFAULT_ICON;
}
ImageView portraitView = new ImageView();
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXDialog;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
@@ -33,7 +31,6 @@ import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.JavaVersion;
import org.jackhuang.hmcl.util.Lang;
import java.util.function.Consumer;
@@ -117,14 +114,9 @@ public final class Controllers {
stage.setTitle(Launcher.TITLE);
}
public static Region getDialogContent() {
return decorator.getDialog().getContent();
}
public static JFXDialog dialog(Region content) {
// TODO: temp fix
decorator.showDialog(Lang.apply(new Region(), region -> region.getProperties().put("controllers", true)));
return decorator.showDialog(content);
public static void dialog(Region content) {
if (decorator != null)
decorator.showDialog(content);
}
public static void dialog(String text) {
@@ -140,31 +132,33 @@ public final class Controllers {
}
public static void dialog(String text, String title, int type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, decorator.getDialog(), type, onAccept));
dialog(new MessageDialogPane(text, title, Controllers::closeDialog, type, onAccept));
}
public static void confirmDialog(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, decorator.getDialog(), onAccept, onCancel));
dialog(new MessageDialogPane(text, title, Controllers::closeDialog, onAccept, onCancel));
}
public static void inputDialog(String text, Consumer<String> onResult) {
dialog(new InputDialogPane(text, decorator.getDialog(), onResult));
dialog(new InputDialogPane(text, Controllers::closeDialog, onResult));
}
public static void taskDialog(TaskExecutor executor, String title, String subtitle) {
taskDialog(executor, title, subtitle, null);
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
return taskDialog(executor, title, subtitle, null);
}
public static void taskDialog(TaskExecutor executor, String title, String subtitle, Runnable onCancel) {
public static Region taskDialog(TaskExecutor executor, String title, String subtitle, Consumer<Region> onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setSubtitle(subtitle);
pane.setExecutor(executor);
dialog(pane);
return pane;
}
public static void closeDialog() {
decorator.getDialog().close();
public static void closeDialog(Region content) {
decorator.closeDialog(content);
}
public static void navigate(Node node) {
@@ -175,6 +169,8 @@ public final class Controllers {
}
public static void showUpdate() {
if (stage == null) // shut down
return;
getLeftPaneController().showUpdate();
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
@@ -167,17 +166,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
btnMax.fire();
});
dialog = new JFXDialog() {
@Override
public void close() {
dialogPane.pop();
if (dialogPane.getChildren().isEmpty())
Platform.runLater(() -> {
if (dialogPane.getChildren().isEmpty())
super.close();
});
}
};
dialog = new JFXDialog();
dialog.setOverlayClose(false);
drawerWrapper.getChildren().add(0, dialog);
dialog.setDialogContainer(drawerWrapper);
@@ -202,14 +191,17 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
private void loadBackground() {
Image background;
try {
Image background;
if (Settings.INSTANCE.getBackgroundImageType() == EnumBackgroundImage.DEFAULT)
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), "");
else
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), Settings.INSTANCE.getBackgroundImage());
if (Settings.INSTANCE.getBackgroundImageType() == EnumBackgroundImage.DEFAULT)
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), "");
else
background = searchBackgroundImage(new Image("/assets/img/background.jpg"), Settings.INSTANCE.getBackgroundImage());
drawerWrapper.setBackground(new Background(new BackgroundImage(background, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true))));
drawerWrapper.setBackground(new Background(new BackgroundImage(background, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true))));
} catch (IllegalArgumentException ignore) {
}
}
private static Image searchBackgroundImage(Image def, String customPath) {
@@ -525,6 +517,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
public void showPage(Node content) {
FXUtils.checkFxUserThread();
contentPlaceHolder.getStyleClass().removeAll("gray-background", "white-background");
if (content != null)
contentPlaceHolder.getStyleClass().add("gray-background");
@@ -545,11 +539,23 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
}
public JFXDialog showDialog(Node node) {
public void showDialog(Node node) {
FXUtils.checkFxUserThread();
if (dialogPane.isEmpty())
dialog.show();
dialogPane.push(node);
return dialog;
}
public void closeDialog(Node node) {
FXUtils.checkFxUserThread();
dialogPane.pop(node);
if (dialogPane.getChildren().isEmpty())
Platform.runLater(() -> {
if (dialogPane.getChildren().isEmpty())
dialog.close();
});
}
public void startWizard(WizardProvider wizardProvider) {
@@ -557,6 +563,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
public void startWizard(WizardProvider wizardProvider, String category) {
FXUtils.checkFxUserThread();
this.category = category;
wizardController.setProvider(wizardProvider);
wizardController.onStart();
@@ -638,10 +646,6 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
return wizardController;
}
public JFXDialog getDialog() {
return dialog;
}
public AdvancedListBox getLeftPane() {
return leftPane;
}
@@ -34,11 +34,11 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
JFXUtilities.runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
AccountLoginPane pane = new AccountLoginPane(account, Controllers::closeDialog, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
pane.setDialog(Controllers.dialog(pane));
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
@@ -47,21 +46,28 @@ import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.ReflectionHelper.call;
import static org.jackhuang.hmcl.util.ReflectionHelper.construct;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public final class FXUtils {
private FXUtils() {
}
public static void checkFxUserThread() {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not on FX application thread; currentThread = "
+ Thread.currentThread().getName());
}
}
public static <T> void onChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener((a, b, c) -> consumer.accept(c));
}
@@ -118,7 +124,7 @@ public final class FXUtils {
}
public static void removeListener(Node node, String key) {
Lang.cast(node.getProperties().get(key), ListenerPair.class)
tryCast(node.getProperties().get(key), ListenerPair.class)
.ifPresent(info -> {
info.unbind();
node.getProperties().remove(key);
@@ -194,12 +200,6 @@ public final class FXUtils {
Lang.invoke((ExceptionalSupplier<Object, IOException>) loader::load);
}
public static void resetChildren(JFXMasonryPane pane, List<Node> children) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane.class, pane, "oldBoxes", null);
pane.getChildren().setAll(children);
}
public static void installTooltip(Node node, String tooltip) {
installTooltip(node, 0, 5000, 0, new Tooltip(tooltip));
}
@@ -207,17 +207,22 @@ public final class FXUtils {
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
try {
// Java 8
call(construct(Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"), new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false),
"install", node, tooltip);
} catch (Throwable e) {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
behaviorConstructor.setAccessible(true);
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installMethod.setAccessible(true);
installMethod.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
try {
// Java 9
call(tooltip, "setShowDelay", new Duration(openDelay));
call(tooltip, "setShowDuration", new Duration(visibleDelay));
call(tooltip, "setHideDelay", new Duration(closeDelay));
} catch (Throwable e2) {
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip by reflection", e);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
}
Tooltip.install(node, tooltip);
}
@@ -326,7 +331,7 @@ public final class FXUtils {
*/
@SuppressWarnings("unchecked")
public static void unbindEnum(JFXComboBox<?> comboBox) {
ChangeListener listener = Lang.get(comboBox.getProperties(), "FXUtils.bindEnum.listener", ChangeListener.class).orElse(null);
ChangeListener listener = tryCast(comboBox.getProperties().get("FXUtils.bindEnum.listener"), ChangeListener.class).orElse(null);
if (listener == null) return;
comboBox.getSelectionModel().selectedIndexProperty().removeListener(listener);
}
@@ -25,6 +25,7 @@ import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.Launcher;
@@ -32,6 +33,7 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -41,6 +43,7 @@ import org.jackhuang.hmcl.mod.UnsupportedModpackException;
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.IconedItem;
@@ -48,8 +51,10 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Lang;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public final class LeftPaneController {
private final AdvancedListBox leftPane;
@@ -57,6 +62,7 @@ public final class LeftPaneController {
private final VBox accountPane = new VBox();
private final IconedItem launcherSettingsItem;
private final VersionListItem missingAccountItem = new VersionListItem(Launcher.i18n("account.missing"), Launcher.i18n("message.unknown"));
private final HashMap<Account, VersionListItem> items = new HashMap<>();
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
@@ -83,6 +89,7 @@ public final class LeftPaneController {
})))
.add(profilePane);
EventBus.EVENT_BUS.channel(AccountAddedEvent.class).register(this::onAccountAdd);
EventBus.EVENT_BUS.channel(AccountLoadingEvent.class).register(this::onAccountsLoading);
EventBus.EVENT_BUS.channel(ProfileLoadingEvent.class).register(this::onProfilesLoading);
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(this::onProfileChanged);
@@ -141,11 +148,24 @@ public final class LeftPaneController {
else throw new Error(Launcher.i18n("account.methods.no_method") + ": " + account);
}
private void onAccountAdd(AccountAddedEvent event) {
Account account = event.getAccount();
VersionListItem item = items.get(account);
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.subscribe(Schedulers.javafx(), () -> {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
});
}
private void onAccountsLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
items.clear();
Account selectedAccount = Settings.INSTANCE.getSelectedAccount();
for (Account account : Settings.INSTANCE.getAccounts()) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountType(account));
items.put(account, item);
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
@@ -164,7 +184,7 @@ public final class LeftPaneController {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
} else
item.setImage(AccountHelper.getDefaultSkin(account, 4), AccountHelper.getViewport(4));
item.setImage(AccountHelper.getDefaultSkin(account.getUUID(), 4), AccountHelper.getViewport(4));
if (account instanceof AuthlibInjectorAccount)
Accounts.getAuthlibInjectorServerNameAsync((AuthlibInjectorAccount) account)
@@ -192,10 +212,10 @@ public final class LeftPaneController {
}
private boolean checkedModpack = false;
private static boolean showNewAccount = true;
private void onRefreshedVersions(RefreshedVersionsEvent event) {
JFXUtilities.runInFX(() -> {
boolean flag = true;
HMCLGameRepository repository = (HMCLGameRepository) event.getSource();
if (!checkedModpack) {
checkedModpack = true;
@@ -204,22 +224,26 @@ public final class LeftPaneController {
File modpackFile = new File("modpack.zip").getAbsoluteFile();
if (modpackFile.exists()) {
try {
AtomicReference<Region> region = new AtomicReference<>();
Modpack modpack = ModpackHelper.readModpackManifest(modpackFile);
Controllers.taskDialog(ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
Controllers.closeDialog();
checkAccount();
})).executor(true),
Launcher.i18n("modpack.installing"), "", null);
flag = false;
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
.with(Task.of(Schedulers.javafx(), () -> {
Controllers.closeDialog(region.get());
checkAccount();
})).executor();
region.set(Controllers.taskDialog(executor, Launcher.i18n("modpack.installing"), ""));
executor.start();
showNewAccount = false;
} catch (UnsupportedModpackException ignore) {
}
}
}
}
if (flag)
if (showNewAccount) {
showNewAccount = false;
checkAccount();
}
});
}
@@ -23,8 +23,10 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
@@ -41,18 +43,20 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.StringUtils.removePrefix;
import static org.jackhuang.hmcl.util.StringUtils.removeSuffix;
public final class MainPage extends StackPane implements DecoratorPage {
@@ -70,6 +74,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
private JFXSpinner spinner;
@FXML
private JFXMasonryPane masonryPane;
@FXML
private ScrollPane scrollPane;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
@@ -82,7 +88,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
});
EventBus.EVENT_BUS.channel(RefreshingVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
JFXUtilities.runInFXAndWait(this::loadingVersions);
// This will occupy 0.5s. Too slow!
JFXUtilities.runInFX(this::loadingVersions);
});
EventBus.EVENT_BUS.channel(ProfileChangedEvent.class).register(event -> {
this.profile = event.getProfile();
@@ -94,28 +101,35 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.installTooltip(btnRefresh, Launcher.i18n("button.refresh"));
}
private Node buildNode(HMCLGameRepository repository, Version version, String game) {
private String modifyVersion(String gameVersion, String version) {
return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_");
}
private Node buildNode(HMCLGameRepository repository, Version version, Callable<String> gameCallable) {
Profile profile = repository.getProfile();
String id = version.getId();
VersionItem item = new VersionItem();
item.setUpdate(repository.isModpack(id));
item.setGameVersion(game);
Task.ofResult("game", gameCallable).subscribe(Schedulers.javafx(), vars -> {
String game = vars.get("game");
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Launcher.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(Launcher.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(Launcher.i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
item.setLibraries(libraries.toString());
});
item.setVersionName(id);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(Launcher.i18n("install.installer.forge")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)forge", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(Launcher.i18n("install.installer.liteloader")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)liteloader", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
libraries.append(Launcher.i18n("install.installer.optifine")).append(": ").append(StringUtils.removeSuffix(StringUtils.removePrefix(library.getVersion().replaceAll("(?i)optifine", "").replace(game, "").trim(), "-"), "-")).append("\n");
}
}
item.setLibraries(libraries.toString());
item.setOnLaunchButtonClicked(e -> {
if (Settings.INSTANCE.getSelectedAccount() == null)
Controllers.getLeftPaneController().checkAccount();
@@ -148,18 +162,20 @@ public final class MainPage extends StackPane implements DecoratorPage {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(Launcher.i18n("modpack"), "*.zip"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
AtomicReference<Region> region = new AtomicReference<>();
try {
TaskExecutor executor = ModpackHelper.getUpdateTask(profile, selectedFile, id, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(id)))
.then(Task.of(Schedulers.javafx(), Controllers::closeDialog)).executor(true);
Controllers.taskDialog(executor, Launcher.i18n("modpack.update"), "", null);
.then(Task.of(Schedulers.javafx(), () -> Controllers.closeDialog(region.get()))).executor();
region.set(Controllers.taskDialog(executor, Launcher.i18n("modpack.update"), ""));
executor.start();
} catch (UnsupportedModpackException e) {
Controllers.closeDialog();
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.unsupported"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (MismatchedModpackTypeException e) {
Controllers.closeDialog();
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.mismatched_type"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
Controllers.closeDialog();
} catch (IOException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.invalid"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
}
}
@@ -176,7 +192,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
Launcher.i18n("modpack.export"),
Launcher.i18n("folder.game")
));
versionList.setOnMouseClicked(e ->{
versionList.setOnMouseClicked(e -> {
versionPopup.hide();
switch (versionList.getSelectionModel().getSelectedIndex()) {
case 0:
@@ -192,8 +208,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.openFolder(repository.getRunDirectory(id));
break;
default:
throw new Error();
}});
break;
}
});
versionPopup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
} else if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
if (Settings.INSTANCE.getSelectedAccount() == null)
@@ -210,18 +227,17 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void loadingVersions() {
getChildren().setAll(spinner);
FXUtils.resetChildren(masonryPane, Collections.emptyList());
masonryPane.getChildren().clear();
}
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = new LinkedList<>();
for (Version version : repository.getVersions()) {
children.add(buildNode(repository, version, GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")));
}
List<Node> children = repository.getVersions().parallelStream()
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile()) {
masonryPane.getChildren().setAll(children);
getChildren().setAll(contentPane);
FXUtils.resetChildren(masonryPane, children);
}
});
}
@@ -37,9 +37,11 @@ import org.jackhuang.hmcl.util.Logging;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class ModController {
@@ -68,21 +70,22 @@ public final class ModController {
});
rootPane.setOnDragDropped(event -> {
List<File> mods = event.getDragboard().getFiles();
Stream<File> stream = null;
if (mods != null)
stream = mods.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)));
if (stream != null && stream.findAny().isPresent()) {
stream.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
List<File> files = event.getDragboard().getFiles();
if (files != null) {
Collection<File> mods = files.stream()
.filter(it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)))
.collect(Collectors.toList());
if (!mods.isEmpty()) {
mods.forEach(it -> {
try {
modManager.addMod(versionId, it);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
}
});
loadMods(modManager, versionId);
event.setDropCompleted(true);
}
}
event.consume();
});
@@ -21,6 +21,8 @@ import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
@@ -28,10 +30,9 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
@@ -65,8 +66,107 @@ public final class VersionItem extends StackPane {
private EventHandler<? super MouseEvent> launchClickedHandler = null;
private void initializeComponents() {
setPickOnBounds(false);
FXUtils.setLimitWidth(this, 160);
FXUtils.setLimitHeight(this, 156);
content = new VBox();
{
header = new StackPane();
VBox.setVgrow(header, Priority.ALWAYS);
header.setPickOnBounds(false);
header.setPadding(new Insets(8));
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87);");
{
VBox headerContent = new VBox();
headerContent.setPadding(new Insets(8, 8, 0, 8));
{
lblVersionName = new Label();
lblVersionName.setStyle("-fx-font-size: 15;");
lblVersionName.setTextAlignment(TextAlignment.JUSTIFY);
lblVersionName.setWrapText(true);
headerContent.getChildren().add(lblVersionName);
lblGameVersion = new Label();
lblGameVersion.setStyle("-fx-font-size: 11;");
lblGameVersion.setTextAlignment(TextAlignment.JUSTIFY);
lblGameVersion.setWrapText(true);
headerContent.getChildren().add(lblGameVersion);
lblLibraries = new Label();
lblLibraries.setStyle("-fx-font-size: 10; -fx-text-fill: gray;");
lblLibraries.setTextAlignment(TextAlignment.JUSTIFY);
lblLibraries.setWrapText(true);
headerContent.getChildren().add(lblLibraries);
}
header.getChildren().add(headerContent);
}
content.getChildren().add(header);
body = new StackPane();
body.setStyle("-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;");
body.setMinHeight(40);
body.setPickOnBounds(false);
{
BorderPane bodyContent = new BorderPane();
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnSettings = new JFXButton();
btnSettings.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnSettings, 30);
FXUtils.setLimitHeight(btnSettings, 30);
hbox.getChildren().add(btnSettings);
btnUpdate = new JFXButton();
btnUpdate.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnUpdate, 30);
FXUtils.setLimitHeight(btnUpdate, 30);
hbox.getChildren().add(btnUpdate);
}
bodyContent.setLeft(hbox);
}
{
HBox hbox = new HBox();
hbox.setSpacing(8);
{
btnScript = new JFXButton();
btnScript.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnScript, 30);
FXUtils.setLimitHeight(btnScript, 30);
hbox.getChildren().add(btnScript);
btnLaunch = new JFXButton();
btnLaunch.getStyleClass().add("toggle-icon4");
FXUtils.setLimitWidth(btnLaunch, 30);
FXUtils.setLimitHeight(btnLaunch, 30);
hbox.getChildren().add(btnLaunch);
}
bodyContent.setRight(hbox);
}
body.getChildren().setAll(bodyContent);
}
content.getChildren().add(body);
}
getChildren().add(content);
icon = new StackPane();
StackPane.setAlignment(icon, Pos.TOP_RIGHT);
icon.setPickOnBounds(false);
{
iconView = new ImageView();
StackPane.setAlignment(iconView, Pos.CENTER_RIGHT);
StackPane.setMargin(iconView, new Insets(0, 12, 0, 0));
iconView.setImage(new Image("/assets/img/icon.png"));
icon.getChildren().add(iconView);
}
getChildren().add(icon);
}
public VersionItem() {
FXUtils.loadFXML(this, "/assets/fxml/version-item.fxml");
initializeComponents();
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0));
btnSettings.setGraphic(SVG.gear(Theme.blackFillBinding(), 15, 15));
btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 15, 15));
@@ -23,7 +23,6 @@ import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
@@ -44,7 +43,7 @@ import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -84,11 +83,14 @@ public final class VersionSettingsController {
FXUtils.smoothScrolling(scroll);
Task.of(variables -> variables.set("list", JavaVersion.getJREs().values().stream().map(javaVersion ->
javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion)
).collect(Collectors.toList()))).subscribe(Schedulers.javafx(), variables ->
javaItem.loadChildren(variables.<Collection<Node>>get("list"))
);
Task.of(variables -> variables.set("list", JavaVersion.getJREs()))
.subscribe(Schedulers.javafx(), variables -> {
javaItem.loadChildren(
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion))
.collect(Collectors.toList()));
initializeSelectedJava();
});
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaItem.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
@@ -151,29 +153,17 @@ public final class VersionSettingsController {
FXUtils.bindBoolean(chkShowLogs, versionSetting.showLogsProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(versionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
}
}
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.isCustomToggle(newValue)) {
versionSetting.setJava("Custom");
versionSetting.setDefaultJavaPath(null);
} else {
versionSetting.setJava(((JavaVersion) newValue.getUserData()).getVersion());
JavaVersion java = (JavaVersion) newValue.getUserData();
versionSetting.setJava(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
}
});
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
versionSetting.javaDirProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
versionSetting.javaProperty().setChangedListener(it -> initJavaSubtitle(versionSetting));
initJavaSubtitle(versionSetting);
@@ -207,9 +197,33 @@ public final class VersionSettingsController {
lastVersionSetting = versionSetting;
initializeSelectedJava();
loadIcon();
}
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !JavaVersion.getJREsImmediately().isPresent() /* JREs are still being loaded */) {
return;
}
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(lastVersionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
}
}
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
}
}
private void initJavaSubtitle(VersionSetting versionSetting) {
Task.of(variables -> variables.set("java", versionSetting.getJavaVersion()))
.subscribe(Task.of(Schedulers.javafx(),
@@ -22,6 +22,7 @@ import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
public final class TransitionHandler implements AnimationHandler {
@@ -36,6 +37,12 @@ public final class TransitionHandler implements AnimationHandler {
public TransitionHandler(StackPane view) {
this.view = view;
currentNode = view.getChildren().stream().findFirst().orElse(null);
// prevent content overflow
Rectangle clip = new Rectangle();
clip.widthProperty().bind(view.widthProperty());
clip.heightProperty().bind(view.heightProperty());
view.setClip(clip);
}
@Override
@@ -18,18 +18,16 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
public class InputDialogPane extends StackPane {
private final String text;
private final JFXDialog dialog;
@FXML
private JFXButton acceptButton;
@@ -40,16 +38,13 @@ public class InputDialogPane extends StackPane {
@FXML
private Label content;
public InputDialogPane(String text, JFXDialog dialog, Consumer<String> onResult) {
this.text = text;
this.dialog = dialog;
public InputDialogPane(String text, Consumer<Region> closeConsumer, Consumer<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text);
cancelButton.setOnMouseClicked(e -> dialog.close());
cancelButton.setOnMouseClicked(e -> closeConsumer.accept(this));
acceptButton.setOnMouseClicked(e -> {
onResult.accept(textField.getText());
dialog.close();
closeConsumer.accept(this);
});
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
@@ -18,10 +18,10 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.Theme;
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.util.Optional;
import java.util.function.Consumer;
public final class MessageDialogPane extends StackPane {
private boolean closingDialog = true;
@@ -46,7 +47,7 @@ public final class MessageDialogPane extends StackPane {
@FXML
private HBox actions;
public MessageDialogPane(String text, String title, JFXDialog dialog, int type, Runnable onAccept) {
public MessageDialogPane(String text, String title, Consumer<Region> closeConsumer, int type, Runnable onAccept) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
if (title != null)
@@ -54,8 +55,7 @@ public final class MessageDialogPane extends StackPane {
content.setText(text);
acceptButton.setOnMouseClicked(e -> {
if (closingDialog)
dialog.close();
closeConsumer.accept(MessageDialogPane.this);
Optional.ofNullable(onAccept).ifPresent(Runnable::run);
});
@@ -82,13 +82,12 @@ public final class MessageDialogPane extends StackPane {
}
}
public MessageDialogPane(String text, String title, JFXDialog dialog, Runnable onAccept, Runnable onCancel) {
this(text, title, dialog, MessageBox.QUESTION_MESSAGE, onAccept);
public MessageDialogPane(String text, String title, Consumer<Region> closeConsumer, Runnable onAccept, Runnable onCancel) {
this(text, title, closeConsumer, MessageBox.QUESTION_MESSAGE, onAccept);
cancelButton.setVisible(true);
cancelButton.setOnMouseClicked(e -> {
if (closingDialog)
dialog.close();
closeConsumer.accept(MessageDialogPane.this);
Optional.ofNullable(onCancel).ifPresent(Runnable::run);
});
@@ -32,7 +32,6 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@@ -52,7 +51,6 @@ public class MultiFileItem extends ComponentList {
private final StringProperty customTitle = new SimpleStringProperty(this, "customTitle", Launcher.i18n("selector.custom"));
private final StringProperty chooserTitle = new SimpleStringProperty(this, "chooserTitle", Launcher.i18n("selector.choose_file"));
private final BooleanProperty directory = new SimpleBooleanProperty(this, "directory", false);
private final SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip");
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private final ToggleGroup group = new ToggleGroup();
@@ -112,10 +110,6 @@ public class MultiFileItem extends ComponentList {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
});
Tooltip tip = new Tooltip();
tip.textProperty().bind(tooltipProperty());
Tooltip.install(this, tip);
}
public Node createChildren(String title) {
@@ -230,16 +224,4 @@ public class MultiFileItem extends ComponentList {
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public String getTooltip() {
return tooltip.get();
}
public StringProperty tooltipProperty() {
return tooltip;
}
public void setTooltip(String tooltip) {
this.tooltip.set(tooltip);
}
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.util.Logging;
import java.util.Stack;
@@ -26,17 +27,20 @@ public class StackContainerPane extends StackPane {
private final Stack<Node> stack = new Stack<>();
public void push(Node node) {
if (node.getProperties().containsKey("controllers"))
stack.push(node);
stack.push(node);
getChildren().setAll(node);
Logging.LOG.info(this + " " + stack);
}
public void pop() {
stack.pop();
public void pop(Node node) {
boolean flag = stack.remove(node);
if (stack.isEmpty())
getChildren().setAll();
else
getChildren().setAll(stack.peek());
Logging.LOG.info(this + " " + stack + ", removed: " + flag + ", object: " + node);
}
public boolean isEmpty() {
@@ -23,15 +23,17 @@ import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.Optional;
import java.util.function.Consumer;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Runnable onCancel;
private Consumer<Region> onCancel;
@FXML
private JFXProgressBar progressBar;
@@ -44,14 +46,14 @@ public class TaskExecutorDialogPane extends StackPane {
@FXML
private TaskListPane taskListPane;
public TaskExecutorDialogPane(Runnable cancel) {
public TaskExecutorDialogPane(Consumer<Region> cancel) {
FXUtils.loadFXML(this, "/assets/fxml/task-dialog.fxml");
setCancel(cancel);
btnCancel.setOnMouseClicked(e -> {
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
onCancel.run();
onCancel.accept(this);
});
}
@@ -93,7 +95,7 @@ public class TaskExecutorDialogPane extends StackPane {
progressBar.setProgress(progress);
}
public void setCancel(Runnable onCancel) {
public void setCancel(Consumer<Region> onCancel) {
this.onCancel = onCancel;
JFXUtilities.runInFX(() -> btnCancel.setDisable(onCancel == null));
@@ -33,8 +33,8 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
@Override
default void handleTask(Map<String, Object> settings, Task task) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(() -> {
Controllers.closeDialog();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(it -> {
Controllers.closeDialog(it);
Controllers.navigate(null);
});
@@ -60,20 +60,24 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
JFXUtilities.runInFX(() -> Controllers.dialog((String) settings.get("success_message"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null)));
else if (!settings.containsKey("forbid_success_message"))
JFXUtilities.runInFX(() -> Controllers.dialog(Launcher.i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null)));
} else {
if (executor.getLastException() == null)
return;
String appendix = StringUtils.getStackTrace(executor.getLastException());
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
JFXUtilities.runInFX(() -> Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null)));
else if (!settings.containsKey("forbid_failure_message"))
JFXUtilities.runInFX(() -> Controllers.dialog(appendix, Launcher.i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null)));
}
JFXUtilities.runInFX(() -> {
Controllers.closeDialog(pane);
if (success) {
if (settings.containsKey("success_message") && settings.get("success_message") instanceof String)
Controllers.dialog((String) settings.get("success_message"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null));
else if (!settings.containsKey("forbid_success_message"))
Controllers.dialog(Launcher.i18n("message.success"), null, MessageBox.FINE_MESSAGE, () -> Controllers.navigate(null));
} else {
if (executor.getLastException() == null)
return;
String appendix = StringUtils.getStackTrace(executor.getLastException());
if (settings.containsKey("failure_message") && settings.get("failure_message") instanceof String)
Controllers.dialog(appendix, (String) settings.get("failure_message"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null));
else if (!settings.containsKey("forbid_failure_message"))
Controllers.dialog(appendix, Launcher.i18n("wizard.failed"), MessageBox.ERROR_MESSAGE, () -> Controllers.navigate(null));
}
});
}
});
pane.setExecutor(executor);
@@ -1,7 +1,6 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TextInputControl;
@@ -17,10 +16,6 @@ public class URLValidator extends ValidatorBase {
super();
}
public URLValidator(@NamedArg("message") String message) {
super(message);
}
public ObservableList<String> getProtocols() {
return protocols;
}
@@ -35,6 +35,7 @@ import java.io.File;
import java.util.Map;
import static org.jackhuang.hmcl.Launcher.i18n;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public final class DownloadWizardProvider implements WizardProvider {
private Profile profile;
@@ -67,9 +68,9 @@ public final class DownloadWizardProvider implements WizardProvider {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null;
File selected = Lang.get(settings, ModpackPage.MODPACK_FILE, File.class).orElse(null);
Modpack modpack = Lang.get(settings, ModpackPage.MODPACK_CURSEFORGE_MANIFEST, Modpack.class).orElse(null);
String name = Lang.get(settings, ModpackPage.MODPACK_NAME, String.class).orElse(null);
File selected = tryCast(settings.get(ModpackPage.MODPACK_FILE), File.class).orElse(null);
Modpack modpack = tryCast(settings.get(ModpackPage.MODPACK_CURSEFORGE_MANIFEST), Modpack.class).orElse(null);
String name = tryCast(settings.get(ModpackPage.MODPACK_NAME), String.class).orElse(null);
if (selected == null || modpack == null || name == null) return null;
return ModpackHelper.getInstallTask(profile, selected, name, modpack);
@@ -18,14 +18,22 @@
package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.ZipEngine;
import java.io.File;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -46,16 +54,72 @@ public final class ExportWizardProvider implements WizardProvider {
public Object finish(Map<String, Object> settings) {
@SuppressWarnings("unchecked")
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
List<File> launcherJar = Launcher.getCurrentJarFiles();
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER) && launcherJar != null;
return new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(
(String) settings.get(ModpackInfoPage.MODPACK_NAME),
(String) settings.get(ModpackInfoPage.MODPACK_AUTHOR),
(String) settings.get(ModpackInfoPage.MODPACK_VERSION),
null,
(String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION),
null
), (File) settings.get(ModpackInfoPage.MODPACK_FILE));
return new Task() {
Task dependency = null;
@Override
public void execute() throws Exception {
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile;
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(
(String) settings.get(ModpackInfoPage.MODPACK_NAME),
(String) settings.get(ModpackInfoPage.MODPACK_AUTHOR),
(String) settings.get(ModpackInfoPage.MODPACK_VERSION),
null,
(String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION),
null
), tempModpack);
if (includeLauncher) {
dependency = dependency.then(Task.of(() -> {
boolean flag = true;
try (ZipEngine zip = new ZipEngine(modpackFile)) {
Config config = Settings.INSTANCE.getRawConfig();
config.hasProxy.set(false);
config.selectedProfile.set("");
config.commonDirectory.set(null);
config.fontFamily.set("Consolas");
config.fontSize.set(12);
config.localization.set(null);
config.accounts.clear();
config.selectedAccount.set("");
config.logLines.set(100);
config.configurations.clear();
zip.putTextFile(Settings.GSON.toJson(config), Settings.SETTINGS_FILE_NAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg);
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
zip.putFile(background_png, "background.png");
File background_jpg = new File("background.jpg").getAbsoluteFile();
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
}
}));
}
}
@Override
public Collection<? extends Task> getDependencies() {
return Collections.singleton(dependency);
}
};
}
@Override
@@ -33,8 +33,6 @@ import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
@@ -43,6 +41,9 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* @author huangyuhui
*/
@@ -154,19 +155,19 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
}
public static final String MODPACK_FILE_SELECTION = "modpack.accepted";
private static final Map<String, String> TRANSLATION = Lang.mapOf(
new Pair<>("minecraft/servers.dat", Launcher.i18n("modpack.files.servers_dat")),
new Pair<>("minecraft/saves", Launcher.i18n("modpack.files.saves")),
new Pair<>("minecraft/mods", Launcher.i18n("modpack.files.mods")),
new Pair<>("minecraft/config", Launcher.i18n("modpack.files.config")),
new Pair<>("minecraft/liteconfig", Launcher.i18n("modpack.files.liteconfig")),
new Pair<>("minecraft/resourcepacks", Launcher.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/resources", Launcher.i18n("modpack.files.resourcepacks")),
new Pair<>("minecraft/options.txt", Launcher.i18n("modpack.files.options_txt")),
new Pair<>("minecraft/optionsshaders.txt", Launcher.i18n("modpack.files.optionsshaders_txt")),
new Pair<>("minecraft/mods/VoxelMods", Launcher.i18n("modpack.files.mods.voxelmods")),
new Pair<>("minecraft/dumps", Launcher.i18n("modpack.files.dumps")),
new Pair<>("minecraft/blueprints", Launcher.i18n("modpack.files.blueprints")),
new Pair<>("minecraft/scripts", Launcher.i18n("modpack.files.scripts"))
private static final Map<String, String> TRANSLATION = mapOf(
pair("minecraft/servers.dat", Launcher.i18n("modpack.files.servers_dat")),
pair("minecraft/saves", Launcher.i18n("modpack.files.saves")),
pair("minecraft/mods", Launcher.i18n("modpack.files.mods")),
pair("minecraft/config", Launcher.i18n("modpack.files.config")),
pair("minecraft/liteconfig", Launcher.i18n("modpack.files.liteconfig")),
pair("minecraft/resourcepacks", Launcher.i18n("modpack.files.resourcepacks")),
pair("minecraft/resources", Launcher.i18n("modpack.files.resourcepacks")),
pair("minecraft/options.txt", Launcher.i18n("modpack.files.options_txt")),
pair("minecraft/optionsshaders.txt", Launcher.i18n("modpack.files.optionsshaders_txt")),
pair("minecraft/mods/VoxelMods", Launcher.i18n("modpack.files.mods.voxelmods")),
pair("minecraft/dumps", Launcher.i18n("modpack.files.dumps")),
pair("minecraft/blueprints", Launcher.i18n("modpack.files.blueprints")),
pair("minecraft/scripts", Launcher.i18n("modpack.files.scripts"))
);
}
@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -45,12 +46,16 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
@FXML
private JFXTextField txtModpackName;
@FXML
private JFXTextField txtModpackAuthor;@FXML
private JFXTextField txtModpackVersion;@FXML
private JFXTextField txtModpackAuthor;
@FXML
private JFXTextField txtModpackVersion;
@FXML
private JFXTextArea txtModpackDescription;
@FXML
private JFXToggleButton chkIncludeLauncher;@FXML
private JFXButton btnNext;@FXML
private JFXToggleButton chkIncludeLauncher;
@FXML
private JFXButton btnNext;
@FXML
private ScrollPane scroll;
public ModpackInfoPage(WizardController controller, String version) {
@@ -63,6 +68,10 @@ public final class ModpackInfoPage extends StackPane implements WizardPage {
txtModpackVersion.textProperty().addListener(e -> checkValidation());
txtModpackAuthor.setText(Optional.ofNullable(Settings.INSTANCE.getSelectedAccount()).map(Account::getUsername).orElse(""));
lblVersionName.setText(version);
List<File> launcherJar = Launcher.getCurrentJarFiles();
if (launcherJar == null)
chkIncludeLauncher.setDisable(true);
}
private void checkValidation() {
@@ -20,9 +20,9 @@ package org.jackhuang.hmcl.upgrade;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
@@ -33,11 +33,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
@@ -50,29 +49,24 @@ import java.util.zip.GZIPInputStream;
*/
public class AppDataUpgrader extends IUpgrader {
private boolean launchNewerVersion(List<String> args, File jar) throws IOException, PrivilegedActionException {
private void launchNewerVersion(List<String> args, File jar) throws IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, InvocationTargetException, IllegalAccessException {
try (JarFile jarFile = new JarFile(jar)) {
String mainClass = jarFile.getManifest().getMainAttributes().getValue("Main-Class");
if (mainClass != null) {
ArrayList<String> al = new ArrayList<>(args);
al.add("--noupdate");
ClassLoader pre = Thread.currentThread().getContextClassLoader();
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Void>) () -> {
Logging.stop();
ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
Thread.currentThread().setContextClassLoader(now);
now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
return null;
});
} finally {
Logging.start();
Thread.currentThread().setContextClassLoader(pre);
}
return true;
if (mainClass == null)
throw new ClassNotFoundException("Main-Class not found in manifest");
ArrayList<String> al = new ArrayList<>(args);
al.add("--noupdate");
ClassLoader pre = Thread.currentThread().getContextClassLoader();
try {
Logging.stop();
ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
Thread.currentThread().setContextClassLoader(now);
now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
} finally {
Logging.start(Launcher.LOG_DIRECTORY);
Thread.currentThread().setContextClassLoader(pre);
}
}
return false;
}
@Override
@@ -88,15 +82,18 @@ public class AppDataUpgrader extends IUpgrader {
String j = m.get("loc");
if (j != null) {
File jar = new File(j);
if (jar.exists() && launchNewerVersion(args, jar))
if (jar.exists()) {
launchNewerVersion(args, jar);
System.exit(0);
}
}
}
}
} catch (JsonParseException ex) {
f.delete();
} catch (IOException | PrivilegedActionException t) {
} catch (IOException | NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException | ClassNotFoundException t) {
Logging.LOG.log(Level.SEVERE, "Unable to execute newer version application", t);
AppDataUpgraderPackGzTask.HMCL_VER_FILE.delete(); // delete version json, let HMCL re-download the newer version.
}
}
@@ -108,56 +105,57 @@ public class AppDataUpgrader extends IUpgrader {
checker.requestDownloadLink().then(Task.of(variables -> {
Map<String, String> map = variables.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID);
if (MessageBox.confirm(Launcher.i18n("update.newest_version", version.toString()) + "\n"
+ Launcher.i18n("update.should_open_link"),
MessageBox.YES_NO_OPTION) == MessageBox.YES_OPTION)
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
Controllers.dialog(Launcher.i18n("message.downloading"));
if (new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash).test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
Controllers.closeDialog();
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
try {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash);
TaskExecutor executor = task.executor();
JFXUtilities.runInFX(() -> Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(Controllers::closeDialog);
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else {
String url = Launcher.PUBLISH;
if (map != null)
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
try {
java.awt.Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(Launcher.i18n("update.no_browser"));
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
Task task = new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
try {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else {
String url = Launcher.PUBLISH;
if (map != null)
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
try {
java.awt.Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(Launcher.i18n("update.no_browser"));
}
}
})).start();
}
@@ -195,7 +193,7 @@ public class AppDataUpgrader extends IUpgrader {
if (!FileUtils.makeDirectory(f.getParentFile()))
throw new IOException("Failed to make directories: " + f.getParent());
for (int i = 0; f.exists(); i++)
for (int i = 0; f.exists() && !f.delete(); i++)
f = new File(BASE_FOLDER, "HMCL-" + newestVersion + (i > 0 ? "-" + i : "") + ".jar");
if (!f.createNewFile())
throw new IOException("Failed to create new file: " + f);
@@ -213,7 +211,7 @@ public class AppDataUpgrader extends IUpgrader {
public static class AppDataUpgraderJarTask extends Task {
public static final File BASE_FOLDER = Launcher.getWorkingDirectory("hmcl");
public static final File BASE_FOLDER = OperatingSystem.getWorkingDirectory("hmcl");
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
public static File getSelf(String ver) {
@@ -17,10 +17,13 @@
*/
package org.jackhuang.hmcl.upgrade;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.VersionNumber;
@@ -28,8 +31,11 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huangyuhui
@@ -52,7 +58,11 @@ public class NewFileUpgrader extends IUpgrader {
if (url == null) return;
File newf = new File(url.getFile());
Controllers.dialog(Launcher.i18n("message.downloading"));
if (new FileDownloadTask(url, newf).test()) {
Task task = new FileDownloadTask(url, newf);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, Launcher.i18n("message.downloading"), "", null)));
if (executor.test()) {
try {
new ProcessBuilder(newf.getCanonicalPath(), "--removeOldLauncher", getRealPath())
.directory(new File("").getAbsoluteFile())
@@ -62,7 +72,7 @@ public class NewFileUpgrader extends IUpgrader {
}
System.exit(0);
}
Controllers.closeDialog();
JFXUtilities.runInFX(() -> Controllers.closeDialog(region.get()));
}
private static String getRealPath() {
@@ -70,7 +80,7 @@ public class NewFileUpgrader extends IUpgrader {
File file = new File(realPath);
realPath = file.getAbsolutePath();
try {
realPath = java.net.URLDecoder.decode(realPath, Charsets.DEFAULT_CHARSET.name());
realPath = java.net.URLDecoder.decode(realPath, UTF_8.name());
} catch (Exception e) {
e.printStackTrace();
}
@@ -22,6 +22,8 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.OutOfDateEvent;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.Constants;
@@ -30,6 +32,8 @@ import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.VersionNumber;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
@@ -65,13 +69,20 @@ public final class UpdateChecker {
*/
public TaskResult<VersionNumber> process(final boolean showMessage) {
return new TaskResult<VersionNumber>() {
GetTask http = new GetTask(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update.php?version=" + Launcher.VERSION));
@Override
public Collection<? extends Task> getDependents() {
return value == null ? Collections.singleton(http) : Collections.emptyList();
}
@Override
public void execute() throws Exception {
if (Launcher.VERSION.contains("@"))
return;
if (value == null) {
versionString = NetworkUtils.doGet(NetworkUtils.toURL("http://huangyuhui.duapp.com/hmcl/update.php?version=" + Launcher.VERSION));
versionString = http.getResult();
value = VersionNumber.asVersion(versionString);
}
@@ -115,7 +126,7 @@ public final class UpdateChecker {
public void execute() {
if (download_link == null)
try {
download_link = Constants.GSON.<Map<String, String>>fromJson(NetworkUtils.doGet(NetworkUtils.toURL("http://huangyuhui.duapp.com/hmcl/update_link.php")), Map.class);
download_link = Constants.GSON.<Map<String, String>>fromJson(NetworkUtils.doGet(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update_link.php")), Map.class);
} catch (JsonSyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to get update link.", e);
}
@@ -42,6 +42,8 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
put("java.lang.NoClassDefFoundError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.VerifyError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchMethodError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.NoSuchFieldError", Launcher.i18n("crash.NoClassDefFound"));
put("netscape.javascript.JSException", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.IncompatibleClassChangeError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.ClassFormatError", Launcher.i18n("crash.NoClassDefFound"));
put("java.lang.OutOfMemoryError", "FUCKING MEMORY LIMIT!");
@@ -93,7 +95,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
Logging.LOG.log(Level.SEVERE, text);
if (checkThrowable(e) && !text.contains("OpenJDK")) {
if (checkThrowable(e)) {
Platform.runLater(() -> new CrashWindow(text).show());
if (!Launcher.UPDATE_CHECKER.isOutOfDate())
reportToServer(text);
@@ -113,7 +115,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
map.put("version", Launcher.VERSION);
map.put("log", Logging.getLogs());
try {
String response = NetworkUtils.doPost(NetworkUtils.toURL("http://huangyuhui.duapp.com/hmcl/crash.php"), map);
String response = NetworkUtils.doPost(NetworkUtils.toURL("https://huangyuhui.duapp.com/hmcl/crash.php"), map);
if (StringUtils.isNotBlank(response))
Logging.LOG.log(Level.SEVERE, "Crash server response: " + response);
} catch (IOException ex) {
@@ -911,6 +911,14 @@
-fx-stroke-width: 5.0;
}
.small-spinner {
-jfx-radius: 10;
}
.small-spinner > .arc {
-fx-stroke-width: 3.0;
}
.second-spinner {
-jfx-radius: 30;
}
@@ -25,13 +25,13 @@
<JFXComboBox fx:id="cboType" GridPane.columnIndex="1" GridPane.rowIndex="0" GridPane.columnSpan="2"/>
<Label fx:id="lblAddInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT"
<Label fx:id="lblInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT"
GridPane.columnIndex="0" GridPane.rowIndex="1"/>
<JFXComboBox fx:id="cboServers" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<Hyperlink fx:id="linkAddInjectorServer" text="%account.injector.add"
onMouseClicked="#onAddInjecterServer" GridPane.columnIndex="2" GridPane.rowIndex="1"/>
<Hyperlink fx:id="linkManageInjectorServers" text="%account.injector.manage"
onMouseClicked="#onManageInjecterServers" GridPane.columnIndex="2" GridPane.rowIndex="1"/>
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
@@ -56,9 +56,11 @@
</body>
<actions>
<Label fx:id="lblCreationWarning"/>
<JFXButton onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
<StackPane fx:id="acceptPane">
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
<JFXSpinner fx:id="spinnerAccept" styleClass="small-spinner" />
</StackPane>
<JFXButton onMouseClicked="#onCreationCancel" text="%button.cancel" styleClass="dialog-cancel"/>
</actions>
</JFXDialogLayout>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER"/>
</fx:root>
@@ -12,7 +12,7 @@
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
<StackPane fx:id="contentPane">
<ScrollPane fitToHeight="true" fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<ScrollPane fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="160">
</JFXMasonryPane>
</ScrollPane>
@@ -31,7 +31,7 @@
</right>
</BorderPane>
<FileItem fx:id="gameDir" name="%profile.instance_directory" title="%settings.choose_gamedir"/>
<FileItem fx:id="gameDir" name="%profile.instance_directory" title="%profile.instance_directory.choose"/>
</ComponentList>
</VBox>
@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane" pickOnBounds="false" FXUtils.limitWidth="160" FXUtils.limitHeight="156">
<VBox fx:id="content" pickOnBounds="false">
<StackPane fx:id="header" VBox.vgrow="ALWAYS" pickOnBounds="false" style="-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;">
<VBox style="-fx-padding: 8 8 0 8;">
<Label fx:id="lblVersionName" style="-fx-font-size: 15;" textAlignment="JUSTIFY" wrapText="true" />
<Label fx:id="lblGameVersion" style="-fx-font-size: 11;" textAlignment="JUSTIFY" wrapText="true" />
<Label fx:id="lblLibraries" style="-fx-font-size: 10; -fx-text-fill: gray;" textAlignment="JUSTIFY" wrapText="true" />
</VBox>
</StackPane>
<StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40" pickOnBounds="false">
<BorderPane>
<left>
<HBox spacing="8">
<JFXButton fx:id="btnSettings" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
<JFXButton fx:id="btnUpdate" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
</HBox>
</left>
<right>
<HBox spacing="8">
<JFXButton fx:id="btnScript" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30" prefWidth="30" prefHeight="30" />
<JFXButton fx:id="btnLaunch" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minHeight="30" minWidth="30" prefWidth="30" prefHeight="30" />
</HBox>
</right>
</BorderPane>
</StackPane>
</VBox>
<StackPane fx:id="icon" StackPane.alignment="TOP_RIGHT" pickOnBounds="false">
<ImageView fx:id="iconView" StackPane.alignment="CENTER_RIGHT">
<StackPane.margin>
<Insets right="12" />
</StackPane.margin>
<Image url="/assets/img/icon.png" />
</ImageView>
</StackPane>
</fx:root>
@@ -25,7 +25,7 @@
<MultiFileItem fx:id="javaItem" title="%settings.game.java_directory" chooserTitle="%settings.game.java_directory.choose"
hasSubtitle="true" customText="%settings.custom" directory="false" />
<MultiFileItem fx:id="gameDirItem" title="%profile.instance_directory" chooserTitle="%settings.choose_gamedir"
<MultiFileItem fx:id="gameDirItem" title="%settings.game.game_directory" chooserTitle="%settings.game.game_directory.choose"
hasSubtitle="true" customText="%settings.custom" directory="true" />
<BorderPane> <!-- Max Memory -->
@@ -18,9 +18,9 @@
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=Author
about.author.statement=huanghongxun (MCF: klkl6523)
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Thanks to
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi.bangbang93.com/)\ngamerteam (Default background image)\nAll contributors.
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi2.bangbang93.com/)\ngamerteam (Default background image)\nAll contributors.
about.dependency=Dependency
about.dependency.statement=Gson (Google, Apache License 2.0)\nCommons Compress (Apache, Apache License 2.0)
about.claim=Statement
@@ -39,7 +39,8 @@ account.failed.invalid_credentials=Incorrect password. Or forbidden to log in te
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to log in.
account.failed.no_charactor=No character in this account.
account.injector.add=Add new authentication server
account.injector.add=Add authentication server
account.injector.manage=Manage authentication servers
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
account.injector.server=Auth Server
account.injector.server_ip=Server URL
@@ -57,8 +58,8 @@ archive.author=Authors
archive.game_version=Game
archive.version=Version
assets.download=Download Assets
assets.download_all=Download Assets Files
assets.download=Download assets
assets.download_all=Check for the completion of assets
assets.failed=Failed to get the list, try again.
assets.failed_download=Failed to download assets, may cause no sounds and language files.
@@ -106,13 +107,12 @@ crash.advice.no=No advice.
crash.advice.no_lwjgl=Maybe drivers caused problems.
crash.advice.otherwise=Maybe mods caused problems.
crash.error=Minecraft has crashed.
crash.headless=If your OS is Linux, please use Oracle JDK instead of OpenJDK, or add "-Djava.awt.headless=false" JVM argument, or check if your Xserver works normally.
crash.launcher=Launcher has crashed!
crash.minecraft=Minecraft has crashed!
crash.user_fault=Your OS or Java environment may not be properly installed resulting in crashing of this software, please check your Java Environment or your computer!
download=Download
download.BMCL=BMCLAPI (bangbang93, http://bmclapi.bangbang93.com/)
download.BMCL=BMCLAPI (bangbang93, http://bmclapi2.bangbang93.com/)
download.failed=Failed to download
download.failed.refresh=Unable to load version list. Click here to retry.
download.mojang=Mojang
@@ -198,7 +198,6 @@ launcher.crash=Hello Minecraft! Launcher has crashed!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Update it!
launcher.exit_failed=Failed to shutdown.
launcher.modpack=Documentations for modpacks.
launcher.open_jdk=We have found that you started this application using OpenJDK, which will cause so many troubles drawing the UI. We suggest you using Oracle JDK instead.
launcher.restart=Options will be in operations only if restart this app.
launcher.tab.about=About
launcher.tab.general=General
@@ -311,6 +310,7 @@ profile=Profile
profile.default=Current directory
profile.home=User home
profile.instance_directory=Game Directory
profile.instance_directory.choose=Choose Game Directory
profile.new=New Config
profile.new_name=New Profile Name:
profile.remove=Sure to remove profile %s?
@@ -343,7 +343,6 @@ settings.advanced.server_ip=Server Host
settings.advanced.wrapper_launcher=Wrapper Launcher(i.e. optirun...)
settings.cannot_remove_default_config=Cannot remove the default configution.
settings.choose_gamedir=Choose Game Directory
settings.custom=Custom
settings.default=Default
settings.failed_load=Failed to load settings file. Remove it?
@@ -352,6 +351,8 @@ settings.game=Games
settings.game.dimension=Game Window Dimension
settings.game.exploration=Explore
settings.game.fullscreen=Fullscreen
settings.game.game_directory=Game Directory
settings.game.game_directory.choose=Choose Game Directory
settings.game.java_directory=Java Dir
settings.game.java_directory.choose=Choose Java Directory.
settings.game.management=Manage
@@ -18,9 +18,9 @@
about.copyright=版权
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun (百度贴吧:huanghongxun20)
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鸣谢
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi.bangbang93.com/)\ngamerteam (默认背景图)\n所有参与本项目,issue,pull requests的贡献者
about.thanks_to.statement=bangbang93 (BMCLAPI, http://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有参与本项目,issue,pull requests的贡献者
about.dependency=依赖
about.dependency.statement=Gson (Google, Apache License 2.0)\nCommons Compress (Apache, Apache License 2.0)
about.claim=免责声明
@@ -39,7 +39,8 @@ account.failed.invalid_credentials=您的用户名或密码错误,或者登录
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_charactor=该帐号没有角色
account.injector.add=添加登录认证服务器
account.injector.add=添加认证服务器
account.injector.manage=管理认证服务器
account.injector.http=警告:此服务器使用不安全的http协议,您的密码在登录时会被明文传输。
account.injector.server=认证服务器
account.injector.server_ip=服务器地址
@@ -58,7 +59,7 @@ archive.game_version=游戏版本
archive.version=版本
assets.download=下载资源
assets.download_all=下载资源文件
assets.download_all=检查资源文件完整性
assets.failed=获取列表失败,请刷新重试。
assets.failed_download=下载资源文件失败,可能导致没有中文和声音。
@@ -93,7 +94,7 @@ color.red=红色
color.recent=推荐
color.custom=自定义颜色
crash.NoClassDefFound=请确认HMCL本体是否完整
crash.NoClassDefFound=请确认HMCL本体是否完整,或更新您的Java。
crash.advice.ClassNotFoundException=Minecraft不完整或Mod冲突,如果有未能下载的文件请下载成功后重试或是客户端损坏请重试请重新制作客户端或下载整合包解决问题,另可尝试游戏设置->(版本)管理->删除库文件解决问题
crash.advice.ConcurrentModificationException=这可能是因为您的Java版本高于Java 1.8.0_11导致的,可以尝试卸载Java8安装Java7。
crash.advice.LWJGLException=您的电脑不正常,可能需要使用驱动精灵或其他安装器更新显卡驱动。
@@ -106,13 +107,12 @@ crash.advice.no=无建议。
crash.advice.no_lwjgl=可能是游戏依赖库不完整或解压依赖库时出错。可以通过下载整合包解决问题。
crash.advice.otherwise=可能是Mod或其他问题。
crash.error=您的Minecraft崩溃了。
crash.headless=如果您的操作系统是Linux,请注意不要使用OpenJDK,务必使用Oracle JDK,或尝试添加-Djava.awt.headless=false参数,或检查您的Xserver是否正常
crash.launcher=启动器崩溃了!
crash.minecraft=Minecraft崩溃了!请认真阅读建议。
crash.user_fault=您的系统或Java环境可能安装不当导致本软件崩溃,请检查您的Java环境或您的电脑!可以尝试重新安装Java。
download=下载
download.BMCL=BMCLAPI (bangbang93, http://bmclapi.bangbang93.com/)
download.BMCL=BMCLAPI (bangbang93, http://bmclapi2.bangbang93.com/)
download.failed=下载失败
download.failed.refresh=加载版本列表失败,点击此处重试。
download.mojang=官方
@@ -147,7 +147,7 @@ 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=安装整合包
@@ -198,7 +198,6 @@ launcher.crash=Hello Minecraft!遇到了无法处理的错误,请复制下列
launcher.crash_out_dated=Hello Minecraft! Launcher遇到了无法处理的错误,已检测到您的启动器不是最新版本,请更新后再试!
launcher.exit_failed=强制退出失败,可能是Forge 1.7.10及更高版本导致的,无法解决。
launcher.modpack=整合包作者帮助
launcher.open_jdk=我们发现您正在使用OpenJDK,这会导致很多界面问题,我们建议您更换Oracle JDK。
launcher.restart=本界面选项需要重启本启动器生效
launcher.tab.about=关于
launcher.tab.general=通用
@@ -311,6 +310,7 @@ profile=配置
profile.default=当前目录
profile.home=主文件夹
profile.instance_directory=游戏路径
profile.instance_directory.choose=选择游戏路径
profile.new=新建配置
profile.new_name=新配置名:
profile.remove=真的要删除配置%s吗?
@@ -343,7 +343,6 @@ settings.advanced.server_ip=直入服务器ip地址(不必填写,启动游戏
settings.advanced.wrapper_launcher=前置指令(不必填写,如optirun)
settings.cannot_remove_default_config=不能删除默认配置
settings.choose_gamedir=选择游戏路径
settings.custom=自定义
settings.default=默认
settings.failed_load=设置文件加载失败,可能是升级了启动器或被人工修改造成错误,是否清除?
@@ -352,6 +351,8 @@ settings.game=游戏设置
settings.game.dimension=游戏窗口分辨率
settings.game.exploration=浏览
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=管理
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.util.ToStringBuilder;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
/**
@@ -29,39 +30,38 @@ import java.util.UUID;
public abstract class Account {
/**
* @return the account name (mostly email)
* @return the name of the account who owns the character
*/
public abstract String getUsername();
/**
*
* @return the character name
*/
public abstract String getCharacter();
/**
* @return the UUID
* @return the character UUID
*/
public abstract UUID getUUID();
/**
* log in.
* @return the specific player's info.
* @throws AuthenticationException if server error occurred.
* Login with stored credentials.
*
* @throws ServerDisconnectException if an network error has occurred, in which case password login won't be tried.
* @throws AuthenticationException if an error has occurred. If it's not a {@link ServerDisconnectException}, password login will be tried.
*/
public abstract AuthInfo logIn() throws AuthenticationException;
public abstract AuthInfo logIn() throws ServerDisconnectException, AuthenticationException;
/**
* Login with specified password.
*/
public abstract AuthInfo logInWithPassword(String password) throws AuthenticationException;
public abstract boolean canPlayOffline();
/**
* Play offline.
* @return the specific offline player's info.
*/
public abstract AuthInfo playOffline();
public abstract void logOut();
public abstract Optional<AuthInfo> playOffline();
public abstract Map<Object, Object> toStorage();
@@ -0,0 +1,61 @@
/*
* 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.auth;
import java.net.Proxy;
import java.util.Objects;
public final class AccountBuilder<T extends Account> {
private CharacterSelector selector = CharacterSelector.DEFAULT;
private String username;
private String password = null;
private Object additionalData = null;
private Proxy proxy = Proxy.NO_PROXY;
public AccountBuilder() {
}
public AccountBuilder setSelector(CharacterSelector selector) {
this.selector = Objects.requireNonNull(selector);
return this;
}
public AccountBuilder setUsername(String username) {
this.username = Objects.requireNonNull(username);
return this;
}
public AccountBuilder setPassword(String password) {
this.password = password;
return this;
}
public AccountBuilder setAdditionalData(Object additionalData) {
this.additionalData = additionalData;
return this;
}
public AccountBuilder setProxy(Proxy proxy) {
this.proxy = Objects.requireNonNull(proxy);
return this;
}
public T create(AccountFactory<T> factory) throws AuthenticationException {
return factory.create(selector, Objects.requireNonNull(username), password, additionalData, proxy);
}
}
@@ -20,6 +20,8 @@ package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.Immutable;
import java.util.UUID;
/**
*
* @author huangyuhui
@@ -28,29 +30,19 @@ import org.jackhuang.hmcl.util.Immutable;
public final class AuthInfo {
private final String username;
private final String userId;
private final UUID uuid;
private final String accessToken;
private final UserType userType;
private final String userProperties;
private final Arguments arguments;
public AuthInfo(String username, String userId, String accessToken) {
this(username, userId, accessToken, UserType.LEGACY);
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties) {
this(username, uuid, accessToken, userProperties, null);
}
public AuthInfo(String username, String userId, String accessToken, UserType userType) {
this(username, userId, accessToken, userType, "{}");
}
public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties) {
this(username, userId, accessToken, userType, userProperties, null);
}
public AuthInfo(String username, String userId, String accessToken, UserType userType, String userProperties, Arguments arguments) {
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties, Arguments arguments) {
this.username = username;
this.userId = userId;
this.uuid = uuid;
this.accessToken = accessToken;
this.userType = userType;
this.userProperties = userProperties;
this.arguments = arguments;
}
@@ -59,18 +51,14 @@ public final class AuthInfo {
return username;
}
public String getUserId() {
return userId;
public UUID getUUID() {
return uuid;
}
public String getAccessToken() {
return accessToken;
}
public UserType getUserType() {
return userType;
}
/**
* Properties of this user.
* Don't know the difference between user properties and user property map.
@@ -81,11 +69,14 @@ public final class AuthInfo {
return userProperties;
}
/**
* @return null if no argument is specified
*/
public Arguments getArguments() {
return arguments;
}
public AuthInfo setArguments(Arguments arguments) {
return new AuthInfo(username, userId, accessToken, userType, userProperties, arguments);
public AuthInfo withArguments(Arguments arguments) {
return new AuthInfo(username, uuid, accessToken, userProperties, arguments);
}
}
@@ -20,23 +20,24 @@ package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import java.util.List;
import java.util.UUID;
/**
* Select character by name.
*/
public class SpecificCharacterSelector implements CharacterSelector {
private final String id;
private UUID uuid;
/**
* Constructor.
* @param id character's name.
* @param uuid character's uuid.
*/
public SpecificCharacterSelector(String id) {
this.id = id;
public SpecificCharacterSelector(UUID uuid) {
this.uuid = uuid;
}
@Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
return names.stream().filter(profile -> profile.getName().equals(id)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
return names.stream().filter(profile -> profile.getId().equals(uuid)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
}
}
@@ -1,49 +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.auth;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
*
* @author huangyuhui
*/
public enum UserType {
LEGACY,
MOJANG;
public static UserType fromName(String name) {
return BY_NAME.get(name.toLowerCase());
}
public static UserType fromLegacy(boolean isLegacy) {
return isLegacy ? LEGACY : MOJANG;
}
static {
HashMap<String, UserType> byName = new HashMap<>();
for (UserType type : values())
byName.put(type.name().toLowerCase(), type);
BY_NAME = Collections.unmodifiableMap(byName);
}
public static final Map<String, UserType> BY_NAME;
}
@@ -25,21 +25,23 @@ import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.ExceptionalSupplier;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.nio.charset.StandardCharsets.UTF_8;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private final String serverBaseURL;
private final ExceptionalSupplier<String, ?> injectorJarPath;
protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier<String, ?> injectorJarPath, String username, String clientToken, String character, YggdrasilSession session) {
super(service, username, clientToken, character, session);
protected AuthlibInjectorAccount(YggdrasilService service, String serverBaseURL, ExceptionalSupplier<String, ?> injectorJarPath, String username, UUID characterUUID, YggdrasilSession session) {
super(service, username, characterUUID, session);
this.injectorJarPath = injectorJarPath;
this.serverBaseURL = serverBaseURL;
@@ -65,13 +67,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
try {
thread.join();
String arg = "-javaagent:" + injectorJarPath.get() + "=" + serverBaseURL;
Arguments arguments = Arguments.addJVMArguments(null, arg);
Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + injectorJarPath.get() + "=" + serverBaseURL);
if (flag.get())
arguments = Arguments.addJVMArguments(arguments, "-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), Charsets.UTF_8));
arguments = arguments.addJVMArguments("-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), UTF_8));
return info.setArguments(arguments);
return info.withArguments(arguments);
} catch (Exception e) {
throw new AuthenticationException("Unable to get authlib injector jar path", e);
}
@@ -6,14 +6,13 @@ import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.util.ExceptionalSupplier;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.net.Proxy;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private final ExceptionalSupplier<String, ?> injectorJarPathSupplier;
@@ -33,7 +32,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
throw new IllegalArgumentException("Additional data should be server base url string for authlib injector accounts.");
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider((String) serverBaseURL), proxy),
(String) serverBaseURL, injectorJarPathSupplier, username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null);
(String) serverBaseURL, injectorJarPathSupplier, username, null, null);
account.logInWithPassword(password, selector);
return account;
}
@@ -43,16 +42,14 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
Objects.requireNonNull(storage);
Objects.requireNonNull(proxy);
String username = Lang.get(storage, "username", String.class)
YggdrasilSession session = YggdrasilSession.fromStorage(storage);
String username = tryCast(storage.get("username"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
String clientToken = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have client token."));
String character = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name."));
String apiRoot = Lang.get(storage, "serverBaseURL", String.class)
String apiRoot = tryCast(storage.get("serverBaseURL"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have API root."));
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(apiRoot), proxy),
apiRoot, injectorJarPathSupplier, username, clientToken, character, YggdrasilSession.fromStorage(storage));
apiRoot, injectorJarPathSupplier, username, session.getSelectedProfile().getId(), session);
}
}
@@ -20,15 +20,18 @@ package org.jackhuang.hmcl.auth.offline;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
*
* @author huang
@@ -36,9 +39,9 @@ import java.util.UUID;
public class OfflineAccount extends Account {
private final String username;
private final String uuid;
private final UUID uuid;
OfflineAccount(String username, String uuid) {
OfflineAccount(String username, UUID uuid) {
Objects.requireNonNull(username);
Objects.requireNonNull(uuid);
@@ -51,7 +54,7 @@ public class OfflineAccount extends Account {
@Override
public UUID getUUID() {
return UUIDTypeAdapter.fromString(uuid);
return uuid;
}
@Override
@@ -66,10 +69,10 @@ public class OfflineAccount extends Account {
@Override
public AuthInfo logIn() throws AuthenticationException {
if (StringUtils.isBlank(username) || StringUtils.isBlank(uuid))
if (StringUtils.isBlank(username))
throw new AuthenticationException("Username cannot be empty");
return new AuthInfo(username, uuid, uuid);
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
}
@Override
@@ -78,25 +81,15 @@ public class OfflineAccount extends Account {
}
@Override
public void logOut() {
// Offline account need not log out.
}
@Override
public boolean canPlayOffline() {
return false;
}
@Override
public AuthInfo playOffline() {
throw new IllegalStateException();
public Optional<AuthInfo> playOffline() {
return Optional.empty();
}
@Override
public Map<Object, Object> toStorage() {
return Lang.mapOf(
new Pair<>("uuid", uuid),
new Pair<>("username", username)
return mapOf(
pair("uuid", UUIDTypeAdapter.fromUUID(uuid)),
pair("username", username)
);
}
@@ -107,6 +100,9 @@ public class OfflineAccount extends Account {
@Override
public String toString() {
return "OfflineAccount[username=" + username + ", uuid=" + uuid + "]";
return new ToStringBuilder(this)
.append("username", username)
.append("uuid", uuid)
.toString();
}
}
@@ -19,11 +19,14 @@ package org.jackhuang.hmcl.auth.offline;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.net.Proxy;
import java.util.Map;
import java.util.UUID;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.tryCast;
/**
*
@@ -42,16 +45,17 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
@Override
public OfflineAccount fromStorage(Map<Object, Object> storage, Proxy proxy) {
String username = Lang.get(storage, "username", String.class)
String username = tryCast(storage.get("username"), String.class)
.orElseThrow(() -> new IllegalStateException("Offline account configuration malformed."));
String uuid = Lang.get(storage, "uuid", String.class)
UUID uuid = tryCast(storage.get("uuid"), String.class)
.map(UUIDTypeAdapter::fromString)
.orElse(getUUIDFromUserName(username));
return new OfflineAccount(username, uuid);
}
private static String getUUIDFromUserName(String username) {
return DigestUtils.md5Hex(username);
private static UUID getUUIDFromUserName(String username) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(UTF_8));
}
}
@@ -1,66 +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.auth.yggdrasil;
/**
*
* @author huangyuhui
*/
final class AuthenticationResponse extends ErrorResponse {
private final String accessToken;
private final String clientToken;
private final GameProfile selectedProfile;
private final GameProfile[] availableProfiles;
private final User user;
public AuthenticationResponse() {
this(null, null, null, null, null, null, null, null);
}
public AuthenticationResponse(String accessToken, String clientToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user, String error, String errorMessage, String cause) {
super(error, errorMessage, cause);
this.accessToken = accessToken;
this.clientToken = clientToken;
this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles;
this.user = user;
}
public String getAccessToken() {
return accessToken;
}
public String getClientToken() {
return clientToken;
}
public GameProfile getSelectedProfile() {
return selectedProfile;
}
public GameProfile[] getAvailableProfiles() {
return availableProfiles;
}
public User getUser() {
return user;
}
}
@@ -1,29 +0,0 @@
package org.jackhuang.hmcl.auth.yggdrasil;
class ErrorResponse {
private final String error;
private final String errorMessage;
private final String cause;
public ErrorResponse() {
this(null, null, null);
}
public ErrorResponse(String error, String errorMessage, String cause) {
this.error = error;
this.errorMessage = errorMessage;
this.cause = cause;
}
public String getError() {
return error;
}
public String getErrorMessage() {
return errorMessage;
}
public String getCause() {
return cause;
}
}
@@ -17,11 +17,8 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.*;
import org.jackhuang.hmcl.auth.UserType;
import org.jackhuang.hmcl.util.Immutable;
import java.lang.reflect.Type;
import java.util.UUID;
/**
@@ -34,7 +31,6 @@ public final class GameProfile {
private final UUID id;
private final String name;
private final PropertyMap properties;
private final boolean legacy;
public GameProfile() {
this(null, null);
@@ -45,14 +41,9 @@ public final class GameProfile {
}
public GameProfile(UUID id, String name, PropertyMap properties) {
this(id, name, properties, false);
}
public GameProfile(UUID id, String name, PropertyMap properties, boolean legacy) {
this.id = id;
this.name = name;
this.properties = properties;
this.legacy = legacy;
}
public UUID getId() {
@@ -63,46 +54,11 @@ public final class GameProfile {
return name;
}
/**
* @return nullable
*/
public PropertyMap getProperties() {
return properties;
}
public boolean isLegacy() {
return legacy;
}
public UserType getUserType() {
return UserType.fromLegacy(isLegacy());
}
public static class Serializer implements JsonSerializer<GameProfile>, JsonDeserializer<GameProfile> {
public static final Serializer INSTANCE = new Serializer();
private Serializer() {
}
@Override
public JsonElement serialize(GameProfile src, Type type, JsonSerializationContext context) {
JsonObject result = new JsonObject();
if (src.getId() != null)
result.add("id", context.serialize(src.getId()));
if (src.getName() != null)
result.addProperty("name", src.getName());
return result;
}
@Override
public GameProfile deserialize(JsonElement je, Type type, JsonDeserializationContext context) throws JsonParseException {
if (!(je instanceof JsonObject))
throw new JsonParseException("The json element is not a JsonObject.");
JsonObject json = (JsonObject) je;
UUID id = json.has("id") ? context.deserialize(json.get("id"), UUID.class) : null;
String name = json.has("name") ? json.getAsJsonPrimitive("name").getAsString() : null;
return new GameProfile(id, name);
}
}
}
@@ -1,53 +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.auth.yggdrasil;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
@Immutable
public final class TextureResponse {
private final UUID profileId;
private final String profileName;
private final Map<TextureType, Texture> textures;
public TextureResponse() {
this(UUID.randomUUID(), "", Collections.emptyMap());
}
public TextureResponse(UUID profileId, String profileName, Map<TextureType, Texture> textures) {
this.profileId = profileId;
this.profileName = profileName;
this.textures = textures;
}
public UUID getProfileId() {
return profileId;
}
public String getProfileName() {
return profileName;
}
public Map<TextureType, Texture> getTextures() {
return textures == null ? null : Collections.unmodifiableMap(textures);
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.GsonBuilder;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
@@ -34,15 +33,13 @@ public class YggdrasilAccount extends Account {
private final YggdrasilService service;
private boolean isOnline = false;
private YggdrasilSession session;
private final String clientToken;
private String character;
private UUID characterUUID;
protected YggdrasilAccount(YggdrasilService service, String username, String clientToken, String character, YggdrasilSession session) {
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) {
this.service = service;
this.username = username;
this.session = session;
this.clientToken = clientToken;
this.character = character;
this.characterUUID = characterUUID;
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
this.session = null;
@@ -70,20 +67,20 @@ public class YggdrasilAccount extends Account {
public AuthInfo logIn() throws AuthenticationException {
if (!canPlayOnline()) {
logInWithToken();
selectProfile(new SpecificCharacterSelector(character));
selectProfile(new SpecificCharacterSelector(characterUUID));
}
return toAuthInfo();
return session.toAuthInfo();
}
@Override
public final AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logInWithPassword(password, new SpecificCharacterSelector(character));
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
}
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
session = service.authenticate(username, password, clientToken);
session = service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID()));
selectProfile(selector);
return toAuthInfo();
return session.toAuthInfo();
}
private void selectProfile(CharacterSelector selector) throws AuthenticationException {
@@ -91,61 +88,42 @@ public class YggdrasilAccount extends Account {
if (session.getAvailableProfiles() == null || session.getAvailableProfiles().length <= 0)
throw new NoCharacterException(this);
session.setSelectedProfile(selector.select(this, Arrays.asList(session.getAvailableProfiles())));
session = service.refresh(session.getAccessToken(), session.getClientToken(), selector.select(this, Arrays.asList(session.getAvailableProfiles())));
}
character = session.getSelectedProfile().getName();
characterUUID = session.getSelectedProfile().getId();
}
private void logInWithToken() throws AuthenticationException {
if (service.validate(session.getAccessToken(), clientToken)) {
if (service.validate(session.getAccessToken(), session.getClientToken())) {
isOnline = true;
return;
}
session = service.refresh(session.getAccessToken(), clientToken);
}
private AuthInfo toAuthInfo() {
GameProfile profile = session.getSelectedProfile();
return new AuthInfo(profile.getName(), UUIDTypeAdapter.fromUUID(profile.getId()), session.getAccessToken(), profile.getUserType(),
new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create().toJson(Optional.ofNullable(session.getUser()).map(User::getProperties).orElseGet(PropertyMap::new)));
session = service.refresh(session.getAccessToken(), session.getClientToken(), null);
}
@Override
public boolean canPlayOffline() {
return isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline();
}
public Optional<AuthInfo> playOffline() {
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline())
return Optional.of(session.toAuthInfo());
@Override
public AuthInfo playOffline() {
if (!canPlayOffline())
throw new IllegalStateException("Current account " + this + " cannot play offline.");
return toAuthInfo();
}
@Override
public void logOut() {
isOnline = false;
session = null;
return Optional.empty();
}
@Override
public Map<Object, Object> toStorage() {
if (session == null)
throw new IllegalStateException("No session is specified");
HashMap<Object, Object> storage = new HashMap<>();
storage.put("username", getUsername());
storage.put("clientToken", clientToken);
storage.put("character", character);
if (session != null)
storage.putAll(session.toStorage());
storage.putAll(session.toStorage());
return storage;
}
@Override
public UUID getUUID() {
if (session == null)
if (session == null || session.getSelectedProfile() == null)
return null;
else
return session.getSelectedProfile().getId();
@@ -157,7 +135,7 @@ public class YggdrasilAccount extends Account {
public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
if (!service.getTextures(profile).isPresent()) {
session.setAvailableProfile(profile = service.getCompleteGameProfile(profile.getId()));
profile = service.getCompleteGameProfile(profile.getId()).orElse(profile);
}
return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.net.Proxy;
@@ -28,6 +27,8 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.tryCast;
/**
*
* @author huangyuhui
@@ -47,7 +48,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
Objects.requireNonNull(password);
Objects.requireNonNull(proxy);
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider, proxy), username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null);
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider, proxy), username, null, null);
account.logInWithPassword(password, selector);
return account;
}
@@ -57,14 +58,12 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
Objects.requireNonNull(storage);
Objects.requireNonNull(proxy);
String username = Lang.get(storage, "username", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
String clientToken = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have client token."));
String character = Lang.get(storage, "clientToken", String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have selected character name."));
YggdrasilSession session = YggdrasilSession.fromStorage(storage);
return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, clientToken, character, YggdrasilSession.fromStorage(storage));
String username = tryCast(storage.get("username"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
return new YggdrasilAccount(new YggdrasilService(provider, proxy), username, session.getSelectedProfile().getId(), session);
}
public static String randomToken() {
@@ -4,13 +4,20 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.util.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Lang.liftFunction;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public class YggdrasilService {
private final YggdrasilProvider provider;
@@ -31,23 +38,39 @@ public class YggdrasilService {
Objects.requireNonNull(clientToken);
Map<String, Object> request = new HashMap<>();
request.put("agent", Lang.mapOf(
new Pair<>("name", "Minecraft"),
new Pair<>("version", 1)
request.put("agent", mapOf(
pair("name", "Minecraft"),
pair("version", 1)
));
request.put("username", username);
request.put("password", password);
request.put("clientToken", clientToken);
request.put("requestUser", true);
return handle(request(provider.getAuthenticationURL(), request), clientToken);
return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken);
}
public YggdrasilSession refresh(String accessToken, String clientToken) throws AuthenticationException {
private Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
Map<String, Object> request = new HashMap<>();
request.put("accessToken", accessToken);
request.put("clientToken", clientToken);
return request;
}
public YggdrasilSession refresh(String accessToken, String clientToken, GameProfile characterToSelect) throws AuthenticationException {
Objects.requireNonNull(accessToken);
Objects.requireNonNull(clientToken);
return handle(request(provider.getRefreshmentURL(), new RefreshRequest(accessToken, clientToken)), clientToken);
Map<String, Object> request = createRequestWithCredentials(accessToken, clientToken);
request.put("requestUser", true);
if (characterToSelect != null) {
request.put("selectedProfile", mapOf(
pair("id", characterToSelect.getId()),
pair("name", characterToSelect.getName())));
}
return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
}
public boolean validate(String accessToken) throws AuthenticationException {
@@ -58,7 +81,7 @@ public class YggdrasilService {
Objects.requireNonNull(accessToken);
try {
requireEmpty(request(provider.getValidationURL(), new RefreshRequest(accessToken, clientToken)));
requireEmpty(request(provider.getValidationURL(), createRequestWithCredentials(accessToken, clientToken)));
return true;
} catch (InvalidCredentialsException | InvalidTokenException e) {
return false;
@@ -72,7 +95,7 @@ public class YggdrasilService {
public void invalidate(String accessToken, String clientToken) throws AuthenticationException {
Objects.requireNonNull(accessToken);
requireEmpty(request(provider.getInvalidationURL(), GSON.toJson(new RefreshRequest(accessToken, clientToken))));
requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken)));
}
/**
@@ -80,49 +103,33 @@ public class YggdrasilService {
*
* Game profile provided from authentication is not complete (no skin data in properties).
*
* @param userId the userId that the character corresponding to.
* @return the complete game profile(filled with more properties), null if character corresponding to {@code userId} does not exist.
* @throws AuthenticationException if an I/O error occurred or server response malformed.
* @param uuid the uuid that the character corresponding to.
* @return the complete game profile(filled with more properties)
*/
public GameProfile getCompleteGameProfile(UUID userId) throws AuthenticationException {
Objects.requireNonNull(userId);
public Optional<GameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
Objects.requireNonNull(uuid);
ProfileResponse response = fromJson(request(provider.getProfilePropertiesURL(userId), null), ProfileResponse.class);
if (response == null)
return null;
return new GameProfile(response.getId(), response.getName(), response.getProperties());
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), GameProfile.class));
}
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
Objects.requireNonNull(profile);
return Optional.ofNullable(profile.getProperties())
.map(properties -> properties.get("textures"))
.map(encodedTextures -> new String(Base64.getDecoder().decode(encodedTextures), Charsets.UTF_8))
.map(Lang.liftFunction(textures -> fromJson(textures, TextureResponse.class)))
.map(TextureResponse::getTextures);
.flatMap(properties -> Optional.ofNullable(properties.get("textures")))
.map(encodedTextures -> new String(Base64.getDecoder().decode(encodedTextures), UTF_8))
.map(liftFunction(textures -> fromJson(textures, TextureResponse.class)))
.flatMap(response -> Optional.ofNullable(response.textures));
}
private String request(URL url, Object input) throws AuthenticationException {
try {
if (input == null)
return NetworkUtils.doGet(url, proxy);
else
return NetworkUtils.doPost(url, input instanceof String ? (String) input : GSON.toJson(input), "application/json");
} catch (IOException e) {
throw new ServerDisconnectException(e);
}
}
private static YggdrasilSession handle(String responseText, String clientToken) throws AuthenticationException {
private static YggdrasilSession handleAuthenticationResponse(String responseText, String clientToken) throws AuthenticationException {
AuthenticationResponse response = fromJson(responseText, AuthenticationResponse.class);
handleErrorMessage(response);
if (!clientToken.equals(response.getClientToken()))
throw new AuthenticationException("Client token changed from " + response.getClientToken() + " to " + clientToken);
if (!clientToken.equals(response.clientToken))
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
return new YggdrasilSession(response.getAccessToken(), response.getSelectedProfile(), response.getAvailableProfiles(), response.getUser());
return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user);
}
private static void requireEmpty(String response) throws AuthenticationException {
@@ -137,15 +144,30 @@ public class YggdrasilService {
}
private static void handleErrorMessage(ErrorResponse response) throws AuthenticationException {
if (!StringUtils.isBlank(response.getError())) {
if (response.getErrorMessage() != null)
if (response.getErrorMessage().contains("Invalid credentials"))
if (!StringUtils.isBlank(response.error)) {
if (response.errorMessage != null && "ForbiddenOperationException".equals(response.error)) {
if (response.errorMessage.contains("Invalid credentials"))
throw new InvalidCredentialsException();
else if (response.getErrorMessage().contains("Invalid token"))
else if (response.errorMessage.contains("Invalid token"))
throw new InvalidTokenException();
else if (response.getErrorMessage().contains("Invalid username or password"))
else if (response.errorMessage.contains("Invalid username or password"))
throw new InvalidPasswordException();
throw new RemoteAuthenticationException(response.getError(), response.getErrorMessage(), response.getCause());
}
throw new RemoteAuthenticationException(response.error, response.errorMessage, response.cause);
}
}
private String request(URL url, Object payload) throws AuthenticationException {
try {
if (payload == null)
return NetworkUtils.doGet(url, proxy);
else
return NetworkUtils.doPost(url, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
} catch (IOException e) {
throw new ServerDisconnectException(e);
}
}
@@ -157,77 +179,27 @@ public class YggdrasilService {
}
}
static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(GameProfile.class, GameProfile.Serializer.INSTANCE)
private class TextureResponse {
public Map<TextureType, Texture> textures;
}
private class AuthenticationResponse extends ErrorResponse {
public String accessToken;
public String clientToken;
public GameProfile selectedProfile;
public GameProfile[] availableProfiles;
public User user;
}
private class ErrorResponse {
public String error;
public String errorMessage;
public String cause;
}
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.create();
private static final class RefreshRequest {
private final String accessToken;
private final String clientToken;
private final GameProfile selectedProfile;
private final boolean requestUser;
public RefreshRequest(String accessToken, String clientToken) {
this(accessToken, clientToken, null);
}
public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile) {
this(accessToken, clientToken, selectedProfile, true);
}
public RefreshRequest(String accessToken, String clientToken, GameProfile selectedProfile, boolean requestUser) {
this.accessToken = accessToken;
this.clientToken = clientToken;
this.selectedProfile = selectedProfile;
this.requestUser = requestUser;
}
public String getAccessToken() {
return accessToken;
}
public String getClientToken() {
return clientToken;
}
public GameProfile getSelectedProfile() {
return selectedProfile;
}
public boolean isRequestUser() {
return requestUser;
}
}
private static final class ProfileResponse {
private final UUID id;
private final String name;
private final PropertyMap properties;
public ProfileResponse() {
this(UUID.randomUUID(), "", null);
}
public ProfileResponse(UUID id, String name, PropertyMap properties) {
this.id = id;
this.name = name;
this.properties = properties;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public PropertyMap getProperties() {
return properties;
}
}
}
@@ -1,34 +1,52 @@
package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.util.Lang;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair;
public class YggdrasilSession {
private final String accessToken;
private String clientToken;
private String accessToken;
private GameProfile selectedProfile;
private final GameProfile[] availableProfiles;
private final User user;
private GameProfile[] availableProfiles;
private User user;
public YggdrasilSession(String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
this.clientToken = clientToken;
this.accessToken = accessToken;
this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles;
this.user = user;
}
public String getClientToken() {
return clientToken;
}
public String getAccessToken() {
return accessToken;
}
/**
* @return nullable (null if no character is selected)
*/
public GameProfile getSelectedProfile() {
return selectedProfile;
}
/**
* @return nullable (null if the YggdrasilSession is loaded from storage)
*/
public GameProfile[] getAvailableProfiles() {
return availableProfiles;
}
@@ -37,53 +55,40 @@ public class YggdrasilSession {
return user;
}
public void setAvailableProfile(GameProfile profile) {
if (availableProfiles != null)
for (int i = 0; i < availableProfiles.length; ++i)
if (availableProfiles[i].getId().equals(profile.getId()))
availableProfiles[i] = profile;
if (selectedProfile != null && profile.getId().equals(selectedProfile.getId()))
selectedProfile = profile;
}
public void setSelectedProfile(GameProfile selectedProfile) {
this.selectedProfile = selectedProfile;
setAvailableProfile(selectedProfile);
}
public static YggdrasilSession fromStorage(Map<?, ?> storage) {
Optional<String> profileId = Lang.get(storage, "uuid", String.class);
Optional<String> profileName = Lang.get(storage, "displayName", String.class);
GameProfile profile = null;
if (profileId.isPresent() && profileName.isPresent()) {
profile = new GameProfile(UUIDTypeAdapter.fromString(profileId.get()), profileName.get(),
Lang.get(storage, "profileProperties", Map.class).map(PropertyMap::fromMap).orElseGet(PropertyMap::new));
}
return new YggdrasilSession(
Lang.get(storage, "accessToken", String.class).orElse(null),
profile,
null,
Lang.get(storage, "userid", String.class)
.map(userId -> new User(userId, Lang.get(storage, "userProperties", Map.class).map(PropertyMap::fromMap).orElse(null)))
.orElse(null)
);
UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString).orElseThrow(() -> new IllegalArgumentException("uuid is missing"));
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null);
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
}
public Map<Object, Object> toStorage() {
Map<Object, Object> storage = new HashMap<>();
storage.put("accessToken", accessToken);
if (selectedProfile != null) {
storage.put("uuid", UUIDTypeAdapter.fromUUID(selectedProfile.getId()));
storage.put("displayName", selectedProfile.getName());
storage.put("profileProperties", selectedProfile.getProperties());
}
if (user != null) {
storage.put("userid", user.getId());
storage.put("userProperties", user.getProperties());
}
return storage;
if (selectedProfile == null)
throw new IllegalStateException("No character is selected");
if (user == null)
throw new IllegalStateException("No user is specified");
return mapOf(
pair("clientToken", clientToken),
pair("accessToken", accessToken),
pair("uuid", UUIDTypeAdapter.fromUUID(selectedProfile.getId())),
pair("displayName", selectedProfile.getName()),
pair("userid", user.getId()),
pair("userProperties", user.getProperties()));
}
public AuthInfo toAuthInfo() {
if (selectedProfile == null)
throw new IllegalStateException("No character is selected");
if (user == null)
throw new IllegalStateException("No user is specified");
return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken,
Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}"));
}
private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create();
}
@@ -19,7 +19,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.liteloader.LiteLoaderBMCLVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
/**
@@ -33,29 +33,14 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
private BMCLAPIDownloadProvider() {
}
@Override
public String getLibraryBaseURL() {
return "http://bmclapi2.bangbang93.com/libraries/";
}
@Override
public String getVersionListURL() {
return "http://bmclapi2.bangbang93.com/mc/game/version_manifest.json";
}
@Override
public String getVersionBaseURL() {
return "http://bmclapi2.bangbang93.com/versions/";
}
@Override
public String getAssetIndexBaseURL() {
return "http://bmclapi2.bangbang93.com/indexes/";
return "https://bmclapi2.bangbang93.com/mc/game/version_manifest.json";
}
@Override
public String getAssetBaseURL() {
return "http://bmclapi2.bangbang93.com/assets/";
return "https://bmclapi2.bangbang93.com/assets/";
}
@Override
@@ -66,7 +51,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
case "forge":
return ForgeVersionList.INSTANCE;
case "liteloader":
return LiteLoaderVersionList.INSTANCE;
return LiteLoaderBMCLVersionList.INSTANCE;
case "optifine":
return OptiFineBMCLVersionList.INSTANCE;
default:
@@ -80,7 +65,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
.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")
.replace("http://files.minecraftforge.net/maven", "https://bmclapi2.bangbang93.com/maven")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://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");
}
@@ -20,12 +20,8 @@ package org.jackhuang.hmcl.download;
public class CurseCDNDownloadProvider extends MojangDownloadProvider {
public static final CurseCDNDownloadProvider INSTANCE = new CurseCDNDownloadProvider();
protected CurseCDNDownloadProvider() {
super(false);
}
@Override
public String injectURL(String baseURL) {
return baseURL == null ? null : baseURL.replace("http://files.minecraftforge.net/maven", "http://ftb.cursecdn.com/FTB2/maven");
return baseURL == null ? null : baseURL.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://ftb.cursecdn.com/FTB2/maven");
}
}
@@ -24,14 +24,8 @@ package org.jackhuang.hmcl.download;
*/
public interface DownloadProvider {
String getLibraryBaseURL();
String getVersionListURL();
String getVersionBaseURL();
String getAssetIndexBaseURL();
String getAssetBaseURL();
/**
@@ -41,9 +41,12 @@ public class MaintainTask extends TaskResult<Version> {
@Override
public void execute() {
Version newVersion = version;
setResult(maintain(version));
}
public static Version maintain(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
List<String> args = new ArrayList<>(StringUtils.tokenize(newVersion.getMinecraftArguments().orElse("")));
List<String> args = new ArrayList<>(StringUtils.tokenize(version.getMinecraftArguments().orElse("")));
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge"))
@@ -84,11 +87,13 @@ public class MaintainTask extends TaskResult<Version> {
}
if ((liteLoader != null || forge != null) && optiFine != null) {
// If forge or LiteLoader installed, OptiFine Tweaker will cause crash.
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
removeTweakClass(args, "optifine");
args.add("--tweakClass");
args.add("optifine.OptiFineForgeTweaker");
}
setResult(newVersion.setMinecraftArguments(StringUtils.makeCommand(args)));
return version.setMinecraftArguments(StringUtils.makeCommand(args));
}
@Override
@@ -28,35 +28,14 @@ import org.jackhuang.hmcl.download.optifine.OptiFineVersionList;
*/
public class MojangDownloadProvider implements DownloadProvider {
private final boolean isChina;
public MojangDownloadProvider(boolean isChina) {
this.isChina = isChina;
}
@Override
public String getLibraryBaseURL() {
return "https://libraries.minecraft.net/";
}
@Override
public String getVersionListURL() {
return "https://launchermeta.mojang.com/mc/game/version_manifest.json";
}
@Override
public String getVersionBaseURL() {
return "http://s3.amazonaws.com/Minecraft.Download/versions/";
}
@Override
public String getAssetIndexBaseURL() {
return "http://s3.amazonaws.com/Minecraft.Download/indexes/";
}
@Override
public String getAssetBaseURL() {
return "http://resources.download.minecraft.net/";
return "https://resources.download.minecraft.net/";
}
@Override
@@ -77,16 +56,6 @@ public class MojangDownloadProvider implements DownloadProvider {
@Override
public String injectURL(String baseURL) {
if (baseURL == null)
return null;
//else if (baseURL.contains("scala-swing") || baseURL.contains("scala-xml") || baseURL.contains("scala-parser-combinators"))
// return baseURL.replace("http://files.minecraftforge.net/maven", "http://ftb.cursecdn.com/FTB2/maven");
/*else if (baseURL.contains("typesafe") || baseURL.contains("scala"))
if (isChina)
return baseURL.replace("http://files.minecraftforge.net/maven", "http://maven.aliyun.com/nexus/content/groups/public");
else
return baseURL.replace("http://files.minecraftforge.net/maven", "http://repo1.maven.org/maven2");
*/else
return baseURL;
}
}
@@ -92,5 +92,5 @@ public final class ForgeVersionList extends VersionList<Void> {
};
}
public static final String FORGE_LIST = "http://files.minecraftforge.net/maven/net/minecraftforge/forge/json";
public static final String FORGE_LIST = "https://files.minecraftforge.net/maven/net/minecraftforge/forge/json";
}
@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.game.AssetObject;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
@@ -35,6 +34,9 @@ import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
/**
*
* @author huangyuhui
@@ -91,7 +93,7 @@ public final class GameAssetDownloadTask extends Task {
try {
// check the checksum of file to ensure that the file is not need to re-download.
if (file.exists()) {
String sha1 = DigestUtils.sha1Hex(FileUtils.readBytes(file));
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");
@@ -33,6 +33,8 @@ import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* This task is to extract all asset objects described in asset index json.
*
@@ -82,7 +84,7 @@ public final class GameAssetRefreshTask extends TaskResult<Collection<Pair<File,
if (Thread.interrupted())
throw new InterruptedException();
res.add(new Pair<>(dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject), assetObject));
res.add(pair(dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject), assetObject));
updateProgress(++progress, index.getObjects().size());
}
setResult(res);
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.game.CompatibilityRule;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.TaskResult;
@@ -24,7 +25,10 @@ import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.VersionNumber;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
public class LibrariesUniqueTask extends TaskResult<Version> {
@@ -44,7 +48,6 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
public void execute() {
List<Library> libraries = new ArrayList<>(version.getLibraries());
Map<String, VersionNumber> versionMap = new HashMap<>();
SimpleMultimap<String, Library> multimap = new SimpleMultimap<String, Library>(HashMap::new, LinkedList::new);
for (Library library : libraries) {
@@ -52,32 +55,36 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
VersionNumber number = VersionNumber.asVersion(library.getVersion());
String serialized = Constants.GSON.toJson(library);
if (versionMap.containsKey(id)) {
VersionNumber otherNumber = versionMap.get(id);
if (number.compareTo(otherNumber) > 0) {
multimap.removeKey(id);
versionMap.put(id, number);
multimap.put(id, library);
} else if (number.compareTo(otherNumber) == 0) { // same library id.
boolean flag = false;
// prevent from duplicated libraries
for (Library otherLibrary : multimap.get(id))
if (library.equals(otherLibrary)) {
String otherSerialized = Constants.GSON.toJson(otherLibrary);
// A trick, the library that has more information is better, which can be
// considered whose serialized JSON text will be longer.
if (serialized.length() <= otherSerialized.length()) {
flag = true;
break;
} else {
multimap.removeValue(id, otherLibrary);
if (multimap.containsKey(id)) {
boolean hasEqualRule = 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;
if (number.compareTo(otherNumber) > 0) { // if this library is newer
multimap.removeValue(otherLibrary);
multimap.put(id, library);
break;
} else if (number.compareTo(otherNumber) == 0) { // same library id.
// prevent from duplicated libraries
if (library.equals(otherLibrary)) {
String otherSerialized = Constants.GSON.toJson(otherLibrary);
// A trick, the library that has more information is better, which can be
// considered whose serialized JSON text will be longer.
if (serialized.length() > otherSerialized.length()) {
multimap.removeValue(id, otherLibrary);
multimap.put(id, library);
break;
}
}
}
if (!flag)
multimap.put(id, library);
}
}
if (!hasEqualRule) {
multimap.put(id, library);
}
} else {
versionMap.put(id, number);
multimap.put(id, library);
}
}
@@ -5,7 +5,10 @@ import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.*;
import java.nio.charset.Charset;
@@ -15,6 +18,9 @@ import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
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;
@@ -77,7 +83,7 @@ public final class LibraryDownloadTask extends Task {
return true;
}
byte[] fileData = FileUtils.readBytes(libPath);
boolean valid = checksums.contains(DigestUtils.sha1Hex(fileData));
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
if ((!valid) && (libPath.getName().endsWith(".jar"))) {
}
return validateJar(fileData, checksums);
@@ -98,7 +104,7 @@ public final class LibraryDownloadTask extends Task {
hashes = new String(eData, Charset.forName("UTF-8")).split("\n");
}
if (!entry.isDirectory()) {
files.put(entry.getName(), DigestUtils.sha1Hex(eData));
files.put(entry.getName(), encodeHex(digest("SHA-1", eData)));
}
entry = jar.getNextJarEntry();
}
@@ -0,0 +1,121 @@
/*
* 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.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.Constants;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.VersionNumber;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/**
*
* @author huangyuhui
*/
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
public static final LiteLoaderBMCLVersionList INSTANCE = new LiteLoaderBMCLVersionList();
private LiteLoaderBMCLVersionList() {
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
return new Task() {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(task);
}
@Override
public void execute() {
lock.writeLock().lock();
try {
LiteLoaderVersionsRoot root = Constants.GSON.fromJson(task.getResult(), LiteLoaderVersionsRoot.class);
versions.clear();
for (Map.Entry<String, LiteLoaderGameVersions> entry : root.getVersions().entrySet()) {
String gameVersion = entry.getKey();
LiteLoaderGameVersions liteLoader = entry.getValue();
Optional<String> gg = VersionNumber.parseVersion(gameVersion);
if (!gg.isPresent())
continue;
doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getArtifacts(), false);
doBranch(gg.get(), gameVersion, liteLoader.getRepoitory(), liteLoader.getSnapshots(), true);
}
} finally {
lock.writeLock().unlock();
}
}
private void doBranch(String key, String gameVersion, LiteLoaderRepository repository, LiteLoaderBranch branch, boolean snapshot) {
if (branch == null || repository == null)
return;
for (Map.Entry<String, LiteLoaderVersion> entry : branch.getLiteLoader().entrySet()) {
String branchName = entry.getKey();
LiteLoaderVersion v = entry.getValue();
if ("latest".equals(branchName))
continue;
String version = v.getVersion();
String url = "http://bmclapi2.bangbang93.com/liteloader/download?version=" + version;
if (snapshot) {
try {
version = version.replace("SNAPSHOT", getLatestSnapshotVersion(repository.getUrl() + "com/mumfrey/liteloader/" + v.getVersion() + "/"));
url = repository.getUrl() + "com/mumfrey/liteloader/" + v.getVersion() + "/liteloader-" + version + "-release.jar";
} catch (Exception ignore) {
}
}
versions.put(key, new LiteLoaderRemoteVersion(gameVersion,
version, downloadProvider.injectURL(url),
v.getTweakClass(), v.getLibraries()
));
}
}
};
}
public static final String LITELOADER_LIST = "http://dl.liteloader.com/versions/versions.json";
private static String getLatestSnapshotVersion(String repo) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(repo + "maven-metadata.xml");
Element r = doc.getDocumentElement();
Element snapshot = (Element) r.getElementsByTagName("snapshot").item(0);
Node timestamp = snapshot.getElementsByTagName("timestamp").item(0);
Node buildNumber = snapshot.getElementsByTagName("buildNumber").item(0);
return timestamp.getTextContent() + "-" + buildNumber.getTextContent();
}
}
@@ -42,7 +42,7 @@ public final class OptiFineBMCLVersionList extends VersionList<Void> {
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("http://bmclapi.bangbang93.com/optifine/versionlist"));
GetTask task = new GetTask(NetworkUtils.toURL("http://bmclapi2.bangbang93.com/optifine/versionlist"));
return new Task() {
@Override
public Collection<Task> getDependents() {
@@ -70,13 +70,13 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
@Override
public void execute() {
String remoteVersion = remote.getSelfVersion();
String remoteVersion = remote.getGameVersion() + "-" + remote.getSelfVersion();
Library library = new Library(
"net.optifine", "optifine", remoteVersion, null, null,
new LibrariesDownloadInfo(new LibraryDownloadInfo(
"net/optifine/optifine/" + remoteVersion + "/optifine-" + remoteVersion + ".jar",
remote.getUrl())), true
remote.getUrl()))
);
List<Library> libraries = new LinkedList<>();
@@ -99,6 +99,8 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
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"));
@@ -52,7 +52,7 @@ public final class OptiFineVersionList extends VersionList<Void> {
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("http://optifine.net/downloads"));
GetTask task = new GetTask(NetworkUtils.toURL("https://optifine.net/downloads"));
return new Task() {
@Override
public Collection<Task> getDependents() {
@@ -116,6 +116,6 @@ public final class OptiFineVersionList extends VersionList<Void> {
result = m.group(1);
if (result == null)
throw new IllegalStateException("Cannot find version in " + content);
return "http://optifine.net/downloadx?f=OptiFine" + result;
return "https://optifine.net/downloadx?f=OptiFine" + result;
}
}
@@ -54,28 +54,22 @@ public final class Arguments {
return jvm == null ? Collections.emptyList() : Collections.unmodifiableList(jvm);
}
public static Arguments addGameArguments(Arguments arguments, String... gameArguments) {
return addGameArguments(arguments, Arrays.asList(gameArguments));
public Arguments addGameArguments(String... gameArguments) {
return addGameArguments(Arrays.asList(gameArguments));
}
public static Arguments addGameArguments(Arguments arguments, List<String> gameArguments) {
public Arguments addGameArguments(List<String> gameArguments) {
List<Argument> list = gameArguments.stream().map(StringArgument::new).collect(Collectors.toList());
if (arguments == null)
return new Arguments(list, null);
else
return new Arguments(Lang.merge(arguments.getGame(), list), arguments.getJvm());
return new Arguments(Lang.merge(getGame(), list), getJvm());
}
public static Arguments addJVMArguments(Arguments arguments, String... jvmArguments) {
return addJVMArguments(arguments, Arrays.asList(jvmArguments));
public Arguments addJVMArguments(String... jvmArguments) {
return addJVMArguments(Arrays.asList(jvmArguments));
}
public static Arguments addJVMArguments(Arguments arguments, List<String> jvmArguments) {
public Arguments addJVMArguments(List<String> jvmArguments) {
List<Argument> list = jvmArguments.stream().map(StringArgument::new).collect(Collectors.toList());
if (arguments == null)
return new Arguments(null, list);
else
return new Arguments(arguments.getGame(), Lang.merge(arguments.getJvm(), list));
return new Arguments(getGame(), Lang.merge(getJvm(), list));
}
public static Arguments merge(Arguments a, Arguments b) {
@@ -40,7 +40,7 @@ public class ClassicVersion extends Version {
public ClassicLibrary(String name) {
super("", "", "", null, null,
new LibrariesDownloadInfo(new LibraryDownloadInfo("bin/" + name + ".jar"), null),
false, null, null, null, null);
null, null, null, null);
}
}
@@ -76,6 +76,25 @@ public final class CompatibilityRule {
return action == Action.ALLOW;
}
public static boolean equals(Collection<CompatibilityRule> rules1, Collection<CompatibilityRule> rules2) {
return Objects.hashCode(rules1) == Objects.hashCode(rules2);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CompatibilityRule that = (CompatibilityRule) o;
return action == that.action &&
Objects.equals(os, that.os) &&
Objects.equals(features, that.features);
}
@Override
public int hashCode() {
return Objects.hash(action, os, features);
}
public enum Action {
ALLOW,
DISALLOW
@@ -30,6 +30,7 @@ import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Stream;
/**
* An implementation of classic Minecraft game repository.
@@ -154,8 +155,26 @@ public class DefaultGameRepository implements GameRepository {
File file = getVersionRoot(id);
if (!file.exists())
return true;
// test if no file in this version directory is occupied.
File removedFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + "_removed");
if (!file.renameTo(removedFile))
return false;
// 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);
return FileUtils.deleteDirectoryQuietly(file);
// remove the version from version list regardless of whether the directory was removed successfully or not.
try {
FileUtils.deleteDirectory(removedFile);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
}
return true;
}
protected void refreshVersionsImpl() {
@@ -170,51 +189,50 @@ public class DefaultGameRepository implements GameRepository {
File[] files = new File(getBaseDirectory(), "versions").listFiles();
if (files != null)
for (File dir : files)
if (dir.isDirectory()) {
String id = dir.getName();
File json = new File(dir, id + ".json");
Arrays.stream(files).parallel().filter(File::isDirectory).flatMap(dir -> {
String id = dir.getName();
File json = new File(dir, id + ".json");
// If user renamed the json file by mistake or created the json file in a wrong name,
// we will find the only json and rename it to correct name.
if (!json.exists()) {
List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
if (jsons.size() == 1)
if (!jsons.get(0).renameTo(json)) {
Logging.LOG.warning("Cannot rename json file " + jsons.get(0) + " to " + json + ", ignoring version " + id);
continue;
}
}
// If user renamed the json file by mistake or created the json file in a wrong name,
// we will find the only json and rename it to correct name.
if (!json.exists()) {
List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
if (jsons.size() == 1)
if (!jsons.get(0).renameTo(json)) {
Logging.LOG.warning("Cannot rename json file " + jsons.get(0) + " to " + json + ", ignoring version " + id);
return Stream.empty();
}
}
Version version;
try {
version = Objects.requireNonNull(readVersionJson(json));
} catch (Exception e) {
// JsonSyntaxException or IOException or NullPointerException(!!)
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
return Stream.empty();
Version version;
try {
version = Objects.requireNonNull(readVersionJson(json));
} catch (Exception e) {
// JsonSyntaxException or IOException or NullPointerException(!!)
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
continue;
try {
version = Objects.requireNonNull(readVersionJson(json));
} catch (Exception e2) {
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed");
continue;
}
} catch (Exception e2) {
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed");
return Stream.empty();
}
if (!id.equals(version.getId())) {
version = version.setId(id);
try {
FileUtils.writeText(json, Constants.GSON.toJson(version));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.");
continue;
}
}
provider.addVersion(version);
}
if (!id.equals(version.getId())) {
version = version.setId(id);
try {
FileUtils.writeText(json, Constants.GSON.toJson(version));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.");
return Stream.empty();
}
}
return Stream.of(version);
}).forEachOrdered(provider::addVersion);
for (Version version : provider.getVersionMap().values()) {
try {
Version resolved = version.resolve(provider);
@@ -19,13 +19,14 @@ package org.jackhuang.hmcl.game;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.IOUtils;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* @author huangyuhui
*/
@@ -55,7 +56,7 @@ public final class GameVersion {
private static Optional<String> getVersionOfOldMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
byte[] tmp = IOUtils.readFullyAsByteArray(file.getInputStream(entry));
byte[] bytes = "Minecraft Minecraft ".getBytes(Charsets.US_ASCII);
byte[] bytes = "Minecraft Minecraft ".getBytes(US_ASCII);
int j = matchArray(tmp, bytes);
if (j < 0)
return Optional.empty();
@@ -64,25 +65,25 @@ public final class GameVersion {
if ((j = lessThan32(tmp, i)) < 0)
return Optional.empty();
return Optional.of(new String(tmp, i, j - i, Charsets.US_ASCII));
return Optional.of(new String(tmp, i, j - i, US_ASCII));
}
private static Optional<String> getVersionOfNewMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
byte[] tmp = IOUtils.readFullyAsByteArray(file.getInputStream(entry));
byte[] str = "-server.txt".getBytes(Charsets.US_ASCII);
byte[] str = "-server.txt".getBytes(US_ASCII);
int j = matchArray(tmp, str);
if (j < 0) return Optional.empty();
int i = j + str.length;
i += 11;
j = lessThan32(tmp, i);
if (j < 0) return Optional.empty();
String result = new String(tmp, i, j - i, Charsets.US_ASCII);
String result = new String(tmp, i, j - i, US_ASCII);
char ch = result.charAt(0);
// 1.8.1+
if (ch < '0' || ch > '9') {
str = "Can't keep up! Did the system time change, or is the server overloaded?".getBytes(Charsets.US_ASCII);
str = "Can't keep up! Did the system time change, or is the server overloaded?".getBytes(US_ASCII);
j = matchArray(tmp, str);
if (j < 0) return Optional.empty();
i = -1;
@@ -100,7 +101,7 @@ public final class GameVersion {
while (tmp[k] >= 48 && tmp[k] <= 57 || tmp[k] == (int) '-' || tmp[k] == (int) '.' || tmp[k] >= 97 && tmp[k] <= (int) 'z')
k--;
k++;
return Optional.of(new String(tmp, k, i - k + 1, Charsets.US_ASCII));
return Optional.of(new String(tmp, k, i - k + 1, US_ASCII));
}
return Optional.of(result);
}
@@ -45,7 +45,6 @@ public class Library implements Comparable<Library> {
private final LibrariesDownloadInfo downloads;
private final LibraryDownloadInfo download;
private final ExtractRules extract;
private final boolean lateload;
private final Map<OperatingSystem, String> natives;
private final List<CompatibilityRule> rules;
private final List<String> checksums;
@@ -55,16 +54,12 @@ public class Library implements Comparable<Library> {
public Library(String groupId, String artifactId, String version) {
this(groupId, artifactId, version, null, null, null);
}
public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads) {
this(groupId, artifactId, version, classifier, url, downloads, false);
this(groupId, artifactId, version, classifier, url, downloads, null, null, null, null);
}
public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads, boolean lateload) {
this(groupId, artifactId, version, classifier, url, downloads, lateload, null, null, null, null);
}
public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads, boolean lateload, List<String> checksums, ExtractRules extract, Map<OperatingSystem, String> natives, List<CompatibilityRule> rules) {
public Library(String groupId, String artifactId, String version, String classifier, String url, LibrariesDownloadInfo downloads, List<String> checksums, ExtractRules extract, Map<OperatingSystem, String> natives, List<CompatibilityRule> rules) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
@@ -78,7 +73,6 @@ public class Library implements Comparable<Library> {
this.url = url;
this.downloads = downloads;
this.extract = extract;
this.lateload = lateload;
this.natives = natives;
this.rules = rules;
this.checksums = checksums;
@@ -139,14 +133,14 @@ public class Library implements Comparable<Library> {
return download;
}
public boolean isLateload() {
return lateload;
}
public List<String> getChecksums() {
return checksums;
}
public List<CompatibilityRule> getRules() {
return rules;
}
public boolean is(String groupId, String artifactId) {
return this.groupId.equals(groupId) && this.artifactId.equals(artifactId);
}
@@ -179,7 +173,7 @@ public class Library implements Comparable<Library> {
}
public Library setClassifier(String classifier) {
return new Library(groupId, artifactId, version, classifier, url, downloads, lateload, checksums, extract, natives, rules);
return new Library(groupId, artifactId, version, classifier, url, downloads, checksums, extract, natives, rules);
}
public static Library fromName(String name) {
@@ -191,7 +185,7 @@ public class Library implements Comparable<Library> {
if (arr.length != 3 && arr.length != 4)
throw new IllegalArgumentException("Library name is malformed. Correct example: group:artifact:version(:classifier).");
return new Library(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, url, downloads, false, checksums, extract, natives, rules);
return new Library(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, url, downloads, checksums, extract, natives, rules);
}
public static class Serializer implements JsonDeserializer<Library>, JsonSerializer<Library> {
@@ -27,6 +27,9 @@ import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
*
* @author huangyuhui
@@ -110,29 +113,23 @@ public class DefaultLauncher extends Launcher {
res.add("-Dfml.ignorePatchDiscrepancies=true");
}
LinkedList<File> lateload = new LinkedList<>();
StringBuilder classpath = new StringBuilder();
LinkedList<String> classpath = new LinkedList<>();
for (Library library : version.getLibraries())
if (library.appliesToCurrentEnvironment() && !library.isNative()) {
File f = repository.getLibraryFile(version, library);
if (f.exists() && f.isFile())
if (library.isLateload())
lateload.add(f);
else
classpath.append(f.getAbsolutePath()).append(OperatingSystem.PATH_SEPARATOR);
classpath.add(f.getAbsolutePath());
}
for (File library : lateload)
classpath.append(library.getAbsolutePath()).append(OperatingSystem.PATH_SEPARATOR);
File jar = repository.getVersionJar(version);
if (!jar.exists() || !jar.isFile())
throw new IOException("Minecraft jar does not exist");
classpath.append(jar.getAbsolutePath());
classpath.add(jar.getAbsolutePath());
// Provided Minecraft arguments
File gameAssets = repository.getActualAssetDirectory(version.getId(), version.getAssetIndex().getId());
Map<String, String> configuration = getConfigurations();
configuration.put("${classpath}", classpath.toString());
configuration.put("${classpath}", String.join(OperatingSystem.PATH_SEPARATOR, classpath));
configuration.put("${natives_directory}", nativeFolder.getAbsolutePath());
configuration.put("${game_assets}", gameAssets.getAbsolutePath());
configuration.put("${assets_root}", gameAssets.getAbsolutePath());
@@ -197,8 +194,8 @@ public class DefaultLauncher extends Launcher {
);
}
private final Map<String, Supplier<Boolean>> forbiddens = Lang.mapOf(
new Pair<String, Supplier<Boolean>>("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9)
private final Map<String, Supplier<Boolean>> forbiddens = mapOf(
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9)
);
protected Map<String, Supplier<Boolean>> getForbiddens() {
@@ -238,18 +235,18 @@ public class DefaultLauncher extends Launcher {
}
protected Map<String, String> getConfigurations() {
return Lang.mapOf(
new Pair<>("${auth_player_name}", authInfo.getUsername()),
new Pair<>("${auth_session}", authInfo.getAccessToken()),
new Pair<>("${auth_access_token}", authInfo.getAccessToken()),
new Pair<>("${auth_uuid}", authInfo.getUserId()),
new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
new Pair<>("${version_type}", version.getType().getId()),
new Pair<>("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
new Pair<>("${user_type}", authInfo.getUserType().toString().toLowerCase()),
new Pair<>("${assets_index_name}", version.getAssetIndex().getId()),
new Pair<>("${user_properties}", authInfo.getUserProperties())
return mapOf(
pair("${auth_player_name}", authInfo.getUsername()),
pair("${auth_session}", authInfo.getAccessToken()),
pair("${auth_access_token}", authInfo.getAccessToken()),
pair("${auth_uuid}", UUIDTypeAdapter.fromUUID(authInfo.getUUID())),
pair("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
pair("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
pair("${version_type}", version.getType().getId()),
pair("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
pair("${user_type}", "mojang"),
pair("${assets_index_name}", version.getAssetIndex().getId()),
pair("${user_properties}", authInfo.getUserProperties())
);
}
@@ -276,9 +273,7 @@ public class DefaultLauncher extends Launcher {
}
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
if (listener == null)
startMonitors(p);
else
if (listener != null)
startMonitors(p, listener, daemon);
return p;
}
@@ -301,7 +296,7 @@ public class DefaultLauncher extends Launcher {
if (isWindows) {
writer.write("@echo off");
writer.newLine();
writer.write("set APPDATA=" + options.getGameDir().getParent());
writer.write("set APPDATA=" + options.getGameDir().getAbsoluteFile().getParent());
writer.newLine();
writer.write("cd /D %APPDATA%");
writer.newLine();
@@ -316,11 +311,6 @@ public class DefaultLauncher extends Launcher {
throw new PermissionException();
}
private void startMonitors(ManagedProcess managedProcess) {
managedProcess.addRelatedThread(Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream()), "stdout-pump", true));
managedProcess.addRelatedThread(Lang.thread(new StreamPump(managedProcess.getProcess().getErrorStream()), "stderr-pump", true));
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener) {
startMonitors(managedProcess, processListener, true);
}
@@ -21,7 +21,6 @@ import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.FileUtils;
import java.io.File;
@@ -29,6 +28,9 @@ import java.io.FileInputStream;
import java.util.LinkedList;
import java.util.List;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public final class MinecraftInstanceTask<T> extends Task {
private final File zipFile;
@@ -62,7 +64,7 @@ public final class MinecraftInstanceTask<T> extends Task {
if (path.startsWith("/") || path.startsWith("\\"))
path = path.substring(1);
overrides.add(new ModpackConfiguration.FileInformation(path, DigestUtils.sha1Hex(zip)));
overrides.add(new ModpackConfiguration.FileInformation(path, encodeHex(digest("SHA-1", zip))));
}
}
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.mod;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
@@ -31,6 +30,9 @@ import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public class ModpackInstallTask<T> extends Task {
private final File modpackFile;
@@ -92,8 +94,8 @@ public class ModpackInstallTask<T> extends Task {
byte[] data = os.toByteArray();
if (files.contains(path) && entryFile.exists()) {
String oldHash = DigestUtils.sha1Hex(new FileInputStream(entryFile));
String newHash = DigestUtils.sha1Hex(new ByteArrayInputStream(data));
String oldHash = encodeHex(digest("SHA-1", new FileInputStream(entryFile)));
String newHash = encodeHex(digest("SHA-1", new ByteArrayInputStream(data)));
if (!oldHash.equals(newHash)) {
try (FileOutputStream fos = new FileOutputStream(entryFile)) {
IOUtils.copyTo(new ByteArrayInputStream(data), fos, buf);
@@ -109,7 +109,7 @@ public final class MultiMCModpackInstallTask extends Task {
version = version
.setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries()))
.setMainClass(patch.getMainClass())
.setArguments(Arguments.addGameArguments(version.getArguments().orElse(null), newArguments));
.setArguments(version.getArguments().orElseGet(Arguments::new).addGameArguments(newArguments));
}
}
}
@@ -19,7 +19,10 @@ package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.event.FailedEvent;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.io.IOException;
@@ -32,6 +35,8 @@ import java.net.URL;
import java.security.MessageDigest;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.DigestUtils.getDigest;
/**
* A task that can download a file online.
*
@@ -154,7 +159,7 @@ public class FileDownloadTask extends Task {
temp = FileUtils.createTempFile();
rFile = new RandomAccessFile(temp, "rw");
MessageDigest digest = DigestUtils.getSha1Digest();
MessageDigest digest = getDigest("SHA-1");
stream = con.getInputStream();
int lastDownloaded = 0, downloaded = 0;
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.Charsets;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
@@ -31,6 +30,8 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huang
@@ -52,7 +53,7 @@ public final class GetTask extends TaskResult<String> {
}
public GetTask(URL url, Proxy proxy, String id) {
this(url, proxy, id, Charsets.DEFAULT_CHARSET);
this(url, proxy, id, UTF_8);
}
public GetTask(URL url, Proxy proxy, String id, Charset charset) {
@@ -28,7 +28,7 @@ import java.util.Collection;
*/
public final class ParallelTask extends Task {
private final Task[] tasks;
private final Collection<Task> tasks;
/**
* Constructor.
@@ -36,6 +36,11 @@ public final class ParallelTask extends Task {
* @param tasks the tasks that can be executed parallelly.
*/
public ParallelTask(Task... tasks) {
this.tasks = Arrays.asList(tasks);
setSignificance(TaskSignificance.MINOR);
}
public ParallelTask(Collection<Task> tasks) {
this.tasks = tasks;
setSignificance(TaskSignificance.MINOR);
}
@@ -46,7 +51,7 @@ public final class ParallelTask extends Task {
@Override
public Collection<Task> getDependents() {
return Arrays.asList(tasks);
return tasks;
}
}
@@ -119,12 +119,12 @@ public final class Schedulers {
Logging.LOG.info("Shutting down executor services.");
if (CACHED_EXECUTOR != null)
CACHED_EXECUTOR.shutdown();
CACHED_EXECUTOR.shutdownNow();
if (IO_EXECUTOR != null)
IO_EXECUTOR.shutdown();
IO_EXECUTOR.shutdownNow();
if (SINGLE_EXECUTOR != null)
SINGLE_EXECUTOR.shutdown();
SINGLE_EXECUTOR.shutdownNow();
}
}
@@ -1,55 +0,0 @@
/*
* Hello Minecraft!.
* 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 java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
public final class Charsets {
private Charsets() {
}
public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
public static final Charset US_ASCII = Charset.forName("US-ASCII");
public static final Charset UTF_16 = Charset.forName("UTF-16");
public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
public static final Charset UTF_8 = Charset.forName("UTF-8");
public static final Charset DEFAULT_CHARSET = UTF_8;
public static Charset toCharset(String charset) {
if (charset == null)
return Charset.defaultCharset();
try {
return Charset.forName(charset);
} catch (UnsupportedCharsetException ignored) {
return Charset.defaultCharset();
}
}
public static Charset toCharset() {
return toCharset(System.getProperty("sun.jnu.encoding", Constants.DEFAULT_ENCODING));
}
}
@@ -43,13 +43,11 @@ public final class Constants {
private Constants() {
}
public static final String DEFAULT_ENCODING = "UTF-8";
public static final Charset DEFAULT_CHARSET = Charsets.UTF_8;
public static final Charset SYSTEM_CHARSET = Charset.forName(OperatingSystem.ENCODING);
public static final String DEFAULT_LIBRARY_URL = "https://libraries.minecraft.net/";
public static final String DEFAULT_VERSION_DOWNLOAD_URL = "http://s3.amazonaws.com/Minecraft.Download/versions/";
public static final String DEFAULT_INDEX_URL = "http://s3.amazonaws.com/Minecraft.Download/indexes/";
public static final String DEFAULT_VERSION_DOWNLOAD_URL = "https://s3.amazonaws.com/Minecraft.Download/versions/";
public static final String DEFAULT_INDEX_URL = "https://s3.amazonaws.com/Minecraft.Download/indexes/";
public static Consumer<Runnable> UI_THREAD_SCHEDULER = s -> Schedulers.computation().schedule(s::run);
@@ -22,6 +22,8 @@ import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huangyuhui
@@ -33,11 +35,6 @@ public final class DigestUtils {
private static final int STREAM_BUFFER_LENGTH = 1024;
private static byte[] digest(MessageDigest digest, InputStream data)
throws IOException {
return updateDigest(digest, data).digest();
}
public static MessageDigest getDigest(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
@@ -46,193 +43,23 @@ public final class DigestUtils {
}
}
public static MessageDigest getMd2Digest() {
return getDigest("MD2");
public static byte[] digest(String algorithm, String data) {
return digest(algorithm, data.getBytes(UTF_8));
}
public static MessageDigest getMd5Digest() {
return getDigest("MD5");
public static byte[] digest(String algorithm, byte[] data) {
return getDigest(algorithm).digest(data);
}
public static MessageDigest getSha1Digest() {
return getDigest("SHA-1");
public static byte[] digest(String algorithm, InputStream data) throws IOException {
return digest(getDigest(algorithm), data);
}
public static MessageDigest getSha256Digest() {
return getDigest("SHA-256");
public static byte[] digest(MessageDigest digest, InputStream data) throws IOException {
return updateDigest(digest, data).digest();
}
public static MessageDigest getSha384Digest() {
return getDigest("SHA-384");
}
public static MessageDigest getSha512Digest() {
return getDigest("SHA-512");
}
public static byte[] md2(byte[] data) {
return getMd2Digest().digest(data);
}
public static byte[] md2(InputStream data)
throws IOException {
return digest(getMd2Digest(), data);
}
public static byte[] md2(String data) {
return md2(data.getBytes(Charsets.UTF_8));
}
public static String md2Hex(byte[] data) {
return Hex.encodeHexString(md2(data));
}
public static String md2Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(md2(data));
}
public static String md2Hex(String data) {
return Hex.encodeHexString(md2(data));
}
public static byte[] md5(byte[] data) {
return getMd5Digest().digest(data);
}
public static byte[] md5(InputStream data)
throws IOException {
return digest(getMd5Digest(), data);
}
public static byte[] md5(String data) {
return md5(data.getBytes(Charsets.UTF_8));
}
public static String md5Hex(byte[] data) {
return Hex.encodeHexString(md5(data));
}
public static String md5Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(md5(data));
}
public static String md5Hex(String data) {
return Hex.encodeHexString(md5(data));
}
public static byte[] sha1(byte[] data) {
return getSha1Digest().digest(data);
}
public static byte[] sha1(InputStream data)
throws IOException {
return digest(getSha1Digest(), data);
}
public static byte[] sha1(String data) {
return sha1(data.getBytes(Charsets.UTF_8));
}
public static String sha1Hex(byte[] data) {
return Hex.encodeHexString(sha1(data));
}
public static String sha1Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha1(data));
}
public static String sha1Hex(String data) {
return Hex.encodeHexString(sha1(data));
}
public static byte[] sha256(byte[] data) {
return getSha256Digest().digest(data);
}
public static byte[] sha256(InputStream data)
throws IOException {
return digest(getSha256Digest(), data);
}
public static byte[] sha256(String data) {
return sha256(data.getBytes(Charsets.UTF_8));
}
public static String sha256Hex(byte[] data) {
return Hex.encodeHexString(sha256(data));
}
public static String sha256Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha256(data));
}
public static String sha256Hex(String data) {
return Hex.encodeHexString(sha256(data));
}
public static byte[] sha384(byte[] data) {
return getSha384Digest().digest(data);
}
public static byte[] sha384(InputStream data)
throws IOException {
return digest(getSha384Digest(), data);
}
public static byte[] sha384(String data) {
return sha384(data.getBytes(Charsets.UTF_8));
}
public static String sha384Hex(byte[] data) {
return Hex.encodeHexString(sha384(data));
}
public static String sha384Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha384(data));
}
public static String sha384Hex(String data) {
return Hex.encodeHexString(sha384(data));
}
public static byte[] sha512(byte[] data) {
return getSha512Digest().digest(data);
}
public static byte[] sha512(InputStream data)
throws IOException {
return digest(getSha512Digest(), data);
}
public static byte[] sha512(String data) {
return sha512(data.getBytes(Charsets.UTF_8));
}
public static String sha512Hex(byte[] data) {
return Hex.encodeHexString(sha512(data));
}
public static String sha512Hex(InputStream data)
throws IOException {
return Hex.encodeHexString(sha512(data));
}
public static String sha512Hex(String data) {
return Hex.encodeHexString(sha512(data));
}
public static MessageDigest updateDigest(MessageDigest messageDigest, byte[] valueToDigest) {
messageDigest.update(valueToDigest);
return messageDigest;
}
public static MessageDigest updateDigest(MessageDigest digest, InputStream data)
throws IOException {
public static MessageDigest updateDigest(MessageDigest digest, InputStream data) throws IOException {
byte[] buffer = new byte[STREAM_BUFFER_LENGTH];
int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
@@ -244,8 +71,4 @@ public final class DigestUtils {
return digest;
}
public static MessageDigest updateDigest(MessageDigest messageDigest, String valueToDigest) {
messageDigest.update(valueToDigest.getBytes(Charsets.UTF_8));
return messageDigest;
}
}
@@ -26,6 +26,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
*
* @author huang
@@ -44,7 +46,7 @@ public final class FileUtils {
}
public static String readText(File file) throws IOException {
return readText(file, Charsets.DEFAULT_CHARSET);
return readText(file, UTF_8);
}
public static String readText(File file, Charset charset) throws IOException {
@@ -60,7 +62,7 @@ public final class FileUtils {
}
public static void writeText(File file, String text) throws IOException {
writeText(file, text, Charsets.DEFAULT_CHARSET);
writeText(file, text, UTF_8);
}
public static void writeText(File file, String text, Charset charset) throws IOException {
@@ -17,20 +17,18 @@
*/
package org.jackhuang.hmcl.util;
import java.nio.charset.Charset;
import java.io.IOException;
public final class Hex {
private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
private final Charset charset;
public static byte[] decodeHex(char[] data) throws Exception {
public static byte[] decodeHex(String str) throws IOException {
char[] data = str.toCharArray();
int len = data.length;
if ((len & 0x1) != 0)
throw new Exception("Odd number of characters.");
throw new IOException("Odd number of characters.");
byte[] out = new byte[len >> 1];
@@ -46,83 +44,24 @@ public final class Hex {
return out;
}
public static char[] encodeHex(byte[] data) {
return encodeHex(data, true);
}
public static char[] encodeHex(byte[] data, boolean toLowerCase) {
return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
}
protected static char[] encodeHex(byte[] data, char[] toDigits) {
public static String encodeHex(byte[] data) {
int l = data.length;
char[] out = new char[l << 1];
int i = 0;
for (int j = 0; i < l; i++) {
out[(j++)] = toDigits[((0xF0 & data[i]) >>> 4)];
out[(j++)] = toDigits[(0xF & data[i])];
out[(j++)] = DIGITS_LOWER[((0xF0 & data[i]) >>> 4)];
out[(j++)] = DIGITS_LOWER[(0xF & data[i])];
}
return out;
return new String(out);
}
public static String encodeHexString(byte[] data) {
return new String(encodeHex(data));
}
protected static int toDigit(char ch, int index) {
private static int toDigit(char ch, int index) throws IOException {
int digit = Character.digit(ch, 16);
if (digit == -1)
throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
throw new IOException("Illegal hexadecimal character " + ch + " at index " + index);
return digit;
}
public Hex() {
this(Charsets.DEFAULT_CHARSET);
}
public Hex(Charset charset) {
this.charset = charset;
}
public byte[] decode(byte[] array) throws Exception {
return decodeHex(new String(array, getCharset()).toCharArray());
}
public Object decode(Object object) throws Exception {
try {
char[] charArray = (object instanceof String) ? ((String) object).toCharArray() : (char[]) object;
return decodeHex(charArray);
} catch (ClassCastException e) {
throw new Exception(e.getMessage(), e);
}
}
public byte[] encode(byte[] array) {
return encodeHexString(array).getBytes(getCharset());
}
public Object encode(Object object)
throws Exception {
try {
byte[] byteArray = (object instanceof String) ? ((String) object).getBytes(getCharset()) : (byte[]) object;
return encodeHex(byteArray);
} catch (ClassCastException e) {
throw new Exception(e.getMessage(), e);
}
}
public Charset getCharset() {
return this.charset;
}
public String getCharsetName() {
return this.charset.name();
}
@Override
public String toString() {
return super.toString() + "[charsetName=" + this.charset + "]";
}
private Hex() {}
}
@@ -18,10 +18,20 @@
package org.jackhuang.hmcl.util;
import java.io.*;
import java.util.*;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
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;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Represents a Java installation.
@@ -69,7 +79,7 @@ public final class JavaVersion implements Serializable {
return version;
}
private static final Pattern REGEX = Pattern.compile("java version \"(?<version>(.*?))\"");
private static final Pattern REGEX = Pattern.compile("version \"(?<version>(.*?))\"");
public static final int UNKNOWN = -1;
public static final int JAVA_5 = 50;
@@ -155,10 +165,14 @@ public final class JavaVersion implements Serializable {
Platform.PLATFORM
);
private static Map<String, JavaVersion> JAVAS;
private static List<JavaVersion> JAVAS;
private static final CountDownLatch LATCH = new CountDownLatch(1);
public static Map<String, JavaVersion> getJREs() throws InterruptedException {
public static Optional<List<JavaVersion>> getJREsImmediately() {
return Optional.ofNullable(JAVAS);
}
public static List<JavaVersion> getJREs() throws InterruptedException {
if (JAVAS != null)
return JAVAS;
LATCH.await();
@@ -168,13 +182,14 @@ public final class JavaVersion implements Serializable {
public static synchronized void initialize() throws IOException {
if (JAVAS != null)
throw new IllegalStateException("JavaVersions have already been initialized.");
HashMap<String, JavaVersion> temp = new HashMap<>();
temp.put(THIS_JAVA.getVersion(), THIS_JAVA);
List<JavaVersion> javaVersions;
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
javaVersions = queryWindows();
break;
case LINUX:
javaVersions = queryLinux();
break;
case OSX:
javaVersions = queryMacintosh();
break;
@@ -182,12 +197,44 @@ public final class JavaVersion implements Serializable {
javaVersions = Collections.emptyList();
break;
}
for (JavaVersion v : javaVersions)
temp.put(v.getVersion(), v);
JAVAS = Collections.unmodifiableMap(temp);
boolean isCurrentJavaIncluded = false;
for (int i = 0; i < javaVersions.size(); i++) {
if (THIS_JAVA.getBinary().equals(javaVersions.get(i).getBinary())) {
javaVersions.set(i, THIS_JAVA);
isCurrentJavaIncluded = true;
break;
}
}
if (!isCurrentJavaIncluded) {
javaVersions.add(THIS_JAVA);
}
JAVAS = Collections.unmodifiableList(javaVersions);
LATCH.countDown();
}
private static List<JavaVersion> queryLinux() throws IOException {
Path jvmDir = Paths.get("/usr/lib/jvm");
if (Files.isDirectory(jvmDir)) {
return Files.list(jvmDir)
.filter(dir -> Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS))
.map(dir -> dir.resolve("bin/java"))
.filter(Files::isExecutable)
.flatMap(executable -> {
try {
return Stream.of(fromExecutable(executable.toFile()));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Couldn't determine java " + executable, e);
return Stream.empty();
}
})
.collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
private static List<JavaVersion> queryMacintosh() throws IOException {
LinkedList<JavaVersion> res = new LinkedList<>();
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more