Compare commits

...
Author SHA1 Message Date
huanghongxun 83b4b58cb2 feat(crash): 1.18 requires Java 17. Closes #1186. 2021-11-22 01:54:05 +08:00
huanghongxun 11f53c0e03 feat(crash): show link for jdk downloading. Closes #1186. 2021-11-22 01:15:15 +08:00
huanghongxun 2fa8aee055 fix(multiplayer): incorrect cato checksum. 2021-11-20 18:42:25 +08:00
huanghongxun d2848e12f3 fix: not reloading VersionPage when renaming the game version. Closes #1176. 2021-11-20 18:24:00 +08:00
huanghongxun 36ba840fe8 fix: should close version settings page when deleting related version. Closes #1175. 2021-11-20 18:19:39 +08:00
asdqp233 819d41a75c Update I18N.properties 2021-11-20 01:57:39 +08:00
huanghongxun 3a6c3dc51d fix(multiplayer): update cato. 2021-11-11 22:00:46 +08:00
huanghongxun db721107ae fix(microsoft): remove redundant login steps. 2021-11-09 00:35:52 +08:00
huanghongxun 5b9be471e3 fix(microsoft): should not pass client secret when refreshing token. Closes #1164. 2021-11-06 22:32:08 +08:00
huanghongxun e8f8617412 fix: crash when all profiles removed. Close #1165. 2021-11-06 22:32:07 +08:00
Glavo eb472037db fix: checkstyle 2021-11-03 17:30:27 +08:00
Glavo 81b7afd08d Update issue template 2021-11-02 22:46:41 +08:00
Glavo 7a71ecf177 Fix #1153: On Windows, the 'classpath' environment variable in the context overrides the 'CLASSPATH' 2021-11-02 22:46:13 +08:00
huanghongxun a12e485a27 chore(multiplayer): adjust some translations. 2021-10-31 18:42:27 +08:00
huanghongxun af521b2893 chore: localize SSLHandshakeException. 2021-10-31 18:42:27 +08:00
Glavo aab6f68492 Fix #1147: shadowJar minimize exclude JFoenix 2021-10-31 17:04:11 +08:00
huanghongxun f67c3d5284 fix(launch): cannot launch game in path that contains CJK characters. Closes #1141. 2021-10-30 23:03:49 +08:00
Glavo 46385a8445 Do not delete soft links when exiting HMCL 2021-10-30 23:01:27 +08:00
Glavo 9f617c33f5 Support path containing unicode characters on Linux 2021-10-30 23:01:27 +08:00
Glavo 6aef034880 Avoid generating command lines repeatedly 2021-10-30 23:01:27 +08:00
huanghongxun c4941a01d6 fix(multiplayer): show "failed to test" instead of "testing". 2021-10-30 22:40:44 +08:00
huanghongxun 7380423eee fix(multiplayer): does not show correct kick message. 2021-10-30 22:13:32 +08:00
huanghongxun f35073ffb5 fix(ui): alignment. 2021-10-30 21:05:40 +08:00
huanghongxun f89ea56d86 fix(multiplayer): session name null. 2021-10-30 21:04:53 +08:00
huanghongxun a240b8ca9f fix(skin): Do not load authlib-injector when skin manually set to steve/alex. 2021-10-29 22:52:16 +08:00
huanghongxun 230ec2bc8e feat(update): show update dialog when found update. 2021-10-29 02:51:08 +08:00
huanghongxun a3c2642b93 feat(download): customize name of the mod to be downloaded. 2021-10-29 01:21:48 +08:00
huanghongxun de755d5495 feat(game): more version icon choices. 2021-10-29 01:21:48 +08:00
huanghongxun 08054f3026 feat(countly): initial 2021-10-27 00:21:42 +08:00
Glavo 026239903d Fix IllegalAccessError: tried to access protected method 2021-10-25 17:23:07 +08:00
Glavo a320fe4cc4 Use shadowJar minimize 2021-10-24 17:26:26 +08:00
vanja-san b122d4c5e4 Update I18N_ru.properties
Full text translation
2021-10-24 17:25:40 +08:00
43 changed files with 1722 additions and 608 deletions

No files matched your search

