Compare commits

..
140 Commits
Author SHA1 Message Date
huangyuhui 7d2ea0b77e Add Tranditional Chinese translation 2018-07-01 13:37:09 +08:00
huangyuhui 1f30c299a5 Fix #360 2018-07-01 13:34:52 +08:00
huanghongxun c887d7e649 Merge pull request #358 from yushijinhun/authlib-injector
重构 authlib-injector 下载,并切换到新 API
2018-07-01 11:54:55 +08:00
yushijinhun 3fc7743a14 Add missing license header 2018-06-30 22:45:57 +08:00
yushijinhun 2a232f70db Refactor authlib-injector downloading, switch to new API 2018-06-30 22:45:57 +08:00
yushijinhun 6fae131287 Display (None) instead of empty string if no Yggdrasil server is available when adding account 2018-06-30 22:44:26 +08:00
yushijinhun 7ee7ef25f9 Refactor integrity check in FileDownloadTask, support multi algorithms 2018-06-30 22:43:57 +08:00
huangyuhui 2bd4bc1472 Add debug message in Task.run 2018-06-30 22:24:46 +08:00
huanghongxun aa170b647e Merge pull request #359 from yushijinhun/fixes
Fix bugs with yggdrasil authentication.
2018-06-30 22:12:06 +08:00
huangyuhui 4fa01216b1 Add debug message for tracing EmptyStackException and IllegalStateException 2018-06-30 22:09:35 +08:00
yushijinhun a82eb996a4 Add missing license header 2018-06-30 21:54:31 +08:00
yushijinhun 9315684ee3 Use CredentialExpiredException to indicate password login
instead of Non-ServerDisconnectException
2018-06-30 21:53:11 +08:00
huangyuhui 34f7774ce9 Fix NPE when the game launched and suddenly crashes 2018-06-30 21:36:57 +08:00
huangyuhui 75031a04cc Fix not on FX application thread 2018-06-30 21:28:41 +08:00
yushijinhun cfb0526cbb Fix potential NPE in YggdrasilAccount(selectedProfile is null) 2018-06-30 20:54:27 +08:00
yushijinhun 9fa56a9cd4 Regard token to be invalid only when remote throws ForbiddenOperationException 2018-06-30 18:52:37 +08:00
huangyuhui c4db9c9e12 Always show the advanced version setting pane as some people cannot find the options 2018-06-27 01:06:43 +08:00
huangyuhui 71272371e4 Fix #356 2018-06-26 21:55:29 +08:00
huangyuhui 91c6c4d233 Reduce size of welcome.png 2018-06-23 22:37:41 +08:00
huangyuhui 8a6653a926 Only show welcome page to Chinese 2018-06-23 22:25:39 +08:00
huangyuhui 76caf84e39 Welcome page 2018-06-23 22:15:23 +08:00
huangyuhui 0fee286b84 Use BMCLAPI as default ownload provider 2018-06-23 19:42:36 +08:00
huangyuhui 4af18a1575 Change build status icon in README.md 2018-06-23 13:03:28 +08:00
huangyuhui d9293f4da8 Display remote error message when RemoteAuthenticationException thrown 2018-06-21 19:31:16 +08:00
huangyuhui db29f97397 Update the link for crash reports 2018-06-17 18:22:22 +08:00
huangyuhui 573f41611d Force using Java 8 when launching Minecraft 1.13 or later 2018-06-17 18:13:47 +08:00
huanghongxun 28ed103ec0 Merge pull request #353 from yushijinhun/translations
完善 UI 提示消息
2018-06-17 17:45:59 +08:00
huanghongxun 5e37750f00 Merge pull request #352 from yushijinhun/refactor-update
添加 hmcl.version.override 选项用于调试更新功能
2018-06-17 17:40:07 +08:00
yushijinhun 5fe3cd9406 移除input.url.http错误提示 2018-06-17 17:28:12 +08:00
yushijinhun b00cc99408 BMCLAPI的链接使用https 2018-06-17 17:27:44 +08:00
yushijinhun 9a66c3bba9 完善翻译
中英文间补白、更正大小写、调整措辞
2018-06-17 17:25:22 +08:00
yushijinhun 546921b445 删除未被使用过的i18n条目 2018-06-17 16:40:22 +08:00
yushijinhun 8b698d3a4c 更新设置页面中依赖信息 2018-06-17 15:33:35 +08:00
yushijinhun c4608a949b 处理未捕获异常时立即进行log,防止异常被忽略
THROWABLE_SET改名为CAUGHT_EXCEPTIONS,使用并发安全集合
2018-06-17 15:04:31 +08:00
yushijinhun bba1c559a3 为连接authlib-injector服务器失败添加错误消息 2018-06-17 14:41:26 +08:00
yushijinhun bdee9ea72f 将获取验证服务器信息的代码移入AuthlibInjectorServer 2018-06-17 14:26:38 +08:00
yushijinhun 520923ba08 跳过更新时记录日志 2018-06-17 13:44:04 +08:00
yushijinhun 936a74597f 若版本号包含SNAPSHOT则也不更新
通过Gradle构建出来的JAR版本号为3.1.SNAPSHOT这样的
2018-06-17 13:40:09 +08:00
yushijinhun 97132745cc 增加hmcl.version.override选项用于覆写HMCL版本 2018-06-17 13:30:05 +08:00
yushijinhun 2bb30f65f3 在AppDataUpgrader中使用ReflectiveOperationException替代具体反射异常 2018-06-17 13:11:56 +08:00
huangyuhui ec04d55793 Fix transition 2018-06-17 12:05:39 +08:00
huangyuhui 88671db566 Use spinner instead of indeterminate progress bar 2018-06-17 12:05:25 +08:00
huanghongxun 538f501bc0 Merge pull request #351 from yushijinhun/refactor-yggdrasil
authlib-injector 部分重构
2018-06-17 10:34:47 +08:00
huangyuhui 3b75d41a2f 修改更新服务器连接 2018-06-17 10:34:13 +08:00
yushijinhun 110149d0e6 AuthlibInjectorAccountFactory的addtionalData改为AuthlibInjectorServer类型 2018-06-16 23:14:04 +08:00
yushijinhun 89b68781fa 修复添加验证服务器页面的错误信息不会被清除 2018-06-16 23:09:16 +08:00
yushijinhun 4cdc54b9c1 修复添加账号时未选中验证服务器会NPE 2018-06-16 23:06:21 +08:00
yushijinhun 7673595a43 AddAccountPane中服务器列表数据视图分离 2018-06-16 22:58:39 +08:00
yushijinhun 76a259c107 AuthlibInjectorAccount中存储AuthlibInjectorServer对象而不是url 2018-06-16 21:26:36 +08:00
yushijinhun 5f40d01fb4 在AuthlibInjectorAccountFactory中使用AuthlibInjectorServer而不是url 2018-06-16 21:26:36 +08:00
yushijinhun df29d47c0a workaround: 将Settings中的Config字段改为静态 2018-06-16 21:26:36 +08:00
yushijinhun 10d4084dde AuthlibInjectorServerInfo重命名为AuthlibInjectorServer
更正其它措辞:ip->url
2018-06-16 21:26:34 +08:00
yushijinhun ba8cdb1b42 移除AuthlibInjectorServersPage的spinner 2018-06-16 21:25:25 +08:00
yushijinhun 22732fee6a 存储AuthlibInjectorServerInfo对象而不是url字符串 2018-06-16 21:24:30 +08:00
yushijinhun bfbc4613b9 在添加authlib-injector服务器的过程中,传递AuthlibInjectorServerInfo而不是url 2018-06-16 20:33:48 +08:00
yushijinhun 92e5eecaf2 将Settings中DEFAULT_PROFILE, HOME_PROFILE字段移至Profiles 2018-06-16 20:30:14 +08:00
yushijinhun 593159f4a6 将DEFAULT_ICON移至Constants, 并使用懒加载
当类初始化时, JavaFX可能还未启动, 这将使图像加载失败
2018-06-16 20:10:10 +08:00
yushijinhun 53cad0e56a 在因无法读取而移除账户时记录日志 2018-06-16 20:10:10 +08:00
huangyuhui d61006dd67 Fix uncaught IOException 2018-06-13 23:40:16 +08:00
huangyuhui eb565285b2 Fix curse mod downloading 2018-06-10 20:05:36 +08:00
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
huangyuhui eb1a109afc Fix IllegalArgumentException: Folder parameter must be a valid folder 2018-03-05 20:36:38 +08:00
huangyuhui a03e3fea12 Logs stopApplication stack trace 2018-03-05 20:23:23 +08:00
huangyuhui c60ae111ef Progress bar in account page 2018-03-05 20:07:30 +08:00
huangyuhui 5eb46395ae Close popup when deleted an account 2018-03-05 19:13:58 +08:00
huangyuhui db08b61cfa Fix #301 2018-03-05 19:02:13 +08:00
andylizi 76c254ee98 Switch the download links to HTTPS 2018-02-28 17:23:49 +08:00
147 changed files with 3414 additions and 2880 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;
@@ -82,42 +90,49 @@ public final class Launcher extends Application {
}
public static void stopApplication() {
JFXUtilities.runInFX(() -> {
stopWithoutPlatform();
Platform.exit();
});
}
public static void stopWithoutPlatform() {
Logging.LOG.info("Stopping application without JavaFX Toolkit");
Logging.LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Logging.LOG.info("Shutting down executor services.");
Schedulers.shutdown();
Controllers.shutdown();
Platform.exit();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
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 void stopWithoutPlatform() {
Logging.LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
return;
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
});
}
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) {
@@ -133,10 +148,11 @@ 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 VERSION = System.getProperty("hmcl.version.override", "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@");
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final ResourceBundle RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
@@ -144,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 = "https://www.huangyuhui.net";
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
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Logging;
@@ -70,7 +71,7 @@ public class HMCLGameDownloadTask extends Task {
NetworkUtils.toURL(profile.getDependency().getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
cache,
profile.getDependency().getProxy(),
version.getDownloadInfo().getSha1()
new IntegrityCheck("SHA-1", version.getDownloadInfo().getSha1())
).then(Task.of(v -> FileUtils.copyFile(cache, jar))));
}
@@ -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,16 +17,19 @@
*/
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;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
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.CurseCompletionException;
import org.jackhuang.hmcl.mod.CurseCompletionTask;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Settings;
@@ -36,22 +39,25 @@ 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.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
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,21 +70,23 @@ 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) {
}
});
}
private void launch0(Profile profile, Account account, String selectedVersion, File scriptFile) {
GameRepository repository = profile.getRepository();
HMCLGameRepository 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;
@@ -86,27 +94,38 @@ public final class LauncherHelper {
return dependencyManager.checkGameCompletionAsync(version);
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.MODS)))
.then(new CurseCompletionTask(dependencyManager, selectedVersion))
.then(var -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if ("Curse".equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
return null;
}
})
.then(Task.of(Schedulers.javafx(), () -> emitStatus(LoadingState.LOGGING_IN)))
.then(Task.of(Launcher.i18n("account.methods"), variables -> {
try {
try {
variables.set("account", account.logIn());
} catch (ServerDisconnectException e) {
if (account.canPlayOffline())
variables.set("account", account.playOffline());
else
throw e;
}
} catch (AuthenticationException e) {
variables.set("account", account.logIn());
} catch (CredentialExpiredException e) {
variables.set("account", DialogController.logIn(account));
JFXUtilities.runInFX(() -> Controllers.dialog(launchingStepsPane));
} catch (AuthenticationException e) {
variables.set("account",
account.playOffline().orElseThrow(() -> e));
}
}))
.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 -> {
@@ -127,13 +146,15 @@ 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.dialog(Launcher.i18n("version.launch_script.success", scriptFile.getAbsolutePath())));
Platform.runLater(() -> {
Controllers.closeDialog(launchingStepsPane);
Controllers.dialog(Launcher.i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
}))
.executor();
@@ -151,14 +172,24 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
if (!success) {
if (!success && !Controllers.isStopped()) {
Platform.runLater(() -> {
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);
else
Controllers.closeDialog();
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
Controllers.closeDialog(launchingStepsPane);
Exception ex = executor.getLastException();
if (ex != null) {
String message;
if (ex instanceof CurseCompletionException)
message = Launcher.i18n("modpack.type.curse.error");
else
message = I18nException.getStackTrace(ex);
Controllers.dialog(message,
scriptFile == null ? Launcher.i18n("launch.failed") : Launcher.i18n("version.launch_script.failed"),
MessageBox.ERROR_MESSAGE);
}
}
});
}
launchingStepsPane.setExecutor(null);
@@ -169,47 +200,57 @@ 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;
// Without onAccept called, the launching operation will be terminated.
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);
Controllers.dialog(Launcher.i18n("launch.wrong_javadir"), Launcher.i18n("message.error"), MessageBox.WARNING_MESSAGE, onAccept);
setting.setJava(null);
setting.setDefaultJavaPath(null);
java = JavaVersion.fromCurrentEnvironment();
flag = true;
}
if (java.getParsedVersion() < JavaVersion.JAVA_8) {
Controllers.dialog(Launcher.i18n("launch.advice.newer_java"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8) {
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(Launcher.i18n("launch.advice.java8_1_13"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(Launcher.i18n("launch.advice.newer_java"), Launcher.i18n("message.error"), MessageBox.WARNING_MESSAGE, onAccept);
}
flag = true;
}
if (java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
// LaunchWrapper will crash because of assuming the system class loader is an instance of URLClassLoader.
// cpw has claimed that he will make MinecraftForge of 1.13 and later versions able to run on Java 9.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
Controllers.dialog(Launcher.i18n("launch.advice.java9"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
suggest = false;
flag = true;
}
if (java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
org.jackhuang.hmcl.util.Platform.IS_64_BIT) {
Controllers.dialog(Launcher.i18n("launch.advice.different_platform"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
Controllers.dialog(Launcher.i18n("launch.advice.too_large_memory_for_32bit"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(Launcher.i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (flag) {
if (suggest && Controllers.getDialogContent() instanceof MessageDialogPane)
((MessageDialogPane) Controllers.getDialogContent()).disableClosingDialog();
} else
if (!flag)
onAccept.run();
}
@@ -219,10 +260,11 @@ 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);
Controllers.dialog(launchingStepsPane);
}
private void checkExit(LauncherVisibility v) {
@@ -298,10 +340,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();
@@ -331,7 +373,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();
@@ -357,7 +399,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(() -> {
@@ -59,12 +59,12 @@ public final class ModpackHelper {
throw new UnsupportedModpackException(file.toString());
}
public static <T> ModpackConfiguration<T> readModpackConfiguration(File file) throws IOException {
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
throw new FileNotFoundException(file.getPath());
else
try {
return Constants.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<T>>() {
return Constants.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
@@ -22,23 +22,20 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorBuildInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerResponse;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
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 java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* @author huangyuhui
@@ -50,14 +47,14 @@ 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(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer))
);
private static final Map<String, String> AUTHLIB_INJECTOR_SERVER_NAMES = new HashMap<>();
public static String getAccountType(Account account) {
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
@@ -73,33 +70,23 @@ public final class Accounts {
return username + ":" + character;
}
private static String downloadAuthlibInjector() throws Exception {
AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo();
File jar = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.jar");
File local = new File(Launcher.HMCL_DIRECTORY, "authlib-injector.txt");
int buildNumber = 0;
try {
buildNumber = Integer.parseInt(FileUtils.readText(local));
} catch (IOException | NumberFormatException ignore) {
}
if (buildNumber < buildInfo.getBuildNumber()) {
new FileDownloadTask(new URL(buildInfo.getUrl()), jar).run();
FileUtils.writeText(local, String.valueOf(buildInfo.getBuildNumber()));
}
return jar.getAbsolutePath();
}
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return Settings.SETTINGS.authlibInjectorServers.stream()
.filter(server -> url.equals(server.getUrl()))
.findFirst()
.orElseGet(() -> {
// this usually happens when migrating data from an older version
AuthlibInjectorServer server;
try {
server = AuthlibInjectorServer.fetchServerInfo(url);
LOG.info("Migrated authlib injector server " + server);
} catch (IOException e) {
server = new AuthlibInjectorServer(url, url);
LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e);
}
public static String getAuthlibInjectorServerName(String serverIp) throws Exception {
if (AUTHLIB_INJECTOR_SERVER_NAMES.containsKey(serverIp))
return AUTHLIB_INJECTOR_SERVER_NAMES.get(serverIp);
else {
AuthlibInjectorServerResponse response = Constants.GSON.fromJson(NetworkUtils.doGet(NetworkUtils.toURL(serverIp)), AuthlibInjectorServerResponse.class);
AUTHLIB_INJECTOR_SERVER_NAMES.put(serverIp, response.getMeta().getServerName());
return response.getMeta().getServerName();
}
}
public static TaskResult<String> getAuthlibInjectorServerNameAsync(AuthlibInjectorAccount account) {
return Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(account.getServerBaseURL()));
Settings.SETTINGS.authlibInjectorServers.add(server);
return server;
});
}
}
@@ -17,274 +17,95 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
import java.util.TreeMap;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.util.JavaVersion;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import java.util.*;
import com.google.gson.annotations.SerializedName;
final class Config {
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;
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(1);
@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<>();
@SerializedName("firstLaunch")
public final BooleanProperty firstLaunch = new SimpleBooleanProperty(true);
public String getSelectedProfile() {
return selectedProfile;
}
public final ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
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));
@@ -53,7 +53,7 @@ public final class Locales {
*/
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN);
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH);
public static SupportedLocale getLocale(int index) {
return Lang.get(LOCALES, index).orElse(DEFAULT);
@@ -116,6 +116,7 @@ public final class Profile {
VersionSetting vs = repository.getVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
getGlobal().setGlobal(true); // always keep global.isGlobal = true
getGlobal().setUsesGlobal(true);
return getGlobal();
} else
return vs;
@@ -20,14 +20,18 @@ package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.Launcher;
public final class Profiles {
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
private Profiles() {
}
public static String getProfileDisplayName(Profile profile) {
switch (profile.getName()) {
case Settings.DEFAULT_PROFILE:
case Profiles.DEFAULT_PROFILE:
return Launcher.i18n("profile.default");
case Settings.HOME_PROFILE:
case Profiles.HOME_PROFILE:
return Launcher.i18n("profile.home");
default:
return profile.getName();
@@ -19,19 +19,26 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
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.auth.authlibinjector.AuthlibInjectorServer;
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,37 +49,48 @@ 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 java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
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 String SETTINGS_FILE_NAME = "hmcl.json";
public static final File SETTINGS_FILE = new File(SETTINGS_FILE_NAME).getAbsoluteFile();
public static final File SETTINGS_FILE = new File("hmcl.json").getAbsoluteFile();
public static final Config SETTINGS = initSettings();
public static final Settings INSTANCE = new Settings();
private Settings() {}
private final Map<String, Account> accounts = new ConcurrentHashMap<>();
private final Config SETTINGS = initSettings();
private final boolean firstLaunch;
private final Map<String, Account> accounts = new HashMap<>();
private Settings() {
firstLaunch = SETTINGS.firstLaunch.get();
{
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.
LOG.warning("Unrecognized account type, removing: " + settings);
iterator.remove();
continue;
}
@@ -81,7 +99,7 @@ public class Settings {
try {
account = factory.fromStorage(settings, getProxy());
} catch (Exception e) {
// storage is malformed, delete.
LOG.log(Level.WARNING, "Malformed account storage, removing: " + settings, e);
iterator.remove();
continue;
}
@@ -89,8 +107,8 @@ public class Settings {
accounts.put(Accounts.getAccountId(account), account);
}
checkAuthlibInjectorServerURLs();
checkAuthlibInjectorAccounts();
SETTINGS.authlibInjectorServers.addListener(onInvalidating(this::removeDanglingAuthlibInjectorAccounts));
checkProfileMap();
save();
@@ -104,7 +122,7 @@ public class Settings {
Lang.ignoringException(() -> Runtime.getRuntime().addShutdownHook(new Thread(this::save)));
}
private Config initSettings() {
private static Config initSettings() {
Config c = new Config();
if (SETTINGS_FILE.exists())
try {
@@ -125,11 +143,12 @@ public class Settings {
public void save() {
try {
SETTINGS.getAccounts().clear();
SETTINGS.accounts.clear();
SETTINGS.firstLaunch.set(false);
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 +157,17 @@ public class Settings {
}
}
private final StringProperty commonPath = new ImmediateStringProperty(this, "commonPath", SETTINGS.getCommonDirectory()) {
public boolean isFirstLaunch() {
return firstLaunch;
}
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 +183,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 +191,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 +201,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 +209,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,59 +295,39 @@ 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();
}
/****************************************
* AUTHLIB INJECTORS *
****************************************/
public Set<String> getAuthlibInjectorServerURLs() {
return SETTINGS.getAuthlibInjectorServerURLs();
}
public void removeAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().remove(serverURL);
checkAuthlibInjectorAccounts();
save();
}
public void addAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().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()))
deleteAccount(account);
}
}
/**
* After an {@link AuthlibInjectorServer} is removed, the associated accounts should also be removed.
* This method performs a check and removes the dangling accounts.
* Don't call this before {@link #migrateAuthlibInjectorServers()} is called, otherwise old data would be lost.
*/
private void removeDanglingAuthlibInjectorAccounts() {
accounts.values().stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.filter(it -> !SETTINGS.authlibInjectorServers.contains(((AuthlibInjectorAccount) it).getServer()))
.collect(toList())
.forEach(this::deleteAccount);
}
/****************************************
@@ -315,21 +335,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 +372,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 +392,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 +422,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 +444,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 +470,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 +499,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 +529,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return SETTINGS.getConfigurations();
return SETTINGS.configurations;
}
public Collection<Profile> getProfiles() {
@@ -529,8 +560,8 @@ public class Settings {
private void checkProfileMap() {
if (getProfileMap().isEmpty()) {
getProfileMap().put(DEFAULT_PROFILE, new Profile(DEFAULT_PROFILE));
getProfileMap().put(HOME_PROFILE, new Profile(HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY));
getProfileMap().put(Profiles.DEFAULT_PROFILE, new Profile(Profiles.DEFAULT_PROFILE));
getProfileMap().put(Profiles.HOME_PROFILE, new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY));
}
}
@@ -555,4 +586,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);
}
}
@@ -18,10 +18,14 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Launcher;
@@ -30,16 +34,18 @@ 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.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import java.util.Optional;
public class AccountPage extends StackPane implements DecoratorPage {
private final StringProperty title;
private final ObjectProperty<Runnable> onDelete = new SimpleObjectProperty<>(this, "onDelete");
private final VersionListItem item;
private final Account account;
@FXML
@@ -60,17 +66,20 @@ public class AccountPage extends StackPane implements DecoratorPage {
private JFXButton btnDelete;
@FXML
private JFXButton btnRefresh;
@FXML
private JFXProgressBar progressBar;
public AccountPage(Account account) {
public AccountPage(Account account, VersionListItem item) {
this.account = account;
this.item = item;
title = new SimpleStringProperty(this, "title", Launcher.i18n("account") + " - " + account.getCharacter());
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
FXUtils.setLimitWidth(this, 300);
if (account instanceof AuthlibInjectorAccount) {
Accounts.getAuthlibInjectorServerNameAsync((AuthlibInjectorAccount) account)
.subscribe(Schedulers.javafx(), variables -> lblServer.setText(variables.get("serverName")));
lblServer.setText(((AuthlibInjectorAccount) account).getServer().getName());
FXUtils.setLimitHeight(this, 182);
} else {
componentList.removeChildren(paneServer);
@@ -95,13 +104,23 @@ public class AccountPage extends StackPane implements DecoratorPage {
@FXML
private void onDelete() {
Settings.INSTANCE.deleteAccount(account);
Controllers.navigate(null);
Optional.ofNullable(onDelete.get()).ifPresent(Runnable::run);
}
@FXML
private void onRefresh() {
if (account instanceof YggdrasilAccount)
AccountHelper.refreshSkinAsync((YggdrasilAccount) account).start();
if (account instanceof YggdrasilAccount) {
progressBar.setVisible(true);
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
progressBar.setVisible(false);
if (isDependentsSucceeded) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, 4);
item.setImage(image, AccountHelper.getViewport(4));
}
}).start();
}
}
public String getTitle() {
@@ -116,4 +135,16 @@ public class AccountPage extends StackPane implements DecoratorPage {
public void setTitle(String title) {
this.title.set(title);
}
public Runnable getOnDelete() {
return onDelete.get();
}
public ObjectProperty<Runnable> onDeleteProperty() {
return onDelete;
}
public void setOnDelete(Runnable onDelete) {
this.onDelete.set(onDelete);
}
}
@@ -28,12 +28,15 @@ 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.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
@@ -42,16 +45,19 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import java.util.Collection;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
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;
public class AddAccountPane extends StackPane {
@@ -60,98 +66,108 @@ public class AddAccountPane extends StackPane {
@FXML private Label lblCreationWarning;
@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 JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
private final Runnable finalization;
@FXML private JFXButton btnAccept;
@FXML private SpinnerPane acceptPane;
private final Consumer<Region> finalization;
public AddAccountPane(Runnable finalization) {
public AddAccountPane(Consumer<Region> finalization) {
this.finalization = finalization;
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
loadServers();
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
cboServers.setItems(Settings.SETTINGS.authlibInjectorServers);
cboServers.setPromptText(Launcher.i18n("general.prompt.empty"));
// workaround: otherwise the combox will be black
if (!cboServers.getItems().isEmpty())
cboServers.getSelectionModel().select(0);
cboType.getItems().setAll(Launcher.i18n("account.methods.offline"), Launcher.i18n("account.methods.yggdrasil"), Launcher.i18n("account.methods.authlib_injector"));
cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> {
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);
// These two lines can eliminate black, don't know why.
cboServers.getItems().setAll(new TwoLineListItem("", ""));
cboServers.getSelectionModel().select(0);
txtPassword.setOnAction(e -> onCreationAccept());
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 loadServers() {
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.flatMap(serverURL -> {
try {
return Stream.of(new TwoLineListItem(Accounts.getAuthlibInjectorServerName(serverURL), serverURL));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Authlib-injector server root " + serverURL + " cannot be recognized.", e);
return Stream.empty();
}
})
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
cboServers.getItems().setAll(variables.<Collection<TwoLineListItem>>get("list"));
if (!cboServers.getItems().isEmpty())
cboServers.getSelectionModel().select(0);
}));
private void validateAcceptButton() {
btnAccept.setDisable(!txtUsername.validate() || (cboType.getSelectionModel().getSelectedIndex() != 0 && !txtPassword.validate()));
}
@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);
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> {
AccountFactory<?> factory;
switch (type) {
case 0: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY); break;
case 1: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY); break;
case 2: factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY); break;
default: throw new Error();
}
Object addtionalData;
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();
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
finalization.run();
} else {
lblCreationWarning.setText(accountException(exception));
}
progressBar.setVisible(false);
}).start();
int type = cboType.getSelectionModel().getSelectedIndex();
AccountFactory<?> factory;
switch (type) {
case 0:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY);
addtionalData = null;
break;
case 1:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY);
addtionalData = null;
break;
case 2:
factory = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY);
Optional<AuthlibInjectorServer> server = Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem());
if (server.isPresent()) {
addtionalData = server.get();
} else {
lblCreationWarning.setText(Launcher.i18n("account.failed.no_selected_server"));
return;
}
break;
default:
throw new Error();
}
acceptPane.showSpinner();
lblCreationWarning.setText("");
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, addtionalData, Settings.INSTANCE.getProxy()))
.finalized(Schedulers.javafx(), variables -> {
Settings.INSTANCE.addAccount(variables.get("create_account"));
acceptPane.hideSpinner();
finalization.accept(this);
}, exception -> {
if (exception instanceof NoSelectedCharacterException) {
finalization.accept(this);
} else {
lblCreationWarning.setText(accountException(exception));
}
acceptPane.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,13 +214,14 @@ public class AddAccountPane extends StackPane {
try {
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, 4, Settings.INSTANCE.getProxy());
} catch (Exception e) {
image = FXUtils.DEFAULT_ICON;
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = null;
}
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
if (image == FXUtils.DEFAULT_ICON)
portraitView.setImage(FXUtils.DEFAULT_ICON);
else {
if (image == null) {
portraitView.setImage(Constants.DEFAULT_ICON.get());
} else {
portraitView.setImage(image);
portraitView.setViewport(AccountHelper.getViewport(4));
}
@@ -236,16 +253,22 @@ public class AddAccountPane extends StackPane {
}
public static String accountException(Exception exception) {
if (exception instanceof InvalidCredentialsException) {
return Launcher.i18n("account.failed.invalid_credentials");
} else if (exception instanceof NoCharacterException) {
return Launcher.i18n("account.failed.no_charactor");
if (exception instanceof NoCharacterException) {
return Launcher.i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
return Launcher.i18n("account.failed.connect_authentication_server");
} else if (exception instanceof InvalidTokenException) {
return Launcher.i18n("account.failed.invalid_token");
} else if (exception instanceof InvalidPasswordException) {
return Launcher.i18n("account.failed.invalid_password");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials"))
return Launcher.i18n("account.failed.invalid_credentials");
else if (remoteMessage.contains("Invalid token"))
return Launcher.i18n("account.failed.invalid_token");
else if (remoteMessage.contains("Invalid username or password"))
return Launcher.i18n("account.failed.invalid_password");
}
return exception.getMessage();
} else {
return exception.getClass() + ": " + exception.getLocalizedMessage();
}
@@ -23,19 +23,19 @@ import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Theme;
import java.util.function.Consumer;
public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServerInfo info;
private final AuthlibInjectorServer server;
private final Label lblServerName = new Label();
private final Label lblServerIp = new Label();
public AuthlibInjectorServerItem(AuthlibInjectorServerInfo info, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.info = info;
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.server = server;
lblServerName.setStyle("-fx-font-size: 15;");
lblServerIp.setStyle("-fx-font-size: 10;");
@@ -54,11 +54,11 @@ public final class AuthlibInjectorServerItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.setText(info.getServerName());
lblServerIp.setText(info.getServerIp());
lblServerName.setText(server.getName());
lblServerIp.setText(server.getUrl());
}
public AuthlibInjectorServerInfo getInfo() {
return info;
public AuthlibInjectorServer getServer() {
return server;
}
}
@@ -4,49 +4,47 @@ import com.jfoenix.controls.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServerInfo;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
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.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.util.Collection;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Launcher.i18n("account.injector.server"));
@FXML private ScrollPane scrollPane;
@FXML private StackPane addServerContainer;
@FXML private Label lblServerIp;
@FXML private Label lblServerUrl;
@FXML private Label lblServerName;
@FXML private Label lblCreationWarning;
@FXML private Label lblServerWarning;
@FXML private VBox listPane;
@FXML private JFXTextField txtServerIp;
@FXML private JFXTextField txtServerUrl;
@FXML private JFXDialogLayout addServerPane;
@FXML private JFXDialogLayout confirmServerPane;
@FXML private JFXDialog dialog;
@FXML private StackPane contentPane;
@FXML private JFXSpinner spinner;
@FXML private JFXProgressBar progressBar;
@FXML private SpinnerPane nextPane;
@FXML private JFXButton btnAddNext;
private final TransitionHandler transitionHandler;
private AuthlibInjectorServer serverBeingAdded;
{
FXUtils.loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
FXUtils.smoothScrolling(scrollPane);
@@ -55,51 +53,35 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
getChildren().remove(dialog);
dialog.setDialogContainer(this);
txtServerIp.textProperty().addListener((a, b, newValue) ->
btnAddNext.setDisable(!txtServerIp.validate()));
txtServerUrl.textProperty().addListener((a, b, newValue) ->
btnAddNext.setDisable(!txtServerUrl.validate()));
loading();
reload();
}
private void removeServer(AuthlibInjectorServerItem item) {
Settings.INSTANCE.removeAuthlibInjectorServerURL(item.getInfo().getServerIp());
loading();
Settings.SETTINGS.authlibInjectorServers.remove(item.getServer());
reload();
}
private void loading() {
getChildren().remove(contentPane);
spinner.setVisible(true);
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.flatMap(serverURL -> {
try {
return Stream.of(new AuthlibInjectorServerItem(new AuthlibInjectorServerInfo(serverURL, Accounts.getAuthlibInjectorServerName(serverURL)), this::removeServer));
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Authlib-injector server root " + serverURL + " cannot be recognized.", e);
return Stream.empty();
}
})
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
listPane.getChildren().setAll(variables.<Collection<? extends Node>>get("list"));
loadingCompleted();
}));
}
private void loadingCompleted() {
getChildren().add(contentPane);
spinner.setVisible(false);
if (Settings.INSTANCE.getAuthlibInjectorServerURLs().isEmpty())
private void reload() {
listPane.getChildren().setAll(
Settings.SETTINGS.authlibInjectorServers.stream()
.map(server -> new AuthlibInjectorServerItem(server, this::removeServer))
.collect(toList()));
if (Settings.SETTINGS.authlibInjectorServers.isEmpty()) {
onAdd();
}
}
@FXML
private void onAdd() {
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
txtServerIp.setText("");
txtServerUrl.setText("");
txtServerUrl.resetValidation();
lblCreationWarning.setText("");
addServerPane.setDisable(false);
progressBar.setVisible(false);
nextPane.hideSpinner();
dialog.show();
}
@@ -110,26 +92,28 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
@FXML
private void onAddNext() {
String serverIp = txtServerIp.getText();
progressBar.setVisible(true);
String url = fixInputUrl(txtServerUrl.getText());
nextPane.showSpinner();
addServerPane.setDisable(true);
Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(serverIp))
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
progressBar.setVisible(false);
addServerPane.setDisable(false);
Task.of(() -> {
serverBeingAdded = AuthlibInjectorServer.fetchServerInfo(url);
}).finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
nextPane.hideSpinner();
addServerPane.setDisable(false);
if (isDependentsSucceeded) {
lblServerName.setText(variables.get("serverName"));
lblServerIp.setText(txtServerIp.getText());
if (isDependentsSucceeded) {
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverIp).getProtocol()));
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else
lblCreationWarning.setText(variables.<Exception>get("lastException").getLocalizedMessage());
}).start();
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
lblCreationWarning.setText(resolveFetchExceptionMessage(variables.<Exception>get("lastException")));
}
}).start();
}
@@ -140,11 +124,10 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
@FXML
private void onAddFinish() {
String ip = txtServerIp.getText();
if (!ip.endsWith("/"))
ip += "/";
Settings.INSTANCE.addAuthlibInjectorServerURL(ip);
loading();
if (!Settings.INSTANCE.SETTINGS.authlibInjectorServers.contains(serverBeingAdded)) {
Settings.INSTANCE.SETTINGS.authlibInjectorServers.add(serverBeingAdded);
}
reload();
dialog.close();
}
@@ -160,4 +143,19 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
public void setTitle(String title) {
this.title.set(title);
}
private String fixInputUrl(String url) {
if (!url.endsWith("/")) {
url += "/";
}
return url;
}
private String resolveFetchExceptionMessage(Throwable exception) {
if (exception instanceof IOException) {
return Launcher.i18n("account.failed.connect_injector_server");
} else {
return exception.getClass() + ": " + exception.getLocalizedMessage();
}
}
}
@@ -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;
@@ -116,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(new Region());
return decorator.showDialog(content);
public static void dialog(Region content) {
if (decorator != null)
decorator.showDialog(content);
}
public static void dialog(String text) {
@@ -139,28 +132,34 @@ 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, Runnable onCancel) {
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
return taskDialog(executor, title, subtitle, null);
}
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);
executor.start();
dialog(pane);
return pane;
}
public static void closeDialog() {
decorator.getDialog().close();
public static void closeDialog(Region content) {
if (stage == null) // shut down
return;
decorator.closeDialog(content);
}
public static void navigate(Node node) {
@@ -171,9 +170,15 @@ public final class Controllers {
}
public static void showUpdate() {
if (stage == null) // shut down
return;
getLeftPaneController().showUpdate();
}
public static boolean isStopped() {
return decorator == null;
}
public static void shutdown() {
mainPage = null;
settingsPage = null;
@@ -22,6 +22,10 @@ import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDrawer;
import com.jfoenix.controls.JFXHamburger;
import com.jfoenix.svg.SVGGlyph;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
@@ -38,6 +42,7 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@@ -45,14 +50,17 @@ import javafx.scene.shape.Rectangle;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Locales;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
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.StackContainerPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer;
import org.jackhuang.hmcl.ui.wizard.*;
import org.jackhuang.hmcl.util.FileUtils;
@@ -60,6 +68,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Locale;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -82,11 +91,13 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private final boolean max, min;
private final WizardController wizardController = new WizardController(this);
private final Queue<Object> cancelQueue = new ConcurrentLinkedQueue<>();
private final JFXDialog dialog;
private double xOffset, yOffset, newX, newY, initX, initY;
private boolean allowMove, isDragging, dialogShown, maximized;
private boolean allowMove, isDragging, maximized;
private BoundingBox originalBox, maximizedBox;
private final TransitionHandler animationHandler;
private final StackContainerPane dialogPane = new StackContainerPane();
@FXML
private StackPane contentPlaceHolder;
@@ -119,8 +130,6 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
@FXML
private JFXHamburger titleBurger;
@FXML
private JFXDialog dialog;
@FXML
private JFXButton btnMin;
@FXML
private JFXButton btnMax;
@@ -128,6 +137,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private JFXButton btnClose;
@FXML
private HBox navLeft;
@FXML
private ImageView welcomeView;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
@@ -165,9 +176,24 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
btnMax.fire();
});
dialog = new JFXDialog();
dialog.setOverlayClose(false);
drawerWrapper.getChildren().add(0, dialog);
dialog.setDialogContainer(drawerWrapper);
dialog.setOnDialogClosed(e -> dialogShown = false);
dialog.setOnDialogOpened(e -> dialogShown = true);
dialog.setContent(dialogPane);
welcomeView.setCursor(Cursor.HAND);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> drawerWrapper.getChildren().remove(welcomeView))
);
nowAnimation.play();
});
if (!Settings.INSTANCE.isFirstLaunch() || Settings.INSTANCE.getLocale().getLocale() != Locale.CHINA)
drawerWrapper.getChildren().remove(welcomeView);
if (!min) buttonsContainer.getChildren().remove(btnMin);
if (!max) buttonsContainer.getChildren().remove(btnMax);
@@ -188,14 +214,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) {
@@ -511,6 +540,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");
@@ -531,11 +562,23 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
}
public JFXDialog showDialog(Region content) {
dialog.setContent(content);
if (!dialogShown)
public void showDialog(Node node) {
FXUtils.checkFxUserThread();
if (dialogPane.isEmpty())
dialog.show();
return dialog;
dialogPane.push(node);
}
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) {
@@ -543,6 +586,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();
@@ -624,10 +669,6 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
return wizardController;
}
public JFXDialog getDialog() {
return dialog;
}
public AdvancedListBox getLeftPane() {
return leftPane;
}
@@ -34,15 +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();
Controllers.closeDialog();
}, () -> {
latch.countDown();
Controllers.closeDialog();
});
pane.setDialog(Controllers.dialog(pane));
}, latch::countDown);
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(SilentException::new);
@@ -17,13 +17,13 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.*;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -31,37 +31,52 @@ import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.Launcher;
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.Function;
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 InvalidationListener onInvalidating(Runnable action) {
return arg -> action.run();
}
public static <T> void onChange(ObservableValue<T> value, Consumer<T> consumer) {
value.addListener((a, b, c) -> consumer.accept(c));
}
@@ -118,7 +133,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 +209,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 +216,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 +340,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);
}
@@ -397,7 +411,32 @@ public final class FXUtils {
}
}
public static final Image DEFAULT_ICON = new Image("/assets/img/icon.png");
public static <T> StringConverter<T> stringConverter(Function<T, String> func) {
return new StringConverter<T>() {
@Override
public String toString(T object) {
return object == null ? "" : func.apply(object);
}
@Override
public T fromString(String string) {
throw new UnsupportedOperationException();
}
};
}
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
return view -> new JFXListCell<T>() {
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setGraphic(graphicBuilder.apply(item));
}
}
};
}
public static final Interpolator SINE = new Interpolator() {
@Override
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.game.GameVersion;
@@ -89,7 +90,7 @@ public class InstallerController {
Optional<String> gameVersion = GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version));
if (!gameVersion.isPresent())
Controllers.dialog("version.cannot_read");
Controllers.dialog(Launcher.i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
}
@@ -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,15 +148,29 @@ 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);
AccountPage accountPage = new AccountPage(account, item);
JFXPopup popup = new JFXPopup(accountPage);
accountPage.setOnDelete(popup::hide);
popup.show((Node) e.getSource(), JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, e.getX(), e.getY());
});
ripplerContainer.setOnMouseClicked(e -> {
@@ -163,11 +184,11 @@ 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)
.subscribe(Schedulers.javafx(), variables -> FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(variables.get("serverName"))));
if (account instanceof AuthlibInjectorAccount) {
FXUtils.installTooltip(ripplerContainer, 500, 5000, 0, new Tooltip(((AuthlibInjectorAccount) account).getServer().getName()));
}
if (selectedAccount == account)
ripplerContainer.setSelected(true);
@@ -191,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;
@@ -203,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(),
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();
@@ -127,7 +141,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
Controllers.dialog(Launcher.i18n("login.empty_username"));
else {
FileChooser chooser = new FileChooser();
chooser.setInitialDirectory(repository.getRunDirectory(id));
if (repository.getRunDirectory(id).isDirectory())
chooser.setInitialDirectory(repository.getRunDirectory(id));
chooser.setTitle(Launcher.i18n("version.launch_script.save"));
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(Launcher.i18n("extension.bat"), "*.bat")
@@ -147,19 +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) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(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();
pane.setExecutor(executor);
pane.setTitle(Launcher.i18n("modpack.update"));
.then(Task.of(Schedulers.javafx(), () -> Controllers.closeDialog(region.get()))).executor();
region.set(Controllers.taskDialog(executor, Launcher.i18n("modpack.update"), ""));
executor.start();
Controllers.dialog(pane);
} catch (UnsupportedModpackException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.unsupported"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (MismatchedModpackTypeException e) {
Controllers.closeDialog(region.get());
Controllers.dialog(Launcher.i18n("modpack.mismatched_type"), Launcher.i18n("message.error"), MessageBox.ERROR_MESSAGE);
} catch (IOException e) {
} 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();
});
@@ -17,10 +17,13 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
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 +31,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,18 +67,119 @@ 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));
btnLaunch.setGraphic(SVG.launch(Theme.blackFillBinding(), 15, 15));
btnScript.setGraphic(SVG.script(Theme.blackFillBinding(), 15, 15));
FXUtils.installTooltip(btnSettings, Launcher.i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, Launcher.i18n("version.update"));
FXUtils.installTooltip(btnLaunch, Launcher.i18n("version.launch"));
FXUtils.installTooltip(btnScript, Launcher.i18n("version.launch_script"));
JFXUtilities.runInFX(() -> {
FXUtils.installTooltip(btnSettings, Launcher.i18n("version.settings"));
FXUtils.installTooltip(btnUpdate, Launcher.i18n("version.update"));
FXUtils.installTooltip(btnLaunch, Launcher.i18n("version.launch"));
FXUtils.installTooltip(btnScript, Launcher.i18n("version.launch_script"));
});
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
FXUtils.limitSize(iconView, 32, 32);
@@ -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"));
@@ -105,8 +107,6 @@ public final class VersionSettingsController {
}
public void loadVersionSetting(Profile profile, String versionId) {
rootPane.getChildren().remove(advancedSettingsPane);
this.profile = profile;
this.versionId = versionId;
@@ -151,29 +151,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);
@@ -183,6 +171,8 @@ public final class VersionSettingsController {
else
globalItem.getGroup().getToggles().stream().filter(it -> it.getUserData() == Boolean.FALSE).findFirst().ifPresent(it -> it.setSelected(true));
globalItem.setToggleSelectedListener(newValue -> {
// do not call versionSettings.setUsesGlobal(true/false)
// because versionSettings can be the global one.
if ((Boolean) newValue.getUserData())
profile.globalizeVersionSetting(versionId);
else
@@ -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(),
@@ -221,14 +235,6 @@ public final class VersionSettingsController {
gameDirItem.setSubtitle(profile.getRepository().getRunDirectory(versionId).getAbsolutePath());
}
@FXML
private void onShowAdvanced() {
if (!rootPane.getChildren().contains(advancedSettingsPane))
rootPane.getChildren().add(advancedSettingsPane);
else
rootPane.getChildren().remove(advancedSettingsPane);
}
@FXML
private void onExploreIcon() {
FileChooser chooser = new FileChooser();
@@ -250,7 +256,7 @@ public final class VersionSettingsController {
if (iconFile.exists())
iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconPickerItem.setImage(FXUtils.DEFAULT_ICON);
iconPickerItem.setImage(Constants.DEFAULT_ICON.get());
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
}
@@ -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
@@ -86,7 +93,7 @@ public final class TransitionHandler implements AnimationHandler {
} else
previousNode = NULL;
if (previousNode == currentNode)
if (previousNode == newView)
previousNode = NULL;
currentNode = newView;
@@ -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);
}
}
@@ -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.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
@DefaultProperty("content")
public class SpinnerPane extends StackPane {
private final TransitionHandler transitionHandler = new TransitionHandler(this);
private final JFXSpinner spinner = new JFXSpinner();
private final StackPane contentPane = new StackPane();
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
public SpinnerPane() {
getChildren().setAll(contentPane);
content.addListener((a, b, newValue) -> contentPane.getChildren().setAll(newValue));
}
public void showSpinner() {
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
}
public void hideSpinner() {
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
}
public Node getContent() {
return content.get();
}
public ObjectProperty<Node> contentProperty() {
return content;
}
public void setContent(Node content) {
this.content.set(content);
}
}
@@ -0,0 +1,49 @@
/*
* 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.ui.construct;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.util.Logging;
import java.util.Stack;
public class StackContainerPane extends StackPane {
private final Stack<Node> stack = new Stack<>();
public void push(Node node) {
stack.push(node);
getChildren().setAll(node);
Logging.LOG.info(this + " " + stack);
}
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() {
return stack.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_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);
@@ -101,10 +102,10 @@ public final class DownloadWizardProvider implements WizardProvider {
case 1:
return new ModpackPage(controller);
default:
throw new IllegalStateException("Error step " + step + ", subStep " + subStep + ", settings: " + settings);
throw new IllegalStateException("Error step " + step + ", subStep " + subStep + ", settings: " + settings + ", pages: " + controller.getPages());
}
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings);
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
}
@@ -90,7 +90,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
try {
manifest = ModpackHelper.readModpackManifest(selectedFile);
controller.getSettings().put(MODPACK_CURSEFORGE_MANIFEST, manifest);
controller.getSettings().put(MODPACK_MANIFEST, manifest);
lblName.setText(manifest.getName());
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
@@ -130,5 +130,5 @@ public final class ModpackPage extends StackPane implements WizardPage {
public static final String MODPACK_FILE = "MODPACK_FILE";
public static final String MODPACK_NAME = "MODPACK_NAME";
public static final String MODPACK_CURSEFORGE_MANIFEST = "CURSEFORGE_MANIFEST";
public static final String MODPACK_MANIFEST = "MODPACK_MANIFEST";
}
@@ -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
*/
@@ -66,6 +67,9 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
}
private CheckBoxTreeItem<String> getTreeItem(File file, String basePath) {
if (!file.exists())
return null;
ModAdviser.ModSuggestion state = ModAdviser.ModSuggestion.SUGGESTED;
if (basePath.length() > "minecraft/".length()) {
state = adviser.advise(StringUtils.substringAfter(basePath, "minecraft/") + (file.isDirectory() ? "/" : ""), file.isDirectory());
@@ -151,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,10 +20,7 @@ package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.task.Task;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Stack;
import java.util.*;
public class WizardController implements Navigation {
private final WizardDisplayer displayer;
@@ -47,6 +44,10 @@ public class WizardController implements Navigation {
this.provider = provider;
}
public List<Node> getPages() {
return Collections.unmodifiableList(pages);
}
@Override
public void onStart() {
Objects.requireNonNull(provider);
@@ -81,6 +82,10 @@ public class WizardController implements Navigation {
@Override
public void onPrev(boolean cleanUp) {
if (!canPrev()) {
throw new IllegalStateException("Cannot go backward since this is the back page. Pages: " + pages);
}
Node page = pages.pop();
if (cleanUp && page instanceof WizardPage)
((WizardPage) page).cleanup(settings);
@@ -20,10 +20,12 @@ 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.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.*;
@@ -33,10 +35,8 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
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;
@@ -49,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, ReflectiveOperationException {
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
@@ -87,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 | ReflectiveOperationException 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.
}
}
@@ -107,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");
JFXUtilities.runInFX(() -> Controllers.dialog(Launcher.i18n("message.downloading")));
if (new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash).test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.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 {
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());
if (url == null)
url = Launcher.PUBLISH;
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();
}
@@ -184,7 +183,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash));
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, new IntegrityCheck("SHA-1", hash)));
}
@Override
@@ -194,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);
@@ -212,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) {
@@ -234,7 +233,7 @@ public class AppDataUpgrader extends IUpgrader {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, hash));
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, Proxy.NO_PROXY, new IntegrityCheck("SHA-1", hash)));
}
@Override
@@ -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,14 +22,19 @@ 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;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.VersionNumber;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
@@ -65,18 +70,27 @@ 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("@"))
if (isDevelopmentVersion(Launcher.VERSION)) {
LOG.info("Current version is a development version, skip updating");
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);
}
if (value == null) {
Logging.LOG.warning("Unable to check update...");
LOG.warning("Unable to check update...");
if (showMessage)
MessageBox.show(Launcher.i18n("update.failed"));
} else if (base.compareTo(value) < 0)
@@ -92,6 +106,11 @@ public final class UpdateChecker {
};
}
private boolean isDevelopmentVersion(String version) {
return version.contains("@") || // eg. @HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@
version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT
}
/**
* Get the <b>cached</b> newest version number, use "process" method to
* download!
@@ -113,12 +132,13 @@ public final class UpdateChecker {
return new TaskResult<Map<String, String>>() {
@Override
public void execute() {
if (download_link == null)
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);
LOG.log(Level.SEVERE, "Failed to get update link.", e);
}
}
setResult(download_link);
}
@@ -22,11 +22,16 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import static java.util.Collections.newSetFromMap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
/**
@@ -34,7 +39,7 @@ import java.util.logging.Level;
*/
public class CrashReporter implements Thread.UncaughtExceptionHandler {
private static final HashMap<String, String> SOURCE = new HashMap<String, String>() {
private static final Map<String, String> SOURCE = new HashMap<String, String>() {
{
put("javafx.fxml.LoadException", Launcher.i18n("crash.NoClassDefFound"));
put("Location is not set", Launcher.i18n("crash.NoClassDefFound"));
@@ -42,6 +47,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!");
@@ -57,11 +64,11 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
if (s.contains(entry.getKey())) {
if (StringUtils.isNotBlank(entry.getValue())) {
String info = entry.getValue();
Logging.LOG.severe(info);
LOG.severe(info);
try {
MessageBox.show(info);
} catch (Throwable t) {
Logging.LOG.log(Level.SEVERE, "Unable to show message", t);
LOG.log(Level.SEVERE, "Unable to show message", t);
}
}
return false;
@@ -69,17 +76,21 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
return true;
}
private static Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
@Override
public void uncaughtException(Thread t, Throwable e) {
String stackTrace = StringUtils.getStackTrace(e);
if (!stackTrace.contains("org.jackhuang"))
return;
if (THROWABLE_SET.contains(stackTrace))
return;
THROWABLE_SET.add(stackTrace);
LOG.log(Level.SEVERE, "Uncaught exception in thread " + t.getName(), e);
try {
String stackTrace = StringUtils.getStackTrace(e);
if (!stackTrace.contains("org.jackhuang"))
return;
if (CAUGHT_EXCEPTIONS.contains(stackTrace))
return;
CAUGHT_EXCEPTIONS.add(stackTrace);
String text = "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Launcher.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
@@ -91,21 +102,18 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n";
Logging.LOG.log(Level.SEVERE, text);
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);
}
} catch (Throwable ex) {
Logging.LOG.log(Level.SEVERE, "Unable to caught exception", ex);
Logging.LOG.log(Level.SEVERE, "There is the original exception", e);
} catch (Throwable handlingException) {
LOG.log(Level.SEVERE, "Unable to handle uncaught exception", handlingException);
}
}
private static final HashSet<String> THROWABLE_SET = new HashSet<>();
private void reportToServer(final String text) {
Thread t = new Thread(() -> {
HashMap<String, String> map = new HashMap<>();
@@ -113,11 +121,11 @@ 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://hmcl.huangyuhui.net/hmcl/crash.php"), map);
if (StringUtils.isNotBlank(response))
Logging.LOG.log(Level.SEVERE, "Crash server response: " + response);
LOG.log(Level.SEVERE, "Crash server response: " + response);
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Unable to post HMCL server.", ex);
LOG.log(Level.SEVERE, "Unable to post HMCL server.", ex);
}
});
t.setDaemon(true);
@@ -911,6 +911,22 @@
-fx-stroke-width: 5.0;
}
.small-spinner {
-jfx-radius: 10;
}
.small-spinner > .arc {
-fx-stroke-width: 3.0;
}
.small-spinner-pane .jfx-spinner {
-jfx-radius: 10;
}
.small-spinner-pane .jfx-spinner > .arc {
-fx-stroke-width: 3.0;
}
.second-spinner {
-jfx-radius: 30;
}
@@ -5,6 +5,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
type="StackPane">
@@ -25,13 +26,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 +57,10 @@
</body>
<actions>
<Label fx:id="lblCreationWarning"/>
<JFXButton onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
<SpinnerPane fx:id="acceptPane" styleClass="small-spinner-pane">
<JFXButton fx:id="btnAccept" onMouseClicked="#onCreationAccept" text="%button.ok" styleClass="dialog-accept"/>
</SpinnerPane>
<JFXButton onMouseClicked="#onCreationCancel" text="%button.cancel" styleClass="dialog-cancel"/>
</actions>
</JFXDialogLayout>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER"/>
</fx:root>
@@ -26,7 +26,7 @@
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onAccept" text="%button.ok" styleClass="dialog-accept"/>
<JFXButton onMouseClicked="#finalization" text="%button.cancel" styleClass="dialog-cancel"/>
<JFXButton onMouseClicked="#onCancel" text="%button.cancel" styleClass="dialog-cancel"/>
</actions>
</JFXDialogLayout>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER"/>
@@ -4,6 +4,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.ComponentList?>
<?import com.jfoenix.controls.JFXProgressBar?>
<fx:root xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
type="StackPane">
@@ -77,4 +78,5 @@
prefWidth="30" prefHeight="30"/>
</right>
</BorderPane>
<JFXProgressBar fx:id="progressBar" StackPane.alignment="TOP_CENTER" visible="false" />
</fx:root>
@@ -5,10 +5,10 @@
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.URLValidator?>
<?import java.lang.String?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<JFXSpinner fx:id="spinner" style="-fx-radius:16" styleClass="materialDesign-purple, first-spinner" />
<StackPane fx:id="contentPane">
<ScrollPane fx:id="scrollPane" fitToWidth="true" fitToHeight="true">
<VBox fx:id="listPane" spacing="10" style="-fx-padding: 20 20 70 20;">
@@ -32,11 +32,9 @@
<Label text="%account.injector.add" />
</heading>
<body>
<JFXTextField fx:id="txtServerIp" promptText="%account.injector.server_ip">
<JFXTextField fx:id="txtServerUrl" promptText="%account.injector.server_url">
<validators>
<URLValidator message="%input.url">
</URLValidator>
<URLValidator message="%input.url.http">
<protocols>
<String fx:value="http" />
<String fx:value="https" />
@@ -48,7 +46,9 @@
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
<JFXButton fx:id="btnAddNext" onMouseClicked="#onAddNext" text="%wizard.next" styleClass="dialog-accept" />
<SpinnerPane fx:id="nextPane" styleClass="small-spinner-pane">
<JFXButton fx:id="btnAddNext" onMouseClicked="#onAddNext" text="%wizard.next" styleClass="dialog-accept" />
</SpinnerPane>
</actions>
</JFXDialogLayout>
@@ -62,10 +62,10 @@
<ColumnConstraints maxWidth="100" />
<ColumnConstraints />
</columnConstraints>
<Label text="%account.injector.server_ip" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<Label text="%account.injector.server_url" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<Label text="%account.injector.server_name" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<Label fx:id="lblServerIp" GridPane.columnIndex="1" GridPane.rowIndex="0" />
<Label fx:id="lblServerUrl" GridPane.columnIndex="1" GridPane.rowIndex="0" />
<Label fx:id="lblServerName" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Label fx:id="lblServerWarning" text="%account.injector.http" style="-fx-text-fill: red;" GridPane.rowIndex="2" GridPane.columnSpan="2" GridPane.columnIndex="0" />
@@ -78,7 +78,6 @@
</actions>
</JFXDialogLayout>
</StackPane>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER" />
</StackPane>
</JFXDialog>
</fx:root>
@@ -8,6 +8,8 @@
<?import org.jackhuang.hmcl.ui.construct.AdvancedListBox?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import java.lang.String?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<fx:root xmlns="http://javafx.com/javafx"
type="StackPane"
xmlns:fx="http://javafx.com/fxml">
@@ -20,7 +22,6 @@
maxWidth="800">
<center>
<StackPane fx:id="drawerWrapper" styleClass="jfx-decorator-drawer" FXUtils.overflowHidden="true">
<JFXDialog fx:id="dialog" overlayClose="false" />
<BorderPane>
<left>
<StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container">
@@ -45,6 +46,9 @@
</StackPane>
</center>
</BorderPane>
<ImageView fx:id="welcomeView">
<Image url="/assets/img/welcome.png" />
</ImageView>
</StackPane>
</center>
<top>
@@ -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 -->
@@ -104,8 +104,8 @@
</right>
</BorderPane>
</ComponentList>
<HBox alignment="CENTER" style="-fx-padding: 10 0 10 0;">
<JFXButton text="%settings.advanced" onMouseClicked="#onShowAdvanced" buttonType="RAISED" styleClass="jfx-button-raised"/>
<HBox alignment="CENTER_LEFT" style="-fx-padding: 20 0 12 0;">
<Label text="%settings.advanced" style="-fx-text-fill: #616161;" />
</HBox>
<ComponentList fx:id="advancedSettingsPane" depth="1">
<JFXTextField labelFloat="true" promptText="%settings.advanced.jvm_args" styleClass="fit-width"
Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

@@ -18,11 +18,12 @@
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.dependency=Dependency
about.dependency.statement=Gson (Google, Apache License 2.0)\nCommons Compress (Apache, Apache License 2.0)
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (Default background image)\nAll contributors who participated in this project via issues, pull requests, etc.
about.dependency=Dependencies
# Due to space limitations, only first authors are listed
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=Statement
about.claim.statement=Minecraft is copyrighted by Mojang AB. We are not responsible for any copyright infringement arising from the use of this software.
about.open_source=Open Source
@@ -31,18 +32,20 @@ about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=Accounts
account.character=character
account.choose=Choose a character
account.current=Current
account.create=Create a new account
account.email=Email
account.failed.connect_authentication_server=Cannot connect the authentication server. Check your network.
account.failed.invalid_credentials=Incorrect password. Or forbidden to log in temporarily.
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
account.failed.invalid_credentials=Incorrect password, or you are forbidden to login temporarily.
account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to log in.
account.failed.no_charactor=No character in this account.
account.injector.add=Add new authentication server
account.failed.invalid_token=Please log out and re-input your password to login.
account.failed.no_character=No character in this account.
account.failed.no_selected_server=No authentication server is selected.
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
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
account.injector.server_url=Server URL
account.injector.server_name=Server Name
account.methods=Login Type
account.methods.authlib_injector=authlib-injector
@@ -57,68 +60,31 @@ archive.author=Authors
archive.game_version=Game
archive.version=Version
assets.download=Download Assets
assets.download_all=Download Assets Files
assets.failed=Failed to get the list, try again.
assets.failed_download=Failed to download assets, may cause no sounds and language files.
assets.download=Download assets
assets.download_all=Check the integrity of assets
button.about=About
button.cancel=Cancel
button.clear=Clear
button.close=Close
button.copy=Copy
button.delete=Delete
button.download=Download
button.edit=Edit
button.explore=Explore
button.info=Info
button.install=Install
button.more=More
button.no=No
button.ok=OK
button.others=Others
button.preview=Preview
button.refresh=Refresh
button.retry=Retry
button.save=Save
button.test=Test
button.yes=Yes
color.blue=Blue
color.dark_blue=Dark Blue
color.green=Green
color.orange=Orange
color.purple=Purple
color.red=Red
color.recent=Recommended
color.custom=Custom Color
crash.NoClassDefFound=Please check "HMCL" software is complete.
crash.advice.ClassNotFoundException=Minecraft or mods are incomplete. Retry if there are some libraries that have not downloaded or update your game and mods! Or you can try Game Settings -> Manage (Version) -> Delete library files to solve the problem.
crash.advice.ConcurrentModificationException=Maybe your Java is newer than 1.8.0_11, you could downgrade to Java 7.
crash.advice.LWJGLException=Maybe your video driver does not work well, please update your video driver.
crash.advice.NoSuchFieldError=Minecraft or mods are incomplete. Retry if there are some libraries that have not downloaded or update your game and mods!
crash.advice.OpenGL=Maybe drivers caused problems.
crash.advice.OutOfMemoryError=The maximum memory of JVM is too small, please modify it.
crash.advice.SecurityException=Maybe you have modified minecraft.jar but have not removed the META-INF.
crash.advice.UnsupportedClassVersionError=Maybe your java is too old, try to update the java.
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, https://bmclapi2.bangbang93.com/)
download.failed=Failed to download
download.failed.refresh=Unable to load version list. Click here to retry.
download.mojang=Mojang
download.not_200=Failed to download, the response code is %s.
download.source=Download Source
download.successfully=Downloaded successfully
extension.bat=Windows Bat file
extension.mod=Mod file
@@ -133,16 +99,16 @@ folder.resourcepacks=Resourcepacks
folder.saves=Saves
folder.screenshots=Screenshots
general.prompt.empty=(None)
input.email=The username must be an e-mail.
input.number=Must be a number.
input.not_empty=Input Requrired!
input.not_empty=Required field
input.url=Must be a valid URL.
input.url.http=Only Http or Https accepted.
install=Install New Game
install.failed=Failed to install
install.installer.choose=Choose a %s version
install.installer.failed=Unable to install %s.
install.installer.forge=Forge
install.installer.game=Game
install.installer.install=
@@ -154,37 +120,29 @@ install.modpack=Install a modpack (CurseForge supported)
install.new_game=Install a New Game
install.new_game.already_exists=This version has already been existing.
install.new_game.current_game_version=Current Game Version
install.no_version=The version is not found.
install.no_version_if_intall=The required version is not found, do you wish to install the version automatically?
install.release_time=Release Time
install.select=Select an operation
install.success=Installed successfully
install.time=Time
install.type=Type
lang=English
lang.default=Belong to OS language.
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or newer Java version.
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
launch.advice.newer_java=Java 8 is suggested, which can make game run more fluently. And many mods and Minecraft 1.12 and newer versions requires Java 8.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game probably crash. The launcher will try to launch it.
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game probably crash. The maximum memory is 1024MB. The launcher will try to launch it.
launch.circular_dependency_versions=Found circular dependency versions, please check if your client has been modified.
launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine cannot be created. Java arguments may have problems. You can enable the no args mode in the settings.
launch.failed.creating_process=Failed to create process, maybe your java path is wrong, please modify your java path.
launch.failed.decompressing_natives=Unable to decompress native libraries.
launch.failed.downloading_libraries=Did not finish downloading libraries, continue launching game?
launch.failed.executable_permission=Unable to add permission to the launch script
launch.failed.exited_abnormally=Game exited abnormally, please visit the log, or ask someone for help.
launch.failed.prelaunch_command=Prelaunch command fail.
launch.state.dependencies=Dependencies
launch.state.done=Done
launch.state.logging_in=Logging In
launch.state.modpack=Preparing for modpack
launch.state.waiting_launching=Waiting for game launching
launch.unsupported_launcher_version=Sorry, the launcher cannot launch minecraft, but will retry launching it.
launch.wrong_javadir=Incorrect Java directory, will reset to default Java directory.
launcher=Launcher
@@ -196,62 +154,31 @@ launcher.common_directory.choose=Choose common directory.
launcher.contact=Contact Us
launcher.crash=Hello Minecraft! Launcher has crashed!
launcher.crash_out_dated=Hello Minecraft! Launcher has crashed! Your launcher is outdated. Update it!
launcher.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
launcher.tab.ui=UI
launcher.update_java=Please upgrade your Java.
launcher.update_launcher=Check for update
launcher.versions_json_not_formatted=The version information of %s is malformed! Redownload it?
launcher.versions_json_not_matched=The version %s is malformed! There are a json:%s in this version. Do you want to fix this problem?
launcher.versions_json_not_matched_cannot_auto_completion=The version %s lost version information file, delete it?
login.changed_client_token=The server response has changed the client token.
login.choose_charactor=Please choose the character you want
login.empty_username=You have not set a username!
login.enter_password=Please enter your password.
login.enter_username=Please enter your name.
login.failed=Failed to login
login.invalid_username=Invalid username
login.invalid_uuid_and_username=Invalid UUID and username
login.no_valid_character=No valid character, please visit skinme.cc and create your own character.
logwindow.search=Search
logwindow.show_lines=Show Lines
logwindow.terminate_game=Terminate Game
logwindow.title=Log
main_page=Home
message.cancelled=Cancelled
message.cannot_open_explorer=Cannot open explorer:
message.confirm=Confirm
message.doing=Please wait
message.downloading=Downloading...
message.error=Error
message.info=Info
message.loading=Loading...
message.success=Tasks succeeded
message.unknown=Unknown
minecraft.invalid=Invalid
minecraft.invalid_jar=Invalid Jar
minecraft.modified=(Modified!)
minecraft.not_a_file=Not a file
minecraft.not_found=Not found
minecraft.not_readable=Not readable
minecraft.wrong_path=Wrong Minecraft path, the launcher could not find the path.
modpack=Mod pack
modpack.choose=Choose a modpack zip file which you want to import. If you want to update the modpack, please enter the version you want to update.
modpack.choose=Choose a modpack zip file which you want to install.
modpack.desc=Describe your modpack, including precautions, changlog, supporting Markdown(also supporting online pictures).
modpack.enter_name=Enter your desired name for this game.
modpack.export=Export Modpack
modpack.export_error=Failed to export the modpack, maybe the format of your game directory is incorrect or failed to manage files.
modpack.export_finished=Exporting the modpack finished. See
modpack.files.blueprints=BuildCraft blueprints
modpack.files.config=Mod configs
modpack.files.dumps=NEI debug output
@@ -264,22 +191,20 @@ modpack.files.resourcepacks=Resource(Texture) packs
modpack.files.saves=Saved games
modpack.files.scripts=MineTweaker configuration
modpack.files.servers_dat=Multiplayer servers list
modpack.included_launcher=The modpack is included the launcher, you can publish it directly.
modpack.install=Install %s modpack
modpack.installing=Installing modpack
modpack.introduction=Curse, MultiMC, HMCL, MCBBS modpacks supprted.
modpack.invalid=Invalid modpack file.
modpack.mismatched_type=Mismatched modpack type, your current game is %s modpack, but your update file is %s modpack.
modpack.name=Modpack Name
modpack.not_a_valid_location=Not a valid modpack path
modpack.not_a_valid_name=Not a valid modpack name
modpack.not_included_launcher=You can use the modpack by clicking the "Import Modpack" button.
modpack.scan=Scanning this modpack
modpack.task.install=Import Modpack
modpack.task.install.error=Failed to install the modpack. Maybe the files is incorrect, or a management issue occurred.
modpack.task.install.will=Install the modpack:
modpack.type.curse=Curse
modpack.type.curse.completion=Install relative files to Curse modpack
modpack.type.curse.error=Unable to complete this Curse modpack. Please retry.
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=Unsupported modpack, only HMCL, MultiMC, Curse modpacks are supported.
@@ -290,7 +215,6 @@ modpack.wizard.step.1.title=Set the basic options to the modpack.
modpack.wizard.step.2=Files selection
modpack.wizard.step.2.title=Choose the files you want to put into the modpack.
modpack.wizard.step.3=Description
modpack.wizard.step.3.title=Describe your modpack.
modpack.wizard.step.initialization.exported_version=The exported game version
modpack.wizard.step.initialization.include_launcher=Include the launcher
modpack.wizard.step.initialization.save=Choose a path to export the game files to
@@ -301,19 +225,15 @@ mods.add=Add mods
mods.add.failed=Failed to add mods %s.
mods.add.success=Successfully added mods %s.
mods.choose_mod=Choose your mods
mods.default_information=Please ensure that you have installed Forge or LiteLoader before installing mods!\nYou can drop your mod files from explorer/finder, and delete mods by the delete button.\nDisable a mod by leaving the check box unchecked; Choose an item to get the information.
mods.remove=Remove
operation.confirm_stop=Terminate the operations?
operation.stopped=The operation was aborted.
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?
profile.title=Game Directories
profile.selected=Selected
@@ -323,7 +243,7 @@ selector.custom=Custom
settings=Settings
settings.advanced=Advanced
settings.advanced=Advanced Settings
settings.advanced.dont_check_game_completeness=Don't check game completeness
settings.advanced.game_dir.default=Default (.minecraft/)
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except assets,libraries)
@@ -342,16 +262,14 @@ settings.advanced.precall_command=Pre-Launch command(will be executed before gam
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?
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
@@ -377,16 +295,11 @@ settings.launcher.theme=Theme
settings.max_memory=Max Memory/MB
settings.physical_memory=Physical Memory Size
settings.run_directory=Run Directory(Version Isolation)
settings.show_log=Show Logs
settings.tabs.assets_downloads=Assets
settings.tabs.game_download=Games
settings.tabs.installers=Installers
settings.test_game=Test game
settings.type.global=Global version settings(all shared)
settings.type=Version setting type
settings.type.special=Specialized version settings(will not affect other versions)
settings.update_version=Update version json.
update=Update
update.failed=Failed to check for updates.
@@ -394,7 +307,6 @@ update.found=Found Update!
update.newest_version=Newest version: %s
update.latest=This is latest Version.
update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update.
update.should_open_link=Are you willing to update the launcher?
update.tooltip=Update
version.cannot_read=Unable to gather the game version. Cannot continue auto-installing.
@@ -408,7 +320,6 @@ version.launch_script.failed=Unable to make launch script.
version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage.redownload_assets_index=Redownload Assets Index
version.manage.redownload_json=Redownload Minecraft Configuration(minecraft.json)
version.manage.remove=Delete this version
version.manage.remove.confirm=Sure to remove version %s?
version.manage.remove_libraries=Delete library files
@@ -418,10 +329,6 @@ version.settings=Settings
version.update=Update
wizard.prev=< Prev
wizard.close=Close
wizard.failed=Failed
wizard.finish=Finish
wizard.help=Help
wizard.next=Next >
wizard.steps=Steps
wizard.summary=Summary
@@ -0,0 +1,334 @@
# Hello Minecraft! Launcher.
# Copyright (C) 2013 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/}.
#
#author: huangyuhui
about.copyright=版權
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鳴謝
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默認背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.dependency=依賴
# 由於篇幅限制,僅列出第一作者
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=免責聲明
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟件產生的版權問題,軟件製作方概不負責。請支持正版。
about.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=賬戶
account.character=角色
account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.no_character=該帳號沒有角色
account.failed.no_selected_server=未選擇認證服務器
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
account.injector.http=警告:此服務器使用不安全的 HTTP 協議,您的密碼在登錄時會被明文傳輸。
account.injector.server=認證服務器
account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.methods.no_method=沒有登入方式
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
account.password=密碼
account.username=用戶名
archive.author=作者
archive.game_version=遊戲版本
archive.version=版本
assets.download=下載資源
assets.download_all=檢查資源文件完整性
button.cancel=取消
button.clear=清除
button.delete=刪除
button.edit=修改
button.install=安裝
button.no=否
button.ok=確定
button.refresh=刷新
button.save=保存
button.yes=是
color.recent=推薦
color.custom=自定義顏色
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟件崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下載失敗
download.failed.refresh=加載版本列表失敗,點擊此處重試。
download.mojang=官方
extension.bat=Windows 腳本
extension.mod=模組文件
extension.png=圖片文件
extension.sh=Bash 腳本
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
folder.game=遊戲文件夾
folder.mod=MOD文件夾
folder.resourcepacks=資源包文件夾
folder.saves=存檔文件夾
folder.screenshots=截圖文件夾
general.prompt.empty=(無)
input.email=用戶名必須是郵箱
input.number=必須是數字
input.not_empty=必填項
input.url=必須是合法的鏈接
install=添加遊戲
install.failed=安裝失敗
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
install.installer.install=安裝 %s
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫不安裝 %s,可以點擊此處安裝
install.installer.optifine=OptiFine
install.installer.version=%s 版本
install.modpack=安裝整合包
install.new_game=安裝新遊戲版本
install.new_game.already_exists=此版本已經存在,請換一個名字
install.new_game.current_game_version=當前遊戲版本
install.select=請選擇安裝方式
install.success=安裝成功
lang=繁體中文
lang.default=跟隨系統語言
launch.advice.different_platform=您的系統是 64 位的但是 Java 是 32 位的,推薦您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java9=不高於 1.13 的安裝了 Mod 的 Minecraft 不支持 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=檢測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更流暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要需要 Java 8。
launch.advice.not_enough_space=您設置的內存大小過大,由於超過了系統內存大小 %dMB,所以可能影響遊戲體驗或無法啓動遊戲,啓動器仍會嘗試啓動。
launch.advice.too_large_memory_for_32bit=您設置的內存大小過大,由於可能超過了 32 位 Java 的內存分配限制,所以可能無法啓動遊戲,請將內存調至 1024MB 或更小,啓動器仍會嘗試啓動。
launch.failed=啓動失敗
launch.failed.cannot_create_jvm=截獲到無法創建 Java 虛擬機,可能是 Java 參數有問題,可以在設置中開啓無參數模式啓動。
launch.failed.creating_process=啓動失敗,在創建新進程時發生錯誤,可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=未能解壓遊戲本地庫。
launch.failed.executable_permission=未能爲啓動文件添加執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看日志文件,或聯繫他人尋求幫助。
launch.state.dependencies=正在處理遊戲依賴
launch.state.done=啓動完成
launch.state.logging_in=登錄中
launch.state.modpack=正在下載必要文件
launch.state.waiting_launching=等待遊戲啓動
launch.wrong_javadir=錯誤的 Java 路徑,將自動重置爲默認 Java 路徑。
launcher=啓動器
launcher.background=背景地址
launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
launcher.contact=聯繫我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並通過 MCBBS、貼吧、GitHub 或 Minecraft Forum 反饋 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已檢測到您的啓動器不是最新版本,請更新後再試!
launcher.update_java=請更新您的 Java
login.empty_username=你還未設置用戶名!
login.enter_password=請輸入您的密碼
logwindow.show_lines=顯示行數
logwindow.terminate_game=結束遊戲進程
logwindow.title=日誌
main_page=主頁
message.confirm=提示
message.doing=請耐心等待
message.downloading=正在下載...
message.error=錯誤
message.info=提示
message.success=已完成
message.unknown=未知
modpack=整合包
modpack.choose=選擇要安裝的遊戲整合包文件
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支持 Markdown(圖片請用網絡圖)。
modpack.enter_name=給遊戲起個你喜歡的名字
modpack.export=導出整合包
modpack.files.blueprints=BuildCraft 藍圖
modpack.files.config=Mod 配置文件
modpack.files.dumps=NEI 調試輸出
modpack.files.liteconfig=Mod 配置文件
modpack.files.mods=Mod
modpack.files.mods.voxelmods=VoxelMods 配置,如小地圖
modpack.files.options_txt=遊戲設定
modpack.files.optionsshaders_txt=光影設定
modpack.files.resourcepacks=資源包(材質包)
modpack.files.saves=遊戲存檔
modpack.files.scripts=MineTweaker 配置
modpack.files.servers_dat=多人遊戲服務器列表
modpack.install=安裝%s整合包
modpack.installing=正在安裝整合包
modpack.introduction=支持 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級文件,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,當前遊戲是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.name=整合包名稱
modpack.not_a_valid_name=不是一個有效的整合包名稱
modpack.scan=解析整合包
modpack.task.install=導入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式不正確或操作文件失敗。
modpack.task.install.will=將會安裝整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 整合包相關文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=該整合包不被支持。僅 HMCL、MultiMC、Curse 整合包受支持。
modpack.update=正在升級整合包
modpack.wizard=導出整合包嚮導
modpack.wizard.step.1=基本設置
modpack.wizard.step.1.title=設置整合包的主要信息
modpack.wizard.step.2=文件選擇
modpack.wizard.step.2.title=選中你想加到整合包中的文件或文件夾
modpack.wizard.step.3=整合包描述
modpack.wizard.step.initialization.exported_version=要導出的遊戲版本
modpack.wizard.step.initialization.include_launcher=包含啓動器
modpack.wizard.step.initialization.save=選擇要導出到的遊戲整合包位置
modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啓動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應當將不允許非官方途徑傳播的 Mod、材質包等納入整合包。\n整合包會保存您目前的下載源設置
mods=模組管理
mods.add=添加模組
mods.add.failed=添加模組 %s 失敗。
mods.add.success=成功添加模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
profile=配置
profile.default=當前目錄
profile.home=主文件夾
profile.instance_directory=遊戲路徑
profile.instance_directory.choose=選擇遊戲路徑
profile.new=新建配置
profile.title=遊戲目錄
profile.selected=已選中
selector.choose=選擇
selector.choose_file=選擇文件
selector.custom=自定義
settings=普通設置
settings.advanced=高級設置
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
settings.advanced.game_dir.default=默認(.minecraft/)
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=啓動器默認添加的參數(請不要重複添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=內存永久保存區域(不必填寫,MB)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.launcher_visibility.close=遊戲啓動後結束啓動器
settings.advanced.launcher_visibility.hide=遊戲啓動後隱藏啓動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啓動器並在遊戲結束後重新打開
settings.advanced.launcher_visibility.keep=保持啓動器可見
settings.advanced.launcher_visible=啓動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
settings.advanced.no_common=不使用公共路徑
settings.advanced.no_jvm_args=不添加默認的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啓動前執行命令(不必填寫,將在遊戲啓動前調用)
settings.advanced.server_ip=直入服務器 IP 地址(不必填寫,啓動遊戲後直接進入對應服務器)
settings.advanced.wrapper_launcher=前置指令(不必填寫,如 optirun)
settings.custom=自定義
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=管理
settings.icon=遊戲圖標
settings.launcher=啓動器設置
settings.launcher.common_path.tooltip=啓動器將所有遊戲資源及依賴庫文件放於此集中管理,如果遊戲文件夾內有現成的將不會使用公共庫文件
settings.launcher.download_source=下載源
settings.launcher.language=語言
settings.launcher.log_font=日誌字體
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=代理賬戶
settings.launcher.proxy.no_proxy=直連
settings.launcher.proxy.has_proxy=啓用驗證
settings.launcher.proxy.host=主機
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密碼
settings.launcher.proxy.port=端口
settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=賬戶
settings.launcher.theme=主題
settings.max_memory=最大內存(MB)
settings.physical_memory=物理內存大小
settings.show_log=查看日誌
settings.tabs.installers=自動安裝
settings.type.global=全局版本設置(使用該設置的版本共用一套設定)
settings.type=版本設置類型
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
update=啓動器更新
update.failed=檢查更新失敗
update.found=發現更新
update.newest_version=最新版本爲:%s
update.latest=當前版本爲最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼板了,您可以手動粘貼網址打開頁面
update.tooltip=更新
version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝
version.forbidden_name=此版本名稱不受支持,請換一個名字
version.game.old=遠古版
version.game.release=穩定版
version.game.snapshot=測試版
version.launch=啓動遊戲
version.launch_script=生成啓動腳本
version.launch_script.failed=生成啓動腳本失敗
version.launch_script.save=保存啓動腳本
version.launch_script.success=啓動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源配置(assets_index.json)
version.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫文件
version.manage.rename=重命名該版本
version.manage.rename.message=請輸入要改成的名字
version.settings=遊戲設置
version.update=更新
wizard.prev=< 上一步
wizard.failed=失敗
wizard.finish=完成
wizard.next=下一步 >
@@ -18,11 +18,12 @@
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, https://bmclapi2.bangbang93.com/)\ngamerteam (默认背景图)\n所有通过 Issues、Pull Requests 等方式参与本项目的贡献者
about.dependency=依赖
about.dependency.statement=Gson (Google, Apache License 2.0)\nCommons Compress (Apache, Apache License 2.0)
# 由于篇幅限制,仅列出第一作者
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=免责声明
about.claim.statement=Minecraft 版权归 Mojang AB 所有,使用本软件产生的版权问题,软件制作方概不负责。请支持正版。
about.open_source=开源
@@ -31,18 +32,20 @@ about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=账户
account.character=角色
account.choose=选择一个角色
account.current=当前账户
account.create=新建账户
account.email=邮箱
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.invalid_credentials=您的用户名或密码错误,或者登录次数过多被暂时禁止登录,请稍后再试
account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_charactor=该帐号没有角色
account.injector.add=添加登录认证服务器
account.injector.http=警告:此服务器使用不安全的http协议,您的密码在登录时会被明文传输。
account.failed.no_character=该帐号没有角色
account.failed.no_selected_server=未选择认证服务器
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.injector.add=添加认证服务器
account.injector.manage=管理认证服务器
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
account.injector.server=认证服务器
account.injector.server_ip=服务器地址
account.injector.server_url=服务器地址
account.injector.server_name=服务器名称
account.methods=登录方式
account.methods.authlib_injector=authlib-injector 登录
@@ -58,67 +61,30 @@ archive.game_version=游戏版本
archive.version=版本
assets.download=下载资源
assets.download_all=下载资源文件
assets.failed=获取列表失败,请刷新重试。
assets.failed_download=下载资源文件失败,可能导致没有中文和声音。
assets.download_all=检查资源文件完整性
button.about=关于
button.cancel=取消
button.clear=清除
button.close=关闭
button.copy=复制
button.delete=删除
button.download=下载
button.edit=修改
button.explore=浏览
button.info=信息
button.install=安装
button.more=更多
button.no=否
button.ok=确定
button.others=其他
button.preview=预览
button.refresh=刷新
button.retry=重试
button.save=保存
button.test=测试
button.yes=是
color.blue=蓝色
color.dark_blue=深蓝色
color.green=绿色
color.orange=橙色
color.purple=紫色
color.red=红色
color.recent=推荐
color.custom=自定义颜色
crash.NoClassDefFound=请确认HMCL本体是否完整
crash.advice.ClassNotFoundException=Minecraft不完整或Mod冲突,如果有未能下载的文件请下载成功后重试或是客户端损坏请重试请重新制作客户端或下载整合包解决问题,另可尝试游戏设置->(版本)管理->删除库文件解决问题
crash.advice.ConcurrentModificationException=这可能是因为您的Java版本高于Java 1.8.0_11导致的,可以尝试卸载Java8安装Java7。
crash.advice.LWJGLException=您的电脑不正常,可能需要使用驱动精灵或其他安装器更新显卡驱动。
crash.advice.NoSuchFieldError=Minecraft不完整或Mod冲突,如果有未能下载的文件请下载成功后重试或是客户端损坏请重试请重新制作客户端或下载整合包解决问题。
crash.advice.OpenGL=可能是显卡/声卡驱动问题,也可能是Mod导致的问题。
crash.advice.OutOfMemoryError=内存溢出,您设置的Minecraft最大内存过小,请调大!
crash.advice.SecurityException=可能是您修改了minecraft.jar但未删除META-INF文件夹的原因。请通过压缩软件删除jar中的META-INF文件夹。
crash.advice.UnsupportedClassVersionError=这可能是因为您的Java版本过于老旧,可以尝试更换最新Java并在版本设置的Java路径中设置.
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。
crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新您的 Java。
crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。
download=下载
download.BMCL=BMCLAPI (bangbang93, http://bmclapi.bangbang93.com/)
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下载失败
download.failed.refresh=加载版本列表失败,点击此处重试。
download.mojang=官方
download.not_200=下载失败,HTTP状态码:%s.
download.source=下载源
download.successfully=下载完成
extension.bat=Windows 脚本
extension.mod=模组文件
@@ -133,187 +99,141 @@ folder.resourcepacks=资源包文件夹
folder.saves=存档文件夹
folder.screenshots=截图文件夹
general.prompt.empty=(无)
input.email=用户名必须是邮箱
input.number=必须是数字
input.not_empty=必填项
input.url=必须是合法的链接
input.url.http=只支持Http或Https协议
install=添加游戏
install.failed=安装失败
install.installer.choose=选择%s版本
install.installer.failed=未能成功安装%s.
install.installer.choose=选择 %s 版本
install.installer.forge=Forge
install.installer.game=游戏
install.installer.install=安装%s
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.installer.version=%s 版本
install.modpack=安装整合包
install.new_game=安装新游戏版本
install.new_game.already_exists=此版本已经存在,请换一个名字
install.new_game.current_game_version=当前游戏版本
install.no_version=未找到要安装的对应MC版本
install.no_version_if_intall=未找到要安装的对应MC版本,是否自动安装需要的MC版本?
install.release_time=发布时间
install.select=请选择安装方式
install.success=安装成功
install.time=时间
install.type=类型
lang=简体中文
lang.default=跟随系统语言
launch.advice.different_platform=您的系统是64位的但是Java是32位的,推荐您安装64位Java.
launch.advice.java9=不高于1.13的Minecraft不支持Java 9或更高版本,请使用Java 8.
launch.advice.newer_java=检测到您未使用Java 8及更新版本,Java 8能使游戏更流畅而且Minecraft 1.12及更新版本和很多Mod强制需要需要Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小%dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了32位Java的内存分配限制,所以可能无法启动游戏,请将内存调至1024MB或更小,启动器仍会尝试启动。
launch.circular_dependency_versions=发现游戏版本循环引用,请确认您的客户端未被修改或修改导致出现此问题。
launch.advice.different_platform=您的系统是 64 位的但是 Java 是 32 位的,推荐您安装 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
launch.advice.java9=不高于 1.13 的安装了 Mod 的 Minecraft 不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要需要 Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小,启动器仍会尝试启动。
launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建Java虚拟机,可能是Java参数有问题,可以在设置中开启无参数模式启动
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是Java路径错误。
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
launch.failed.decompressing_natives=未能解压游戏本地库。
launch.failed.downloading_libraries=未完成游戏依赖库的下载,还要继续启动游戏吗?
launch.failed.executable_permission=未能为启动文件添加执行权限
launch.failed.executable_permission=未能为启动文件添加执行权限。
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
launch.failed.prelaunch_command=启动前执行命令执行失败。
launch.state.dependencies=正在处理游戏依赖
launch.state.done=启动完成
launch.state.logging_in=登录中
launch.state.modpack=正在下载必要文件
launch.state.waiting_launching=等待游戏启动
launch.unsupported_launcher_version=对不起,本启动器现在可能不能启动这个版本的Minecraft,但启动器还是会尝试启动,请尽快将此问题报告给作者。
launch.wrong_javadir=错误的Java路径,将自动重置为默认Java路径。
launch.wrong_javadir=错误的 Java 路径,将自动重置为默认 Java 路径。
launcher=启动器
launcher.background=背景地址
launcher.background.choose=选择背景路径
launcher.background.default=默认(自动检索启动器同目录下的background.png/jpg及bg文件夹内的图片)
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
launcher.common_directory=公共文件夹
launcher.common_directory.choose=选择公共文件夹
launcher.contact=联系我们
launcher.crash=Hello Minecraft!遇到了无法处理的错误,请复制下列内容并通过mcbbs、贴吧、Github或Minecraft Forum反馈bug。
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=通用
launcher.tab.ui=界面
launcher.update_java=请更新您的Java
launcher.update_launcher=检查更新
launcher.versions_json_not_formatted=版本%s信息文件格式错误,是否重新下载?
launcher.versions_json_not_matched=版本%s格式不规范!该版本文件夹下有json:%s,是否更名这个文件来规范格式?
launcher.versions_json_not_matched_cannot_auto_completion=版本%s缺失必要的版本信息文件,是否删除该版本?
launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误,请复制下列内容并通过 MCBBS、贴吧、GitHub 或 Minecraft Forum 反馈 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误,已检测到您的启动器不是最新版本,请更新后再试!
launcher.update_java=请更新您的 Java
login.changed_client_token=服务器回应已经修改客户端令牌
login.choose_charactor=请选择您要使用的角色
login.empty_username=你还未设置用户名!
login.enter_password=请输入您的密码
login.enter_username=请输入您的账号
login.failed=登录失败:
login.invalid_username=无效的用户名
login.invalid_uuid_and_username=无效的UUID和用户名
login.no_valid_character=无有效的角色,自行到skinme.cc登陆并创建角色
logwindow.search=查找
logwindow.show_lines=显示行数
logwindow.terminate_game=结束游戏进程
logwindow.title=日志
main_page=主页
message.cancelled=已取消
message.cannot_open_explorer=无法打开文件管理器:
message.confirm=提示
message.doing=请耐心等待
message.downloading=正在下载...
message.error=错误
message.info=提示
message.loading=加载中...
message.success=已完成
message.unknown=未知
minecraft.invalid=无效的
minecraft.invalid_jar=无效的jar包
minecraft.modified=(修改的!)
minecraft.not_a_file=不是文件
minecraft.not_found=找不到minecraft.jar
minecraft.not_readable=minecraft.jar不可读
minecraft.wrong_path=错误的Minecraft路径,启动器未找到设定的Minecraft路径,请检查。
modpack=整合包
modpack.choose=选择要导入的游戏整合包文件,如果您希望更新整合包,请输入要更新的版本名
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持Markdown(图片请用网络图片)。
modpack.choose=选择要安装的游戏整合包文件
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 Markdown(图片请用网络图)。
modpack.enter_name=给游戏起个你喜欢的名字
modpack.export=导出整合包
modpack.export_error=导出失败,可能是您的游戏文件夹格式不正确或操作文件失败
modpack.export_finished=整合包导出完成,参见
modpack.files.blueprints=BuildCraft蓝图
modpack.files.config=Mod配置文件
modpack.files.dumps=NEI调试输出
modpack.files.liteconfig=Mod配置文件
modpack.files.blueprints=BuildCraft 蓝图
modpack.files.config=Mod 配置文件
modpack.files.dumps=NEI 调试输出
modpack.files.liteconfig=Mod 配置文件
modpack.files.mods=Mod
modpack.files.mods.voxelmods=VoxelMods配置,如小地图
modpack.files.mods.voxelmods=VoxelMods 配置,如小地图
modpack.files.options_txt=游戏设定
modpack.files.optionsshaders_txt=光影设定
modpack.files.resourcepacks=资源包(材质包)
modpack.files.saves=游戏存档
modpack.files.scripts=MineTweaker配置
modpack.files.scripts=MineTweaker 配置
modpack.files.servers_dat=多人游戏服务器列表
modpack.included_launcher=整合包已包含启动器,可直接发布
modpack.install=安装%s整合包
modpack.installing=正在安装整合包
modpack.introduction=支持 Curse, MultiMC, HMCL, MCBBS 整合包。
modpack.introduction=支持 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=无效的整合包升级文件,可能是下载时出现问题。
modpack.mismatched_type=整合包类型不匹配,当前游戏是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.name=整合包名称
modpack.not_a_valid_location=不是一个有效整合包位置
modpack.not_a_valid_name=不是一个有效的整合包名称
modpack.not_included_launcher=整合包未包含启动器,可使用本软件的导入整合包功能导入整合包
modpack.scan=解析整合包
modpack.task.install=导入整合包
modpack.task.install.error=安装失败,可能是整合包格式不正确或操作文件失败
modpack.task.install.error=安装失败,可能是整合包格式不正确或操作文件失败。
modpack.task.install.will=将会安装整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下载Curse整合包相关文件
modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=该整合包不被支持。仅HMCL、MultiMC、Curse整合包受支持。
modpack.unsupported=该整合包不被支持。仅 HMCL、MultiMC、Curse 整合包受支持。
modpack.update=正在升级整合包
modpack.wizard=导出整合包向导
modpack.wizard.step.1=基本设置
modpack.wizard.step.1.title=设置整合包的主要信息
modpack.wizard.step.2=文件选择
modpack.wizard.step.2.title=选中你想加到整合包中的文件(夹)
modpack.wizard.step.2.title=选中你想加到整合包中的文件或文件夹
modpack.wizard.step.3=整合包描述
modpack.wizard.step.3.title=描述你要制作的整合包
modpack.wizard.step.initialization.exported_version=要导出的游戏版本
modpack.wizard.step.initialization.include_launcher=包含启动器
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的Minecraft是正式版而非快照版,\n而且不应当将不允许非官方途径传播的Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置
modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的 Minecraft 是正式版而非快照版,\n而且不应当将不允许非官方途径传播的 Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置
mods=模组管理
mods.add=添加模组
mods.add.failed=添加模组%s失败。
mods.add.success=成功添加模组%s。
mods.add.failed=添加模组 %s 失败。
mods.add.success=成功添加模组 %s。
mods.choose_mod=选择模组
mods.default_information=安装Mod前你需要确保已安装Forge或LiteLoader!\n您可以从资源管理器拖动mod文件到列表中来添加mod,同时使用删除键可快速删除选中mod\n点掉mod前面的勾可禁用mod,不会加载;选择mod可以获取mod信息
mods.remove=删除
operation.confirm_stop=真的要终止操作吗?
operation.stopped=操作被强行终止
profile=配置
profile.default=当前目录
profile.home=主文件夹
profile.instance_directory=游戏路径
profile.instance_directory.choose=选择游戏路径
profile.new=新建配置
profile.new_name=新配置名:
profile.remove=真的要删除配置%s吗?
profile.title=游戏目录
profile.selected=已选中
@@ -325,35 +245,33 @@ settings=普通设置
settings.advanced=高级设置
settings.advanced.dont_check_game_completeness=不检查游戏完整性
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除assets,libraries)
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=启动器默认添加的参数(请不要重复添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,MB)
settings.advanced.jvm_args=Java虚拟机参数(不必填写)
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,MB)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
settings.advanced.launcher_visibility.hide_reopen=隐藏启动器并在游戏结束后重新打开
settings.advanced.launcher_visibility.keep=保持启动器可见
settings.advanced.launcher_visible=启动器可见性
settings.advanced.minecraft_arguments=Minecraft额外参数(不必填写)
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
settings.advanced.no_common=不使用公共路径
settings.advanced.no_jvm_args=不添加默认的JVM参数(使用Java9时必勾)
settings.advanced.precall_command=启动前执行命令(不必填写,将在游戏启动前调用)
settings.advanced.server_ip=直入服务器ip地址(不必填写,启动游戏后直接进入对应服务器)
settings.advanced.wrapper_launcher=前置指令(不必填写,如optirun)
settings.advanced.no_jvm_args=不添加默认的 JVM 参数(使用 Java 9 时必勾)
settings.advanced.precall_command=启动前执行命令(不必填写,将在游戏启动前调用)
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=设置文件加载失败,可能是升级了启动器或被人工修改造成错误,是否清除?
settings.game=游戏设置
settings.game.dimension=游戏窗口分辨率
settings.game.exploration=浏览
settings.game.fullscreen=全屏
settings.game.java_directory=Java路径
settings.game.java_directory.choose=选择Java路径
settings.game.game_directory=游戏路径(版本隔离)
settings.game.game_directory.choose=选择游戏路径
settings.game.java_directory=Java 路径
settings.game.java_directory.choose=选择 Java 路径
settings.game.management=管理
settings.icon=游戏图标
@@ -375,18 +293,13 @@ settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=账户
settings.launcher.theme=主题
settings.max_memory=最大内存/MB
settings.max_memory=最大内存(MB)
settings.physical_memory=物理内存大小
settings.run_directory=运行路径(版本隔离)
settings.show_log=查看日志
settings.tabs.assets_downloads=资源下载
settings.tabs.game_download=游戏下载
settings.tabs.installers=自动安装
settings.test_game=测试游戏
settings.type.global=全局版本设置(使用该设置的版本共用一套设定)
settings.type=版本设置类型
settings.type.special=单独版本设置(不会影响到其他版本的设定)
settings.update_version=更新版本文件
update=启动器更新
update.failed=检查更新失败
@@ -394,7 +307,6 @@ update.found=发现更新
update.newest_version=最新版本为:%s
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
update.should_open_link=是否更新?
update.tooltip=更新
version.cannot_read=读取游戏版本失败,无法进行自动安装
@@ -406,9 +318,8 @@ version.launch=启动游戏
version.launch_script=生成启动脚本
version.launch_script.failed=生成启动脚本失败
version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕: %s.
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.redownload_json=重新下载版本配置(minecraft.json)
version.launch_script.success=启动脚本已生成完毕:%s
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.remove=删除该版本
version.manage.remove.confirm=真的要删除版本 %s 吗?
version.manage.remove_libraries=删除所有库文件
@@ -418,10 +329,6 @@ version.settings=游戏设置
version.update=更新
wizard.prev=< 上一步
wizard.close=关闭
wizard.failed=失败
wizard.finish=完成
wizard.help=帮助
wizard.next=下一步 >
wizard.steps=步骤
wizard.summary=概要
Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

@@ -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,37 @@ 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 CredentialExpiredException when the stored credentials has expired, in which case a password login will be performed
*/
public abstract AuthInfo logIn() throws AuthenticationException;
public abstract AuthInfo logIn() throws CredentialExpiredException, 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);
}
}
@@ -1,7 +1,7 @@
/*
* 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
@@ -18,8 +18,25 @@
package org.jackhuang.hmcl.auth;
/**
* Thrown when the stored credentials has expired.
* This exception indicates that a password login should be performed.
*
* @author huangyuhui
* @author yushijinhun
* @see Account#logIn()
*/
public class InvalidTokenException extends AuthenticationException {
public class CredentialExpiredException extends AuthenticationException {
public CredentialExpiredException() {}
public CredentialExpiredException(String message, Throwable cause) {
super(message, cause);
}
public CredentialExpiredException(String message) {
super(message);
}
public CredentialExpiredException(Throwable cause) {
super(cause);
}
}
@@ -1,25 +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;
/**
* throws if wrong password or logging fails for too many times.
* @author huangyuhui
*/
public final class InvalidCredentialsException extends AuthenticationException {
}
@@ -1,3 +1,20 @@
/*
* 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;
public class ServerResponseMalformedException extends AuthenticationException {
@@ -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,24 +25,30 @@ 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.concurrent.atomic.AtomicBoolean;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private final String serverBaseURL;
private final ExceptionalSupplier<String, ?> injectorJarPath;
private AuthlibInjectorServer server;
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
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, AuthlibInjectorServer server, ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, String username, UUID characterUUID, YggdrasilSession session) {
super(service, username, characterUUID, session);
this.injectorJarPath = injectorJarPath;
this.serverBaseURL = serverBaseURL;
this.authlibInjectorDownloader = authlibInjectorDownloader;
this.server = server;
}
@Override
@@ -55,37 +61,53 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return inject(() -> super.logInWithPassword(password, selector));
}
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> supplier) throws AuthenticationException {
// Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time.
GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL));
AtomicBoolean flag = new AtomicBoolean(true);
Thread thread = Lang.thread(() -> flag.set(getTask.test()));
private AuthInfo inject(ExceptionalSupplier<AuthInfo, AuthenticationException> loginAction) throws AuthenticationException {
// Pre-fetch metadata
GetTask metadataFetchTask = new GetTask(NetworkUtils.toURL(server.getUrl()));
Thread metadataFetchThread = Lang.thread(() -> {
try {
metadataFetchTask.run();
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to pre-fetch Yggdrasil metadata", e);
}
}, "Yggdrasil metadata fetch thread");
AuthInfo info = supplier.get();
// Update authlib-injector
AuthlibInjectorArtifactInfo artifact;
try {
thread.join();
String arg = "-javaagent:" + injectorJarPath.get() + "=" + serverBaseURL;
Arguments arguments = Arguments.addJVMArguments(null, arg);
if (flag.get())
arguments = Arguments.addJVMArguments(arguments, "-Dorg.to2mbn.authlibinjector.config.prefetched=" + new String(Base64.getEncoder().encode(getTask.getResult().getBytes()), Charsets.UTF_8));
return info.setArguments(arguments);
} catch (Exception e) {
throw new AuthenticationException("Unable to get authlib injector jar path", e);
artifact = authlibInjectorDownloader.get();
} catch (IOException e) {
throw new AuthenticationException("Failed to download authlib-injector", e);
}
// Perform authentication
AuthInfo info = loginAction.get();
Arguments arguments = new Arguments().addJVMArguments("-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl());
// Wait for metadata to be fetched
try {
metadataFetchThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Optional<String> metadata = Optional.ofNullable(metadataFetchTask.getResult());
if (metadata.isPresent()) {
arguments = arguments.addJVMArguments(
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(metadata.get().getBytes(UTF_8)));
}
return info.withArguments(arguments);
}
@Override
public Map<Object, Object> toStorage() {
Map<Object, Object> map = super.toStorage();
map.put("serverBaseURL", serverBaseURL);
map.put("serverBaseURL", server.getUrl());
return map;
}
public String getServerBaseURL() {
return serverBaseURL;
public AuthlibInjectorServer getServer() {
return server;
}
}
@@ -1,3 +1,20 @@
/*
* 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.authlibinjector;
import org.jackhuang.hmcl.auth.AccountFactory;
@@ -6,34 +23,38 @@ 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.io.IOException;
import java.net.Proxy;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import static org.jackhuang.hmcl.util.Lang.tryCast;
public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjectorAccount> {
private final ExceptionalSupplier<String, ?> injectorJarPathSupplier;
private ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader;
private Function<String, AuthlibInjectorServer> serverLookup;
public AuthlibInjectorAccountFactory(ExceptionalSupplier<String, ?> injectorJarPathSupplier) {
this.injectorJarPathSupplier = injectorJarPathSupplier;
/**
* @param serverLookup a function that looks up {@link AuthlibInjectorServer} by url
*/
public AuthlibInjectorAccountFactory(ExceptionalSupplier<AuthlibInjectorArtifactInfo, ? extends IOException> authlibInjectorDownloader, Function<String, AuthlibInjectorServer> serverLookup) {
this.authlibInjectorDownloader = authlibInjectorDownloader;
this.serverLookup = serverLookup;
}
@Override
public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, Object serverBaseURL, Proxy proxy) throws AuthenticationException {
public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, Object additionalData, Proxy proxy) throws AuthenticationException {
Objects.requireNonNull(selector);
Objects.requireNonNull(username);
Objects.requireNonNull(password);
Objects.requireNonNull(proxy);
if (!(serverBaseURL instanceof String) || !NetworkUtils.isURL((String) serverBaseURL))
throw new IllegalArgumentException("Additional data should be server base url string for authlib injector accounts.");
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider((String) serverBaseURL), proxy),
(String) serverBaseURL, injectorJarPathSupplier, username, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), null, null);
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy),
server, authlibInjectorDownloader, username, null, null);
account.logInWithPassword(password, selector);
return account;
}
@@ -43,16 +64,16 @@ 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));
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl()), proxy),
server, authlibInjectorDownloader, username, session.getSelectedProfile().getId(), session);
}
}
@@ -17,35 +17,34 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
public class AuthlibInjectorServerResponse {
import java.nio.file.Path;
private final Meta meta;
public class AuthlibInjectorArtifactInfo {
public AuthlibInjectorServerResponse() {
this(new Meta());
private int buildNumber;
private String version;
private Path location;
public AuthlibInjectorArtifactInfo(int buildNumber, String version, Path location) {
this.buildNumber = buildNumber;
this.version = version;
this.location = location;
}
public AuthlibInjectorServerResponse(Meta meta) {
this.meta = meta;
public int getBuildNumber() {
return buildNumber;
}
public Meta getMeta() {
return meta;
public String getVersion() {
return version;
}
public static class Meta {
private final String serverName;
public Path getLocation() {
return location;
}
public Meta() {
this("");
}
public Meta(String serverName) {
this.serverName = serverName;
}
public String getServerName() {
return serverName;
}
@Override
public String toString() {
return "authlib-injector [buildNumber=" + buildNumber + ", version=" + version + "]";
}
}
@@ -1,59 +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.authlibinjector;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.JsonUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.IOException;
@Immutable
public final class AuthlibInjectorBuildInfo {
private final int buildNumber;
private final String url;
public AuthlibInjectorBuildInfo() {
this(0, "");
}
public AuthlibInjectorBuildInfo(int buildNumber, String url) {
this.buildNumber = buildNumber;
this.url = url;
}
public int getBuildNumber() {
return buildNumber;
}
public String getUrl() {
return url;
}
public static AuthlibInjectorBuildInfo requestBuildInfo() throws IOException, JsonParseException {
return requestBuildInfo(UPDATE_URL);
}
public static AuthlibInjectorBuildInfo requestBuildInfo(String updateUrl) throws IOException, JsonParseException {
return JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(updateUrl)), AuthlibInjectorBuildInfo.class);
}
public static final String UPDATE_URL = "https://authlib-injector.to2mbn.org/api/buildInfo";
}
@@ -0,0 +1,158 @@
/*
* 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.authlibinjector;
import static org.jackhuang.hmcl.util.Logging.LOG;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.logging.Level;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.JsonUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
public class AuthlibInjectorDownloader {
private static final String LATEST_BUILD_URL = "https://authlib-injector.yushi.moe/artifact/latest.json";
private Path artifactLocation;
private Supplier<DownloadProvider> downloadProvider;
/**
* @param artifactsDirectory where to save authlib-injector artifacts
*/
public AuthlibInjectorDownloader(Path artifactsDirectory, Supplier<DownloadProvider> downloadProvider) {
this.artifactLocation = artifactsDirectory.resolve("authlib-injector.jar");
this.downloadProvider = downloadProvider;
}
public AuthlibInjectorArtifactInfo getArtifactInfo() throws IOException {
synchronized (artifactLocation) {
Optional<AuthlibInjectorArtifactInfo> local = getLocalArtifact();
try {
update(local);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to download authlib-injector", e);
if (!local.isPresent()) {
throw e;
}
LOG.warning("Fallback to use cached artifact: " + local.get());
}
return getLocalArtifact().orElseThrow(() -> new IOException("The updated authlib-inejector cannot be recognized"));
}
}
private void update(Optional<AuthlibInjectorArtifactInfo> local) throws IOException {
AuthlibInjectorVersionInfo latest = getLatestArtifactInfo();
if (local.isPresent() && local.get().getBuildNumber() >= latest.buildNumber) {
return;
}
try {
new FileDownloadTask(new URL(downloadProvider.get().injectURL(latest.downloadUrl)), artifactLocation.toFile(), Proxy.NO_PROXY,
Optional.ofNullable(latest.checksums.get("sha256"))
.map(checksum -> new IntegrityCheck("SHA-256", checksum))
.orElse(null))
.run();
} catch (Exception e) {
throw new IOException("Failed to download authlib-injector", e);
}
LOG.info("Updated authlib-injector to " + latest.version);
}
private AuthlibInjectorVersionInfo getLatestArtifactInfo() throws IOException {
try {
return JsonUtils.fromNonNullJson(
NetworkUtils.doGet(
new URL(downloadProvider.get().injectURL(LATEST_BUILD_URL))),
AuthlibInjectorVersionInfo.class);
} catch (JsonParseException e) {
throw new IOException("Malformed response", e);
}
}
private Optional<AuthlibInjectorArtifactInfo> getLocalArtifact() {
if (!Files.isRegularFile(artifactLocation)) {
return Optional.empty();
}
try {
return Optional.of(readArtifactInfo(artifactLocation));
} catch (IOException e) {
LOG.log(Level.WARNING, "Bad authlib-injector artifact", e);
return Optional.empty();
}
}
private static AuthlibInjectorArtifactInfo readArtifactInfo(Path location) throws IOException {
try (JarFile jarFile = new JarFile(location.toFile())) {
Attributes attributes = jarFile.getManifest().getMainAttributes();
String title = Optional.ofNullable(attributes.getValue("Implementation-Title"))
.orElseThrow(() -> new IOException("Missing Implementation-Title"));
if (!"authlib-injector".equals(title)) {
throw new IOException("Bad Implementation-Title");
}
String version = Optional.ofNullable(attributes.getValue("Implementation-Version"))
.orElseThrow(() -> new IOException("Missing Implementation-Version"));
int buildNumber;
try {
buildNumber = Optional.ofNullable(attributes.getValue("Build-Number"))
.map(Integer::parseInt)
.orElseThrow(() -> new IOException("Missing Build-Number"));
} catch (NumberFormatException e) {
throw new IOException("Bad Build-Number", e);
}
return new AuthlibInjectorArtifactInfo(buildNumber, version, location.toAbsolutePath());
}
}
private class AuthlibInjectorVersionInfo {
@SerializedName("build_number")
public int buildNumber;
@SerializedName("version")
public String version;
@SerializedName("download_url")
public String downloadUrl;
@SerializedName("checksums")
public Map<String, String> checksums;
}
}
@@ -1,3 +1,20 @@
/*
* 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.authlibinjector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider;
@@ -0,0 +1,84 @@
/*
* 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.authlibinjector;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import java.io.IOException;
import java.util.Optional;
import org.jackhuang.hmcl.util.JsonUtils;
import org.jackhuang.hmcl.util.NetworkUtils;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
public class AuthlibInjectorServer {
public static AuthlibInjectorServer fetchServerInfo(String url) throws IOException {
try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
String name = extractServerName(response).orElse(url);
return new AuthlibInjectorServer(url, name);
} catch (JsonSyntaxException e) {
throw new IOException("Malformed response", e);
}
}
private static Optional<String> extractServerName(JsonObject response){
return tryCast(response.get("meta"), JsonObject.class)
.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString));
}
private String url;
private String name;
public AuthlibInjectorServer(String url, String name) {
this.url = url;
this.name = name;
}
public String getUrl() {
return url;
}
public String getName() {
return name;
}
@Override
public String toString() {
return url + " (" + name + ")";
}
@Override
public int hashCode() {
return url.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof AuthlibInjectorServer))
return false;
AuthlibInjectorServer another = (AuthlibInjectorServer) obj;
return this.url.equals(another.url);
}
}
@@ -1,36 +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.authlibinjector;
public class AuthlibInjectorServerInfo {
private final String serverIp;
private final String serverName;
public AuthlibInjectorServerInfo(String serverIp, String serverName) {
this.serverIp = serverIp;
this.serverName = serverName;
}
public String getServerIp() {
return serverIp;
}
public String getServerName() {
return serverName;
}
}
@@ -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;
@@ -69,83 +66,78 @@ public class YggdrasilAccount extends Account {
@Override
public AuthInfo logIn() throws AuthenticationException {
if (!canPlayOnline()) {
logInWithToken();
selectProfile(new SpecificCharacterSelector(character));
if (service.validate(session.getAccessToken(), session.getClientToken())) {
isOnline = true;
} else {
try {
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
} catch (RemoteAuthenticationException e) {
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
throw new CredentialExpiredException(e);
} else {
throw e;
}
}
}
}
return toAuthInfo();
return session.toAuthInfo();
}
@Override
public final AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logInWithPassword(password, new SpecificCharacterSelector(character));
public AuthInfo logInWithPassword(String password) throws AuthenticationException {
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
}
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
session = service.authenticate(username, password, clientToken);
selectProfile(selector);
return toAuthInfo();
updateSession(service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID())), selector);
return session.toAuthInfo();
}
private void selectProfile(CharacterSelector selector) throws AuthenticationException {
if (session.getSelectedProfile() == null) {
if (session.getAvailableProfiles() == null || session.getAvailableProfiles().length <= 0)
/**
* Updates the current session. This method shall be invoked after authenticate/refresh operation.
* {@link #session} field shall be set only using this method. This method ensures {@link #session}
* has a profile selected.
*
* @param acquiredSession the session acquired by making an authenticate/refresh request
*/
private void updateSession(YggdrasilSession acquiredSession, CharacterSelector selector) throws AuthenticationException {
if (acquiredSession.getSelectedProfile() == null) {
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0)
throw new NoCharacterException(this);
session.setSelectedProfile(selector.select(this, Arrays.asList(session.getAvailableProfiles())));
this.session = service.refresh(
acquiredSession.getAccessToken(),
acquiredSession.getClientToken(),
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles())));
} else {
this.session = acquiredSession;
}
character = session.getSelectedProfile().getName();
}
private void logInWithToken() throws AuthenticationException {
if (service.validate(session.getAccessToken(), clientToken)) {
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)));
this.characterUUID = this.session.getSelectedProfile().getId();
}
@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 +149,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,10 +81,13 @@ 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;
} catch (RemoteAuthenticationException e) {
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
return false;
}
throw e;
}
}
@@ -72,7 +98,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 +106,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 +147,19 @@ 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"))
throw new InvalidCredentialsException();
else if (response.getErrorMessage().contains("Invalid token"))
throw new InvalidTokenException();
else if (response.getErrorMessage().contains("Invalid username or password"))
throw new InvalidPasswordException();
throw new RemoteAuthenticationException(response.getError(), response.getErrorMessage(), response.getCause());
if (!StringUtils.isBlank(response.error)) {
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 +171,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";
}
Loaded 100 of 147 files, more files were not shown because too many files have changed in this diff. Show more