Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c31269a358 | ||
|
|
55092326a4 | ||
|
|
10dbcf63b7 | ||
|
|
32fad77104 | ||
|
|
05d2fbbbb9 | ||
|
|
916733a1e8 | ||
|
|
83b4b58cb2 | ||
|
|
11f53c0e03 | ||
|
|
2fa8aee055 | ||
|
|
d2848e12f3 | ||
|
|
36ba840fe8 | ||
|
|
819d41a75c | ||
|
|
3a6c3dc51d | ||
|
|
db721107ae | ||
|
|
5b9be471e3 | ||
|
|
e8f8617412 | ||
|
|
eb472037db | ||
|
|
81b7afd08d | ||
|
|
7a71ecf177 | ||
|
|
a12e485a27 | ||
|
|
af521b2893 | ||
|
|
aab6f68492 | ||
|
|
f67c3d5284 | ||
|
|
46385a8445 | ||
|
|
9f617c33f5 | ||
|
|
6aef034880 | ||
|
|
c4941a01d6 | ||
|
|
7380423eee | ||
|
|
f35073ffb5 | ||
|
|
f89ea56d86 | ||
|
|
a240b8ca9f | ||
|
|
230ec2bc8e | ||
|
|
a3c2642b93 | ||
|
|
de755d5495 | ||
|
|
08054f3026 | ||
|
|
026239903d | ||
|
|
a320fe4cc4 | ||
|
|
b122d4c5e4 |
No files matched your search
@@ -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):
|
||||
* 游戏崩溃报告(如果有):
|
||||
* 启动器崩溃报告(如果有):
|
||||
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
|
||||
* 问题描述(如何触发问题):
|
||||
@@ -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 的。
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -23,12 +23,12 @@ import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -40,20 +40,20 @@ public final class HMCLModpackManager {
|
||||
/**
|
||||
* Read the manifest in a HMCL modpack.
|
||||
*
|
||||
* @param fs a HMCL modpack file.
|
||||
* @param file a HMCL modpack file.
|
||||
* @param encoding encoding of modpack zip file.
|
||||
* @return the manifest of HMCL modpack.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
*/
|
||||
public static Modpack readHMCLModpackManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = FileUtils.readText(fs.getPath("modpack.json"));
|
||||
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
String gameJson = FileUtils.readText(fs.getPath("minecraft/pack.json"));
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
|
||||
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
|
||||
if (game.getJar() == null)
|
||||
if (StringUtils.isBlank(manifest.getVersion()))
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack");
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
||||
else
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
else
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -62,38 +61,34 @@ public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try (ZipFileSystem zfs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception ignored) {
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception ignored) {
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zfs, file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
}
|
||||
|
||||
} catch (IOException ignored) {
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
@@ -50,7 +49,7 @@ class HMCLDownloadTask extends FileDownloadTask {
|
||||
break;
|
||||
|
||||
case PACK_XZ:
|
||||
byte[] raw = FileUtils.readAllBytes(target);
|
||||
byte[] raw = Files.readAllBytes(target);
|
||||
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
|
||||
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
|
||||
Pack200Utils.unpack(in, out);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -103,7 +103,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
else
|
||||
throw new LibraryDownloadException(library, t);
|
||||
} else {
|
||||
if (xz) unpackLibrary(jar, FileUtils.readAllBytes(xzFile.toPath()));
|
||||
if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
if (checksums == null || checksums.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
byte[] fileData = FileUtils.readAllBytes(libPath.toPath());
|
||||
byte[] fileData = Files.readAllBytes(libPath.toPath());
|
||||
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
|
||||
if (!valid && libPath.getName().endsWith(".jar")) {
|
||||
valid = validateJar(fileData, checksums);
|
||||
|
||||
+1
-1
@@ -230,7 +230,7 @@ public final class OptiFineInstallTask extends Task<Version> {
|
||||
Path configClass = fs.getPath("Config.class");
|
||||
if (!Files.exists(configClass)) configClass = fs.getPath("net/optifine/Config.class");
|
||||
if (!Files.exists(configClass)) throw new IOException("Unrecognized installer");
|
||||
ConstantPool pool = ConstantPoolScanner.parse(FileUtils.readAllBytes(configClass), ConstantType.UTF8);
|
||||
ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(configClass), ConstantType.UTF8);
|
||||
List<String> constants = new ArrayList<>();
|
||||
pool.list(Utf8Constant.class).forEach(utf8 -> constants.add(utf8.get()));
|
||||
String mcVersion = getOrDefault(constants, constants.indexOf("MC_VERSION") + 1, null);
|
||||
|
||||
@@ -104,13 +104,13 @@ public final class GameVersion {
|
||||
|
||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||
if (Files.exists(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(FileUtils.readAllBytes(minecraft));
|
||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
}
|
||||
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
|
||||
if (Files.exists(minecraftServer))
|
||||
return getVersionFromClassMinecraftServer(FileUtils.readAllBytes(minecraftServer));
|
||||
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
|
||||
return Optional.empty();
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
|
||||
@@ -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()) {
|
||||
@@ -177,6 +178,9 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
res.addDefault("-Dfml.ignoreInvalidMinecraftCertificates=", "true");
|
||||
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
|
||||
|
||||
// Fix RCE vulnerability of log4j2
|
||||
res.addDefault("-Dlog4j2.formatMsgNoLookups=", "true");
|
||||
}
|
||||
|
||||
Proxy proxy = options.getProxy();
|
||||
@@ -208,10 +212,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 +272,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 +377,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 +390,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 +419,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 +490,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 +535,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 +559,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 +570,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 +581,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 +608,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,13 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -122,11 +121,11 @@ public final class CurseManifest {
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
* @return the manifest.
|
||||
*/
|
||||
public static Modpack readCurseForgeModpackManifest(ZipFileSystem zip, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = FileUtils.readText(zip.getPath("manifest.json"));
|
||||
public static Modpack readCurseForgeModpackManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = CompressingUtils.readTextZipEntry(zip, "manifest.json", encoding);
|
||||
CurseManifest manifest = JsonUtils.fromNonNullJson(json, CurseManifest.class);
|
||||
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(),
|
||||
Lang.ignoringException(() -> FileUtils.readText(zip.getPath("modlist.html")), "No description"), encoding, manifest) {
|
||||
CompressingUtils.readTextZipEntryQuietly(zip, "modlist.html", encoding).orElse( "No description"), encoding, manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -32,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -433,26 +435,29 @@ public class McbbsModpackManifest implements Validation {
|
||||
}
|
||||
|
||||
private static Modpack fromManifestFile(Path manifestFile, Charset encoding) throws IOException, JsonParseException {
|
||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(FileUtils.readText(manifestFile), McbbsModpackManifest.class);
|
||||
String json = FileUtils.readText(manifestFile, StandardCharsets.UTF_8);
|
||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
|
||||
return manifest.toModpack(encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fs the MCBBS modpack file.
|
||||
* @param zip the MCBBS modpack file.
|
||||
* @param encoding the modpack zip file encoding.
|
||||
* @return the manifest.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the server-manifest.json is missing or malformed.
|
||||
* @return the manifest.
|
||||
*/
|
||||
public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta");
|
||||
if (Files.exists(mcbbsPackMeta)) {
|
||||
return fromManifestFile(mcbbsPackMeta, encoding);
|
||||
public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zip, encoding)) {
|
||||
Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta");
|
||||
if (Files.exists(mcbbsPackMeta)) {
|
||||
return fromManifestFile(mcbbsPackMeta, encoding);
|
||||
}
|
||||
Path manifestJson = fs.getPath("manifest.json");
|
||||
if (Files.exists(manifestJson)) {
|
||||
return fromManifestFile(manifestJson, encoding);
|
||||
}
|
||||
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
|
||||
}
|
||||
Path manifestJson = fs.getPath("manifest.json");
|
||||
if (Files.exists(manifestJson)) {
|
||||
return fromManifestFile(manifestJson, encoding);
|
||||
}
|
||||
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
|
||||
}
|
||||
}
|
||||
+20
-17
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* 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
|
||||
@@ -21,8 +21,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -30,6 +30,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
@@ -347,22 +348,24 @@ public final class MultiMCInstanceConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
public static Modpack readMultiMCModpackManifest(ZipFileSystem zipFile, Path filePath, Charset encoding) throws IOException {
|
||||
Path root = getRootPath(zipFile.getPath("/"));
|
||||
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root);
|
||||
String name = FileUtils.getNameWithoutExtension(filePath);
|
||||
public static Modpack readMultiMCModpackManifest(Path modpackFile, Charset encoding) throws IOException {
|
||||
try (FileSystem fs = CompressingUtils.readonly(modpackFile).setEncoding(encoding).build()) {
|
||||
Path root = getRootPath(fs.getPath("/"));
|
||||
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root);
|
||||
String name = FileUtils.getName(root, FileUtils.getNameWithoutExtension(modpackFile));
|
||||
|
||||
Path instancePath = root.resolve("instance.cfg");
|
||||
if (Files.notExists(instancePath))
|
||||
throw new IOException("`instance.cfg` not found, " + filePath + " is not a valid MultiMC modpack.");
|
||||
try (InputStream instanceStream = Files.newInputStream(instancePath)) {
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
|
||||
}
|
||||
};
|
||||
Path instancePath = root.resolve("instance.cfg");
|
||||
if (Files.notExists(instancePath))
|
||||
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
|
||||
try (InputStream instanceStream = Files.newInputStream(instancePath)) {
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -32,7 +32,6 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -145,11 +144,11 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
|
||||
public void execute() throws Exception {
|
||||
Version version = repository.readVersionJson(name);
|
||||
|
||||
try (ZipFileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) {
|
||||
Path root = MultiMCInstanceConfiguration.getRootPath(fs.getPath("/"));
|
||||
Path patches = root.resolve("patches");
|
||||
|
||||
if (Files.isDirectory(patches)) {
|
||||
if (Files.exists(patches)) {
|
||||
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(patches)) {
|
||||
for (Path patchJson : directoryStream) {
|
||||
if (patchJson.toString().endsWith(".json")) {
|
||||
|
||||
@@ -25,12 +25,12 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -129,13 +129,13 @@ public class ServerModpackManifest implements Validation {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fs the CurseForge modpack file.
|
||||
* @return the manifest.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @param zip the CurseForge modpack file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the server-manifest.json is missing or malformed.
|
||||
* @return the manifest.
|
||||
*/
|
||||
public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = FileUtils.readText(fs.getPath("server-manifest.json"));
|
||||
public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json", encoding);
|
||||
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
|
||||
return manifest.toModpack(encoding);
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Date;
|
||||
import java.util.logging.*;
|
||||
|
||||
@@ -35,7 +36,7 @@ public final class Logging {
|
||||
}
|
||||
|
||||
public static final Logger LOG = Logger.getLogger("HMCL");
|
||||
private static ByteArrayOutputStream storedLogs = new ByteArrayOutputStream();
|
||||
private static final ByteArrayOutputStream storedLogs = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE);
|
||||
|
||||
public static void start(Path logFolder) {
|
||||
LOG.setLevel(Level.ALL);
|
||||
@@ -93,21 +94,19 @@ public final class Logging {
|
||||
private static final class DefaultFormatter extends Formatter {
|
||||
|
||||
static final DefaultFormatter INSTANCE = new DefaultFormatter();
|
||||
private final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
|
||||
private static final MessageFormat format = new MessageFormat("[{0,date,HH:mm:ss}] [{1}.{2}/{3}] {4}\n");
|
||||
|
||||
@Override
|
||||
public String format(LogRecord record) {
|
||||
String date = format.format(new Date(record.getMillis()));
|
||||
String log = String.format("[%s] [%s.%s/%s] %s%n",
|
||||
date, record.getSourceClassName(), record.getSourceMethodName(),
|
||||
record.getLevel().getName(), record.getMessage()
|
||||
);
|
||||
ByteArrayOutputStream builder = new ByteArrayOutputStream();
|
||||
String log = format.format(new Object[]{
|
||||
new Date(record.getMillis()),
|
||||
record.getSourceClassName(), record.getSourceMethodName(), record.getLevel().getName(),
|
||||
record.getMessage()
|
||||
}, new StringBuffer(128), null).toString();
|
||||
if (record.getThrown() != null)
|
||||
try (PrintWriter writer = new PrintWriter(builder)) {
|
||||
record.getThrown().printStackTrace(writer);
|
||||
}
|
||||
return log + builder.toString();
|
||||
log += StringUtils.getStackTrace(record.getThrown());
|
||||
|
||||
return log;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.*;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -35,13 +35,11 @@ public final class StringUtils {
|
||||
}
|
||||
|
||||
public static String getStackTrace(Throwable throwable) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
try {
|
||||
throwable.printStackTrace(new PrintStream(stream, false, "UTF-8"));
|
||||
return stream.toString("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new InternalError(e);
|
||||
StringWriter stringWriter = new StringWriter(512);
|
||||
try (PrintWriter printWriter = new PrintWriter(stringWriter)) {
|
||||
throwable.printStackTrace(printWriter);
|
||||
}
|
||||
return stringWriter.toString();
|
||||
}
|
||||
|
||||
public static String getStackTrace(StackTraceElement[] elements) {
|
||||
@@ -258,6 +256,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 +297,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();
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import java.util.zip.ZipException;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class CompressingUtils {
|
||||
private static final ZipFileSystemProvider MY_ZIPFS_PROVIDER = new ZipFileSystemProvider();
|
||||
|
||||
private static final FileSystemProvider ZIPFS_PROVIDER = FileSystemProvider.installedProviders().stream()
|
||||
.filter(it -> "jar".equalsIgnoreCase(it.getScheme()))
|
||||
@@ -154,12 +153,12 @@ public final class CompressingUtils {
|
||||
return new Builder(zipFile, true).setUseTempFile(true);
|
||||
}
|
||||
|
||||
public static ZipFileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException {
|
||||
return new ZipFileSystem(MY_ZIPFS_PROVIDER, new ZipFile(zipFile.toFile()), true);
|
||||
public static FileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException {
|
||||
return createReadOnlyZipFileSystem(zipFile, null);
|
||||
}
|
||||
|
||||
public static ZipFileSystem createReadOnlyZipFileSystem(Path zipFile, Charset charset) throws IOException {
|
||||
return new ZipFileSystem(MY_ZIPFS_PROVIDER, new ZipFile(zipFile.toFile(), charset.name()), true);
|
||||
public static FileSystem createReadOnlyZipFileSystem(Path zipFile, Charset charset) throws IOException {
|
||||
return createZipFileSystem(zipFile, false, false, charset);
|
||||
}
|
||||
|
||||
public static FileSystem createWritableZipFileSystem(Path zipFile) throws IOException {
|
||||
@@ -200,7 +199,7 @@ public final class CompressingUtils {
|
||||
*/
|
||||
public static String readTextZipEntry(File zipFile, String name) throws IOException {
|
||||
try (ZipFile s = new ZipFile(zipFile)) {
|
||||
return readTextZipEntry(s, name);
|
||||
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,20 +208,15 @@ public final class CompressingUtils {
|
||||
*
|
||||
* @param zipFile the zip file
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @param encoding encoding of zip file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @return the plain text content of given file.
|
||||
*/
|
||||
public static String readTextZipEntry(Path zipFile, String name, Charset encoding) throws IOException {
|
||||
try (ZipFile s = new ZipFile(zipFile.toFile(), encoding.name())) {
|
||||
return readTextZipEntry(s, name);
|
||||
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
public static String readTextZipEntry(ZipFile s, String name) throws IOException {
|
||||
return IOUtils.readFullyAsString(s.getInputStream(s.getEntry(name)), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the text content of a file in zip.
|
||||
*
|
||||
@@ -230,7 +224,7 @@ public final class CompressingUtils {
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @return the plain text content of given file.
|
||||
*/
|
||||
public static Optional<String> readTextZipEntryQuietly(ZipFile file, String name) {
|
||||
public static Optional<String> readTextZipEntryQuietly(File file, String name) {
|
||||
try {
|
||||
return Optional.of(readTextZipEntry(file, name));
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
public final class FileSystemUtils {
|
||||
private FileSystemUtils() {
|
||||
}
|
||||
|
||||
private static char EOL = 0;
|
||||
|
||||
private static boolean isRegexMeta(char var0) {
|
||||
return ".^$+{[]|()".indexOf(var0) != -1;
|
||||
}
|
||||
|
||||
private static boolean isGlobMeta(char var0) {
|
||||
return "\\*?[{".indexOf(var0) != -1;
|
||||
}
|
||||
|
||||
private static char next(String var0, int var1) {
|
||||
return var1 < var0.length() ? var0.charAt(var1) : EOL;
|
||||
}
|
||||
|
||||
public static String toRegexPattern(String var0) {
|
||||
boolean var1 = false;
|
||||
StringBuilder var2 = new StringBuilder("^");
|
||||
int var3 = 0;
|
||||
|
||||
while(true) {
|
||||
while(var3 < var0.length()) {
|
||||
char var4 = var0.charAt(var3++);
|
||||
switch(var4) {
|
||||
case '*':
|
||||
if (next(var0, var3) == '*') {
|
||||
var2.append(".*");
|
||||
++var3;
|
||||
} else {
|
||||
var2.append("[^/]*");
|
||||
}
|
||||
break;
|
||||
case ',':
|
||||
if (var1) {
|
||||
var2.append(")|(?:");
|
||||
} else {
|
||||
var2.append(',');
|
||||
}
|
||||
break;
|
||||
case '/':
|
||||
var2.append(var4);
|
||||
break;
|
||||
case '?':
|
||||
var2.append("[^/]");
|
||||
break;
|
||||
case '[':
|
||||
var2.append("[[^/]&&[");
|
||||
if (next(var0, var3) == '^') {
|
||||
var2.append("\\^");
|
||||
++var3;
|
||||
} else {
|
||||
if (next(var0, var3) == '!') {
|
||||
var2.append('^');
|
||||
++var3;
|
||||
}
|
||||
|
||||
if (next(var0, var3) == '-') {
|
||||
var2.append('-');
|
||||
++var3;
|
||||
}
|
||||
}
|
||||
|
||||
boolean var6 = false;
|
||||
char var7 = 0;
|
||||
|
||||
while(var3 < var0.length()) {
|
||||
var4 = var0.charAt(var3++);
|
||||
if (var4 == ']') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (var4 == '/') {
|
||||
throw new PatternSyntaxException("Explicit 'name separator' in class", var0, var3 - 1);
|
||||
}
|
||||
|
||||
if (var4 == '\\' || var4 == '[' || var4 == '&' && next(var0, var3) == '&') {
|
||||
var2.append('\\');
|
||||
}
|
||||
|
||||
var2.append(var4);
|
||||
if (var4 == '-') {
|
||||
if (!var6) {
|
||||
throw new PatternSyntaxException("Invalid range", var0, var3 - 1);
|
||||
}
|
||||
|
||||
if ((var4 = next(var0, var3++)) == EOL || var4 == ']') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (var4 < var7) {
|
||||
throw new PatternSyntaxException("Invalid range", var0, var3 - 3);
|
||||
}
|
||||
|
||||
var2.append(var4);
|
||||
var6 = false;
|
||||
} else {
|
||||
var6 = true;
|
||||
var7 = var4;
|
||||
}
|
||||
}
|
||||
|
||||
if (var4 != ']') {
|
||||
throw new PatternSyntaxException("Missing ']", var0, var3 - 1);
|
||||
}
|
||||
|
||||
var2.append("]]");
|
||||
break;
|
||||
case '\\':
|
||||
if (var3 == var0.length()) {
|
||||
throw new PatternSyntaxException("No character to escape", var0, var3 - 1);
|
||||
}
|
||||
|
||||
char var5 = var0.charAt(var3++);
|
||||
if (isGlobMeta(var5) || isRegexMeta(var5)) {
|
||||
var2.append('\\');
|
||||
}
|
||||
|
||||
var2.append(var5);
|
||||
break;
|
||||
case '{':
|
||||
if (var1) {
|
||||
throw new PatternSyntaxException("Cannot nest groups", var0, var3 - 1);
|
||||
}
|
||||
|
||||
var2.append("(?:(?:");
|
||||
var1 = true;
|
||||
break;
|
||||
case '}':
|
||||
if (var1) {
|
||||
var2.append("))");
|
||||
var1 = false;
|
||||
} else {
|
||||
var2.append('}');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (isRegexMeta(var4)) {
|
||||
var2.append('\\');
|
||||
}
|
||||
|
||||
var2.append(var4);
|
||||
}
|
||||
}
|
||||
|
||||
if (var1) {
|
||||
throw new PatternSyntaxException("Missing '}", var0, var3 - 1);
|
||||
}
|
||||
|
||||
return var2.append('$').toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,11 +113,11 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static String readText(File file) throws IOException {
|
||||
return readText(file.toPath());
|
||||
return readText(file, UTF_8);
|
||||
}
|
||||
|
||||
public static String readText(File file, Charset charset) throws IOException {
|
||||
return readText(file.toPath(), charset);
|
||||
return new String(Files.readAllBytes(file.toPath()), charset);
|
||||
}
|
||||
|
||||
public static String readText(Path file) throws IOException {
|
||||
@@ -125,11 +125,7 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static String readText(Path file, Charset charset) throws IOException {
|
||||
return IOUtils.readFullyAsString(Files.newInputStream(file), charset);
|
||||
}
|
||||
|
||||
public static byte[] readAllBytes(Path file) throws IOException {
|
||||
return IOUtils.readFullyAsByteArray(Files.newInputStream(file));
|
||||
return new String(Files.readAllBytes(file), charset);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,7 +101,7 @@ public class Unzipper {
|
||||
*/
|
||||
public void unzip() throws IOException {
|
||||
Files.createDirectories(dest);
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile, encoding)) {
|
||||
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(encoding).setAutoDetectEncoding(true).build()) {
|
||||
Path root = fs.getPath(subDirectory);
|
||||
if (!root.isAbsolute() || (subDirectory.length() > 1 && subDirectory.endsWith("/")))
|
||||
throw new IllegalArgumentException("Subdirectory for unzipper must be absolute");
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
|
||||
public class ZipFileAttributes implements BasicFileAttributes {
|
||||
|
||||
private final long size;
|
||||
private final boolean symbolicLink;
|
||||
private final boolean regularFile;
|
||||
private final boolean directory;
|
||||
|
||||
public ZipFileAttributes(long size, boolean symbolicLink, boolean regularFile, boolean directory) {
|
||||
this.size = size;
|
||||
this.symbolicLink = symbolicLink;
|
||||
this.regularFile = regularFile;
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime lastModifiedTime() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime lastAccessTime() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime creationTime() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegularFile() {
|
||||
return regularFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSymbolicLink() {
|
||||
return symbolicLink;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOther() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fileKey() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
/*
|
||||
* 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.util.io;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.UserPrincipalLookupService;
|
||||
import java.nio.file.spi.FileSystemProvider;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.toIterable;
|
||||
import static org.jackhuang.hmcl.util.io.ZipPath.getPathComponents;
|
||||
|
||||
public class ZipFileSystem extends FileSystem {
|
||||
|
||||
private final ZipFileSystemProvider provider;
|
||||
private final ZipFile zipFile;
|
||||
private final boolean readOnly;
|
||||
private final IndexNode root;
|
||||
private final Map<String, IndexNode> entries = new HashMap<>();
|
||||
final ZipPath rootDir;
|
||||
|
||||
private volatile boolean isOpen = true;
|
||||
|
||||
public ZipFileSystem(ZipFileSystemProvider provider, ZipFile zipFile, boolean readOnly) {
|
||||
this.provider = provider;
|
||||
this.zipFile = zipFile;
|
||||
this.readOnly = readOnly;
|
||||
|
||||
this.root = new IndexNode(null, true, "");
|
||||
this.rootDir = new ZipPath(this, "/");
|
||||
|
||||
buildTree();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileSystemProvider provider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
isOpen = false;
|
||||
zipFile.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSeparator() {
|
||||
return "/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Path> getRootDirectories() {
|
||||
return Collections.singleton(rootDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<FileStore> getFileStores() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> supportedFileAttributeViews() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Path getPath(@NotNull String first, @NotNull String @NotNull ... more) {
|
||||
StringBuilder sb = new StringBuilder(first);
|
||||
for (String segment : more) {
|
||||
if (segment.length() > 0) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append('/');
|
||||
}
|
||||
sb.append(segment);
|
||||
}
|
||||
}
|
||||
return new ZipPath(this, sb.toString());
|
||||
}
|
||||
|
||||
ZipFileAttributes readAttributes(ZipPath path) {
|
||||
ensureOpen();
|
||||
|
||||
Optional<IndexNode> inode = getInode(path);
|
||||
if (!inode.isPresent()) return null;
|
||||
return inode.get().getAttributes();
|
||||
}
|
||||
|
||||
InputStream newInputStream(ZipPath path, OpenOption... options) throws IOException {
|
||||
ensureOpen();
|
||||
|
||||
ZipPath realPath = path.toRealPath();
|
||||
ZipArchiveEntry entry = zipFile.getEntry(realPath.getEntryName());
|
||||
return zipFile.getInputStream(entry);
|
||||
}
|
||||
|
||||
DirectoryStream<Path> newDirectoryStream(ZipPath dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
|
||||
Optional<IndexNode> inode = getInode(dir);
|
||||
if (!inode.isPresent() || !inode.get().isDirectory()) throw new NotDirectoryException(dir.toString());
|
||||
|
||||
List<ZipPath> list = new ArrayList<>();
|
||||
for (IndexNode child = inode.get().child; child != null; child = child.sibling) {
|
||||
list.add(new ZipPath(this, child.name));
|
||||
}
|
||||
|
||||
return new DirectoryStream<Path>() {
|
||||
private volatile boolean isClosed = false;
|
||||
private volatile Iterator<ZipPath> itr;
|
||||
|
||||
@Override
|
||||
public synchronized Iterator<Path> iterator() {
|
||||
if (isClosed)
|
||||
throw new ClosedDirectoryStreamException();
|
||||
if (itr != null)
|
||||
throw new IllegalStateException("Iterator has already been returned");
|
||||
itr = list.iterator();
|
||||
|
||||
return new Iterator<Path>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (isClosed) return false;
|
||||
return itr.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path next() {
|
||||
if (isClosed) throw new NoSuchElementException();
|
||||
return itr.next();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
isClosed = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void checkAccess(ZipPath path) throws IOException {
|
||||
ensureOpen();
|
||||
|
||||
if (!getInode(path.getEntryName()).isPresent()) {
|
||||
throw new NoSuchFileException(path.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static final String GLOB_SYNTAX = "glob";
|
||||
private static final String REGEX_SYNTAX = "regex";
|
||||
|
||||
@Override
|
||||
public PathMatcher getPathMatcher(String syntaxAndInput) {
|
||||
int pos = syntaxAndInput.indexOf(':');
|
||||
if (pos <= 0 || pos == syntaxAndInput.length()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
String syntax = syntaxAndInput.substring(0, pos);
|
||||
String input = syntaxAndInput.substring(pos + 1);
|
||||
String expr;
|
||||
if (syntax.equalsIgnoreCase(GLOB_SYNTAX)) {
|
||||
expr = FileSystemUtils.toRegexPattern(input);
|
||||
} else {
|
||||
if (syntax.equalsIgnoreCase(REGEX_SYNTAX)) {
|
||||
expr = input;
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Syntax '" + syntax +
|
||||
"' not recognized");
|
||||
}
|
||||
}
|
||||
// return matcher
|
||||
final Pattern pattern = Pattern.compile(expr);
|
||||
return path -> pattern.matcher(path.toString()).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserPrincipalLookupService getUserPrincipalLookupService() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WatchService newWatchService() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private void ensureOpen() {
|
||||
if (!isOpen) {
|
||||
throw new ClosedFileSystemException();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IndexNode> getInode(String entryName) {
|
||||
return Optional.ofNullable(entries.get(entryName));
|
||||
}
|
||||
|
||||
private Optional<IndexNode> getInode(ZipPath path) {
|
||||
return getInode(path.toAbsolutePath().normalize().getEntryName());
|
||||
}
|
||||
|
||||
private void buildTree() {
|
||||
entries.put("", root);
|
||||
|
||||
for (ZipArchiveEntry entry : toIterable(zipFile.getEntriesInPhysicalOrder())) {
|
||||
List<String> components = getPathComponents(entry.getName());
|
||||
|
||||
IndexNode node = new IndexNode(entry, entry.isDirectory(), String.join("/", components));
|
||||
entries.put(node.name, node);
|
||||
while (true) {
|
||||
if (components.size() == 0) break;
|
||||
if (components.size() == 1) {
|
||||
node.sibling = root.child;
|
||||
root.child = node;
|
||||
break;
|
||||
}
|
||||
|
||||
String parentName = String.join("/", components.subList(0, components.size() - 1));
|
||||
if (entries.containsKey(parentName)) {
|
||||
IndexNode parent = entries.get(parentName);
|
||||
node.sibling = parent.child;
|
||||
parent.child = node;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add new pseudo directory entry
|
||||
IndexNode parent = new IndexNode(null, true, parentName);
|
||||
entries.put(parentName, parent);
|
||||
node.sibling = parent.child;
|
||||
parent.child = node;
|
||||
node = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class IndexNode {
|
||||
private final ZipArchiveEntry entry;
|
||||
private final boolean isDirectory;
|
||||
private final String name;
|
||||
|
||||
private ZipFileAttributes attributes;
|
||||
|
||||
public IndexNode(ZipArchiveEntry entry, boolean isDirectory, String name) {
|
||||
this.entry = entry;
|
||||
this.isDirectory = isDirectory;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return isDirectory;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
if (entry == null) throw new IOException("Entry " + name + " cannot open");
|
||||
return zipFile.getInputStream(entry);
|
||||
}
|
||||
|
||||
public ZipFileAttributes getAttributes() {
|
||||
if (attributes == null) {
|
||||
if (entry == null) {
|
||||
attributes = new ZipFileAttributes(0, false, false, true);
|
||||
} else {
|
||||
attributes = new ZipFileAttributes(
|
||||
entry.getSize(),
|
||||
entry.isUnixSymlink(),
|
||||
!entry.isDirectory(),
|
||||
entry.isDirectory()
|
||||
);
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
IndexNode sibling;
|
||||
IndexNode child;
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* 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.util.io;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.nio.file.attribute.FileAttributeView;
|
||||
import java.nio.file.spi.FileSystemProvider;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.io.ZipPath.ensurePath;
|
||||
|
||||
public class ZipFileSystemProvider extends FileSystemProvider {
|
||||
@Override
|
||||
public String getScheme() {
|
||||
return "zip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipFileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileSystem getFileSystem(URI uri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Path getPath(@NotNull URI uri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
|
||||
ZipPath zipPath = ensurePath(path);
|
||||
return zipPath.getFileSystem().newInputStream(zipPath, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
|
||||
ZipPath zipPath = ensurePath(dir);
|
||||
return zipPath.getFileSystem().newDirectoryStream(zipPath, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
|
||||
throw new ReadOnlyFileSystemException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Path path) throws IOException {
|
||||
throw new ReadOnlyFileSystemException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(Path source, Path target, CopyOption... options) throws IOException {
|
||||
throw new ReadOnlyFileSystemException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(Path source, Path target, CopyOption... options) throws IOException {
|
||||
throw new ReadOnlyFileSystemException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameFile(Path path, Path path2) throws IOException {
|
||||
return path.toRealPath().equals(path2.toRealPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHidden(Path path) throws IOException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileStore getFileStore(Path path) throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkAccess(Path path, AccessMode... modes) throws IOException {
|
||||
ZipPath zipPath = ensurePath(path);
|
||||
zipPath.checkAccess(modes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
|
||||
if (type == BasicFileAttributes.class || type == ZipFileAttributes.class) {
|
||||
//noinspection unchecked
|
||||
return (A) ensurePath(path).getAttributes();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
|
||||
throw new ReadOnlyFileSystemException();
|
||||
}
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
/*
|
||||
* 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.util.io;
|
||||
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public class ZipPath implements Path {
|
||||
|
||||
private final ZipFileSystem zfs;
|
||||
private final List<String> path;
|
||||
private List<String> normalized;
|
||||
private final boolean absolute;
|
||||
|
||||
ZipPath(ZipFileSystem zfs, String path) {
|
||||
this(zfs, getPathComponents(path), path.startsWith("/"));
|
||||
}
|
||||
|
||||
ZipPath(ZipFileSystem zfs, List<String> path, boolean absolute) {
|
||||
this.zfs = zfs;
|
||||
this.path = path;
|
||||
this.absolute = absolute;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipFileSystem getFileSystem() {
|
||||
return zfs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAbsolute() {
|
||||
return absolute;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipPath getRoot() {
|
||||
if (this.isAbsolute())
|
||||
return zfs.rootDir;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipPath getFileName() {
|
||||
if (path.size() <= 1) return this;
|
||||
else return new ZipPath(zfs, Collections.singletonList(path.get(path.size() - 1)), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipPath getParent() {
|
||||
if (path.isEmpty()) return null;
|
||||
else if (path.size() == 1) return getRoot();
|
||||
else return new ZipPath(zfs, path.subList(0, path.size() - 1), absolute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNameCount() {
|
||||
return path.size();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath getName(int index) {
|
||||
if (index < 0 || index >= path.size()) throw new IllegalArgumentException();
|
||||
return new ZipPath(zfs, Collections.singletonList(path.get(index)), false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath subpath(int beginIndex, int endIndex) {
|
||||
if (beginIndex < 0 || beginIndex >= path.size() || endIndex > path.size() || beginIndex >= endIndex) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
return new ZipPath(zfs, path.subList(beginIndex, endIndex), absolute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startsWith(@NotNull Path other) {
|
||||
ZipPath p1 = this;
|
||||
ZipPath p2 = ensurePath(other);
|
||||
if (p1.isAbsolute() != p2.isAbsolute() || p1.path.size() < p2.path.size()) {
|
||||
return false;
|
||||
}
|
||||
int length = p2.path.size();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (!Objects.equals(p1.path.get(i), p2.path.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startsWith(@NotNull String other) {
|
||||
return startsWith(getFileSystem().getPath(other));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean endsWith(@NotNull Path other) {
|
||||
ZipPath p1 = this;
|
||||
ZipPath p2 = ensurePath(other);
|
||||
|
||||
if (p2.isAbsolute() && !p1.isAbsolute() ||
|
||||
p2.isAbsolute() && p1.isAbsolute() && p1.path.size() != p2.path.size() ||
|
||||
p1.path.size() < p2.path.size()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int length = p2.path.size();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (!Objects.equals(p1.path.get(p1.path.size() - i - 1), p2.path.get(p2.path.size() - i - 1))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean endsWith(@NotNull String other) {
|
||||
return endsWith(getFileSystem().getPath(other));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath normalize() {
|
||||
if (isNormalizable()) {
|
||||
doNormalize();
|
||||
return new ZipPath(zfs, normalized, absolute);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean isNormalizable() {
|
||||
for (String component : path) {
|
||||
if (".".equals(component) || "..".equals(component)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void doNormalize() {
|
||||
if (normalized != null) return;
|
||||
Stack<String> stack = new Stack<>();
|
||||
for (String component : path) {
|
||||
if (".".equals(component)) {
|
||||
continue;
|
||||
} else if ("..".equals(component)) {
|
||||
if (!stack.isEmpty()) stack.pop();
|
||||
} else {
|
||||
stack.push(component);
|
||||
}
|
||||
}
|
||||
normalized = new ArrayList<>(stack);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath resolve(@NotNull Path other) {
|
||||
ZipPath p1 = this;
|
||||
ZipPath p2 = ensurePath(other);
|
||||
if (p2.isAbsolute()) return p2;
|
||||
return new ZipPath(zfs, Lang.merge(p1.path, p2.path), absolute);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath resolve(@NotNull String other) {
|
||||
return resolve(getFileSystem().getPath(other));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath resolveSibling(@NotNull Path other) {
|
||||
ZipPath parent = getParent();
|
||||
return parent == null ? ensurePath(other) : parent.resolve(other);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath resolveSibling(@NotNull String other) {
|
||||
return resolveSibling(zfs.getPath(other));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Path relativize(@NotNull Path other) {
|
||||
ZipPath p1 = this;
|
||||
ZipPath p2 = ensurePath(other);
|
||||
|
||||
if (p2.equals(p1)) return new ZipPath(zfs, Collections.emptyList(), false);
|
||||
if (p1.isAbsolute() != p2.isAbsolute()) throw new IllegalArgumentException();
|
||||
|
||||
int l = Math.min(p1.path.size(), p2.path.size());
|
||||
int common = 0;
|
||||
while (common < l && Objects.equals(p1.path.get(common), p2.path.get(common))) common++;
|
||||
int up = p1.path.size() - common;
|
||||
List<String> result = new ArrayList<>();
|
||||
for (int i = 0; i < up; i++) result.add("..");
|
||||
result.addAll(p2.path);
|
||||
return new ZipPath(zfs, result, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public URI toUri() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath toAbsolutePath() {
|
||||
if (isAbsolute()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new ZipPath(zfs, path, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ZipPath toRealPath(@NotNull LinkOption... options) throws IOException {
|
||||
ZipPath absolute = toAbsolutePath().normalize();
|
||||
absolute.checkAccess();
|
||||
return absolute;
|
||||
}
|
||||
|
||||
void checkAccess(AccessMode... modes) throws IOException {
|
||||
boolean w = false;
|
||||
boolean x = false;
|
||||
for (AccessMode mode : modes) {
|
||||
switch (mode) {
|
||||
case READ:
|
||||
break;
|
||||
case WRITE:
|
||||
w = true;
|
||||
break;
|
||||
case EXECUTE:
|
||||
x = true;
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
zfs.checkAccess(toAbsolutePath().normalize());
|
||||
if ((w && zfs.isReadOnly()) || x) {
|
||||
throw new AccessDeniedException(toString());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public File toFile() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public WatchKey register(@NotNull WatchService watcher, @NotNull WatchEvent.Kind<?> @NotNull [] events, WatchEvent.Modifier... modifiers) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public WatchKey register(@NotNull WatchService watcher, @NotNull WatchEvent.Kind<?> @NotNull ... events) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Iterator<Path> iterator() {
|
||||
return new Iterator<Path>() {
|
||||
private int index = 0;
|
||||
|
||||
public boolean hasNext() {
|
||||
return index < getNameCount();
|
||||
}
|
||||
|
||||
public Path next() {
|
||||
if (index < getNameCount()) {
|
||||
return getName(index++);
|
||||
}
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ZipPath paths = (ZipPath) o;
|
||||
return absolute == paths.absolute && path.equals(paths.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(path, absolute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Path other) {
|
||||
ZipPath p1 = this;
|
||||
ZipPath p2 = ensurePath(other);
|
||||
return p1.toString().compareTo(p2.toString());
|
||||
}
|
||||
|
||||
ZipFileAttributes getAttributes() throws IOException {
|
||||
ZipFileAttributes attributes = zfs.readAttributes(this);
|
||||
if (attributes == null) throw new NoSuchFileException(toString());
|
||||
else return attributes;
|
||||
}
|
||||
|
||||
static List<String> getPathComponents(String path) {
|
||||
List<String> components = new ArrayList<>();
|
||||
int lastSlash = 0;
|
||||
for (int i = 0; i <= path.length(); i++) {
|
||||
if (i == path.length() || path.charAt(i) == '/' || path.charAt(i) == '\\') {
|
||||
if (i != lastSlash) {
|
||||
String component = path.substring(lastSlash, i);
|
||||
components.add(component);
|
||||
}
|
||||
|
||||
lastSlash = i + 1;
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
private static String normalizePath(String path) {
|
||||
return String.join("/", getPathComponents(path));
|
||||
}
|
||||
|
||||
static ZipPath ensurePath(Path path) {
|
||||
if (path == null) throw new NullPointerException();
|
||||
if (!(path instanceof ZipPath)) throw new ProviderMismatchException();
|
||||
return (ZipPath) path;
|
||||
}
|
||||
|
||||
String getEntryName() {
|
||||
if (!isAbsolute()) throw new IllegalStateException();
|
||||
return String.join("/", path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String str = String.join("/", path);
|
||||
if (absolute) return "/" + str;
|
||||
else return str;
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public final class Zipper implements Closeable {
|
||||
if (filter != null && !filter.test(relativePath.replace('\\', '/'))) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
Files.copy(file, root.resolve(relativePath));
|
||||
Files.copy(file, root.resolve(relativePath), StandardCopyOption.COPY_ATTRIBUTES);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@@ -104,11 +104,11 @@ public final class Zipper implements Closeable {
|
||||
}
|
||||
|
||||
public void putFile(Path file, String path) throws IOException {
|
||||
Files.copy(file, fs.getPath(path));
|
||||
Files.copy(file, fs.getPath(path), StandardCopyOption.COPY_ATTRIBUTES);
|
||||
}
|
||||
|
||||
public void putStream(InputStream in, String path) throws IOException {
|
||||
Files.copy(in, fs.getPath(path));
|
||||
Files.copy(in, fs.getPath(path), StandardCopyOption.COPY_ATTRIBUTES);
|
||||
}
|
||||
|
||||
public void putTextFile(String text, String path) throws IOException {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* 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.util.io;
|
||||
|
||||
public class ZipFileSystemTest {
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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.io;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
public class ZipPathTest {
|
||||
ZipFileSystemProvider provider = new ZipFileSystemProvider();
|
||||
ZipFileSystem zfs = new ZipFileSystem(provider, new ZipFile(new SeekableInMemoryByteChannel(IOUtils.readFullyAsByteArray(ZipPathTest.class.getResourceAsStream("/test.zip")))), true);
|
||||
|
||||
public ZipPathTest() throws IOException {
|
||||
|
||||
}
|
||||
|
||||
private Path p(String path) {
|
||||
return zfs.getPath(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizePath() throws IOException {
|
||||
BiConsumer<String, String> equals = (expected, actual) -> {
|
||||
assertEquals(zfs.getPath(expected), zfs.getPath(actual).toAbsolutePath().normalize());
|
||||
};
|
||||
|
||||
BiConsumer<String, String> notEquals = (expected, actual) -> {
|
||||
assertNotEquals(zfs.getPath(expected), zfs.getPath(actual).toAbsolutePath().normalize());
|
||||
};
|
||||
|
||||
equals.accept("/a/b/c/d", "/a\\b/c/d");
|
||||
equals.accept("/a/b/c/d", "/a\\b/c/d/");
|
||||
equals.accept("/a/b/c/d", "/a\\b/c//d");
|
||||
equals.accept("/a/b/c/d", "/a\\\\b/c/d");
|
||||
equals.accept("/a/b/c/d", "a/b/c/d");
|
||||
equals.accept("/a/b/c/d", "a/b/.c/../c/d");
|
||||
|
||||
notEquals.accept("/a/b/c/d", "/a\\b/c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelativizePath() throws IOException {
|
||||
assertEquals(p("../../a/b/c"), p("/a/b/c/a/b/c").relativize(p("/a/b/c/d/e")));
|
||||
|
||||
assertEquals(p("../.."), p("/a/b/c").relativize(p("/a/b/c/d/e")));
|
||||
assertEquals(p("../../"), p("/a/b/c").relativize(p("/a/b/c/d/e")));
|
||||
assertEquals(p("../../"), p("/a/b/c/").relativize(p("/a/b/c/d/e")));
|
||||
assertEquals(p("../.."), p("/a/b/c/").relativize(p("/a/b/c/d/e")));
|
||||
|
||||
assertEquals(p(""), p("/a/b/c/").relativize(p("/a/b/c")));
|
||||
assertEquals(p(""), p("/a/b/c").relativize(p("/a/b/c")));
|
||||
assertEquals(p(""), p("/a/b/c").relativize(p("/a/b/c/")));
|
||||
assertEquals(p(""), p("/a/b/c/").relativize(p("/a/b/c/")));
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@ No plugin API is provided.
|
||||
## Download
|
||||
Download the latest version [from the official website](https://hmcl.huangyuhui.net/download)
|
||||
|
||||
Note: Github releases are outdated.
|
||||
Note: The recent versions in Github release is the beta version, which contains extra testing functions compared to the release versions on the official website. However, they may be unstable and you're more likely to encounter bugs or unexpected problems.
|
||||
|
||||
If not necessary, it is recommended to download the ones from the official website.
|
||||
|
||||
## License
|
||||
The software is distributed under [GPL v3](https://www.gnu.org/licenses/gpl-3.0.html) with additional terms.
|
||||
|
||||
@@ -22,6 +22,8 @@ subprojects {
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
repositories {
|
||||
flatDir name: 'libs', dirs: rootProject.file('lib')
|
||||
|
||||
mavenCentral()
|
||||
|
||||
maven { url 'https://jitpack.io' }
|
||||
|
||||
Reference in new issue
Block a user