-29
View File
@@ -1,29 +0,0 @@
# 公告
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [Discord频道](https://discord.gg/jVvC7HfM6U) [开黑啦频道](https://kaihei.co/Kx7n3t) 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://afdian.net/@huanghongxun) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
# 问题提交
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
* 启动器版本:
* 操作系统:
* Java 版本:
* 错误截图(最好请将整个电脑屏幕的截图发上来):
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
* 问题描述(如何触发问题):
+68
View File
@@ -0,0 +1,68 @@
name: Bug 反馈
description: 反馈一个 Bug。为了提高反馈效率,请优先在开黑啦或者或 Discord 频道中反馈。
body:
- type: markdown
attributes:
value: |
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [开黑啦频道](https://kaihei.co/Kx7n3t)
或 [Discord 频道](https://discord.gg/jVvC7HfM6U)中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://afdian.net/@huanghongxun) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
- type: input
id: hmcl-version
attributes:
label: 启动器版本
description: 请输入您所使用的的 HMCL 启动器版本。
placeholder: 例如:3.3.188
validations:
required: true
- type: input
id: platform
attributes:
label: 平台
description: 请输入您遇到 BUG 的平台。
placeholder: 例如:Windows 11
validations:
required: true
- type: input
id: java-version
attributes:
label: Java
description: 请输入您所使用的 Java 的名称与版本。
placeholder: 例如:LibericaJDK Full JDK 17.0.1
- type: input
id: game-version
attributes:
label: 游戏版本
description: 如果您是在启动通过 HMCL 启动器自动下载的游戏时遇到了问题,请输入您所启动的游戏版本。
placeholder: 例如:1.17.1
- type: input
id: game-modify
attributes:
label: 对游戏所做的修改
description: 如果您为游戏安装了 OptiFine、Forge,或者是其他的 mod,请将您所做的修改填入下方。
placeholder: 例如:OptiFine HD_U_G9, Rift
- type: textarea
id: game-crash-report
attributes:
label: 游戏崩溃报告
description: 如果有游戏崩溃报告的话,请填入(或将文件拖入)下方。
- type: textarea
id: hmcl-crash-report
attributes:
label: 启动器崩溃报告
description: 如果有启动器崩溃报告的话,请填入(或将文件拖入)下方。
- type: textarea
id: hmcl-logs
attributes:
label: 启动器日志文件
description: 遇到问题后,请在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。
- type: textarea
id: bug-report
attributes:
label: 问题描述
description: 请尽可能地详细描述问题,特别是您是怎么遇到这个 BUG 的。
+14
View File
@@ -0,0 +1,14 @@
blank_issues_enabled: true
contact_links:
- name: 开黑啦频道
url: https://kaihei.co/Kx7n3t
about: Hello Minecraft! Launcher 的官方开黑啦频道。
- name: Discord 频道
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 频道。
- name: 赞助通道
url: https://afdian.net/@huanghongxun
about: 前往爱发电赞助 HMCL。赞助后可以申请加入官方 QQ 群反馈问题。
- name: 其他反馈
url: https://github.com/huanghongxun/HMCL/issues/new
about: 通过 Issue 反馈其他问题。
+15 -92
View File
@@ -6,8 +6,6 @@ buildscript {
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
classpath 'com.guardsquare:proguard-gradle:7.1.0' // The ProGuard Gradle plugin.
}
}
@@ -47,7 +45,7 @@ mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation rootProject.files("lib/JFoenix.jar")
implementation 'libs:JFoenix'
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
}
@@ -118,8 +116,21 @@ compileJava11Java {
targetCompatibility = 11
}
jar {
enabled = false
dependsOn shadowJar
}
shadowJar {
classifier = "shadow"
classifier = null
minimize {
exclude(dependency('com.google.code.gson:.*:.*'))
exclude(dependency('com.github.steveice10:.*:.*'))
exclude(dependency('libs:JFoenix:.*'))
exclude(project(":HMCLTransformerDiscoveryService"))
}
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
@@ -152,89 +163,6 @@ shadowJar {
'javafx.base/com.sun.javafx.event'
].join(" ")
}
}
task proguard(type: proguard.gradle.ProGuardTask) {
dependsOn shadowJar
injars shadowJar
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
dontobfuscate
dontoptimize
keep 'class org.jackhuang.** { *; }'
keep 'class com.jfoenix.** { *; }'
keep 'class com.google.gson.** { *; }'
keep 'class com.github.steveice10.opennbt.** { *; }' // TagRegistry use reflection to create object instance
keep 'class * extends com.google.gson.TypeAdapter'
keep 'class * implements com.google.gson.TypeAdapterFactory'
keep 'class * implements com.google.gson.JsonSerializer'
keep 'class * implements com.google.gson.JsonDeserializer'
printusage "${buildDir}/libs/proguard-shrinking-details.txt"
System.out.println(new File(".").absolutePath)
dontwarn 'com.nqzero.**'
dontwarn 'org.slf4j.**'
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
dontwarn 'com.sun.javafx.**'
dontwarn 'com.jfoenix.**'
dontwarn 'org.apache.**'
adaptclassstrings
// next block taken verbatim from Proguard's documentation examples:
libraryjars files(configurations.compileClasspath.collect())
keepattributes 'SourceFile,LineNumberTable'
keepattributes '*Annotation*'
keepattributes 'Signature'
var javaHome = System.getProperty('java.home')
// Automatically handle the Java version of this build.
if (System.getProperty('java.version').startsWith('1.')) {
// Before Java 9, the runtime classes were packaged in a single jar file.
libraryjars "${javaHome}/lib/rt.jar"
libraryjars "${javaHome}/lib/ext/jfxrt.jar"
} else {
// As of Java 9, the runtime classes are packaged in modular jmod files.
libraryjars "${javaHome}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.desktop.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.logging.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.sql.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/java.xml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/jdk.unsupported.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
if (new File("${javaHome}/jmods/javafx.base.jmod").exists()) {
libraryjars "${javaHome}/jmods/javafx.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.controls.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.graphics.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.media.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.fxml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
libraryjars "${javaHome}/jmods/javafx.web.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
}
}
}
task finalJar(type: Jar) {
dependsOn proguard
manifest = shadowJar.manifest
from { proguard.outJarFiles.collect { zipTree(it)} }
into('/') {
from { shadowJar.outputs.files.collect { zipTree(it) } }
include("META-INF/versions/**")
}
doLast {
repack(jar.archivePath) // see repack()
@@ -243,11 +171,6 @@ task finalJar(type: Jar) {
}
}
jar {
enabled = false
dependsOn finalJar
}
def createExecutable(String suffix, String header) {
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
output.bytes = new File(project.projectDir, header).bytes
@@ -0,0 +1,87 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Countly {
private String deviceId;
private String endpoint;
private String serverURL;
public void sendMetric(String metrics) throws IOException {
HttpRequest.GET(serverURL + endpoint,
pair("begin_session", "1"),
pair("session_id", "1"),
pair("metrics", metrics),
pair("device_id", deviceId),
pair("timestamp", Long.toString(System.currentTimeMillis())),
pair("tz", Integer.toString(TimeZone.getDefault().getOffset(new Date().getTime()) / 60000)),
pair("hour", Integer.toString(currentHour())),
pair("dow", Integer.toString(currentDayOfWeek())),
pair("app_key", APP_KEY),
pair("sdk_name", "java-native"),
pair("sdk_version", "20.11.1"))
.getString();
}
private static int getTimezoneOffset() {
return TimeZone.getDefault().getOffset(new Date().getTime()) / 60000;
}
private static String getLocale() {
final Locale locale = Locale.getDefault();
return locale.getLanguage() + "_" + locale.getCountry();
}
private static int currentHour() {
return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
}
private int currentDayOfWeek() {
int day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
switch (day) {
case Calendar.SUNDAY:
return 0;
case Calendar.MONDAY:
return 1;
case Calendar.TUESDAY:
return 2;
case Calendar.WEDNESDAY:
return 3;
case Calendar.THURSDAY:
return 4;
case Calendar.FRIDAY:
return 5;
case Calendar.SATURDAY:
return 6;
}
return 0;
}
private static final String APP_KEY = "";
}
@@ -0,0 +1,103 @@
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public class CrashReport {
private final Thread thread;
private final Throwable throwable;
private final String stackTrace;
private boolean nonFatal;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
nonFatal = false;
}
public CrashReport setNonFatal() {
nonFatal = true;
return this;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
return true;
}
public Map<String, Object> getMetrics(long runningTime) {
return mapOf(
pair("_run", runningTime),
pair("_app_version", Metadata.VERSION),
pair("_os", OperatingSystem.SYSTEM_NAME),
pair("_os_version", OperatingSystem.SYSTEM_VERSION),
pair("_disk_current", getDiskAvailable()),
pair("_disk_total", getDiskTotal()),
pair("_ram_current", getMemoryAvailable()),
pair("_ram_total", Runtime.getRuntime().maxMemory() / BYTES_IN_MB),
pair("_error", stackTrace),
pair("_logs", Logging.getLogs()),
pair("_name", throwable.getLocalizedMessage()),
pair("_nonfatal", nonFatal)
);
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
" Thread: " + thread + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
" 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" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
private static final Long BYTES_IN_MB = 1024L * 1024;
private static long getMemoryAvailable() {
Long total = Runtime.getRuntime().totalMemory();
Long availMem = Runtime.getRuntime().freeMemory();
return (total - availMem) / BYTES_IN_MB;
}
private static long getDiskAvailable() {
long total = 0, free = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
free += f.getUsableSpace();
}
return (total - free) / BYTES_IN_MB;
}
private static long getDiskTotal() {
long total = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
}
return total / BYTES_IN_MB;
}
}
@@ -24,12 +24,15 @@ import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
@@ -38,6 +41,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
@@ -58,6 +62,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
this.profile = profile;
@@ -231,6 +237,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
public VersionSetting getLocalVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
loadLocalVersionSetting(id);
@@ -240,6 +247,15 @@ public class HMCLGameRepository extends DefaultGameRepository {
return setting;
}
@Nullable
public VersionSetting getLocalVersionSettingOrCreate(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null) {
vs = createLocalVersionSetting(id);
}
return vs;
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
@@ -258,14 +274,21 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (id == null || !isLoaded())
return newImage("/assets/img/grass.png");
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
VersionSetting vs = getLocalVersionSettingOrCreate(id);
VersionIconType iconType = Optional.ofNullable(vs).map(VersionSetting::getVersionIcon).orElse(VersionIconType.DEFAULT);
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
} else {
return newImage(iconType.getResourceUrl());
}
}
public boolean saveVersionSetting(String id) {
@@ -425,6 +425,11 @@ public final class LauncherHelper {
FXUtils.openLink("https://adoptium.net/?variant=openjdk17");
}, breakAction);
return null;
case VANILLA_JAVA_17:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"), () -> {
FXUtils.openLink("https://adoptium.net/?variant=openjdk17");
}, breakAction);
return null;
case VANILLA_JAVA_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
@@ -169,6 +169,10 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
public static class GrantDeviceCodeEvent extends Event {
@@ -163,6 +163,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -579,4 +582,16 @@ public final class Config implements Cloneable, Observable {
public void setTitleTransparent(boolean titleTransparent) {
this.titleTransparent.set(titleTransparent);
}
public String getPromptedVersion() {
return promptedVersion.get();
}
public StringProperty promptedVersionProperty() {
return promptedVersion;
}
public void setPromptedVersion(String promptedVersion) {
this.promptedVersion.set(promptedVersion);
}
}
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URL;
@@ -156,6 +157,8 @@ public final class DownloadProviders {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
@@ -0,0 +1,43 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
public enum VersionIconType {
DEFAULT("/assets/img/grass.png"),
GRASS("/assets/img/grass.png"),
CHEST("/assets/img/chest.png"),
CHICKEN("/assets/img/chicken.png"),
COMMAND("/assets/img/command.png"),
CRAFT_TABLE("/assets/img/craft_table.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
FURNACE("/assets/img/furnace.png");
// Please append new items at last
private final String resourceUrl;
VersionIconType(String resourceUrl) {
this.resourceUrl = resourceUrl;
}
public String getResourceUrl() {
return resourceUrl;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -568,6 +568,20 @@ public final class VersionSetting implements Cloneable {
this.useNativeOpenAL.set(useNativeOpenAL);
}
private final ObjectProperty<VersionIconType> versionIcon = new SimpleObjectProperty<>(this, "versionIcon", VersionIconType.DEFAULT);
public VersionIconType getVersionIcon() {
return versionIcon.get();
}
public ObjectProperty<VersionIconType> versionIconProperty() {
return versionIcon;
}
public void setVersionIcon(VersionIconType versionIcon) {
this.versionIcon.set(versionIcon);
}
// launcher settings
/**
@@ -670,6 +684,7 @@ public final class VersionSetting implements Cloneable {
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
versionIcon.addListener(listener);
}
@Override
@@ -703,6 +718,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
versionSetting.setVersionIcon(getVersionIcon());
return versionSetting;
}
@@ -741,6 +757,7 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
return obj;
}
@@ -784,6 +801,7 @@ public final class VersionSetting implements Cloneable {
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setVersionIcon(VersionIconType.values()[Optional.ofNullable(obj.get("versionIcon")).map(JsonElement::getAsInt).orElse(0)]);
return vs;
}
@@ -24,12 +24,15 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
@@ -71,7 +74,7 @@ public class GameCrashWindow extends Stage {
private final LibraryAnalyzer analyzer;
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
private final StringProperty reason = new SimpleStringProperty(i18n("game.crash.reason.unknown"));
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final Label feedbackLabel = new Label(i18n("game.crash.feedback"));
@@ -133,52 +136,53 @@ public class GameCrashWindow extends Stage {
if (exception != null) {
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
reason.set(i18n("game.crash.reason.unknown"));
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.unknown")));
} else {
List<CrashReportAnalyzer.Result> results = pair.getKey();
Set<String> keywords = pair.getValue();
StringBuilder reasonText = new StringBuilder();
List<Node> segments = new ArrayList<>();
for (CrashReportAnalyzer.Result result : results) {
switch (result.getRule()) {
case TOO_OLD_JAVA:
reasonText.append(i18n("game.crash.reason.too_old_java",
CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected")))))
.append("\n");
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.too_old_java",
CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected")))), Controllers::onHyperlinkAction));
break;
case MOD_RESOLUTION_CONFLICT:
case MOD_RESOLUTION_MISSING:
case MOD_RESOLUTION_COLLECTION:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("sourcemod")),
parseFabricModId(result.getMatcher().group("destmod")),
parseFabricModId(result.getMatcher().group("destmod"))));
parseFabricModId(result.getMatcher().group("destmod"))), Controllers::onHyperlinkAction));
break;
case MOD_RESOLUTION_MISSING_MINECRAFT:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("mod")),
result.getMatcher().group("version")));
result.getMatcher().group("version")), Controllers::onHyperlinkAction));
break;
case TWILIGHT_FOREST_OPTIFINE:
reasonText.append(i18n("game.crash.reason.mod", "OptiFine"));
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.mod", "OptiFine"), Controllers::onHyperlinkAction));
break;
default:
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
.toArray()))
.append("\n");
.toArray()), Controllers::onHyperlinkAction));
break;
}
segments.add(new Text("\n"));
}
if (results.isEmpty()) {
if (!keywords.isEmpty()) {
reason.set(i18n("game.crash.reason.stacktrace", String.join(", ", keywords)));
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.stacktrace", String.join(", ", keywords))));
} else {
reason.set(i18n("game.crash.reason.unknown"));
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.unknown")));
}
feedbackLabel.setVisible(true);
} else {
reason.set(reasonText.toString());
feedbackLabel.setVisible(false);
reasonTextFlow.getChildren().setAll(segments);
}
}
}, Schedulers.javafx()).exceptionally(Lang::handleUncaughtException);
@@ -333,14 +337,12 @@ public class GameCrashWindow extends Stage {
javaDir.setTitle(i18n("settings.game.java_directory"));
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
TwoLineListItem reason = new TwoLineListItem();
reason.getStyleClass().setAll("two-line-item-second-large", "wrap-text");
reason.setTitle(i18n("game.crash.reason"));
reason.subtitleProperty().bind(GameCrashWindow.this.reason);
Label reasonTitle = new Label(i18n("game.crash.reason"));
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
gameDirPane.getChildren().setAll(gameDir, javaDir, reason);
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonTextFlow));
}
HBox toolBar = new HBox();
@@ -52,6 +52,7 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
@@ -166,30 +167,40 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
if (version == null) version = profile.getSelectedVersion();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
Path dest = runDirectory.resolve(subdirectoryName).resolve(file.getFile().getFilename());
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
Controllers.prompt(i18n("archive.name"), (result, resolve, reject) -> {
if (!OperatingSystem.isNameValid(result)) {
reject.accept(i18n("install.new_game.malformed"));
return;
}
}).executor(false);
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
}
} else {
Controllers.showToast(i18n("install.success"));
}
}).executor(false);
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
resolve.run();
}, file.getFile().getFilename());
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
}
private void loadVersions(Profile profile) {
@@ -42,9 +42,11 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AnnouncementCard;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -53,11 +55,14 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -69,7 +74,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame");
private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate");
private final StringProperty latestVersion = new SimpleStringProperty(this, "latestVersion");
private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion");
private final ObservableList<Version> versions = FXCollections.observableArrayList();
private final ObservableList<Node> versionNodes;
private Profile profile;
@@ -103,7 +108,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.setLimitHeight(updatePane, 55);
StackPane.setAlignment(updatePane, Pos.TOP_RIGHT);
updatePane.setOnMouseClicked(e -> onUpgrade());
FXUtils.onChange(showUpdateProperty(), this::doAnimation);
FXUtils.onChange(showUpdateProperty(), this::showUpdate);
{
HBox hBox = new HBox();
@@ -118,7 +123,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
TwoLineListItem prompt = new TwoLineListItem();
prompt.setSubtitle(i18n("update.bubble.subtitle"));
prompt.setPickOnBounds(false);
prompt.titleProperty().bind(latestVersionProperty());
prompt.titleProperty().bind(BindingMapping.of(latestVersionProperty()).map(latestVersion ->
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.getVersion())));
hBox.getChildren().setAll(lblIcon, prompt);
}
@@ -224,6 +230,17 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
}
private void showUpdate(boolean show) {
doAnimation(show);
if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
Controllers.dialog("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO, () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
});
}
}
private void doAnimation(boolean show) {
Duration duration = Duration.millis(320);
Timeline nowAnimation = new Timeline();
@@ -289,15 +306,15 @@ public final class MainPage extends StackPane implements DecoratorPage {
this.showUpdate.set(showUpdate);
}
public String getLatestVersion() {
public RemoteVersion getLatestVersion() {
return latestVersion.get();
}
public StringProperty latestVersionProperty() {
public ObjectProperty<RemoteVersion> latestVersionProperty() {
return latestVersion;
}
public void setLatestVersion(String latestVersion) {
public void setLatestVersion(RemoteVersion latestVersion) {
this.latestVersion.set(latestVersion);
}
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
@@ -92,8 +91,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), mainPage::setCurrentGame);
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
mainPage.latestVersionProperty().bind(BindingMapping.of(UpdateChecker.latestVersionProperty())
.map(version -> version == null ? "" : i18n("update.bubble.title", version.getVersion())));
mainPage.latestVersionProperty().bind(UpdateChecker.latestVersionProperty());
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
@@ -87,6 +87,7 @@ public class MultiplayerClient extends Thread {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
MultiplayerServer.Endpoint endpoint = new MultiplayerServer.Endpoint(socket, writer);
socket.setSoTimeout(30000);
LOG.info("Connected to 127.0.0.1:" + port);
endpoint.write(new HandshakeRequest());
@@ -107,7 +108,7 @@ public class MultiplayerClient extends Thread {
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to close socket", e);
}
}, 15 * 1000);
}, 25 * 1000);
String line;
while ((line = reader.readLine()) != null) {
@@ -44,7 +44,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.logging.Level;
@@ -60,40 +59,39 @@ import static org.jackhuang.hmcl.util.Pair.pair;
* Cato Management.
*/
public final class MultiplayerManager {
static final String CATO_VERSION = "1.1.1-202110231414";
static final String CATO_VERSION = "1.1.1-3f36a56";
// private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/cato/cato/" + MultiplayerManager.CATO_VERSION;
private static final String CATO_DOWNLOAD_URL = "https://codechina.csdn.net/to/ioi_bin/-/raw/acb0524bcad82a31fa5a09bf4c79248ebd674de1/client/";
private static final String CATO_DOWNLOAD_URL = "https://codechina.csdn.net/huanghongxun1/ioi_bin/-/raw/3f36a567d9381e5fa404b96bcecd6d6a555f235c/client/";
private static final String CATO_PATH = getCatoPath();
public static final int CATO_AGREEMENT_VERSION = 2;
private static final String REMOTE_ADDRESS = "127.0.0.1";
private static final String LOCAL_ADDRESS = "0.0.0.0";
private static final Map<String, String> HASH = mapOf(
pair("cato-client-darwin-amd64", "6b7630f843d4b8f8e11feff4c1cb3dc1916d1fdb"),
pair("cato-client-darwin-arm64", "5f00153f0117eebb4209a4484152ec4077d34b34"),
pair("cato-client-freebsd-amd64", "ac401f51e69058696e342ddaa62968d21c8252e8"),
pair("cato-client-freebsd-arm7", "168dfdabc773ce87f0f1d901bee76f7ea3beacb3"),
pair("cato-client-freebsd-arm64", "08daef0f3acc279411ff39e8a22510a9876d06cb"),
pair("cato-client-freebsd-i386", "febdf99be30c671708dd80f8d3b48335a0d5920a"),
pair("cato-client-js.wasm", "0439d2a1cd8ee854ef6057a8ae6898db47aba2bd"),
pair("cato-client-linux-amd64", "816aecb116e2bc0727de9362e893bd9cefdf0485"),
pair("cato-client-linux-arm7", "710a047fdf528917d8a45ed0161ca155e2e05ff1"),
pair("cato-client-linux-arm64", "7a9a22d39f0ba200e1e4b2f6fca8cbe218a3eedf"),
pair("cato-client-linux-i386", "3eb0b37bdff8b9c8dc1e425b91ddb4d0a72a0da0"),
pair("cato-client-linux-mips", "8d2b383fcd4edb7903a14a93947ed5cb54838e60"),
pair("cato-client-linux-mips64", "248f1a3fe69ec97f6c095b63fa597ee099b4cb8c"),
pair("cato-client-linux-mips64le", "8d2f8d93ca582ab5f43dc4570a0d5b18dbe06df3"),
pair("cato-client-linux-mipsle", "f38bd14e6cdd6bfcd045ccd5a0a183bc8083c028"),
pair("cato-client-linux-ppc64", "db420847ed8e60a58a69b2e1cb55cf21b7b56e4b"),
pair("cato-client-linux-ppc64le", "717149f52a0808ee09cd5a7e89f8b9a6ed604cb0"),
pair("cato-client-openbsd-amd64", "248aaf3ca3bcaa18d4d1391325d171ef17e65244"),
pair("cato-client-openbsd-arm7", "1ab97264dba5d2b61388ee68e49f890c29c4e09f"),
pair("cato-client-openbsd-arm64", "5f28541ace9d298b816eb43963be8e4cd87bd0fd"),
pair("cato-client-openbsd-i386", "613b5c3fc0382815843a6e9b2a5281b05967bc1c"),
pair("cato-client-windows-amd64.exe", "64f19648d281882eefc5afaa3af347907af5cb23"),
pair("cato-client-windows-arm64.exe", "48a4bc5e18c35c93c9a3ecc500420d3abb5ae6f0"),
pair("cato-client-windows-i386.exe", "fcb4f245da2e293badb73fac04cf4e83fbd79b18")
pair("cato-client-darwin-amd64", "e733fa4b72be24b99026249c31c4ae8b52ad3906"),
pair("cato-client-darwin-arm64", "19ab41b7f78263cad81009ae1a391b0d0367d95f"),
pair("cato-client-freebsd-amd64", "6de8439ab9267e444d45bb9f02394023ce0bfe5e"),
pair("cato-client-freebsd-arm64", "5cd8930e7cd17d72386b085950e63467e4ce72ef"),
pair("cato-client-freebsd-arm7", "d0e54a4a1b0f8c2f769d74a6a600eec4a9c4e0da"),
pair("cato-client-freebsd-i386", "d9d22faf85e01e77b18f28f1609e87623ac9b1a4"),
pair("cato-client-js.wasm", "acb366a4d6a3c6cd464ef5919ce7d2d58a52964e"),
pair("cato-client-linux-amd64", "04e94a3adff1e4d865dead2c6c5da584b74bff0a"),
pair("cato-client-linux-arm64", "ba662bc461995dae5e78d21b9fea35c2db81b652"),
pair("cato-client-linux-arm7", "28021eff56e1e273c051853808e70e7ab3af80d9"),
pair("cato-client-linux-i386", "4f524a9cfbcea560bdf07d3468b883f946c431ed"),
pair("cato-client-linux-mips", "91c3108b046511c2aafbe88f82b38a06c03d88e1"),
pair("cato-client-linux-mips64", "04d1b66494dbdb0443eaae85bda6b61408e300ed"),
pair("cato-client-linux-mips64le", "d838ac71052f7873b28f4b4a2f23470b918ef85d"),
pair("cato-client-linux-mipsle", "54918ed9cdbe48f7adf11680947281f5fc7c5a7d"),
pair("cato-client-linux-ppc64", "f2679259516b0db0422f85afcce35bfa6f7b3192"),
pair("cato-client-linux-ppc64le", "463d9b86316dfc83e668ddfae96e51181a9e3ffa"),
pair("cato-client-openbsd-amd64", "b776155e955340485faf62c3b2aad3a788c57e2b"),
pair("cato-client-openbsd-arm64", "fd6ec21248fd9c8be3d9d2876faf65b95c63b3a9"),
pair("cato-client-openbsd-arm7", "19eabe71530bb4eac7a32f6b8aeef72f6f9382e9"),
pair("cato-client-openbsd-i386", "3e755798628a379997dbc43dfd71765d62263ebe"),
pair("cato-client-windows-amd64.exe", "21a862f455c909c9fb786cd308e969629eb3c8d9"),
pair("cato-client-windows-arm64.exe", "7f2fe5d054ebc3265edb046badafdd6ac94bf7eb"),
pair("cato-client-windows-i386.exe", "4e03e640c6cabfd7e1ddf60bba1726353bc3afee")
);
private MultiplayerManager() {
@@ -198,7 +196,7 @@ public final class MultiplayerManager {
task.cancel();
});
client.onKicked().register(kickedEvent -> {
future.completeExceptionally(new CancellationException());
future.completeExceptionally(new KickedException(kickedEvent.getReason()));
session.stop();
task.cancel();
});
@@ -229,6 +227,7 @@ public final class MultiplayerManager {
MultiplayerServer server = new MultiplayerServer(sessionName, gamePort, allowAllJoinRequests);
server.startServer();
session.setName(sessionName);
session.allowForwardingAddress(REMOTE_ADDRESS, server.getPort());
session.allowForwardingAddress(REMOTE_ADDRESS, gamePort);
session.showAllowedAddress();
@@ -264,7 +263,7 @@ public final class MultiplayerManager {
}));
}
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>(idx|mix)(.*?))#(?<port>\\d{2,5})$");
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>.*?)#(?<port>\\d{2,5})$");
public static Invitation parseInvitationCode(String invitationCode) throws JsonParseException {
Matcher matcher = INVITATION_CODE_PATTERN.matcher(invitationCode);
@@ -595,6 +594,18 @@ public final class MultiplayerManager {
public static class ConnectionErrorException extends RuntimeException {
}
public static class KickedException extends RuntimeException {
private final String reason;
public KickedException(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
}
public static class CatoNotExistsException extends RuntimeException {
private final Path file;
@@ -38,7 +38,10 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -58,7 +61,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
private final ObjectProperty<MultiplayerManager.State> multiplayerState = new SimpleObjectProperty<>(MultiplayerManager.State.DISCONNECTED);
private final ReadOnlyStringWrapper token = new ReadOnlyStringWrapper();
private final ReadOnlyObjectWrapper<DiscoveryInfo> natState = new ReadOnlyObjectWrapper<>();
private final ReadOnlyObjectWrapper<@Nullable Result<DiscoveryInfo>> natState = new ReadOnlyObjectWrapper<>();
private final ReadOnlyIntegerWrapper gamePort = new ReadOnlyIntegerWrapper(-1);
private final ReadOnlyObjectWrapper<MultiplayerManager.CatoSession> session = new ReadOnlyObjectWrapper<>();
private final ObservableList<MultiplayerChannel.CatoClient> clients = FXCollections.observableArrayList();
@@ -97,11 +100,11 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
this.multiplayerState.set(multiplayerState);
}
public DiscoveryInfo getNatState() {
public Result<DiscoveryInfo> getNatState() {
return natState.get();
}
public ReadOnlyObjectProperty<DiscoveryInfo> natStateProperty() {
public ReadOnlyObjectProperty<Result<DiscoveryInfo>> natStateProperty() {
return natState.getReadOnlyProperty();
}
@@ -134,12 +137,12 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
DiscoveryTest tester = new DiscoveryTest(null, 0, "stun.stunprotocol.org", 3478);
return tester.test();
}).whenComplete(Schedulers.javafx(), (info, exception) -> {
LOG.log(Level.INFO, "Nat test result " + MultiplayerPageSkin.getNATType(info), exception);
if (exception == null) {
natState.set(info);
natState.set(Result.ok(info));
} else {
natState.set(null);
natState.set(Result.error());
}
LOG.log(Level.INFO, "Nat test result " + MultiplayerPageSkin.getNATType(natState.get()), exception);
}).start();
}
@@ -345,7 +348,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
Throwable e = resolveException(t);
if (e instanceof CancellationException) {
LOG.info("Connection rejected by the server");
return i18n("multiplayer.session.join.rejected");
return i18n("message.cancelled");
} else if (e instanceof MultiplayerManager.KickedException) {
LOG.info("Kicked by server");
return i18n("multiplayer.session.join.kicked", localizeKickMessage(((MultiplayerManager.KickedException) e).getReason()));
} else if (e instanceof MultiplayerManager.CatoAlreadyStartedException) {
LOG.info("Cato already started");
return i18n("multiplayer.session.error.already_started");
@@ -372,6 +378,8 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
} else {
return i18n("multiplayer.exit.after_ready");
}
} else if (e instanceof ChecksumMismatchException) {
return i18n("exception.artifact_malformed");
} else {
return fallback.apply(e);
}
@@ -503,4 +511,5 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
}
@@ -41,8 +41,10 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
@@ -295,22 +297,24 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
}
}
public static String getNATType(DiscoveryInfo info) {
public static String getNATType(@Nullable Result<DiscoveryInfo> info) {
if (info == null) {
return i18n("multiplayer.nat.testing");
} else if (info.isBlockedUDP()) {
} else if (info.isError()) {
return i18n("multiplayer.nat.failed");
} else if (info.get().isBlockedUDP()) {
return i18n("multiplayer.nat.type.blocked_udp");
} else if (info.isFullCone()) {
} else if (info.get().isFullCone()) {
return i18n("multiplayer.nat.type.full_cone");
} else if (info.isOpenAccess()) {
} else if (info.get().isOpenAccess()) {
return i18n("multiplayer.nat.type.open_access");
} else if (info.isPortRestrictedCone()) {
} else if (info.get().isPortRestrictedCone()) {
return i18n("multiplayer.nat.type.port_restricted_cone");
} else if (info.isRestrictedCone()) {
} else if (info.get().isRestrictedCone()) {
return i18n("multiplayer.nat.type.restricted_cone");
} else if (info.isSymmetric()) {
} else if (info.get().isSymmetric()) {
return i18n("multiplayer.nat.type.symmetric");
} else if (info.isSymmetricUDPFirewall()) {
} else if (info.get().isSymmetricUDPFirewall()) {
return i18n("multiplayer.nat.type.symmetric_udp_firewall");
} else {
return i18n("multiplayer.nat.type.unknown");
@@ -20,17 +20,25 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private final WeakListenerHolder holder = new WeakListenerHolder();
private Profile profile;
private Consumer<Event> onVersionIconChangedListener;
public GameAdvancedListItem() {
tooltip = new Tooltip();
@@ -39,23 +47,33 @@ public class GameAdvancedListItem extends AdvancedListItem {
setLeftGraphic(view.getKey());
imageView = view.getValue();
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
FXUtils.installFastTooltip(this, tooltip);
setTitle(version);
setSubtitle(null);
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
tooltip.setText(version);
} else {
Tooltip.uninstall(this,tooltip);
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
imageView.setImage(newImage("/assets/img/grass.png"));
tooltip.setText("");
}
});
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(), this::loadVersion));
setActionButtonVisible(false);
}
private void loadVersion(String version) {
if (Profiles.getSelectedProfile() != profile) {
profile = Profiles.getSelectedProfile();
if (profile != null) {
onVersionIconChangedListener = profile.getRepository().onVersionIconChanged.registerWeak(event -> {
this.loadVersion(Profiles.getSelectedVersion());
});
}
}
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
FXUtils.installFastTooltip(this, tooltip);
setTitle(version);
setSubtitle(null);
imageView.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
tooltip.setText(version);
} else {
Tooltip.uninstall(this,tooltip);
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
imageView.setImage(newImage("/assets/img/grass.png"));
tooltip.setText("");
}
}
}
@@ -0,0 +1,126 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionIconDialog extends DialogPane {
private final Profile profile;
private final String versionId;
private final Runnable onFinish;
private final VersionSetting vs;
public VersionIconDialog(Profile profile, String versionId, Runnable onFinish) {
this.profile = profile;
this.versionId = versionId;
this.onFinish = onFinish;
this.vs = profile.getRepository().getLocalVersionSettingOrCreate(versionId);
setTitle(i18n("settings.icon"));
FlowPane pane = new FlowPane();
setBody(pane);
pane.getChildren().setAll(
createCustomIcon(),
createIcon(VersionIconType.GRASS),
createIcon(VersionIconType.CHEST),
createIcon(VersionIconType.CHICKEN),
createIcon(VersionIconType.COMMAND),
createIcon(VersionIconType.CRAFT_TABLE),
createIcon(VersionIconType.FABRIC),
createIcon(VersionIconType.FORGE),
createIcon(VersionIconType.FURNACE)
);
}
private void exploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIconFile(versionId);
try {
FileUtils.copyFile(selectedFile, iconFile);
if (vs != null) {
vs.setVersionIcon(VersionIconType.DEFAULT);
}
onAccept();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
}
}
}
private Node createCustomIcon() {
Node shape = SVG.plusCircleOutline(Theme.blackFillBinding(), 32, 32);
shape.setMouseTransparent(true);
RipplerContainer container = new RipplerContainer(shape);
FXUtils.setLimitWidth(container, 36);
FXUtils.setLimitHeight(container, 36);
container.setOnMouseClicked(e -> {
exploreIcon();
});
return container;
}
private Node createIcon(VersionIconType type) {
ImageView imageView = new ImageView(new Image(type.getResourceUrl(), 32, 32, true, true));
imageView.setMouseTransparent(true);
RipplerContainer container = new RipplerContainer(imageView);
FXUtils.setLimitWidth(container, 36);
FXUtils.setLimitHeight(container, 36);
container.setOnMouseClicked(e -> {
if (vs != null) {
vs.setVersionIcon(type);
onAccept();
}
});
return container;
}
@Override
protected void onAccept() {
profile.getRepository().onVersionIconChanged.fireEvent(new Event(this));
onFinish.run();
super.onAccept();
}
}
@@ -28,10 +28,15 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
@@ -43,6 +48,7 @@ import java.io.File;
import java.util.Optional;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage {
@@ -55,6 +61,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private String preferredVersionName = null;
@@ -72,6 +79,22 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
}
private void checkSelectedVersion() {
runInFX(() -> {
if (this.version.get() == null) return;
GameRepository repository = this.version.get().getProfile().getRepository();
if (!repository.hasVersion(this.version.get().getVersion())) {
if (preferredVersionName != null) {
loadVersion(preferredVersionName, this.version.get().getProfile());
} else {
fireEvent(new PageCloseEvent());
}
}
});
}
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
@@ -32,10 +32,7 @@ import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -44,9 +41,7 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.Architecture;
@@ -55,17 +50,14 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -775,18 +767,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (versionId == null)
return;
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIconFile(versionId);
try {
FileUtils.copyFile(selectedFile, iconFile);
loadIcon();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
}
}
Controllers.dialog(new VersionIconDialog(profile, versionId, this::loadIcon));
}
private void onDeleteIcon() {
@@ -796,6 +777,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
File iconFile = profile.getRepository().getVersionIconFile(versionId);
if (iconFile.exists())
iconFile.delete();
VersionSetting localVersionSetting = profile.getRepository().getLocalVersionSettingOrCreate(versionId);
if (localVersionSetting != null) {
localVersionSetting.setVersionIcon(VersionIconType.DEFAULT);
}
loadIcon();
}
@@ -804,11 +789,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
return;
}
File iconFile = profile.getRepository().getVersionIconFile(versionId);
if (iconFile.exists())
iconPickerItem.setImage(new Image("file:" + iconFile.getAbsolutePath()));
else
iconPickerItem.setImage(newImage("/assets/img/grass.png"));
iconPickerItem.setImage(profile.getRepository().getVersionIconImage(versionId));
FXUtils.limitSize(iconPickerItem.getImageView(), 32, 32);
}
@@ -122,8 +122,8 @@ public final class Versions {
return;
}
if (profile.getRepository().renameVersion(version, newName)) {
profile.getRepository().refreshVersionsAsync().start();
resolve.run();
profile.getRepository().refreshVersionsAsync().start();
} else {
reject.accept(i18n("version.manage.rename.fail"));
}
@@ -21,22 +21,16 @@ import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import static java.util.Collections.newSetFromMap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -65,8 +59,6 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
pair("com.sun.javafx.css.StyleManager.findMatchingStyles", i18n("launcher.update_java")),
pair("NoSuchAlgorithmException", "Has your operating system been installed completely or is a ghost system?")
};
static final Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
}
private boolean checkThrowable(Throwable e) {
@@ -101,29 +93,11 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
LOG.log(Level.SEVERE, "Uncaught exception in thread " + t.getName(), e);
try {
String stackTrace = StringUtils.getStackTrace(e);
if (!stackTrace.contains("org.jackhuang"))
CrashReport report = new CrashReport(t, e);
if (!report.shouldBeReport())
return;
if (Hole.CAUGHT_EXCEPTIONS.contains(stackTrace))
return;
Hole.CAUGHT_EXCEPTIONS.add(stackTrace);
String text = "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
" Thread: " + t + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
" 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" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
String text = report.getDisplayText();
LOG.log(Level.SEVERE, text);
Platform.runLater(() -> {
@@ -132,7 +106,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
new CrashWindow(text).show();
}
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
reportToServer(text);
reportToServer(report);
}
}
});
@@ -141,10 +115,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
}
}
private void reportToServer(final String text) {
private void reportToServer(CrashReport crashReport) {
Thread t = new Thread(() -> {
HashMap<String, String> map = new HashMap<>();
map.put("crash_report", text);
map.put("crash_report", crashReport.getDisplayText());
map.put("version", Metadata.VERSION);
map.put("log", Logging.getLogs());
try {
+1 -1
View File
@@ -285,7 +285,7 @@
}
.two-line-item-second-large > .first-line > .title {
.two-line-item-second-large > .first-line > .title, .two-line-item-second-large-title {
-fx-text-fill: rgba(0, 0, 0, 0.5);
-fx-font-weight: normal;
-fx-font-size: 12px;
@@ -18,7 +18,7 @@
<VBox fx:id="borderPane" alignment="CENTER" FXUtils.limitWidth="500">
<ComponentList>
<BorderPane><left><Label text="%modpack.task.install.will" /></left><right><Label fx:id="lblModpackLocation" /></right></BorderPane>
<BorderPane><left><Label text="%modpack.enter_name" /></left><right><JFXTextField fx:id="txtModpackName" StackPane.margin="$insets" /></right></BorderPane>
<BorderPane><left><Label BorderPane.alignment="CENTER_LEFT" text="%archive.name" /></left><right><JFXTextField fx:id="txtModpackName" StackPane.margin="$insets" /></right></BorderPane>
<BorderPane><left><Label text="%modpack.name"/></left><right><Label fx:id="lblName" /></right></BorderPane>
<BorderPane><left><Label text="%archive.version"/></left><right><Label fx:id="lblVersion" /></right></BorderPane>
@@ -293,6 +293,7 @@ exception.access_denied=It's denied by operating system to access file %s. Maybe
Please check if current operating system user has permission to access that file.\n\
For Windows users, you can also try the Resource Monitor, find if some programs is holding the file, try to close related program, or restart your computer.
exception.artifact_malformed=The file cannot pass verification.
exception.ssl_handshake=Cannot establish SSL connection because of missing SSL certifcate in current Java installation. Try to start HMCL on another Java VM.
extension.bat=Windows Bat file
extension.mod=Mod file
@@ -386,7 +387,7 @@ game.crash.reason.openj9=Game cannot run on OpenJ9 virtual machine, please switc
game.crash.reason.out_of_memory=Game cannot run because out of memory.\nThis may be due to too little memory allocation, or too many mods.\nYou can increase the game memory allocation in the game settings to allow the game running on larger memory.\nIf you still have these problems, you may need to change a better computer.
game.crash.reason.resolution_too_high=Game cannot run because you are using a resource pack whose resolution is too high\nYou should use a resource pack with lower resolution, or buy a graphics card with larger memory.
game.crash.reason.stacktrace=Unknown. You can look into details by clicking "Logs" button.\nThere are some keywords that may represent Mod ID. You can search them online to figure out the reason.\n%s
game.crash.reason.too_old_java=Game cannot run because the Java virtual machine version is too low.\nYou need to switch to a newer version (%1$s) of Java virtual machine in the game settings and restart the game. If not, you can download it online.
game.crash.reason.too_old_java=Game cannot run because the Java virtual machine version is too low.\nYou need to switch to a newer version (%1$s) of Java virtual machine in the game settings and restart the game. If not, you can download it from <a href="https://docs.microsoft.com/en-US/java/openjdk/download">here</a>.
game.crash.reason.unknown=Unknown. You can look into details by clicking "Logs" button.
game.crash.reason.unsatisfied_link_error=Game cannot run because the required native libraries are missing.\nThe missing native library: %1$s。\nIf you have modified the native library path option in game settings, you'd better switch back to standard mode.\nIf it's already standard mode, please check if the missing native library belongs to a mod or the game. If you are sure that it's caused by HMCL, you can give feedback to us.\nIf you do really need to customize native library path, you should place all the native libraries the the game requires in the directory.
game.crash.title=Game crashed
@@ -654,6 +655,7 @@ multiplayer.exit.timeout.static_token=Failed to connect to multiplayer server. P
multiplayer.exit.timeout.dynamic_token=Failed to connect to multiplayer server.
multiplayer.hint=Multiplayer functionality is experimental. Please give feedback.
multiplayer.nat=Network Type Detection
multiplayer.nat.failed=Failed to detect network type.
multiplayer.nat.hint=Network type detection will make it clear whether your network fulfills our requirement for multiplayer mode.
multiplayer.nat.latency=Latency
multiplayer.nat.not_yet_tested=Not yet testsed
@@ -697,7 +699,6 @@ multiplayer.session.create.token.prompt=Default randomized. You can apply for yo
multiplayer.session.error.already_started=Cato service has already started locally. Please check whether there are other multiplayer services served or not. Or you can kill cato process in task manager.
multiplayer.session.error.file_not_found=Cannot find cato executable. This file should be downloaded when entering this page. Please check if your anti-virus software has isolated the file. If true, please rollback.
multiplayer.session.expired=Multiplayer session has expired. You should re-create or re-join a room to continue.
multiplayer.session.hint=You must click "Open LAN Server" in game in order to enable multiplayer functionality.
multiplayer.session.join=Join Session
multiplayer.session.join.error=Failed to join multiplayer session
multiplayer.session.join.error.connection=Failed to join multiplayer session. Cannot establish connection.
@@ -970,7 +971,7 @@ version.manage.clean.tooltip=Clear logs, crash-reports
version.manage.duplicate=Copy game instance
version.manage.duplicate.duplicate_save=Copy saves
version.manage.duplicate.prompt=Type new version name
version.manage.duplicate.confirm=
version.manage.duplicate.confirm=New version generated by replication will be locked: forced independent mode, game setting independence.
version.manage.manage=Manage Version
version.manage.manage.title=Manage Version - %1s
version.manage.redownload_assets_index=Update Game Asset Files
File diff suppressed because it is too large. Load diff
@@ -293,6 +293,7 @@ exception.access_denied=因為無法訪問文件 %s,HMCL 沒有對該文件的
請你檢查當前操作系統帳戶是否能訪問該文件,比如非管理員用戶可能不能訪問其他帳戶的個人文件夾內的文件。\n\
對於 Windows 用戶,你還可以嘗試通過資源監視器查看是否有程序占用了該文件,如果是,你可以關閉占用此文件相關程序,或者重啟電腦再試。
exception.artifact_malformed=下載的文件正確,無法通過校驗。
exception.ssl_handshake=無法建立 SSL 連接,因為當前 Java 虛擬機缺少相關的 SSL 證書。你可以嘗試使用其他的 Java 虛擬機啟動 HMCL 再試。
extension.bat=Windows 指令碼
extension.mod=模組檔案
@@ -386,7 +387,7 @@ game.crash.reason.openj9=當前遊戲無法運行在 OpenJ9 虛擬機上,請
game.crash.reason.out_of_memory=當前遊戲因為記憶體不足,無法繼續運行。\n這可能是記憶體分配太小,或者 Mod 數量過多導致的。\n你可以在遊戲設置中調大遊戲記憶體分配值以允許遊戲在更大的記憶體下運行。\n如果仍然出現該錯誤,你可能需要換一台更好的電腦。
game.crash.reason.resolution_too_high=當前遊戲因為材質包解析度過高,無法繼續運行\n你可以更換一個解析度更低的材質,或者更換一個視訊記憶體更大的顯示卡。
game.crash.reason.stacktrace=原因未知,請點擊日誌按鈕查看詳細訊息。\n下面是一些關鍵字,其中可能包含 Mod 名稱,你可以透過搜索的方式查找有關訊息。\n%s
game.crash.reason.too_old_java=當前遊戲因為 Java 虛擬機版本過低,無法繼續運行。\n你需要在遊戲設置中更換 %1$s 或更新版本的 Java 虛擬機,並重新啟動遊戲。如果沒有下載安裝,你可以在網路上自行下載。
game.crash.reason.too_old_java=當前遊戲因為 Java 虛擬機版本過低,無法繼續運行。\n你需要在遊戲設置中更換 %1$s 或更新版本的 Java 虛擬機,並重新啟動遊戲。如果沒有下載安裝,你可以點擊 <a href="https://docs.microsoft.com/zh-TW/java/openjdk/download">此處</a> 下載微軟 JDK。
game.crash.reason.unknown=原因未知,請點擊日誌按鈕查看詳細訊息。\n不要將本界面截圖給他人!如果你要求助他人,請你點擊左下角導出遊戲崩潰訊息後將導出的文件發送給他人以供分析!\n你可以向我們提供遊戲崩潰日誌以提升 HMCL 的日誌分析能力。
game.crash.reason.unsatisfied_link_error=當前遊戲因為缺少本地庫,無法繼續運行。\n這些本地庫缺失:%1$s。\n如果你在遊戲設置中修改了本地庫路徑選項,請你修改回預設模式。\n如果已經在預設模式下,請檢查本地庫缺失是否是 Mod 引起的,或由 HMCL 引起的。如果你確定是 HMCL 引起的,建議你向我們回饋。\n如果你確實需要自訂本地庫路徑,你需要保證其中包含缺失的本地庫。
game.crash.title=遊戲意外退出
@@ -650,10 +651,11 @@ multiplayer.download.success=多人聯機初始化完成
multiplayer.download.unsupported=多人聯機依賴不支持當前系統或平台
multiplayer.exit.after_ready=多人聯機會話意外退出,退出碼 %d
multiplayer.exit.before_ready=多人聯機房間創建失敗,cato 退出碼 %d
multiplayer.exit.timeout.static_token=無法連接多人聯機服務,請你切換動態 Token 重試。
multiplayer.exit.timeout.static_token=無法連接多人聯機服務,你的憑證可能無法正常工作,你可以使用空憑證再試。
multiplayer.exit.timeout.dynamic_token=無法連接多人聯機服務,你可以在多人聯機頁面的回饋中回饋問題。
multiplayer.hint=多人聯機功能處於實驗階段,如果有問題請回饋。
multiplayer.nat=網路檢測
multiplayer.nat.failed=檢測失敗,但你仍然可以繼續使用聯機功能。
multiplayer.nat.hint=執行網路檢測可以讓你更清楚你的網路狀況是否符合聯機功能的需求。不符合聯機功能運行條件的網路狀況將可能導致聯機失敗。
multiplayer.nat.latency=延遲
multiplayer.nat.not_yet_tested=尚未檢測
@@ -670,8 +672,8 @@ multiplayer.nat.type.symmetric_udp_firewall=差(對稱型+防火牆)
multiplayer.nat.type.unknown=未知
multiplayer.powered_by=多人聯機服務由 這裡 (<a href="https://noin.cn">noin.cn</a>) 提供。<a href="https://noin.cn/agreement">用戶協議與免責聲明</a>
multiplayer.report=違法違規檢舉
multiplayer.relay=中繼模式
multiplayer.relay.hint=NAT 網路類型為差-對稱型的用戶,若無法聯機,申請靜態 Token 後可以啟用本選項
multiplayer.relay=橋接服務
multiplayer.relay.hint=Cato 面向較差網路情況提供的網路質量增強最佳化解決方案。若無法聯機請到 這裡 的個人中心兌換憑證後啟用。
multiplayer.session=房間
multiplayer.session.name.format=%1$s 的房間
multiplayer.session.name.motd=HMCL 多人聯機房間 - %s
@@ -680,7 +682,7 @@ multiplayer.session.close.warning=關閉房間後,已經加入聯機房間的
multiplayer.session.copy_room_code=複製邀請碼
multiplayer.session.create=創建房間
multiplayer.session.create.error.dynamic_token=創建聯機房間失敗,請稍後再試。
multiplayer.session.create.error.static_token=創建聯機房間失敗,你可以使用動態 Token 再試。
multiplayer.session.create.error.static_token=創建聯機房間失敗,你的憑證可能無法正常工作,你可以使用空憑證再試。
multiplayer.session.create.hint=創建聯機房間前,你需要先在正在運行的遊戲內的遊戲菜單中選擇 對區域網路開放 選項,然後在下方的輸入框中輸入遊戲內提示的埠號(通常是 5 位的數字)
multiplayer.session.create.join=連接申請
multiplayer.session.create.join.allow=自動接受所有連接申請(不啟用此選項時,你需要手動同意申請,以避免不相關人士誤連你的伺服器)
@@ -691,9 +693,9 @@ multiplayer.session.create.members.kick.prompt=是否踢出該玩家?踢出後
multiplayer.session.create.name=房間名稱
multiplayer.session.create.port=埠號
multiplayer.session.create.port.error=無法檢測遊戲埠號,你必須先啟動遊戲並在遊戲內打開對區域網路開放選項後才能啟動聯機。
multiplayer.session.create.token=Token
multiplayer.session.create.token.apply=申請靜態 Token
multiplayer.session.create.token.prompt=預設為臨時 Token。你可以在 noin.cn 上申請靜態 Token 並填寫至此處
multiplayer.session.create.token=憑證
multiplayer.session.create.token.apply=申請憑證
multiplayer.session.create.token.prompt=預設為無憑證。你可以在 這裡 的個人中心兌換憑證
multiplayer.session.error.already_started=本地已經開啟 cato 服務,請檢查是否有其他 HMCL 正在運行聯機服務。或者你可以在任務管理器裡殺掉 cato 進程以繼續。
multiplayer.session.error.file_not_found=找不到 cato 程序。該程序應該在進入多人聯機頁面時完成下載。請檢查你電腦的防毒軟體是否將 cato 標記為病毒,如果是,請恢復 cato。
multiplayer.session.expired=聯機會話連續使用時間超過了 3 小時,你需要重新創建/加入房間以繼續聯機。
@@ -293,6 +293,7 @@ exception.access_denied=因为无法访问文件 %s,HMCL 没有对该文件的
请你检查当前操作系统帐户是否能访问该文件,比如非管理员用户可能不能访问其他帐户的个人文件夹内的文件。\n\
对于 Windows 用户,你还可以尝试通过资源监视器查看是否有程序占用了该文件,如果是,你可以关闭占用此文件相关程序,或者重启电脑再试。
exception.artifact_malformed=下载的文件正确,无法通过校验。
exception.ssl_handshake=无法建立 SSL 连接,因为当前 Java 虚拟机缺少相关的 SSL 证书。你可以尝试使用其他的 Java 虚拟机启动 HMCL 再试。
extension.bat=Windows 脚本
extension.mod=模组文件
@@ -386,7 +387,7 @@ game.crash.reason.openj9=当前游戏无法运行在 OpenJ9 虚拟机上,请
game.crash.reason.out_of_memory=当前游戏因为内存不足,无法继续运行。\n这可能是内存分配太小,或者 Mod 数量过多导致的。\n你可以在游戏设置中调大游戏内存分配值以允许游戏在更大的内存下运行。\n如果仍然出现该错误,你可能需要换一台更好的电脑。
game.crash.reason.resolution_too_high=当前游戏因为材质包分辨率过高,无法继续运行\n你可以更换一个分辨率更低的材质,或者更换一个显存更大的显卡。
game.crash.reason.stacktrace=原因未知,请点击日志按钮查看详细信息。\n下面是一些关键词,其中可能包含 Mod 名称,你可以通过搜索的方式查找有关信息。\n%s
game.crash.reason.too_old_java=当前游戏因为 Java 虚拟机版本过低,无法继续运行。\n你需要在游戏设置中更换 Java %1$s 或更新版本的 Java 虚拟机,并重新启动游戏。如果没有下载安装,你可以在 https://adoptopenjdk.net 上自行下载。
game.crash.reason.too_old_java=当前游戏因为 Java 虚拟机版本过低,无法继续运行。\n你需要在游戏设置中更换 Java %1$s 或更新版本的 Java 虚拟机,并重新启动游戏。如果没有下载安装,你可以点击 <a href="https://docs.microsoft.com/zh-cn/java/openjdk/download">此处</a> 下载微软 JDK。
game.crash.reason.unknown=原因未知,请点击日志按钮查看详细信息。\n不要将本界面截图给他人!如果你要求助他人,请你点击左下角导出游戏崩溃信息后将导出的文件发送给他人以供分析!\n你可以向我们提供游戏崩溃日志以提升 HMCL 的日志分析能力。
game.crash.reason.unsatisfied_link_error=当前游戏因为缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在游戏设置中修改了本地库路径选项,请你修改回预设模式。\n如果已经在预设模式下,请检查本地库缺失是否是 Mod 引起的,或由 HMCL 引起的。如果你确定是 HMCL 引起的,建议你向我们反馈。\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库。
game.crash.title=游戏意外退出
@@ -650,10 +651,11 @@ multiplayer.download.success=多人联机初始化完成
multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台
multiplayer.exit.after_ready=多人联机会话意外退出,退出码 %d
multiplayer.exit.before_ready=多人联机房间创建失败,cato 退出码 %d
multiplayer.exit.timeout.static_token=无法连接多人联机服务,请你切换动态 Token 重试。
multiplayer.exit.timeout.static_token=无法连接多人联机服务,你的凭证可能无法正常工作,你可以使用空凭证再试。
multiplayer.exit.timeout.dynamic_token=无法连接多人联机服务,你可以在多人联机页面的反馈中反馈问题。
multiplayer.hint=多人联机功能处于实验阶段,如果有问题请反馈。
multiplayer.nat=网络检测
multiplayer.nat.failed=检测失败,但你仍然可以继续使用联机功能。
multiplayer.nat.hint=执行网络检测可以让你更清楚你的网络状况是否符合联机功能的需求。检测结果为差的网络可能导致联机失败。
multiplayer.nat.latency=延迟
multiplayer.nat.not_yet_tested=尚未检测
@@ -669,8 +671,8 @@ multiplayer.nat.type.symmetric=差(对称型)
multiplayer.nat.type.symmetric_udp_firewall=差(对称型+防火墙)
multiplayer.nat.type.unknown=未知
multiplayer.powered_by=多人联机服务由 这里 (<a href="https://noin.cn">noin.cn</a>) 提供。<a href="https://noin.cn/agreement">用户协议与免责声明</a>
multiplayer.relay=中继模式
multiplayer.relay.hint=NAT 网络类型为差-对称型的用户,若无法联机,申请静态 Token 后可以启用本选项
multiplayer.relay=桥接服务
multiplayer.relay.hint=Cato 面向较差网络情况提供的网络质量增强优化解决方案。若无法联机请到 这里 的个人中心兑换凭证后启用。
multiplayer.report=违法违规举报
multiplayer.session=房间
multiplayer.session.name.format=%1$s 的房间
@@ -680,7 +682,7 @@ multiplayer.session.close.warning=关闭房间后,已经加入联机房间的
multiplayer.session.copy_room_code=复制邀请码
multiplayer.session.create=创建房间
multiplayer.session.create.error.dynamic_token=创建联机房间失败,请稍后再试。
multiplayer.session.create.error.static_token=创建联机房间失败,你可以使用动态 Token 再试。
multiplayer.session.create.error.static_token=创建联机房间失败,你的凭证可能无法正常工作,你可以使用空凭证再试。
multiplayer.session.create.hint=创建联机房间前,你需要先在正在运行的游戏内的游戏菜单中选择 对局域网开放 选项,然后在下方的输入框中确认游戏内提示的端口号(通常是 5 位的数字)
multiplayer.session.create.join=连接申请
multiplayer.session.create.join.allow=自动接受所有连接申请(不启用此选项时,你需要手动同意申请,以避免不相关人士误连你的服务器)
@@ -691,9 +693,9 @@ multiplayer.session.create.members.kick.prompt=是否踢出该玩家?踢出后
multiplayer.session.create.name=房间名称
multiplayer.session.create.port=端口号
multiplayer.session.create.port.error=无法检测游戏端口号,你必须先启动游戏并在游戏内打开对局域网开放选项后才能启动联机。
multiplayer.session.create.token=Token
multiplayer.session.create.token.apply=申请静态 Token
multiplayer.session.create.token.prompt=默认为临时 Token。你可以在 noin.cn 上申请静态 Token 并填写至此处
multiplayer.session.create.token=凭证
multiplayer.session.create.token.apply=申请凭证
multiplayer.session.create.token.prompt=默认为无凭证。你可以在 这里 的个人中心兑换凭证
multiplayer.session.error.already_started=本地已经开启 cato 服务,请检查是否有其他 HMCL 正在运行联机服务。或者你可以在任务管理器里杀掉 cato 进程以继续。
multiplayer.session.error.file_not_found=找不到 cato 程序。该程序应该在进入多人联机页面时完成下载。请检查你电脑的杀毒软件是否将 cato 标记为病毒,如果是,请恢复 cato。
multiplayer.session.expired=联机会话连续使用时间超过了 3 小时,你需要重新创建/加入房间以继续联机。
@@ -46,7 +46,7 @@ public class GameCrashWindowTest {
ManagedProcess process = new ManagedProcess(null, Arrays.asList("commands", "2"));
String logs = FileUtils.readText(new File("C:\\Users\\huang\\Downloads\\minecraft-exported-logs-2021-10-18T21-15-43.log"));
String logs = FileUtils.readText(new File("../HMCLCore/src/test/resources/logs/too_old_java.txt"));
CountDownLatch latch = new CountDownLatch(1);
FXUtils.runInFX(() -> {
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -144,11 +145,17 @@ public class OAuth {
public Result refresh(String refreshToken, Options options) throws AuthenticationException {
try {
RefreshResponse response = HttpRequest.POST(accessTokenURL)
.form(pair("client_id", options.callback.getClientId()),
pair("client_secret", options.callback.getClientSecret()),
pair("refresh_token", refreshToken),
pair("grant_type", "refresh_token"))
Map<String, String> query = mapOf(pair("client_id", options.callback.getClientId()),
pair("refresh_token", refreshToken),
pair("grant_type", "refresh_token")
);
if (!options.callback.isPublicClient()) {
query.put("client_secret", options.callback.getClientSecret());
}
RefreshResponse response = HttpRequest.POST(tokenURL)
.form(query)
.accept("application/json")
.ignoreHttpCode()
.getJson(RefreshResponse.class);
@@ -233,6 +240,8 @@ public class OAuth {
String getClientId();
String getClientSecret();
boolean isPublicClient();
}
public enum GrantFlow {
@@ -136,20 +136,6 @@ public class MicrosoftService {
getUhs(minecraftXstsResponse, uhs);
// Authenticate XBox with XSTS
XBoxLiveAuthenticationResponse xboxXstsResponse = HttpRequest
.POST("https://xsts.auth.xboxlive.com/xsts/authorize")
.json(mapOf(
pair("Properties",
mapOf(pair("SandboxId", "RETAIL"),
pair("UserTokens", Collections.singletonList(xboxResponse.token)))),
pair("RelyingParty", "http://xboxlive.com"), pair("TokenType", "JWT")))
.getJson(XBoxLiveAuthenticationResponse.class);
getUhs(xboxXstsResponse, uhs);
getXBoxProfile(uhs, xboxXstsResponse.token);
// Authenticate with Minecraft
MinecraftLoginWithXBoxResponse minecraftResponse = HttpRequest
.POST("https://api.minecraftservices.com/authentication/login_with_xbox")
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.StringUtils;
@@ -89,11 +90,22 @@ public class OfflineAccount extends Account {
invalidate();
}
private boolean loadAuthlibInjector(Skin skin) {
if (skin == null) return false;
if (skin.getType() == Skin.Type.DEFAULT) return false;
TextureModel defaultModel = TextureModel.detectUUID(getUUID());
if (skin.getType() == Skin.Type.ALEX && defaultModel == TextureModel.ALEX ||
skin.getType() == Skin.Type.STEVE && defaultModel == TextureModel.STEVE) {
return false;
}
return true;
}
@Override
public AuthInfo logIn() throws AuthenticationException {
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
if (skin != null) {
if (loadAuthlibInjector(skin)) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try {
return downloader.getArtifactInfo();
@@ -94,9 +94,16 @@ public class YggdrasilServer extends HttpServer {
if (!request.getQuery().containsKey("username")) {
return badRequest();
}
return findCharacterByName(request.getQuery().get("username"))
.map(character -> ok(character.toCompleteResponse(getRootUrl())))
.orElseGet(HttpServer::noContent);
Optional<Character> character = findCharacterByName(request.getQuery().get("username"));
//Workaround for JDK-8138667
//noinspection OptionalIsPresent
if (character.isPresent()) {
return ok(character.get().toCompleteResponse(getRootUrl()));
} else {
return HttpServer.noContent();
}
}
private Response joinServer(Request request) {
@@ -106,9 +113,15 @@ public class YggdrasilServer extends HttpServer {
private Response profile(Request request) {
String uuid = request.getPathVariables().group("uuid");
return findCharacterByUuid(UUIDTypeAdapter.fromString(uuid))
.map(character -> ok(character.toCompleteResponse(getRootUrl())))
.orElseGet(HttpServer::noContent);
Optional<Character> character = findCharacterByUuid(UUIDTypeAdapter.fromString(uuid));
//Workaround for JDK-8138667
//noinspection OptionalIsPresent
if (character.isPresent()) {
return ok(character.get().toCompleteResponse(getRootUrl()));
} else {
return HttpServer.noContent();
}
}
private Response texture(Request request) {
@@ -35,6 +35,7 @@ public enum JavaVersionConstraint {
// Minecraft>=1.17 requires Java 16
VANILLA_JAVA_16(JavaVersionConstraint.RULE_MANDATORY, versionRange("1.17", JavaVersionConstraint.MAX), versionRange("16", JavaVersionConstraint.MAX)),
VANILLA_JAVA_17(JavaVersionConstraint.RULE_MANDATORY, versionRange("1.18", JavaVersionConstraint.MAX), versionRange("17", JavaVersionConstraint.MAX)),
// Minecraft>=1.13 requires Java 8
VANILLA_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, versionRange("1.13", JavaVersionConstraint.MAX), versionRange("1.8", JavaVersionConstraint.MAX)),
// Minecraft>=1.7.10+Forge accepts Java 8
@@ -45,6 +45,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Supplier;
@@ -68,7 +69,7 @@ public class DefaultLauncher extends Launcher {
super(repository, version, authInfo, options, listener, daemon);
}
private CommandBuilder generateCommandLine(File nativeFolder) throws IOException {
private Command generateCommandLine(File nativeFolder) throws IOException {
CommandBuilder res = new CommandBuilder();
switch (options.getProcessPriority()) {
@@ -208,10 +209,20 @@ public class DefaultLauncher extends Launcher {
Path gameAssets = repository.getActualAssetDirectory(version.getId(), version.getAssetIndex().getId());
Map<String, String> configuration = getConfigurations();
configuration.put("${classpath}", String.join(OperatingSystem.PATH_SEPARATOR, classpath));
configuration.put("${natives_directory}", nativeFolder.getAbsolutePath());
configuration.put("${game_assets}", gameAssets.toAbsolutePath().toString());
configuration.put("${assets_root}", gameAssets.toAbsolutePath().toString());
// lwjgl assumes path to native libraries encoded by ASCII.
// Here is a workaround for this issue: https://github.com/huanghongxun/HMCL/issues/1141.
String nativeFolderPath = nativeFolder.getAbsolutePath();
Path tempNativeFolder = null;
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX
&& !StringUtils.isASCII(nativeFolderPath)) {
tempNativeFolder = Paths.get("/", "tmp", "hmcl-natives-" + UUID.randomUUID());
nativeFolderPath = tempNativeFolder + File.pathSeparator + nativeFolderPath;
}
configuration.put("${natives_directory}", nativeFolderPath);
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
if (authInfo.getArguments() != null && authInfo.getArguments().getJvm() != null && !authInfo.getArguments().getJvm().isEmpty())
res.addAll(Arguments.parseArguments(authInfo.getArguments().getJvm(), configuration));
@@ -258,7 +269,7 @@ public class DefaultLauncher extends Launcher {
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
return res;
return new Command(res, tempNativeFolder);
}
public Map<String, Boolean> getFeatures() {
@@ -363,8 +374,10 @@ public class DefaultLauncher extends Launcher {
nativeFolder = new File(options.getNativesDir());
}
final Command command = generateCommandLine(nativeFolder);
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = generateCommandLine(nativeFolder).asMutableList();
List<String> rawCommandLine = command.commandLine.asMutableList();
// Pass classpath using the environment variable, to reduce the command length
String classpath = null;
@@ -374,6 +387,11 @@ public class DefaultLauncher extends Launcher {
classpath = rawCommandLine.remove(cpIndex);
}
if (command.tempNativeFolder != null) {
Files.deleteIfExists(command.tempNativeFolder);
Files.createSymbolicLink(command.tempNativeFolder, nativeFolder.toPath().toAbsolutePath());
}
if (rawCommandLine.stream().anyMatch(StringUtils::isBlank)) {
throw new IllegalStateException("Illegal command line " + rawCommandLine);
}
@@ -398,7 +416,12 @@ public class DefaultLauncher extends Launcher {
}
String appdata = options.getGameDir().getAbsoluteFile().getParent();
if (appdata != null) builder.environment().put("APPDATA", appdata);
if (classpath != null) builder.environment().put("CLASSPATH", classpath);
if (classpath != null) {
builder.environment().put("CLASSPATH", classpath);
// Fix #1153: On Windows, the 'classpath' environment variable in the context overrides the 'CLASSPATH'
// Environment variables on Windows are not case-sensitive; The lowercase 'classpath' overwrites any other case.
builder.environment().put("classpath", classpath);
}
builder.environment().putAll(getEnvVars());
process = builder.start();
} catch (IOException e) {
@@ -464,8 +487,8 @@ public class DefaultLauncher extends Launcher {
if (!FileUtils.makeFile(scriptFile))
throw new IOException("Script file: " + scriptFile + " cannot be created.");
final CommandBuilder commandLine = generateCommandLine(nativeFolder);
final String command = usePowerShell ? null : commandLine.toString();
final Command commandLine = generateCommandLine(nativeFolder);
final String command = usePowerShell ? null : commandLine.commandLine.toString();
if (!usePowerShell && isWindows) {
if (command.length() > 8192) { // maximum length of the command in cmd
@@ -509,7 +532,7 @@ public class DefaultLauncher extends Launcher {
writer.newLine();
writer.write('&');
for (String rawCommand : commandLine.asList()) {
for (String rawCommand : commandLine.commandLine.asList()) {
writer.write(' ');
writer.write(CommandBuilder.pwshString(rawCommand));
}
@@ -533,6 +556,10 @@ public class DefaultLauncher extends Launcher {
writer.write("export " + entry.getKey() + "=" + entry.getValue());
writer.newLine();
}
if (commandLine.tempNativeFolder != null) {
writer.write(new CommandBuilder().add("ln", "-s", nativeFolder.getAbsolutePath(), commandLine.tempNativeFolder.toString()).toString());
writer.newLine();
}
writer.write(new CommandBuilder().add("cd", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
}
writer.newLine();
@@ -540,7 +567,7 @@ public class DefaultLauncher extends Launcher {
writer.write(options.getPreLaunchCommand());
writer.newLine();
}
writer.write(generateCommandLine(nativeFolder).toString());
writer.write(command);
writer.newLine();
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
writer.write(options.getPostExitCommand());
@@ -551,6 +578,10 @@ public class DefaultLauncher extends Launcher {
writer.write("pause");
writer.newLine();
}
if (commandLine.tempNativeFolder != null) {
writer.write(new CommandBuilder().add("rm", commandLine.tempNativeFolder.toString()).toString());
writer.newLine();
}
}
}
if (!scriptFile.setExecutable(true))
@@ -574,4 +605,14 @@ public class DefaultLauncher extends Launcher {
managedProcess.addRelatedThread(stderr);
managedProcess.addRelatedThread(Lang.thread(new ExitWaiter(managedProcess, Arrays.asList(stdout, stderr), processListener::onExit), "exit-waiter", isDaemon));
}
private static final class Command {
final CommandBuilder commandLine;
final Path tempNativeFolder;
Command(CommandBuilder commandBuilder, Path tempNativeFolder) {
this.commandLine = commandBuilder;
this.tempNativeFolder = tempNativeFolder;
}
}
}
@@ -0,0 +1,67 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.util.Objects;
public abstract class Result<T> {
public T get() {
throw new IllegalStateException("TriState not ok");
}
public boolean isOK() {
return false;
}
public boolean isError() {
return false;
}
public static <T> Result<T> ok(T result) {
return new OK<>(Objects.requireNonNull(result));
}
@SuppressWarnings("unchecked")
public static <T> Result<T> error() {
return (Result<T>) Error.INSTANCE;
}
private static class OK<T> extends Result<T> {
private final T result;
public OK(T result) {
this.result = result;
}
@Override
public T get() {
return result;
}
}
private static class Error<T> extends Result<T> {
public static final Error<Void> INSTANCE = new Error<>();
@Override
public boolean isError() {
return true;
}
}
}
@@ -22,6 +22,8 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;
@@ -258,6 +260,10 @@ public final class StringUtils {
return Optional.of(str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength));
}
public static boolean isASCII(CharSequence cs) {
return US_ASCII_ENCODER.canEncode(cs);
}
/**
* Class for computing the longest common subsequence between strings.
*/
@@ -295,4 +301,5 @@ public final class StringUtils {
public static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
public static final CharsetEncoder US_ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
}
+2
View File
@@ -22,6 +22,8 @@ subprojects {
apply plugin: 'checkstyle'
repositories {
flatDir name: 'libs', dirs: rootProject.file('lib')
mavenCentral()
maven { url 'https://jitpack.io' }