Compare commits
195
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebe880bd39 | ||
|
|
10fea79a35 | ||
|
|
451600448e | ||
|
|
7fccd6261e | ||
|
|
2436dab797 | ||
|
|
56538623a5 | ||
|
|
42a667c0ef | ||
|
|
5df95ad0fa | ||
|
|
345598016a | ||
|
|
6da38e537b | ||
|
|
e65f664821 | ||
|
|
cfa1eedae7 | ||
|
|
130c8d0e89 | ||
|
|
c4a2695b69 | ||
|
|
246069cc48 | ||
|
|
36a7d4a510 | ||
|
|
1b4a53f572 | ||
|
|
61a9d80e2c | ||
|
|
31568a5806 | ||
|
|
9aa73f5a5c | ||
|
|
35943feb08 | ||
|
|
73de21f936 | ||
|
|
4f1af87ba0 | ||
|
|
d129bd3e6b | ||
|
|
c2c52e8747 | ||
|
|
7e2ab49f89 | ||
|
|
3192d41fe3 | ||
|
|
ad5b260460 | ||
|
|
e57cb0ce51 | ||
|
|
9514357391 | ||
|
|
6fd0a40cc6 | ||
|
|
350d63231f | ||
|
|
cc28f1d5b5 | ||
|
|
4b73af9250 | ||
|
|
6e0efe521d | ||
|
|
041ab30778 | ||
|
|
678be4b92f | ||
|
|
74d759a45b | ||
|
|
295499c17b | ||
|
|
043d1c88c8 | ||
|
|
55e9b93b1c | ||
|
|
aa5e3e9697 | ||
|
|
fbf7ff6ebd | ||
|
|
3a5e8a827c | ||
|
|
6176dfaaf7 | ||
|
|
76c361e9bc | ||
|
|
ccb8981634 | ||
|
|
99b55d03c1 | ||
|
|
651fa6e2d1 | ||
|
|
4b258da0a0 | ||
|
|
9a66605616 | ||
|
|
da13895b72 | ||
|
|
c657a1d2cc | ||
|
|
eac8896f3e | ||
|
|
113f4f948b | ||
|
|
1b8dc1f75e | ||
|
|
001236e8e6 | ||
|
|
8025b8e24b | ||
|
|
e48bf012a6 | ||
|
|
94e32694b2 | ||
|
|
cbcf033b15 | ||
|
|
7da2671ec5 | ||
|
|
b6ea4c7003 | ||
|
|
49378061bf | ||
|
|
38013a9298 | ||
|
|
5ffbd16800 | ||
|
|
bcb29ef792 | ||
|
|
d20adb20d3 | ||
|
|
4f28cdd58f | ||
|
|
4b6b5c2f0e | ||
|
|
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 | ||
|
|
ec37b32eaa | ||
|
|
f893eeea1f | ||
|
|
8d559dba02 | ||
|
|
d167c712a6 | ||
|
|
8b224cb240 | ||
|
|
651aedaa50 | ||
|
|
a36b0ebed2 | ||
|
|
5a9e8683e4 | ||
|
|
843b29eff0 | ||
|
|
7bad0be2c4 | ||
|
|
5561011122 | ||
|
|
192a0c987e | ||
|
|
85a9a21e57 | ||
|
|
610864d674 | ||
|
|
06d0fa6e22 | ||
|
|
fb9a6fd314 | ||
|
|
5cab8ad41e | ||
|
|
d9f2b964c9 | ||
|
|
ac78b69ec2 | ||
|
|
516975688f | ||
|
|
7f07439388 | ||
|
|
bf87c4ac52 | ||
|
|
272132af51 | ||
|
|
04dd179631 | ||
|
|
57554c8d4b | ||
|
|
74a20bf11d | ||
|
|
16c96419d3 | ||
|
|
0b444bc5a2 | ||
|
|
7846e9e196 | ||
|
|
a74b971b98 | ||
|
|
6707ce1ed9 | ||
|
|
de421f93b5 | ||
|
|
1762e78cb3 | ||
|
|
2631f3a007 | ||
|
|
f70ec8a090 | ||
|
|
a4f22671c6 | ||
|
|
71c23df971 | ||
|
|
1bb2593c5b | ||
|
|
45d5ef4602 | ||
|
|
7b5b2596b9 | ||
|
|
23a805d576 | ||
|
|
f8cf3c22a1 | ||
|
|
8e5725946c | ||
|
|
99bb0d7a93 | ||
|
|
9695ddf8de | ||
|
|
50cff0bcba | ||
|
|
1832cf5924 | ||
|
|
10fba0a2b2 | ||
|
|
e2050fd47a | ||
|
|
f72da5973f | ||
|
|
0ebeffac32 | ||
|
|
dcfe724803 | ||
|
|
821020e7b6 | ||
|
|
33efc38f4c | ||
|
|
f01c0408e1 | ||
|
|
365ae0ef8c | ||
|
|
a6e593e3a1 | ||
|
|
67d38d6333 | ||
|
|
513d101fc4 | ||
|
|
5e1304f699 | ||
|
|
955a06d689 | ||
|
|
59e1a470e3 | ||
|
|
16952f5695 | ||
|
|
67feca075b | ||
|
|
12368375cf | ||
|
|
284ffb7aa0 | ||
|
|
1aefb26575 | ||
|
|
670d0f8462 | ||
|
|
3bbe422421 | ||
|
|
d68e988c15 | ||
|
|
74fd35047c | ||
|
|
21a9d19105 | ||
|
|
f662e983a4 | ||
|
|
7db379492a | ||
|
|
4a82daccec | ||
|
|
21e44bbc90 | ||
|
|
42a40a844a | ||
|
|
cd7c515097 | ||
|
|
9595626a27 | ||
|
|
db105c90b5 | ||
|
|
74a305c32a | ||
|
|
3af115ce37 | ||
|
|
b4e7934396 | ||
|
|
0a54037460 | ||
|
|
dd8da57cfb | ||
|
|
9b6f2d00ca | ||
|
|
2852821d2c |
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 反馈其他问题。
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Check Update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '30 * * * *'
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
|
||||
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
|
||||
|
||||
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- name: Check for existing tags
|
||||
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Sync to Gitee
|
||||
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
run:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Mirror Github to Gitee
|
||||
uses: Yikun/hub-mirror-action@v1.1
|
||||
with:
|
||||
src: github/huanghongxun
|
||||
dst: gitee/huanghongxun
|
||||
static_list: 'HMCL'
|
||||
force_update: true
|
||||
debug: true
|
||||
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
|
||||
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
|
||||
cache_path: /github/workspace/hub-mirror-cache
|
||||
@@ -12,15 +12,17 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: HMCL
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check and set HMCL version
|
||||
run: |
|
||||
export HMCL_VERSION=`echo ${{ github.ref }} | awk '{print substr($0,12)}'`
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
|
||||
if [ -z `echo $HMCL_VERSION | grep "^[0-9]\.[0-9]\.[0-9]\{3\}\$"` ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "HMCL_BUILD_NUMBER=`echo $HMCL_VERSION | awk '{print substr($0,5)}'`" >> $GITHUB_ENV
|
||||
- name: Download artifacts
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
run: |
|
||||
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
|
||||
+6
-3
@@ -17,20 +17,23 @@ NVIDIA
|
||||
/build/
|
||||
/HMCL/build/
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/log4j-patch/build/
|
||||
|
||||
# idea
|
||||
.idea
|
||||
/out/
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/HMCLTransformerDiscoveryService/out/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
|
||||
/minecraft/libraries/log4j-patch/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/HMCLTransformerDiscoveryService/bin/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
||||
/minecraft/libraries/log4j-patch/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'com.github.johnrengelman.shadow' version '7.0.0'
|
||||
}
|
||||
|
||||
import java.nio.file.FileSystems
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import java.nio.file.Files
|
||||
|
||||
import org.tukaani.xz.LZMA2Options
|
||||
import org.tukaani.xz.XZOutputStream
|
||||
import org.glavo.pack200.Pack200
|
||||
|
||||
def dev = null
|
||||
def shortcommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7) ?: null
|
||||
if (shortcommit != null && !shortcommit.isEmpty()) dev = "dev-" + shortcommit
|
||||
def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "SNAPSHOT"
|
||||
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
|
||||
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
|
||||
def versionroot = System.getenv("VERSION_ROOT") ?: "3.4"
|
||||
def microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
def microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
def versionType = System.getenv("VERSION_TYPE") ?: "nightly"
|
||||
version = versionroot + '.' + buildnumber
|
||||
|
||||
mainClassName = 'org.jackhuang.hmcl.Main'
|
||||
|
||||
dependencies {
|
||||
implementation project(":HMCLCore")
|
||||
implementation project(":JSTUN")
|
||||
implementation rootProject.files("lib/JFoenix.jar")
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
return MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
}
|
||||
|
||||
def createChecksum(File file) {
|
||||
def algorithm = "SHA-1"
|
||||
def suffix = "sha1"
|
||||
new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n"
|
||||
}
|
||||
|
||||
def attachSignature(File jar) {
|
||||
def keyLocation = System.getenv("HMCL_SIGNATURE_KEY");
|
||||
if (keyLocation == null)
|
||||
return
|
||||
def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes))
|
||||
|
||||
def signer = Signature.getInstance("SHA512withRSA")
|
||||
signer.initSign(privatekey)
|
||||
new ZipFile(jar).withCloseable { zip ->
|
||||
zip.stream()
|
||||
.sorted(Comparator.comparing { it.name })
|
||||
.filter { it.name != "META-INF/hmcl_signature" }
|
||||
.forEach {
|
||||
signer.update(digest("SHA-512", it.name.getBytes("UTF-8")))
|
||||
signer.update(digest("SHA-512", zip.getInputStream(it).bytes))
|
||||
}
|
||||
}
|
||||
def signature = signer.sign()
|
||||
|
||||
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs ->
|
||||
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it << signature }
|
||||
}
|
||||
}
|
||||
|
||||
ext.packer = Pack200.newPacker()
|
||||
packer.properties()["pack.effort"] = "9"
|
||||
ext.unpacker = Pack200.newUnpacker()
|
||||
|
||||
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
|
||||
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
|
||||
// .class files remain the same.
|
||||
def repack(File file) {
|
||||
def packed = new ByteArrayOutputStream()
|
||||
new JarFile(file).withCloseable { packer.pack(it, packed) }
|
||||
new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
java11 {
|
||||
java {
|
||||
srcDirs = ['src/main/java11']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileJava11Java {
|
||||
if(JavaVersion.current() < JavaVersion.VERSION_11) {
|
||||
javaCompiler = javaToolchains.compilerFor {
|
||||
languageVersion = JavaLanguageVersion.of(11)
|
||||
}
|
||||
}
|
||||
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn shadowJar
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = null
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
|
||||
'Main-Class': mainClassName,
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': project.version,
|
||||
'Microsoft-Auth-Id': microsoftAuthId,
|
||||
'Microsoft-Auth-Secret': microsoftAuthSecret,
|
||||
'Build-Channel': versionType,
|
||||
'Class-Path': 'pack200.jar',
|
||||
'Add-Opens': [
|
||||
'java.base/java.lang',
|
||||
'java.base/java.lang.reflect',
|
||||
'javafx.graphics/javafx.css',
|
||||
'javafx.base/com.sun.javafx.runtime',
|
||||
'javafx.controls/com.sun.javafx.scene.control.behavior',
|
||||
'javafx.controls/javafx.scene.control.skin',
|
||||
'javafx.controls/com.sun.javafx.scene.control',
|
||||
'javafx.base/com.sun.javafx.binding',
|
||||
'javafx.base/com.sun.javafx.event',
|
||||
'javafx.graphics/com.sun.javafx.stage'
|
||||
].join(" "),
|
||||
'Add-Exports': [
|
||||
'java.base/jdk.internal.loader',
|
||||
'javafx.controls/com.sun.javafx.scene.control.behavior',
|
||||
'javafx.controls/javafx.scene.control.skin',
|
||||
'javafx.controls/com.sun.javafx.scene.control',
|
||||
'javafx.base/com.sun.javafx.binding',
|
||||
'javafx.graphics/com.sun.javafx.stage',
|
||||
'javafx.base/com.sun.javafx.event'
|
||||
].join(" ")
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
task proguard(type: proguard.gradle.ProGuardTask) {
|
||||
dependsOn shadowJar
|
||||
injars shadowJar
|
||||
|
||||
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
|
||||
|
||||
|
||||
dontobfuscate
|
||||
dontoptimize
|
||||
keep 'public class org.jackhuang.**'
|
||||
keep 'public class org.slf4j.**'
|
||||
|
||||
dontwarn 'com.nqzero.**'
|
||||
dontwarn 'org.slf4j.**'
|
||||
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
|
||||
|
||||
adaptclassstrings
|
||||
|
||||
// next block taken verbatim from Proguard's documentation examples:
|
||||
|
||||
libraryjars files(configurations.compileClasspath.collect())
|
||||
|
||||
keepattributes 'SourceFile,LineNumberTable'
|
||||
|
||||
// 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 "${System.getProperty('java.home')}/lib/rt.jar"
|
||||
libraryjars "${System.getProperty('java.home')}/lib/ext/jfxrt.jar"
|
||||
// } else {
|
||||
// // As of Java 9, the runtime classes are packaged in modular jmod files.
|
||||
// libraryjars "${System.getProperty('java.home')}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
// }
|
||||
}
|
||||
|
||||
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
|
||||
output << jar.archivePath.bytes
|
||||
createChecksum(output)
|
||||
}
|
||||
|
||||
processResources {
|
||||
ext.convertToBSS = { String resource ->
|
||||
// exclude resource
|
||||
doFirst {
|
||||
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
|
||||
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
|
||||
bssFile.parentFile.mkdirs()
|
||||
javaexec {
|
||||
classpath = sourceSets.main.compileClasspath
|
||||
mainClass = "com.sun.javafx.css.parser.Css2Bin"
|
||||
args = [cssFile, bssFile]
|
||||
}
|
||||
}
|
||||
}
|
||||
from "build/compiled-resources"
|
||||
|
||||
convertToBSS "assets/css/root.css"
|
||||
convertToBSS "assets/css/blue.css"
|
||||
|
||||
into('META-INF/versions/11') {
|
||||
from sourceSets.java11.output
|
||||
}
|
||||
dependsOn java11Classes
|
||||
}
|
||||
|
||||
task makePack(dependsOn: jar) {
|
||||
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
|
||||
doLast {
|
||||
outputPath.newOutputStream().withCloseable { out ->
|
||||
new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) }
|
||||
}
|
||||
createChecksum(outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
task makePackXz(dependsOn: makePack) doLast {
|
||||
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
|
||||
// Our CI server does not have enough memory space to compress file at highest level.
|
||||
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
|
||||
createChecksum(packXz)
|
||||
}
|
||||
|
||||
task makePackGz(dependsOn: makePack) doLast {
|
||||
def packGz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".gz")
|
||||
new GZIPOutputStream(packGz.newOutputStream()).withCloseable { it << makePack.outputPath.bytes }
|
||||
createChecksum(packGz)
|
||||
}
|
||||
|
||||
|
||||
task makeExecutables(dependsOn: jar) doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
}
|
||||
|
||||
build.dependsOn makePackXz
|
||||
build.dependsOn makePackGz
|
||||
build.dependsOn makeExecutables
|
||||
@@ -0,0 +1,273 @@
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.URI
|
||||
|
||||
import org.glavo.pack200.Pack200
|
||||
import org.tukaani.xz.LZMA2Options
|
||||
import org.tukaani.xz.XZOutputStream
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
dependencies {
|
||||
classpath("org.tukaani:xz:1.8")
|
||||
classpath("org.glavo:pack200:0.3.0")
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
application
|
||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||
}
|
||||
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
||||
if (number != null) {
|
||||
(number - offset).toString()
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7)
|
||||
if (!shortCommit.isNullOrEmpty()) "dev-$shortCommit" else "SNAPSHOT"
|
||||
}
|
||||
}
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
|
||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val versionType = System.getenv("VERSION_TYPE") ?: "nightly"
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
|
||||
application {
|
||||
mainClass.set("org.jackhuang.hmcl.Main")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation("libs:JFoenix")
|
||||
|
||||
implementation("de.javawi.jstun:jstun:0.7.4")
|
||||
}
|
||||
|
||||
fun digest(algorithm: String, bytes: ByteArray) = MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
|
||||
fun createChecksum(file: File) {
|
||||
val algorithms = linkedMapOf(
|
||||
"MD5" to "md5",
|
||||
"SHA-1" to "sha1",
|
||||
"SHA-256" to "sha256",
|
||||
"SHA-512" to "sha512"
|
||||
)
|
||||
|
||||
algorithms.forEach { (algorithm, ext) ->
|
||||
File(file.parentFile, "${file.name}.$ext").writeText(
|
||||
digest(algorithm, file.readBytes()).joinToString(separator = "", postfix = "\n") { "%02x".format(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun attachSignature(jar: File) {
|
||||
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY") ?: return
|
||||
val privatekey = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(File(keyLocation).readBytes()))
|
||||
val signer = Signature.getInstance("SHA512withRSA")
|
||||
signer.initSign(privatekey)
|
||||
ZipFile(jar).use { zip ->
|
||||
zip.stream()
|
||||
.sorted(Comparator.comparing { it.name })
|
||||
.filter { it.name != "META-INF/hmcl_signature" }
|
||||
.forEach {
|
||||
signer.update(digest("SHA-512", it.name.toByteArray()))
|
||||
signer.update(digest("SHA-512", zip.getInputStream(it).readBytes()))
|
||||
}
|
||||
}
|
||||
val signature = signer.sign()
|
||||
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), emptyMap<String, Any>()).use { zipfs ->
|
||||
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).use { it.write(signature) }
|
||||
}
|
||||
}
|
||||
|
||||
val packer = Pack200.newPacker().apply {
|
||||
properties()["pack.effort"] = "9"
|
||||
}
|
||||
|
||||
val unpacker = Pack200.newUnpacker()
|
||||
|
||||
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
|
||||
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
|
||||
// .class files remain the same.
|
||||
fun repack(file: File) {
|
||||
val packed = ByteArrayOutputStream()
|
||||
JarFile(file).use { packer.pack(it, packed) }
|
||||
JarOutputStream(file.outputStream()).use { unpacker.unpack(ByteArrayInputStream(packed.toByteArray()), it) }
|
||||
}
|
||||
|
||||
val java11 = sourceSets.create("java11") {
|
||||
java {
|
||||
srcDir("src/main/java11")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
|
||||
if (JavaVersion.current() < JavaVersion.VERSION_11) {
|
||||
javaCompiler.set(javaToolchains.compilerFor {
|
||||
languageVersion.set(JavaLanguageVersion.of(11))
|
||||
})
|
||||
}
|
||||
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
enabled = false
|
||||
dependsOn(tasks["shadowJar"])
|
||||
}
|
||||
|
||||
val jarPath = tasks.jar.get().archiveFile.get().asFile
|
||||
|
||||
tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
|
||||
archiveClassifier.set(null as String?)
|
||||
|
||||
minimize {
|
||||
exclude(dependency("com.google.code.gson:.*:.*"))
|
||||
exclude(dependency("com.github.steveice10:.*:.*"))
|
||||
exclude(dependency("libs:JFoenix:.*"))
|
||||
}
|
||||
|
||||
manifest {
|
||||
attributes(
|
||||
"Created-By" to "Copyright(c) 2013-2021 huangyuhui.",
|
||||
"Main-Class" to application.mainClass.get(),
|
||||
"Multi-Release" to "true",
|
||||
"Implementation-Version" to project.version,
|
||||
"Microsoft-Auth-Id" to microsoftAuthId,
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"Build-Channel" to versionType,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
"java.base/java.lang.reflect",
|
||||
"javafx.graphics/javafx.css",
|
||||
"javafx.base/com.sun.javafx.runtime",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.base/com.sun.javafx.event",
|
||||
"javafx.graphics/com.sun.javafx.stage"
|
||||
).joinToString(" "),
|
||||
"Add-Exports" to listOf(
|
||||
"java.base/jdk.internal.loader",
|
||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||
"javafx.controls/javafx.scene.control.skin",
|
||||
"javafx.controls/com.sun.javafx.scene.control",
|
||||
"javafx.base/com.sun.javafx.binding",
|
||||
"javafx.graphics/com.sun.javafx.stage",
|
||||
"javafx.base/com.sun.javafx.event"
|
||||
).joinToString(" ")
|
||||
)
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jarPath) // see repack()
|
||||
attachSignature(jarPath)
|
||||
createChecksum(jarPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun createExecutable(suffix: String, header: String) {
|
||||
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + suffix)
|
||||
|
||||
output.outputStream().use {
|
||||
it.write(File(project.projectDir, header).readBytes())
|
||||
it.write(jarPath.readBytes())
|
||||
}
|
||||
|
||||
createChecksum(output)
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
fun convertToBSS(resource: String) {
|
||||
doFirst {
|
||||
val cssFile = File(projectDir, "src/main/resources/$resource")
|
||||
val bssFile = File(projectDir, "build/compiled-resources/${resource.substring(0, resource.length - 4)}.bss")
|
||||
bssFile.parentFile.mkdirs()
|
||||
javaexec {
|
||||
classpath = sourceSets["main"].compileClasspath
|
||||
mainClass.set("com.sun.javafx.css.parser.Css2Bin")
|
||||
args(cssFile, bssFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from("build/compiled-resources")
|
||||
|
||||
convertToBSS("assets/css/root.css")
|
||||
convertToBSS("assets/css/blue.css")
|
||||
|
||||
into("META-INF/versions/11") {
|
||||
from(sourceSets["java11"].output)
|
||||
}
|
||||
dependsOn(tasks["java11Classes"])
|
||||
}
|
||||
|
||||
val packFile = File(jarPath.parentFile, jarPath.nameWithoutExtension + ".pack")
|
||||
|
||||
val makePack = tasks.create("makePack") {
|
||||
dependsOn(tasks.jar)
|
||||
|
||||
doLast {
|
||||
packFile.outputStream().use { out ->
|
||||
JarFile(jarPath).use { jarFile -> packer.pack(jarFile, out) }
|
||||
}
|
||||
createChecksum(packFile)
|
||||
}
|
||||
}
|
||||
|
||||
val makePackXz = tasks.create("makePackXz") {
|
||||
dependsOn(makePack)
|
||||
|
||||
val packXz = File(packFile.parentFile, packFile.name + ".xz")
|
||||
|
||||
doLast {
|
||||
// Our CI server does not have enough memory space to compress file at highest level.
|
||||
XZOutputStream(packXz.outputStream(), LZMA2Options(5))
|
||||
.use { it.write(packFile.readBytes()) }
|
||||
createChecksum(packXz)
|
||||
}
|
||||
}
|
||||
|
||||
val makePackGz = tasks.create("makePackGz") {
|
||||
dependsOn(makePack)
|
||||
|
||||
val packGz = File(packFile.parentFile, packFile.name + ".gz")
|
||||
|
||||
doLast {
|
||||
GZIPOutputStream(packGz.outputStream()).use { it.write(packFile.readBytes()) }
|
||||
createChecksum(packGz)
|
||||
}
|
||||
}
|
||||
|
||||
val makeExecutables = tasks.create("makeExecutables") {
|
||||
dependsOn(makePack)
|
||||
|
||||
doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.build {
|
||||
dependsOn(makePackXz, makePackGz, makeExecutables)
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package moe.mickey.minecraft.skin.fx;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SkinAnimation {
|
||||
@@ -14,7 +14,7 @@ public class SkinAnimation {
|
||||
|
||||
@Deprecated
|
||||
public SkinAnimation() {
|
||||
this.transitions = new LinkedList<>();
|
||||
this.transitions = new ArrayList<>();
|
||||
}
|
||||
|
||||
public SkinAnimation(int weight, SkinTransition... transitions) {
|
||||
|
||||
@@ -2,14 +2,15 @@ package moe.mickey.minecraft.skin.fx;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class SkinAnimationPlayer {
|
||||
|
||||
protected final Random random = new Random();
|
||||
protected LinkedList<SkinAnimation> animations = new LinkedList<>();
|
||||
protected List<SkinAnimation> animations = new ArrayList<>();
|
||||
protected SkinAnimation playing;
|
||||
protected boolean running;
|
||||
protected int weightedSum = 0;
|
||||
@@ -28,7 +29,7 @@ public class SkinAnimationPlayer {
|
||||
}
|
||||
playing = tmp;
|
||||
if (playing == null && animations.size() > 0)
|
||||
playing = animations.getLast();
|
||||
playing = animations.get(animations.size() - 1);
|
||||
if (playing != null) {
|
||||
playing.playFromStart();
|
||||
lastPlayTime = now;
|
||||
|
||||
@@ -4,7 +4,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.shape.Mesh;
|
||||
import javafx.scene.shape.MeshView;
|
||||
import javafx.scene.shape.TriangleMesh;
|
||||
import org.jackhuang.hmcl.util.ArrayUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
public class SkinCube extends MeshView {
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
@@ -31,6 +32,7 @@ import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
@@ -61,11 +63,17 @@ public final class Launcher extends Application {
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (SambaException ignored) {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load config", e);
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_DIRECTORY.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
|
||||
}
|
||||
|
||||
// runLater to ensure ConfigHolder.init() finished initialization
|
||||
Platform.runLater(() -> {
|
||||
// When launcher visibility is set to "hide and reopen" without Platform.implicitExit = false,
|
||||
@@ -106,7 +114,9 @@ public final class Launcher extends Application {
|
||||
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
|
||||
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
|
||||
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
|
||||
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
@@ -116,7 +126,7 @@ public final class Launcher extends Application {
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
|
||||
launch(args);
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ package org.jackhuang.hmcl;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -33,7 +35,6 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
@@ -60,10 +61,7 @@ public final class Main {
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(() -> {
|
||||
fixLetsEncrypt();
|
||||
checkDSTRootCAX3();
|
||||
}, "CA Certificate Check", true);
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
@@ -106,28 +104,6 @@ public final class Main {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDSTRootCAX3() {
|
||||
TrustManagerFactory tmf;
|
||||
try {
|
||||
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
} catch (NoSuchAlgorithmException | KeyStoreException e) {
|
||||
LOG.log(Level.WARNING, "Failed to init TrustManagerFactory", e);
|
||||
// don't know what to do here
|
||||
return;
|
||||
}
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
|
||||
if ("CN=DST Root CA X3, O=Digital Signature Trust Co.".equals((cert.getSubjectDN().getName()))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
@@ -157,8 +133,9 @@ public final class Main {
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks");
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
merged.load(null, new char[0]);
|
||||
|
||||
@@ -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,10 +41,12 @@ 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;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
@@ -57,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;
|
||||
@@ -137,15 +144,26 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
File srcDir = getVersionRoot(srcId);
|
||||
File dstDir = getVersionRoot(dstId);
|
||||
Path srcDir = getVersionRoot(srcId).toPath();
|
||||
Path dstDir = getVersionRoot(dstId).toPath();
|
||||
|
||||
if (dstDir.exists()) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
|
||||
if (Files.exists(dstDir.toPath().resolve(srcId + ".jar"))) {
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
|
||||
Version fromVersion = getVersion(srcId);
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir, dstDir);
|
||||
|
||||
Path fromJson = dstDir.resolve(srcId + ".json");
|
||||
Path fromJar = dstDir.resolve(srcId + ".jar");
|
||||
Path toJson = dstDir.resolve(dstId + ".json");
|
||||
Path toJar = dstDir.resolve(dstId + ".jar");
|
||||
|
||||
if (Files.exists(fromJar)) {
|
||||
Files.move(fromJar, toJar);
|
||||
}
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
|
||||
Files.move(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
oldVersionSetting.setUsesGlobal(false);
|
||||
@@ -219,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);
|
||||
@@ -228,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()) {
|
||||
@@ -246,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) {
|
||||
@@ -293,7 +328,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) {
|
||||
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
|
||||
VersionSetting vs = getVersionSetting(version);
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
@@ -323,7 +358,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
.setUseNativeOpenAL(vs.isUseNativeOpenAL());
|
||||
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
|
||||
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
|
||||
.setJavaAgents(javaAgents);
|
||||
if (config().hasProxy()) {
|
||||
builder.setProxy(ProxyManager.getProxy());
|
||||
if (config().hasProxyAuth()) {
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
@@ -42,8 +42,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
|
||||
private final HMCLGameRepository repository;
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final Modpack modpack;
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
private final List<Task<?>> dependents = new LinkedList<>();
|
||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
|
||||
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
|
||||
dependency = profile.getDependency();
|
||||
|
||||
@@ -17,22 +17,20 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
|
||||
import org.jackhuang.hmcl.launch.PermissionException;
|
||||
import org.jackhuang.hmcl.launch.ProcessCreationException;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.launch.*;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
|
||||
@@ -41,15 +39,14 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
@@ -59,8 +56,13 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -68,6 +70,7 @@ import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
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;
|
||||
@@ -92,7 +95,8 @@ public final class LauncherHelper {
|
||||
this.launchingStepsPane.setTitle(i18n("version.launch"));
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
public void setTestMode() {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
@@ -125,9 +129,12 @@ public final class LauncherHelper {
|
||||
Optional<String> gameVersion = repository.getGameVersion(version);
|
||||
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
|
||||
CountDownLatch launchingLatch = new CountDownLatch(1);
|
||||
List<String> javaAgents = new ArrayList<>(0);
|
||||
|
||||
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
|
||||
|
||||
getLog4jPatch(version).ifPresent(javaAgents::add);
|
||||
|
||||
TaskExecutor executor = checkGameState(profile, setting, version)
|
||||
.thenComposeAsync(javaVersion -> {
|
||||
javaVersionRef.set(Objects.requireNonNull(javaVersion));
|
||||
@@ -173,7 +180,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
}).withStage("launch.state.logging_in"))
|
||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir());
|
||||
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
|
||||
return new HMCLGameLauncher(
|
||||
repository,
|
||||
version,
|
||||
@@ -283,6 +290,25 @@ public final class LauncherHelper {
|
||||
message = i18n("download.code.404", url);
|
||||
else
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
} else if (ex instanceof CommandTooLongException) {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
||||
(result, resolve, reject) -> {
|
||||
if (CommandBuilder.setExecutionPolicy()) {
|
||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||
resolve.run();
|
||||
} else {
|
||||
LOG.warning("Failed to set ExecutionPolicy");
|
||||
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
}
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (ex instanceof AccessDeniedException) {
|
||||
message = i18n("exception.access_denied", ((AccessDeniedException) ex).getFile());
|
||||
} else {
|
||||
message = StringUtils.getStackTrace(ex);
|
||||
}
|
||||
@@ -318,9 +344,28 @@ public final class LauncherHelper {
|
||||
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
|
||||
|
||||
if (setting.isJavaAutoSelected()) {
|
||||
// JavaVersionConstraint.VersionRange range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
|
||||
// TODO: download java 16 if necessary!
|
||||
Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
|
||||
GameJavaVersion targetJavaVersion;
|
||||
|
||||
if (range.getMandatory().contains(VersionNumber.asVersion("16"))) {
|
||||
targetJavaVersion = GameJavaVersion.JAVA_16;
|
||||
} else if (range.getMandatory().contains(VersionNumber.asVersion("1.8.0_51"))) {
|
||||
targetJavaVersion = GameJavaVersion.JAVA_8;
|
||||
} else {
|
||||
targetJavaVersion = null;
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null) {
|
||||
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
|
||||
.thenAcceptAsync(downloadedJavaVersion -> {
|
||||
future.complete(downloadedJavaVersion);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
|
||||
@@ -342,9 +387,9 @@ public final class LauncherHelper {
|
||||
|
||||
JavaVersionConstraint violatedMandatoryConstraint = null;
|
||||
JavaVersionConstraint violatedSuggestedConstraint = null;
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.values()) {
|
||||
if (constraint.getGameVersion().contains(gameVersion) && constraint.appliesToVersion(gameVersion, version)) {
|
||||
if (!constraint.getJavaVersion(version).contains(javaVersion.getVersionNumber())) {
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, javaVersion)) {
|
||||
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
|
||||
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
|
||||
violatedMandatoryConstraint = constraint;
|
||||
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
|
||||
@@ -372,59 +417,51 @@ public final class LauncherHelper {
|
||||
} else {
|
||||
switch (violatedMandatoryConstraint) {
|
||||
case GAME_JSON:
|
||||
MessageDialogPane dialog = new MessageDialogPane(
|
||||
i18n("launch.advice.require_newer_java_version",
|
||||
gameVersion.toString(),
|
||||
version.getJavaVersion().getMajorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION);
|
||||
|
||||
JFXButton linkButton = new JFXButton(i18n("download.external_link"));
|
||||
linkButton.setOnAction(e -> FXUtils.openLink("https://adoptopenjdk.net/"));
|
||||
linkButton.getStyleClass().add("dialog-accept");
|
||||
dialog.addButton(linkButton);
|
||||
|
||||
JFXButton yesButton = new JFXButton(i18n("button.ok"));
|
||||
yesButton.setOnAction(event -> {
|
||||
downloadJava(version.getJavaVersion(), profile)
|
||||
.thenAcceptAsync(x -> {
|
||||
try {
|
||||
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
|
||||
.filter(newJava -> newJava.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (newAcceptableJava.isPresent()) {
|
||||
setting.setJavaVersion(newAcceptableJava.get());
|
||||
future.complete(newAcceptableJava.get());
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.log(Level.SEVERE, "Cannot list javas", e);
|
||||
}
|
||||
future.complete(javaVersion);
|
||||
}, Platform::runLater)
|
||||
.exceptionally(Lang.handleUncaught);
|
||||
});
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
dialog.addButton(yesButton);
|
||||
|
||||
JFXButton noButton = new JFXButton(i18n("button.cancel"));
|
||||
noButton.getStyleClass().add("dialog-cancel");
|
||||
dialog.addButton(noButton);
|
||||
dialog.setCancelButton(noButton);
|
||||
|
||||
Controllers.dialog(dialog);
|
||||
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
|
||||
.thenAcceptAsync(downloadedJavaVersion -> {
|
||||
setting.setJavaVersion(downloadedJavaVersion);
|
||||
future.complete(downloadedJavaVersion);
|
||||
}, Schedulers.javafx())
|
||||
.whenCompleteAsync((result, throwable) -> {
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
breakAction.run();
|
||||
}, Schedulers.javafx());
|
||||
return Task.fromCompletableFuture(future);
|
||||
case VANILLA_JAVA_16:
|
||||
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
|
||||
FXUtils.openLink("https://adoptopenjdk.net/");
|
||||
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;
|
||||
case VANILLA_LINUX_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case VANILLA_X86:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
|
||||
// Windows on ARM introduced translation support for x86-64 after 10.0.21277.
|
||||
|| (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)) {
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_x86"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case LAUNCH_WRAPPER:
|
||||
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
@@ -432,13 +469,14 @@ public final class LauncherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (javaVersion.getPlatform() != Architecture.SYSTEM.getPlatform()) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64
|
||||
&& javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
|
||||
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.warning"), MessageType.ERROR, continueAction);
|
||||
suggested = true;
|
||||
}
|
||||
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
if (javaVersion.getBits() == Bits.BIT_32 &&
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
// Actual memory limit depends on operating system and memory.
|
||||
@@ -450,7 +488,7 @@ public final class LauncherHelper {
|
||||
suggested = true;
|
||||
switch (violatedSuggestedConstraint) {
|
||||
case MODDED_JAVA_7:
|
||||
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.error"), MessageType.ERROR, continueAction);
|
||||
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
return null;
|
||||
case MODDED_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
@@ -463,7 +501,7 @@ public final class LauncherHelper {
|
||||
|
||||
// Cannot allocate too much memory exceeding free space.
|
||||
if (!suggested && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
|
||||
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, null);
|
||||
Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, breakAction);
|
||||
suggested = true;
|
||||
}
|
||||
|
||||
@@ -475,7 +513,7 @@ public final class LauncherHelper {
|
||||
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, null);
|
||||
Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, breakAction);
|
||||
suggested = true;
|
||||
}
|
||||
}
|
||||
@@ -487,12 +525,12 @@ public final class LauncherHelper {
|
||||
VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0);
|
||||
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
|
||||
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) {
|
||||
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, null);
|
||||
Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, breakAction);
|
||||
suggested = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!suggested) {
|
||||
if (!future.isDone()) {
|
||||
future.complete(javaVersion);
|
||||
}
|
||||
|
||||
@@ -500,36 +538,114 @@ public final class LauncherHelper {
|
||||
}).withStage("launch.state.java");
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
|
||||
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
|
||||
link.setOnAction(e -> {
|
||||
FXUtils.openLink("https://docs.microsoft.com/zh-cn/java/openjdk/download");
|
||||
future.completeExceptionally(new CancellationException());
|
||||
});
|
||||
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("launch.advice.require_newer_java_version",
|
||||
gameVersion,
|
||||
javaVersion.getMajorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION)
|
||||
.addAction(link)
|
||||
.yesOrNo(() -> {
|
||||
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
|
||||
.thenAcceptAsync(downloadedJava -> {
|
||||
future.complete(downloadedJava);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolvedException = resolveException(throwable);
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
if (!(resolvedException instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
}
|
||||
future.completeExceptionally(new CancellationException());
|
||||
return null;
|
||||
});
|
||||
}, () -> {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
}).build());
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly start java downloading.
|
||||
*
|
||||
* @param javaVersion target Java version
|
||||
* @param downloadProvider download provider
|
||||
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
|
||||
*/
|
||||
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
|
||||
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
|
||||
profile.getDependency().getDownloadProvider()).executor(false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
super.onStop(success, executor);
|
||||
Platform.runLater(() -> {
|
||||
if (!success) {
|
||||
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion, downloadProvider)
|
||||
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
|
||||
if (exception != null) {
|
||||
future.completeExceptionally(exception);
|
||||
} else {
|
||||
future.complete(null);
|
||||
future.complete(downloadedJava);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.executor(false);
|
||||
|
||||
javaDownloadingPane.setExecutor(executor, true);
|
||||
Controllers.dialog(javaDownloadingPane);
|
||||
executor.start();
|
||||
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private static Optional<String> getLog4jPatch(Version version) {
|
||||
Optional<String> log4jVersion = version.getLibraries().stream()
|
||||
.filter(it -> it.is("org.apache.logging.log4j", "log4j-core")
|
||||
&& (VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "2.17") < 0 || "2.0-beta9".equals(it.getVersion())))
|
||||
.map(Library::getVersion)
|
||||
.findFirst();
|
||||
|
||||
if (log4jVersion.isPresent()) {
|
||||
final String agentFileName = "log4j-patch-agent-1.0.jar";
|
||||
|
||||
Path agentFile = Metadata.HMCL_DIRECTORY.resolve(agentFileName).toAbsolutePath();
|
||||
String agentFilePath = agentFile.toString();
|
||||
if (agentFilePath.indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the HMCL directory path, unable to attach log4j-patch");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (Files.notExists(agentFile)) {
|
||||
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/game/" + agentFileName)) {
|
||||
LOG.info("Extract log4j patch to " + agentFilePath);
|
||||
Files.createDirectories(agentFile.getParent());
|
||||
Files.copy(input, agentFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to extract log4j patch");
|
||||
try {
|
||||
Files.deleteIfExists(agentFile);
|
||||
} catch (IOException ex) {
|
||||
LOG.log(Level.WARNING, "Failed to delete incomplete log4j patch", ex);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isBeta = log4jVersion.get().startsWith("2.0-beta");
|
||||
return Optional.of(agentFilePath + "=" + isBeta);
|
||||
} else {
|
||||
LOG.info("No log4j with security vulnerabilities found");
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkExit() {
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
@@ -600,6 +716,11 @@ public final class LauncherHelper {
|
||||
|
||||
LOG.info("Launched process: " + command);
|
||||
|
||||
String classpath = process.getClasspath();
|
||||
if (classpath != null) {
|
||||
LOG.info("Process ClassPath: " + classpath);
|
||||
}
|
||||
|
||||
if (showLogs)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
@@ -695,6 +816,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static void stopManagedProcesses() {
|
||||
while (!PROCESSES.isEmpty())
|
||||
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
|
||||
|
||||
+54
-7
@@ -19,7 +19,9 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -39,7 +41,7 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MicrosoftAuthenticationServer extends NanoHTTPD implements MicrosoftService.OAuthSession {
|
||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private final int port;
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
@@ -47,7 +49,7 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
|
||||
private String idToken;
|
||||
|
||||
private MicrosoftAuthenticationServer(int port) {
|
||||
private OAuthServer(int port) {
|
||||
super(port);
|
||||
|
||||
this.port = port;
|
||||
@@ -102,7 +104,7 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(MicrosoftAuthenticationServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||
@@ -119,10 +121,12 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
||||
}
|
||||
|
||||
public static class Factory implements MicrosoftService.OAuthCallback {
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||
|
||||
@Override
|
||||
public MicrosoftService.OAuthSession startServer() throws IOException, AuthenticationException {
|
||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||
if (StringUtils.isBlank(getClientId())) {
|
||||
throw new MicrosoftAuthenticationNotSupportedException();
|
||||
}
|
||||
@@ -130,7 +134,7 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
IOException exception = null;
|
||||
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
|
||||
try {
|
||||
MicrosoftAuthenticationServer server = new MicrosoftAuthenticationServer(port);
|
||||
OAuthServer server = new OAuthServer(port);
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
|
||||
return server;
|
||||
} catch (IOException e) {
|
||||
@@ -140,10 +144,17 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void grantDeviceCode(String userCode, String verificationURI) {
|
||||
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -158,6 +169,42 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
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 {
|
||||
private final String userCode;
|
||||
private final String verificationUri;
|
||||
|
||||
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
|
||||
super(source);
|
||||
this.userCode = userCode;
|
||||
this.verificationUri = verificationUri;
|
||||
}
|
||||
|
||||
public String getUserCode() {
|
||||
return userCode;
|
||||
}
|
||||
|
||||
public String getVerificationUri() {
|
||||
return verificationUri;
|
||||
}
|
||||
}
|
||||
|
||||
public static class OpenBrowserEvent extends Event {
|
||||
private final String url;
|
||||
|
||||
public OpenBrowserEvent(Object source, String url) {
|
||||
super(source);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MicrosoftAuthenticationNotSupportedException extends AuthenticationException {
|
||||
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
@@ -75,12 +75,12 @@ public final class Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
public static final MicrosoftService.OAuthCallback MICROSOFT_OAUTH_CALLBACK = new MicrosoftAuthenticationServer.Factory();
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MICROSOFT_OAUTH_CALLBACK));
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
@@ -374,7 +374,7 @@ public final class Accounts {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family_probably");
|
||||
} else if (exception instanceof MicrosoftAuthenticationServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
return i18n("account.methods.microsoft.snapshot");
|
||||
} else if (exception instanceof OAuthAccount.WrongAccountException) {
|
||||
return i18n("account.failed.wrong_account");
|
||||
|
||||
@@ -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,18 +26,16 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.*;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public final class ConfigHolder {
|
||||
|
||||
private ConfigHolder() {}
|
||||
private ConfigHolder() {
|
||||
}
|
||||
|
||||
public static final String CONFIG_FILENAME = "hmcl.json";
|
||||
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
|
||||
@@ -97,9 +95,16 @@ public final class ConfigHolder {
|
||||
}
|
||||
|
||||
if (!Files.isWritable(configLocation)) {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& configLocation.getFileSystem() == FileSystems.getDefault()
|
||||
&& configLocation.toFile().canWrite()) {
|
||||
// There are some serious problems with the implementation of Samba or OpenJDK
|
||||
throw new SambaException();
|
||||
} else {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +174,7 @@ public final class ConfigHolder {
|
||||
private static void writeToConfig(String content) throws IOException {
|
||||
LOG.info("Saving config");
|
||||
synchronized (configLocation) {
|
||||
Files.write(configLocation, content.getBytes(UTF_8));
|
||||
FileUtils.saveSafely(configLocation, content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +218,7 @@ public final class ConfigHolder {
|
||||
private static void writeToGlobalConfig(String content) throws IOException {
|
||||
LOG.info("Saving global config");
|
||||
synchronized (GLOBAL_CONFIG_PATH) {
|
||||
Files.write(GLOBAL_CONFIG_PATH, content.getBytes(UTF_8));
|
||||
FileUtils.saveSafely(GLOBAL_CONFIG_PATH, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -33,6 +34,7 @@ import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -137,7 +139,7 @@ public final class DownloadProviders {
|
||||
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Exception exception) {
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
@@ -152,10 +154,18 @@ public final class DownloadProviders {
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.access_denied", url, ((AccessDeniedException) exception.getCause()).getFile());
|
||||
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());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else if (exception instanceof CancellationException) {
|
||||
return i18n("message.cancelled");
|
||||
}
|
||||
return StringUtils.getStackTrace(exception);
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@ public enum EnumBackgroundImage {
|
||||
DEFAULT,
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
}
|
||||
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.setting;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -57,11 +57,11 @@ public final class HMCLAccounts {
|
||||
String scope = "openid offline_access";
|
||||
|
||||
return Task.supplyAsync(() -> {
|
||||
MicrosoftService.OAuthSession session = Accounts.MICROSOFT_OAUTH_CALLBACK.startServer();
|
||||
Accounts.MICROSOFT_OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
|
||||
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
|
||||
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
mapOf(
|
||||
pair("client_id", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientId()),
|
||||
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
|
||||
pair("response_type", "id_token code"),
|
||||
pair("response_mode", "form_post"),
|
||||
pair("scope", scope),
|
||||
@@ -72,12 +72,12 @@ public final class HMCLAccounts {
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
|
||||
.form(mapOf(pair("client_id", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientSecret()),
|
||||
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
|
||||
.getString();
|
||||
MicrosoftService.LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
MicrosoftService.LiveAuthorizationResponse.class);
|
||||
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
OAuth.AuthorizationResponse.class);
|
||||
|
||||
HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
|
||||
.header("Token-Type", response.tokenType)
|
||||
|
||||
@@ -43,5 +43,9 @@ public enum LauncherVisibility {
|
||||
/**
|
||||
* Hide the launcher and reopen it when game closes.
|
||||
*/
|
||||
HIDE_AND_REOPEN
|
||||
HIDE_AND_REOPEN;
|
||||
|
||||
public boolean isDaemon() {
|
||||
return this != CLOSE;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -204,7 +204,7 @@ public final class Profiles {
|
||||
return selectedVersion.get();
|
||||
}
|
||||
|
||||
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
|
||||
private static final List<Consumer<Profile>> versionsListeners = new ArrayList<>(4);
|
||||
|
||||
public static void registerVersionsListener(Consumer<Profile> listener) {
|
||||
Profile profile = getSelectedProfile();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public final class SambaException extends RuntimeException {
|
||||
public SambaException() {
|
||||
}
|
||||
|
||||
public SambaException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SambaException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public SambaException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -55,7 +56,7 @@ public class Theme {
|
||||
|
||||
Theme(String name, String color) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.color = Objects.requireNonNull(color);
|
||||
this.paint = Color.web(color);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -59,7 +60,7 @@ public final class VersionSetting implements Cloneable {
|
||||
this.global = global;
|
||||
}
|
||||
|
||||
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
|
||||
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
|
||||
|
||||
public BooleanProperty usesGlobalProperty() {
|
||||
return usesGlobalProperty;
|
||||
@@ -567,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
|
||||
|
||||
/**
|
||||
@@ -608,7 +623,7 @@ public final class VersionSetting implements Cloneable {
|
||||
if (checkJava)
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
else
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Architecture.SYSTEM.getPlatform());
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
|
||||
} catch (IOException | InvalidPathException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
}
|
||||
@@ -669,6 +684,7 @@ public final class VersionSetting implements Cloneable {
|
||||
defaultJavaPathProperty.addListener(listener);
|
||||
nativesDirProperty.addListener(listener);
|
||||
nativesDirTypeProperty.addListener(listener);
|
||||
versionIcon.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -702,6 +718,7 @@ public final class VersionSetting implements Cloneable {
|
||||
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
|
||||
versionSetting.setLauncherVisibility(getLauncherVisibility());
|
||||
versionSetting.setNativesDir(getNativesDir());
|
||||
versionSetting.setVersionIcon(getVersionIcon());
|
||||
return versionSetting;
|
||||
}
|
||||
|
||||
@@ -740,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;
|
||||
}
|
||||
@@ -783,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;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -333,7 +334,7 @@ public final class FXUtils {
|
||||
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
|
||||
animationKey = "FXUTILS.ANIMATION." + animationKey;
|
||||
Object oldTimeline = node.getProperties().get(animationKey);
|
||||
if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
if (timeline != null) timeline.play();
|
||||
node.getProperties().put(animationKey, timeline);
|
||||
}
|
||||
@@ -700,7 +701,7 @@ public final class FXUtils {
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
@@ -719,6 +720,10 @@ public final class FXUtils {
|
||||
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
|
||||
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
|
||||
texts.add(hyperlink);
|
||||
} else if ("b".equals(element.getTagName())) {
|
||||
Text text = new Text(element.getTextContent());
|
||||
text.getStyleClass().add("bold");
|
||||
texts.add(text);
|
||||
} else if ("br".equals(element.getTagName())) {
|
||||
texts.add(new Text("\n"));
|
||||
} else {
|
||||
@@ -728,12 +733,17 @@ public final class FXUtils {
|
||||
texts.add(new Text(node.getTextContent()));
|
||||
}
|
||||
}
|
||||
final TextFlow tf = new TextFlow(texts.toArray(new javafx.scene.Node[0]));
|
||||
return tf;
|
||||
return texts;
|
||||
} catch (SAXException | ParserConfigurationException | IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse xml", e);
|
||||
return new TextFlow(new Text(segment));
|
||||
return Collections.singletonList(new Text(segment));
|
||||
}
|
||||
}
|
||||
|
||||
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
|
||||
TextFlow tf = new TextFlow();
|
||||
tf.getChildren().setAll(parseSegment(segment, hyperlinkAction));
|
||||
return tf;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.*;
|
||||
@@ -69,9 +72,9 @@ public class GameCrashWindow extends Stage {
|
||||
private final String memory;
|
||||
private final String java;
|
||||
private final LibraryAnalyzer analyzer;
|
||||
private final StringProperty os = new SimpleStringProperty(System.getProperty("os.name"));
|
||||
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCHITECTURE);
|
||||
private final StringProperty reason = new SimpleStringProperty(i18n("game.crash.reason.unknown"));
|
||||
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
|
||||
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
|
||||
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"));
|
||||
|
||||
@@ -81,9 +84,9 @@ public class GameCrashWindow extends Stage {
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final LinkedList<Pair<String, Log4jLevel>> logs;
|
||||
private final List<Pair<String, Log4jLevel>> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, LinkedList<Pair<String, Log4jLevel>> logs) {
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
@@ -93,7 +96,10 @@ public class GameCrashWindow extends Stage {
|
||||
this.analyzer = LibraryAnalyzer.analyze(version);
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
java = launchOptions.getJava().getVersion();
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
|
||||
|
||||
this.view = new View();
|
||||
|
||||
@@ -110,14 +116,18 @@ public class GameCrashWindow extends Stage {
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
|
||||
Set<String> keywords = Collections.emptySet();
|
||||
String crashReport = null;
|
||||
try {
|
||||
String crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
if (crashReport != null) {
|
||||
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
|
||||
}
|
||||
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to read crash report", e);
|
||||
}
|
||||
if (crashReport == null) {
|
||||
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
|
||||
}
|
||||
if (crashReport != null) {
|
||||
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
|
||||
}
|
||||
return pair(
|
||||
CrashReportAnalyzer.anaylze(rawLog),
|
||||
keywords);
|
||||
@@ -126,49 +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:
|
||||
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);
|
||||
@@ -207,6 +221,7 @@ public class GameCrashWindow extends Stage {
|
||||
LogWindow logWindow = new LogWindow();
|
||||
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
|
||||
if (managedProcess.getClasspath() != null) logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
|
||||
@@ -300,13 +315,15 @@ public class GameCrashWindow extends Stage {
|
||||
moddedPane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
analyzer.getVersion(type).ifPresent(ver -> {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.getStyleClass().setAll("two-line-item-second-large");
|
||||
item.setTitle(i18n("install.installer." + type.getPatchId()));
|
||||
item.setSubtitle(ver);
|
||||
moddedPane.getChildren().add(item);
|
||||
});
|
||||
if (!type.getPatchId().isEmpty()) {
|
||||
analyzer.getVersion(type).ifPresent(ver -> {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.getStyleClass().setAll("two-line-item-second-large");
|
||||
item.setTitle(i18n("install.installer." + type.getPatchId()));
|
||||
item.setSubtitle(ver);
|
||||
moddedPane.getChildren().add(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,14 +339,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();
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
@@ -41,23 +42,23 @@ public class UpgradeDialog extends JFXDialogLayout {
|
||||
|
||||
{
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
try {
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
WebEngine engine = webView.getEngine();
|
||||
engine.load(CHANGELOG_URL + remoteVersion.getChannel().channelName);
|
||||
engine.load(url);
|
||||
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
|
||||
String viewURL = engine.getLoadWorker().getMessage().trim();
|
||||
if (!viewURL.startsWith(CHANGELOG_URL)) {
|
||||
engine.getLoadWorker().cancel();
|
||||
FXUtils.openLink(viewURL);
|
||||
if (newValue == Worker.State.FAILED) {
|
||||
LOG.warning("Failed to load update log, trying to open it in browser");
|
||||
FXUtils.openLink(url);
|
||||
setBody();
|
||||
}
|
||||
});
|
||||
setBody(webView);
|
||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||
LOG.log(Level.WARNING, "WebView is missing or initialization failed", e);
|
||||
FXUtils.openLink(url);
|
||||
}
|
||||
setBody(webView);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -23,7 +23,9 @@ import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -43,7 +45,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -53,6 +55,7 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
@@ -88,6 +91,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private final Pane detailsContainer;
|
||||
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private TaskExecutor loginTask;
|
||||
|
||||
@@ -217,6 +222,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
|
||||
logging.set(true);
|
||||
deviceCode.set(null);
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
@@ -262,15 +268,23 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||
VBox vbox = new VBox(8);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
}
|
||||
});
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
|
||||
runInFX(() -> deviceCode.set(value));
|
||||
}));
|
||||
|
||||
HBox box = new HBox(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -9,13 +9,16 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.OAuthAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
@@ -27,7 +30,9 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
private final OAuthAccount account;
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
|
||||
this.account = account;
|
||||
@@ -40,13 +45,17 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
Label usernameLabel = new Label(account.getUsername());
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
}
|
||||
});
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,15 +71,21 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
|
||||
vbox.getChildren().setAll(usernameLabel, hintPane, box);
|
||||
setBody(vbox);
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
|
||||
}
|
||||
|
||||
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
|
||||
FXUtils.runInFX(() -> {
|
||||
deviceCode.set(event);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
logging.set(true);
|
||||
Task.supplyAsync(account::logInWhenCredentialsExpired)
|
||||
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
|
||||
logging.set(false);
|
||||
if (exception == null) {
|
||||
success.accept(authInfo);
|
||||
onSuccess();
|
||||
|
||||
@@ -63,6 +63,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
|
||||
updateContent(newView);
|
||||
|
||||
if (previousNode == EMPTY_PANE) {
|
||||
setMouseTransparent(false);
|
||||
getChildren().setAll(newView);
|
||||
return;
|
||||
}
|
||||
|
||||
transition.init(this);
|
||||
|
||||
// runLater or "init" will not work
|
||||
|
||||
@@ -19,37 +19,26 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class AdvancedListBox extends ScrollPane {
|
||||
private final VBox container = new VBox();
|
||||
public class AdvancedListBox extends VBox {
|
||||
|
||||
{
|
||||
setContent(container);
|
||||
|
||||
FXUtils.smoothScrolling(this);
|
||||
|
||||
setFitToHeight(true);
|
||||
setFitToWidth(true);
|
||||
setHbarPolicy(ScrollBarPolicy.NEVER);
|
||||
|
||||
container.getStyleClass().add("advanced-list-box-content");
|
||||
getStyleClass().add("advanced-list-box-content");
|
||||
}
|
||||
|
||||
public AdvancedListBox add(Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(child);
|
||||
getChildren().add(child);
|
||||
else {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("advanced-list-box-item");
|
||||
pane.getChildren().setAll(child);
|
||||
container.getChildren().add(pane);
|
||||
getChildren().add(pane);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -64,27 +53,27 @@ public class AdvancedListBox extends ScrollPane {
|
||||
|
||||
public AdvancedListBox add(int index, Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(index, child);
|
||||
getChildren().add(index, child);
|
||||
else {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("advanced-list-box-item");
|
||||
pane.getChildren().setAll(child);
|
||||
container.getChildren().add(index, pane);
|
||||
getChildren().add(index, pane);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdvancedListBox remove(Node child) {
|
||||
container.getChildren().remove(indexOf(child));
|
||||
getChildren().remove(indexOf(child));
|
||||
return this;
|
||||
}
|
||||
|
||||
public int indexOf(Node child) {
|
||||
if (child instanceof Pane) {
|
||||
return container.getChildren().indexOf(child);
|
||||
return getChildren().indexOf(child);
|
||||
} else {
|
||||
for (int i = 0; i < container.getChildren().size(); ++i) {
|
||||
Node node = container.getChildren().get(i);
|
||||
for (int i = 0; i < getChildren().size(); ++i) {
|
||||
Node node = getChildren().get(i);
|
||||
if (node instanceof StackPane) {
|
||||
ObservableList<Node> list = ((StackPane) node).getChildren();
|
||||
if (list.size() == 1 && list.get(0) == child)
|
||||
@@ -99,11 +88,7 @@ public class AdvancedListBox extends ScrollPane {
|
||||
return add(new ClassTitle(category));
|
||||
}
|
||||
|
||||
public void setSpacing(double spacing) {
|
||||
container.setSpacing(spacing);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
container.getChildren().clear();
|
||||
getChildren().clear();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
public class HintPane extends VBox {
|
||||
@@ -84,6 +86,10 @@ public class HintPane extends VBox {
|
||||
this.text.set(text);
|
||||
}
|
||||
|
||||
public void setSegment(String segment) {
|
||||
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
|
||||
}
|
||||
|
||||
public void setChildren(Node... children) {
|
||||
flow.getChildren().setAll(children);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -99,7 +100,7 @@ public final class MessageDialogPane extends StackPane {
|
||||
});
|
||||
}
|
||||
|
||||
public void addButton(ButtonBase btn) {
|
||||
public void addButton(Node btn) {
|
||||
btn.addEventHandler(ActionEvent.ACTION, e -> fireEvent(new DialogCloseEvent()));
|
||||
actions.getChildren().add(btn);
|
||||
}
|
||||
@@ -119,6 +120,11 @@ public final class MessageDialogPane extends StackPane {
|
||||
this.dialog = new MessageDialogPane(text, title, type);
|
||||
}
|
||||
|
||||
public Builder addAction(Node actionNode) {
|
||||
dialog.addButton(actionNode);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder ok(Runnable ok) {
|
||||
JFXButton btnOk = new JFXButton(i18n("button.ok"));
|
||||
btnOk.getStyleClass().add("dialog-accept");
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -61,7 +62,7 @@ public class MultiFileItem<T> extends VBox {
|
||||
});
|
||||
selectedData.addListener((a, b, newValue) -> {
|
||||
Optional<Toggle> selecting = group.getToggles().stream()
|
||||
.filter(it -> it.getUserData() == newValue)
|
||||
.filter(it -> Objects.equals(it.getUserData(), newValue))
|
||||
.findFirst();
|
||||
if (!selecting.isPresent()) {
|
||||
selecting = group.getToggles().stream()
|
||||
@@ -226,6 +227,11 @@ public class MultiFileItem<T> extends VBox {
|
||||
|
||||
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
|
||||
customField.disableProperty().bind(left.selectedProperty().not());
|
||||
|
||||
if (!customField.getValidators().isEmpty()) {
|
||||
FXUtils.setValidateWhileTextChanged(customField, true);
|
||||
}
|
||||
|
||||
pane.setRight(customField);
|
||||
|
||||
return pane;
|
||||
|
||||
@@ -25,7 +25,6 @@ import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -52,8 +51,8 @@ public class TwoLineListItem extends VBox {
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
|
||||
FlowPane firstLine = new FlowPane();
|
||||
firstLine.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
|
||||
Label lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
@@ -63,7 +62,7 @@ public class TwoLineListItem extends VBox {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setText(tag);
|
||||
FlowPane.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
});
|
||||
firstLineChildren = new AggregatedObservableList<>();
|
||||
|
||||
@@ -24,11 +24,13 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
@@ -43,6 +45,7 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -140,9 +143,15 @@ public class DecoratorController {
|
||||
.orElse(null);
|
||||
}
|
||||
if (config().getBackgroundImageType() == EnumBackgroundImage.NETWORK) {
|
||||
image = new Image(config().getBackgroundImageUrl(), true);
|
||||
if (!NetworkUtils.isURL(config().getBackgroundImageUrl())) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
} else {
|
||||
image = new Image(config().getBackgroundImageUrl(), true);
|
||||
}
|
||||
} else if (config().getBackgroundImageType() == EnumBackgroundImage.CLASSIC) {
|
||||
image = newImage("/assets/img/background-classic.jpg");
|
||||
} else if (config().getBackgroundImageType() == EnumBackgroundImage.TRANSLUCENT) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}
|
||||
if (image == null) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
|
||||
@@ -37,7 +37,6 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -56,7 +55,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final StackPane titleContainer;
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
private final StackPane leftPane;
|
||||
|
||||
private double xOffset, yOffset, newX, newY, initX, initY;
|
||||
private boolean titleBarTransparent = true;
|
||||
@@ -109,15 +107,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
StackPane container = new StackPane();
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
// animation layer at bottom
|
||||
HBox drawerPane = new HBox();
|
||||
{
|
||||
leftPane = new StackPane();
|
||||
leftPane.setPrefWidth(0);
|
||||
leftPane.getStyleClass().add("jfx-decorator-drawer");
|
||||
drawerPane.getChildren().setAll(leftPane);
|
||||
}
|
||||
|
||||
// content layer at middle
|
||||
{
|
||||
StackPane contentPlaceHolder = new StackPane();
|
||||
@@ -159,16 +148,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
container.setBackground(null);
|
||||
titleContainer.getStyleClass().remove("background");
|
||||
titleContainer.getStyleClass().add("gray-background");
|
||||
container.getChildren().remove(drawerPane);
|
||||
wrapper.getChildren().add(0, drawerPane);
|
||||
} else {
|
||||
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
wrapper.backgroundProperty().unbind();
|
||||
wrapper.setBackground(null);
|
||||
titleContainer.getStyleClass().add("background");
|
||||
titleContainer.getStyleClass().remove("gray-background");
|
||||
wrapper.getChildren().remove(drawerPane);
|
||||
container.getChildren().add(0, drawerPane);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -180,6 +165,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
navBarPane = new TransitionPane();
|
||||
navBarPane.setId("decoratorTitleTransitionPane");
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
|
||||
@@ -197,9 +183,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(node);
|
||||
}
|
||||
|
||||
FXUtils.playAnimation(leftPane, "animation",
|
||||
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
|
||||
});
|
||||
titleBar.setCenter(navBarPane);
|
||||
titleBar.setRight(buttonsContainerPlaceHolder);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.zip.ZipException;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -173,6 +174,8 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
} else if (exception instanceof VersionMismatchException) {
|
||||
VersionMismatchException e = ((VersionMismatchException) exception);
|
||||
Controllers.dialog(i18n("install.failed.version_mismatch", e.getExpect(), e.getActual()), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
} else if (exception instanceof CancellationException) {
|
||||
// Ignore cancel
|
||||
} else {
|
||||
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
@@ -39,7 +40,9 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
@@ -48,6 +51,7 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
|
||||
@@ -57,6 +61,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -83,6 +88,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
@FXML
|
||||
private JFXCheckBox chkOld;
|
||||
@FXML
|
||||
private JFXButton btnRefresh;
|
||||
@FXML
|
||||
private HBox checkPane;
|
||||
@FXML
|
||||
private ComponentList centrePane;
|
||||
@@ -113,6 +120,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
chkSnapshot.selectedProperty().addListener(listener);
|
||||
chkOld.selectedProperty().addListener(listener);
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
|
||||
|
||||
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
|
||||
IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
@@ -279,6 +288,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
@FXML
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/bmclapi_sponsor");
|
||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -147,7 +147,7 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
|
||||
|
||||
@FXML
|
||||
private void onNext() {
|
||||
LinkedList<String> list = new LinkedList<>();
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
getFilesNeeded(rootNode, "minecraft", list);
|
||||
controller.getSettings().put(MODPACK_FILE_SELECTION, list);
|
||||
controller.onFinish();
|
||||
|
||||
@@ -167,9 +167,9 @@ public class AboutPage extends StackPane {
|
||||
jstun.setExternalLink("https://github.com/tking/JSTUN");
|
||||
|
||||
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
|
||||
jstun.setTitle("minecraft-jfx-skin");
|
||||
jstun.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
|
||||
jstun.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
|
||||
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
|
||||
minecraftJFXSkin.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
|
||||
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
|
||||
|
||||
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, jstun, minecraftJFXSkin);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.HMCLAccounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -128,7 +128,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
content.setMouseTransparent(true);
|
||||
content.setMouseTransparent(false);
|
||||
setSelectable();
|
||||
|
||||
likeButton.getStyleClass().add("toggle-icon4");
|
||||
@@ -152,7 +152,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
"#" + feedback.getId(),
|
||||
i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)),
|
||||
i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US)));
|
||||
container.setOnMouseClicked(e -> {
|
||||
content.setOnMouseClicked(e -> {
|
||||
getFeedback(feedback.getId())
|
||||
.thenAcceptAsync(Schedulers.javafx(), f -> {
|
||||
Controllers.dialog(new ViewFeedbackDialog(f));
|
||||
@@ -246,8 +246,8 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
|
||||
}
|
||||
});
|
||||
vbox.getChildren().setAll(hintPane);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,10 +102,12 @@ public class PersonalizationPage extends StackPane {
|
||||
backgroundItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT),
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.classic"), EnumBackgroundImage.CLASSIC),
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.translucent"), EnumBackgroundImage.TRANSLUCENT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.background.choose"))
|
||||
.bindBidirectional(config().backgroundImageProperty()),
|
||||
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
|
||||
.setValidators(new URLValidator(true))
|
||||
.bindBidirectional(config().backgroundImageUrlProperty())
|
||||
));
|
||||
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
|
||||
|
||||
@@ -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,12 +87,18 @@ public final class MultiplayerChannel {
|
||||
}
|
||||
|
||||
public static class JoinResponse extends Response {
|
||||
private final String sessionName;
|
||||
private final int port;
|
||||
|
||||
public JoinResponse(int port) {
|
||||
public JoinResponse(String sessionName, int port) {
|
||||
this.sessionName = sessionName;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getSessionName() {
|
||||
return sessionName;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
@@ -120,6 +126,10 @@ public final class MultiplayerChannel {
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static final String VERSION_NOT_MATCHED = "version_not_matched";
|
||||
public static final String KICKED = "kicked";
|
||||
public static final String JOIN_ACEEPTANCE_TIMEOUT = "join_acceptance_timeout";
|
||||
}
|
||||
|
||||
public static class CatoClient extends Event {
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.io.*;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.TimerTask;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -42,7 +43,7 @@ public class MultiplayerClient extends Thread {
|
||||
|
||||
private final EventManager<ConnectedEvent> onConnected = new EventManager<>();
|
||||
private final EventManager<Event> onDisconnected = new EventManager<>();
|
||||
private final EventManager<Event> onKicked = new EventManager<>();
|
||||
private final EventManager<KickEvent> onKicked = new EventManager<>();
|
||||
private final EventManager<Event> onHandshake = new EventManager<>();
|
||||
|
||||
public MultiplayerClient(String id, int port) {
|
||||
@@ -53,11 +54,11 @@ public class MultiplayerClient extends Thread {
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
public void setGamePort(int gamePort) {
|
||||
public synchronized void setGamePort(int gamePort) {
|
||||
this.gamePort = gamePort;
|
||||
}
|
||||
|
||||
public int getGamePort() {
|
||||
public synchronized int getGamePort() {
|
||||
return gamePort;
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ public class MultiplayerClient extends Thread {
|
||||
return onDisconnected;
|
||||
}
|
||||
|
||||
public EventManager<Event> onKicked() {
|
||||
public EventManager<KickEvent> onKicked() {
|
||||
return onKicked;
|
||||
}
|
||||
|
||||
@@ -83,9 +84,10 @@ public class MultiplayerClient extends Thread {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
KeepAliveThread keepAliveThread = null;
|
||||
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
|
||||
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());
|
||||
@@ -106,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) {
|
||||
@@ -124,12 +126,12 @@ public class MultiplayerClient extends Thread {
|
||||
|
||||
connected = true;
|
||||
|
||||
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getPort()));
|
||||
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getSessionName(), joinResponse.getPort()));
|
||||
|
||||
LOG.fine("Received join response with port " + joinResponse.getPort());
|
||||
} else if (response instanceof KickResponse) {
|
||||
LOG.fine("Kicked by the server");
|
||||
onKicked.fireEvent(new Event(this));
|
||||
onKicked.fireEvent(new KickEvent(this, ((KickResponse) response).getMsg()));
|
||||
return;
|
||||
} else if (response instanceof KeepAliveResponse) {
|
||||
} else if (response instanceof HandshakeResponse) {
|
||||
@@ -192,15 +194,34 @@ public class MultiplayerClient extends Thread {
|
||||
}
|
||||
|
||||
public static class ConnectedEvent extends Event {
|
||||
private final String sessionName;
|
||||
private final int port;
|
||||
|
||||
public ConnectedEvent(Object source, int port) {
|
||||
public ConnectedEvent(Object source, String sessionName, int port) {
|
||||
super(source);
|
||||
this.sessionName = sessionName;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getSessionName() {
|
||||
return sessionName;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
public static class KickEvent extends Event {
|
||||
private final String reason;
|
||||
|
||||
public KickEvent(Object source, String reason) {
|
||||
super(source);
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,18 @@ package org.jackhuang.hmcl.ui.multiplayer;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.application.Platform;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.launch.StreamPump;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
@@ -35,61 +38,82 @@ import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
/**
|
||||
* Cato Management.
|
||||
*/
|
||||
public final class MultiplayerManager {
|
||||
private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/";
|
||||
static final String CATO_VERSION = "1.0.c";
|
||||
static final String CATO_VERSION = "1.2.0-202112171527";
|
||||
private static final String CATO_DOWNLOAD_URL = "https://gitcode.net/huanghongxun1/ioi_bin/-/raw/3.5.3/client/";
|
||||
private static final String CATO_HASH_URL = CATO_DOWNLOAD_URL + "cato-all-files.sha1";
|
||||
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 CompletableFuture<Map<String, String>> HASH;
|
||||
|
||||
private MultiplayerManager() {
|
||||
}
|
||||
|
||||
private static CompletableFuture<Map<String, String>> getCatoHash() {
|
||||
FXUtils.checkFxUserThread();
|
||||
if (HASH == null) {
|
||||
HASH = CompletableFuture.supplyAsync(wrap(() -> {
|
||||
String hashList = HttpRequest.GET(CATO_HASH_URL).getString();
|
||||
Map<String, String> hashes = new HashMap<>();
|
||||
for (String line : hashList.split("\n")) {
|
||||
String[] items = line.trim().split(" ");
|
||||
if (items.length == 2 && items[0].length() == 40) {
|
||||
hashes.put(items[1], items[0]);
|
||||
} else {
|
||||
LOG.warning("Failed to parse cato hash file, hash line " + line);
|
||||
}
|
||||
}
|
||||
return hashes;
|
||||
}));
|
||||
}
|
||||
return HASH;
|
||||
}
|
||||
|
||||
public static Task<Void> downloadCato() {
|
||||
return new FileDownloadTask(
|
||||
NetworkUtils.toURL(CATO_DOWNLOAD_URL + CATO_PATH),
|
||||
getCatoExecutable().toFile()
|
||||
).thenRunAsync(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
Set<PosixFilePermission> perm = Files.getPosixFilePermissions(getCatoExecutable());
|
||||
perm.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
Files.setPosixFilePermissions(getCatoExecutable(), perm);
|
||||
}
|
||||
});
|
||||
return Task.fromCompletableFuture(getCatoHash()).thenComposeAsync(catoHashes ->
|
||||
new FileDownloadTask(
|
||||
NetworkUtils.toURL(CATO_DOWNLOAD_URL + getCatoFileName()),
|
||||
getCatoExecutable().toFile(),
|
||||
catoHashes.get(getCatoFileName()) == null ? null : new FileDownloadTask.IntegrityCheck("SHA-1", catoHashes.get(getCatoFileName()))
|
||||
).thenRunAsync(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
Set<PosixFilePermission> perm = Files.getPosixFilePermissions(getCatoExecutable());
|
||||
perm.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
Files.setPosixFilePermissions(getCatoExecutable(), perm);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static Path getCatoExecutable() {
|
||||
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
|
||||
}
|
||||
|
||||
public static CompletableFuture<CatoSession> joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
|
||||
if (!CATO_VERSION.equals(version)) {
|
||||
throw new IncompatibleCatoVersionException(version, CATO_VERSION);
|
||||
}
|
||||
|
||||
LOG.info(String.format("Joining session (token=%s,version=%s,sessionName=%s,peer=%s,mode=%s,remotePort=%d,localPort=%d)", token, version, sessionName, peer, mode, remotePort, localPort));
|
||||
|
||||
return CompletableFuture.completedFuture(null).thenComposeAsync(unused -> {
|
||||
private static CompletableFuture<CatoSession> startCato(String token, State state) {
|
||||
return getCatoHash().thenApplyAsync(wrap(catoHashes -> {
|
||||
Path exe = getCatoExecutable();
|
||||
if (!Files.isRegularFile(exe)) {
|
||||
throw new CatoNotExistsException(exe);
|
||||
@@ -99,72 +123,94 @@ public final class MultiplayerManager {
|
||||
throw new CatoAlreadyStartedException();
|
||||
}
|
||||
|
||||
String[] commands = new String[]{exe.toString(),
|
||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||
"--id", peer,
|
||||
"--local", String.format("%s:%d", LOCAL_ADDRESS, localPort),
|
||||
"--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort),
|
||||
"--mode", mode.getName()};
|
||||
Process process;
|
||||
try {
|
||||
process = new ProcessBuilder()
|
||||
.command(commands)
|
||||
.start();
|
||||
String hash = catoHashes.get(getCatoFileName());
|
||||
if (hash != null) {
|
||||
ChecksumMismatchException.verifyChecksum(exe, "SHA-1", hash);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
Files.deleteIfExists(exe);
|
||||
throw e;
|
||||
}
|
||||
|
||||
CatoSession session = new CatoSession(sessionName, State.SLAVE, process, Arrays.asList(commands));
|
||||
String[] commands = new String[]{exe.toString(), "-client.token", StringUtils.isBlank(token) ? "new" : token};
|
||||
Process process = new ProcessBuilder()
|
||||
.command(commands)
|
||||
.start();
|
||||
|
||||
return new CatoSession(state, process, Arrays.asList(commands));
|
||||
}));
|
||||
}
|
||||
|
||||
public static CompletableFuture<CatoSession> joinSession(String token, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
|
||||
LOG.info(String.format("Joining session (token=%s,peer=%s,mode=%s,remotePort=%d,localPort=%d)", token, peer, mode, remotePort, localPort));
|
||||
|
||||
return startCato(token, State.SLAVE).thenComposeAsync(wrap(session -> {
|
||||
CompletableFuture<CatoSession> future = new CompletableFuture<>();
|
||||
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
|
||||
session.forwardPort(peer, LOCAL_ADDRESS, localPort, REMOTE_ADDRESS, remotePort, mode);
|
||||
|
||||
session.onExit().register(() -> {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to close cato session stdin writer", e);
|
||||
Consumer<CatoExitEvent> onExit = event -> {
|
||||
boolean ready = session.isReady();
|
||||
switch (event.getExitCode()) {
|
||||
case 1:
|
||||
if (!ready) {
|
||||
future.completeExceptionally(new CatoExitTimeoutException());
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
future.completeExceptionally(new CatoExitException(event.getExitCode(), ready));
|
||||
};
|
||||
session.onExit.register(onExit);
|
||||
|
||||
TimerTask peerConnectionTimeoutTask = Lang.setTimeout(() -> {
|
||||
future.completeExceptionally(new PeerConnectionTimeoutException());
|
||||
session.stop();
|
||||
}, 15 * 1000);
|
||||
|
||||
session.onPeerConnected.register(event -> {
|
||||
peerConnectionTimeoutTask.cancel();
|
||||
|
||||
MultiplayerClient client = new MultiplayerClient(session.getId(), localPort);
|
||||
session.addRelatedThread(client);
|
||||
session.setClient(client);
|
||||
|
||||
TimerTask task = Lang.setTimeout(() -> {
|
||||
future.completeExceptionally(new JoinRequestTimeoutException());
|
||||
Platform.runLater(() -> future.completeExceptionally(new JoinRequestTimeoutException()));
|
||||
session.stop();
|
||||
}, 30 * 1000);
|
||||
|
||||
client.onConnected().register(connectedEvent -> {
|
||||
try {
|
||||
int port = findAvailablePort();
|
||||
String command = String.format("net add %s %s:%d %s:%d %s", peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), mode.getName());
|
||||
LOG.info("Invoking cato: " + command);
|
||||
session.forwardPort(peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), mode);
|
||||
session.addRelatedThread(Lang.thread(new LocalServerBroadcaster(port, session), "LocalServerBroadcaster", true));
|
||||
session.setName(connectedEvent.getSessionName());
|
||||
client.setGamePort(port);
|
||||
writer.write(command);
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
future.complete(session);
|
||||
Platform.runLater(() -> {
|
||||
session.onExit.unregister(onExit);
|
||||
future.complete(session);
|
||||
});
|
||||
} catch (IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
session.stop();
|
||||
Platform.runLater(() -> future.completeExceptionally(e));
|
||||
}
|
||||
task.cancel();
|
||||
});
|
||||
client.onKicked().register(kickedEvent -> {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
session.stop();
|
||||
task.cancel();
|
||||
Platform.runLater(() -> {
|
||||
future.completeExceptionally(new KickedException(kickedEvent.getReason()));
|
||||
});
|
||||
});
|
||||
client.onDisconnected().register(disconnectedEvent -> {
|
||||
if (!client.isConnected()) {
|
||||
// We fail to establish connection with server
|
||||
future.completeExceptionally(new ConnectionErrorException());
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
if (!client.isConnected()) {
|
||||
// We fail to establish connection with server
|
||||
future.completeExceptionally(new ConnectionErrorException());
|
||||
}
|
||||
});
|
||||
});
|
||||
client.onHandshake().register(handshakeEvent -> {
|
||||
if (handler != null) {
|
||||
@@ -175,40 +221,62 @@ public final class MultiplayerManager {
|
||||
});
|
||||
|
||||
return future;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public static CatoSession createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) throws IOException {
|
||||
Path exe = getCatoExecutable();
|
||||
if (!Files.isRegularFile(exe)) {
|
||||
throw new FileNotFoundException("Cato file not found");
|
||||
}
|
||||
|
||||
if (!isPortAvailable(3478)) {
|
||||
throw new CatoAlreadyStartedException();
|
||||
}
|
||||
|
||||
public static CompletableFuture<CatoSession> createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) {
|
||||
LOG.info(String.format("Creating session (token=%s,sessionName=%s,gamePort=%d)", token, sessionName, gamePort));
|
||||
|
||||
MultiplayerServer server = new MultiplayerServer(gamePort, allowAllJoinRequests);
|
||||
server.startServer();
|
||||
return startCato(token, State.MASTER).thenComposeAsync(wrap(session -> {
|
||||
CompletableFuture<CatoSession> future = new CompletableFuture<>();
|
||||
|
||||
String[] commands = new String[]{exe.toString(),
|
||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||
"--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort)};
|
||||
Process process = new ProcessBuilder()
|
||||
.command(commands)
|
||||
.start();
|
||||
MultiplayerServer server = new MultiplayerServer(sessionName, gamePort, allowAllJoinRequests);
|
||||
server.startServer();
|
||||
|
||||
CatoSession session = new CatoSession(sessionName, State.MASTER, process, Arrays.asList(commands));
|
||||
session.setServer(server);
|
||||
session.addRelatedThread(server);
|
||||
return session;
|
||||
session.setName(sessionName);
|
||||
session.allowForwardingAddress(REMOTE_ADDRESS, server.getPort());
|
||||
session.allowForwardingAddress(REMOTE_ADDRESS, gamePort);
|
||||
session.showAllowedAddress();
|
||||
|
||||
Consumer<CatoExitEvent> onExit = event -> {
|
||||
boolean ready = session.isReady();
|
||||
switch (event.getExitCode()) {
|
||||
case 1:
|
||||
if (!ready) {
|
||||
future.completeExceptionally(new CatoExitTimeoutException());
|
||||
}
|
||||
break;
|
||||
}
|
||||
future.completeExceptionally(new CatoExitException(event.getExitCode(), ready));
|
||||
};
|
||||
|
||||
session.onExit.register(onExit);
|
||||
session.setServer(server);
|
||||
session.addRelatedThread(server);
|
||||
|
||||
TimerTask peerConnectionTimeoutTask = Lang.setTimeout(() -> {
|
||||
future.completeExceptionally(new PeerConnectionTimeoutException());
|
||||
session.stop();
|
||||
}, 15 * 1000);
|
||||
|
||||
session.onPeerConnected.register(event -> {
|
||||
peerConnectionTimeoutTask.cancel();
|
||||
Platform.runLater(() -> {
|
||||
session.onExit.unregister(onExit);
|
||||
future.complete(session);
|
||||
});
|
||||
});
|
||||
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
|
||||
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>.*?)#(?<port>\\d{2,5})$");
|
||||
|
||||
public static Invitation parseInvitationCode(String invitationCode) throws JsonParseException {
|
||||
String json = new String(Base64.getDecoder().decode(invitationCode.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
|
||||
return JsonUtils.fromNonNullJson(json, Invitation.class);
|
||||
Matcher matcher = INVITATION_CODE_PATTERN.matcher(invitationCode);
|
||||
if (!matcher.find()) throw new IllegalArgumentException("Invalid invitation code");
|
||||
return new Invitation(matcher.group("id"), Integer.parseInt(matcher.group("port")));
|
||||
}
|
||||
|
||||
public static int findAvailablePort() throws IOException {
|
||||
@@ -225,27 +293,33 @@ public final class MultiplayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCatoPath() {
|
||||
private static String getCatoFileName() {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-windows-amd64.exe";
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "cato-client-windows-amd64.exe";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "cato-client-windows-arm64.exe";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.X86) {
|
||||
return "cato-client-windows-i386.exe";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case OSX:
|
||||
if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-darwin-amd64";
|
||||
} else if (Architecture.SYSTEM == Architecture.ARM64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-darwin-arm64";
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "cato-client-darwin-amd64";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "cato-client-darwin-arm64";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case LINUX:
|
||||
if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-linux-amd64";
|
||||
} else if (Architecture.SYSTEM == Architecture.ARM || Architecture.SYSTEM == Architecture.ARM64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-linux-arm7";
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "cato-client-linux-amd64";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM32) {
|
||||
return "cato-client-linux-arm7";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "cato-client-linux-arm64";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@@ -254,37 +328,45 @@ public final class MultiplayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCatoPath() {
|
||||
String name = getCatoFileName();
|
||||
if (StringUtils.isBlank(name)) return "";
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/" + name;
|
||||
}
|
||||
|
||||
public static class CatoSession extends ManagedProcess {
|
||||
private final EventManager<CatoExitEvent> onExit = new EventManager<>();
|
||||
private final EventManager<CatoIdEvent> onIdGenerated = new EventManager<>();
|
||||
private final EventManager<Event> onPeerConnected = new EventManager<>();
|
||||
|
||||
private final String name;
|
||||
private String name;
|
||||
private final State type;
|
||||
private String id;
|
||||
private boolean peerConnected = false;
|
||||
private MultiplayerClient client;
|
||||
private MultiplayerServer server;
|
||||
private final BufferedWriter writer;
|
||||
|
||||
CatoSession(String name, State type, Process process, List<String> commands) {
|
||||
CatoSession(State type, Process process, List<String> commands) {
|
||||
super(process, commands);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
|
||||
|
||||
LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands).toString());
|
||||
LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands));
|
||||
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
addRelatedThread(Lang.thread(this::waitFor, "CatoExitWaiter", true));
|
||||
addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), this::checkCatoLog), "CatoInputStreamPump", true));
|
||||
addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), this::checkCatoLog), "CatoErrorStreamPump", true));
|
||||
|
||||
writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public MultiplayerClient getClient() {
|
||||
public synchronized MultiplayerClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public CatoSession setClient(MultiplayerClient client) {
|
||||
public synchronized CatoSession setClient(MultiplayerClient client) {
|
||||
this.client = client;
|
||||
return this;
|
||||
}
|
||||
@@ -324,6 +406,13 @@ public final class MultiplayerManager {
|
||||
onExit.fireEvent(new CatoExitEvent(this, exitCode));
|
||||
} catch (InterruptedException e) {
|
||||
onExit.fireEvent(new CatoExitEvent(this, CatoExitEvent.EXIT_CODE_INTERRUPTED));
|
||||
} finally {
|
||||
try {
|
||||
if (writer != null)
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to close cato stdin writer", e);
|
||||
}
|
||||
}
|
||||
destroyRelatedThreads();
|
||||
}
|
||||
@@ -332,10 +421,14 @@ public final class MultiplayerManager {
|
||||
return id != null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
public synchronized String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public synchronized void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public State getType() {
|
||||
return type;
|
||||
}
|
||||
@@ -349,8 +442,26 @@ public final class MultiplayerManager {
|
||||
if (id == null) {
|
||||
throw new IllegalStateException("id not generated");
|
||||
}
|
||||
String json = JsonUtils.GSON.toJson(new Invitation(CATO_VERSION, id, name, serverPort));
|
||||
return new String(Base64.getEncoder().encode(json.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
|
||||
return id + "#" + serverPort;
|
||||
}
|
||||
|
||||
public synchronized void invokeCommand(String command) throws IOException {
|
||||
LOG.info("Invoking cato: " + command);
|
||||
writer.write(command);
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
public void forwardPort(String peerId, String localAddress, int localPort, String remoteAddress, int remotePort, Mode mode) throws IOException {
|
||||
invokeCommand(String.format("net add %s %s:%d %s:%d %s", peerId, localAddress, localPort, remoteAddress, remotePort, mode.getName()));
|
||||
}
|
||||
|
||||
public void allowForwardingAddress(String address, int port) throws IOException {
|
||||
invokeCommand(String.format("ufw net open %s:%d", address, port));
|
||||
}
|
||||
|
||||
public void showAllowedAddress() throws IOException {
|
||||
invokeCommand("ufw net whitelist");
|
||||
}
|
||||
|
||||
public EventManager<CatoExitEvent> onExit() {
|
||||
@@ -407,33 +518,19 @@ public final class MultiplayerManager {
|
||||
}
|
||||
|
||||
public static class Invitation {
|
||||
@SerializedName("v")
|
||||
private final String version;
|
||||
private final String id;
|
||||
@SerializedName("n")
|
||||
private final String sessionName;
|
||||
@SerializedName("p")
|
||||
private final int channelPort;
|
||||
|
||||
public Invitation(String version, String id, String sessionName, int channelPort) {
|
||||
this.version = version;
|
||||
public Invitation(String id, int channelPort) {
|
||||
this.id = id;
|
||||
this.sessionName = sessionName;
|
||||
this.channelPort = channelPort;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getSessionName() {
|
||||
return sessionName;
|
||||
}
|
||||
|
||||
public int getChannelPort() {
|
||||
return channelPort;
|
||||
}
|
||||
@@ -463,22 +560,61 @@ public final class MultiplayerManager {
|
||||
|
||||
public enum Mode {
|
||||
P2P,
|
||||
RELAY;
|
||||
BRIDGE;
|
||||
|
||||
String getName() {
|
||||
return name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CatoExitException extends RuntimeException {
|
||||
private final int exitCode;
|
||||
private final boolean ready;
|
||||
|
||||
public CatoExitException(int exitCode, boolean ready) {
|
||||
this.exitCode = exitCode;
|
||||
this.ready = ready;
|
||||
}
|
||||
|
||||
public int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CatoExitTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class CatoSessionExpiredException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class CatoAlreadyStartedException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class JoinRequestTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class PeerConnectionTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -37,15 +37,21 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
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;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.KickResponse.*;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -55,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();
|
||||
@@ -94,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();
|
||||
}
|
||||
|
||||
@@ -128,15 +134,15 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
|
||||
private void testNAT() {
|
||||
Task.supplyAsync(() -> {
|
||||
DiscoveryTest tester = new DiscoveryTest(null, 0, "stun.stunprotocol.org", 3478);
|
||||
DiscoveryTest tester = new DiscoveryTest(null, 0, "stun.miwifi.com", 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();
|
||||
}
|
||||
|
||||
@@ -146,7 +152,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
agreementPane.setBody(new Label(i18n("multiplayer.agreement.prompt")));
|
||||
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
|
||||
agreementLink.setOnAction(e -> FXUtils.openLink("https://noin.cn/agreement"));
|
||||
agreementLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-agreement"));
|
||||
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
yesButton.setOnAction(e -> {
|
||||
@@ -213,48 +219,36 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> {
|
||||
int gamePort = result.getServer().getAd();
|
||||
boolean isStaticToken = StringUtils.isNotBlank(globalConfig().getMultiplayerToken());
|
||||
try {
|
||||
MultiplayerManager.CatoSession session = MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getServer().getMotd(), gamePort, result.isAllowAllJoinRequests());
|
||||
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
|
||||
runInFX(() -> {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO)
|
||||
.yesOrNo(resolveClient, () -> rejectClient.accept(i18n("multiplayer.session.join.wait_timeout")))
|
||||
.cancelOnTimeout(30 * 1000)
|
||||
.build());
|
||||
});
|
||||
});
|
||||
session.getServer().onClientAdded().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.add(event);
|
||||
});
|
||||
});
|
||||
session.getServer().onClientDisconnected().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.remove(event);
|
||||
});
|
||||
});
|
||||
initCatoSession(session);
|
||||
} catch (MultiplayerManager.CatoAlreadyStartedException e) {
|
||||
LOG.log(Level.WARNING, "Cato already started", e);
|
||||
reject.accept(i18n("multiplayer.session.error.already_started"));
|
||||
return;
|
||||
} catch (MultiplayerManager.CatoNotExistsException e) {
|
||||
LOG.log(Level.WARNING, "Cato not found " + e.getFile(), e);
|
||||
reject.accept(i18n("multiplayer.session.error.file_not_found"));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to create session", e);
|
||||
if (isStaticToken) {
|
||||
reject.accept(i18n("multiplayer.session.create.error.static_token") + e.getLocalizedMessage());
|
||||
} else {
|
||||
reject.accept(i18n("multiplayer.session.create.error.dynamic_token") + e.getLocalizedMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getServer().getMotd(), gamePort, result.isAllowAllJoinRequests())
|
||||
.thenAcceptAsync(session -> {
|
||||
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
|
||||
runInFX(() -> {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO)
|
||||
.yesOrNo(resolveClient, () -> rejectClient.accept(MultiplayerChannel.KickResponse.JOIN_ACEEPTANCE_TIMEOUT))
|
||||
.cancelOnTimeout(30 * 1000)
|
||||
.build());
|
||||
});
|
||||
});
|
||||
session.getServer().onClientAdded().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.add(event);
|
||||
});
|
||||
});
|
||||
session.getServer().onClientDisconnected().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.remove(event);
|
||||
});
|
||||
});
|
||||
initCatoSession(session);
|
||||
|
||||
this.gamePort.set(gamePort);
|
||||
setMultiplayerState(MultiplayerManager.State.CONNECTING);
|
||||
resolve.run();
|
||||
this.gamePort.set(gamePort);
|
||||
setMultiplayerState(MultiplayerManager.State.MASTER);
|
||||
resolve.run();
|
||||
}, Platform::runLater)
|
||||
.exceptionally(throwable -> {
|
||||
reject.accept(localizeCreateErrorMessage(throwable, isStaticToken));
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -265,6 +259,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> {
|
||||
PromptDialogPane.Builder.HintQuestion hintQuestion = (PromptDialogPane.Builder.HintQuestion) result.get(0);
|
||||
boolean isStaticToken = StringUtils.isNotBlank(globalConfig().getMultiplayerToken());
|
||||
|
||||
String invitationCode = ((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue();
|
||||
MultiplayerManager.Invitation invitation;
|
||||
@@ -287,11 +282,9 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
try {
|
||||
MultiplayerManager.joinSession(
|
||||
globalConfig().getMultiplayerToken(),
|
||||
invitation.getVersion(),
|
||||
invitation.getSessionName(),
|
||||
invitation.getId(),
|
||||
globalConfig().isMultiplayerRelay() && StringUtils.isNotBlank(globalConfig().getMultiplayerToken())
|
||||
? MultiplayerManager.Mode.RELAY
|
||||
globalConfig().isMultiplayerRelay() && (StringUtils.isNotBlank(globalConfig().getMultiplayerToken()) || StringUtils.isNotBlank(System.getProperty("hmcl.multiplayer.relay")))
|
||||
? MultiplayerManager.Mode.BRIDGE
|
||||
: MultiplayerManager.Mode.P2P,
|
||||
invitation.getChannelPort(),
|
||||
localPort, new MultiplayerManager.JoinSessionHandler() {
|
||||
@@ -316,10 +309,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
});
|
||||
});
|
||||
|
||||
session.getClient().onKicked().register(() -> {
|
||||
session.getClient().onKicked().register(kickedEvent -> {
|
||||
runInFX(() -> {
|
||||
kicked.set(true);
|
||||
Controllers.dialog(i18n("multiplayer.session.join.kicked"));
|
||||
Controllers.dialog(i18n("multiplayer.session.join.kicked", localizeKickMessage(kickedEvent.getReason())));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,31 +321,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
resolve.run();
|
||||
}, Platform::runLater)
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolved = resolveException(throwable);
|
||||
if (resolved instanceof CancellationException) {
|
||||
LOG.info("Connection rejected by the server");
|
||||
reject.accept(i18n("multiplayer.session.join.rejected"));
|
||||
return null;
|
||||
} else if (resolved instanceof MultiplayerManager.CatoAlreadyStartedException) {
|
||||
LOG.info("Cato already started");
|
||||
reject.accept(i18n("multiplayer.session.error.already_started"));
|
||||
return null;
|
||||
} else if (throwable instanceof MultiplayerManager.CatoNotExistsException) {
|
||||
LOG.log(Level.WARNING, "Cato not found " + ((MultiplayerManager.CatoNotExistsException) throwable).getFile(), throwable);
|
||||
reject.accept(i18n("multiplayer.session.error.file_not_found"));
|
||||
return null;
|
||||
} else if (resolved instanceof MultiplayerManager.JoinRequestTimeoutException) {
|
||||
LOG.info("Cato already started");
|
||||
reject.accept(i18n("multiplayer.session.join.wait_timeout"));
|
||||
return null;
|
||||
} else if (resolved instanceof MultiplayerManager.ConnectionErrorException) {
|
||||
LOG.info("Failed to establish connection with server");
|
||||
reject.accept(i18n("multiplayer.session.join.error.connection"));
|
||||
return null;
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Failed to join session", resolved);
|
||||
reject.accept(i18n("multiplayer.session.join.error"));
|
||||
}
|
||||
reject.accept(localizeJoinErrorMessage(throwable, isStaticToken));
|
||||
return null;
|
||||
});
|
||||
} catch (MultiplayerManager.IncompatibleCatoVersionException e) {
|
||||
@@ -363,6 +332,77 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.join.invitation_code"), "", new RequiredValidator())));
|
||||
}
|
||||
|
||||
private String localizeKickMessage(String message) {
|
||||
if (VERSION_NOT_MATCHED.equals(message)) {
|
||||
return i18n("multiplayer.session.join.kicked.version_not_matched");
|
||||
} else if (KICKED.equals(message)) {
|
||||
return i18n("multiplayer.session.join.kicked.kicked");
|
||||
} else if (JOIN_ACEEPTANCE_TIMEOUT.equals(message)) {
|
||||
return i18n("multiplayer.session.join.kicked.join_acceptance_timeout");
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
private String localizeErrorMessage(Throwable t, boolean isStaticToken, Function<Throwable, String> fallback) {
|
||||
Throwable e = resolveException(t);
|
||||
if (e instanceof CancellationException) {
|
||||
LOG.info("Connection rejected by the server");
|
||||
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");
|
||||
} else if (e instanceof MultiplayerManager.CatoNotExistsException) {
|
||||
LOG.log(Level.WARNING, "Cato not found " + ((MultiplayerManager.CatoNotExistsException) e).getFile(), e);
|
||||
return i18n("multiplayer.session.error.file_not_found");
|
||||
} else if (e instanceof MultiplayerManager.JoinRequestTimeoutException) {
|
||||
LOG.info("Cato already started");
|
||||
return i18n("multiplayer.session.join.wait_timeout");
|
||||
} else if (e instanceof MultiplayerManager.ConnectionErrorException) {
|
||||
LOG.info("Failed to establish connection with server");
|
||||
return i18n("multiplayer.session.join.error.connection");
|
||||
} else if (e instanceof MultiplayerManager.CatoExitTimeoutException) {
|
||||
LOG.info("Cato failed to connect to main net");
|
||||
if (isStaticToken) {
|
||||
return i18n("multiplayer.exit.timeout.static_token");
|
||||
} else {
|
||||
return i18n("multiplayer.exit.timeout.dynamic_token");
|
||||
}
|
||||
} else if (e instanceof MultiplayerManager.CatoExitException) {
|
||||
LOG.info("Cato exited accidentally");
|
||||
if (!((MultiplayerManager.CatoExitException) e).isReady()) {
|
||||
return i18n("multiplayer.exit.before_ready");
|
||||
} else {
|
||||
return i18n("multiplayer.exit.after_ready");
|
||||
}
|
||||
} else if (e instanceof ChecksumMismatchException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else {
|
||||
return fallback.apply(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String localizeCreateErrorMessage(Throwable t, boolean isStaticToken) {
|
||||
return localizeErrorMessage(t, isStaticToken, e -> {
|
||||
LOG.log(Level.WARNING, "Failed to create session", e);
|
||||
if (isStaticToken) {
|
||||
return i18n("multiplayer.session.create.error.static_token") + e.getLocalizedMessage();
|
||||
} else {
|
||||
return i18n("multiplayer.session.create.error.dynamic_token") + e.getLocalizedMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String localizeJoinErrorMessage(Throwable t, boolean isStaticToken) {
|
||||
return localizeErrorMessage(t, isStaticToken, e -> {
|
||||
LOG.log(Level.WARNING, "Failed to join session", e);
|
||||
return i18n("multiplayer.session.join.error");
|
||||
});
|
||||
}
|
||||
|
||||
public void kickPlayer(MultiplayerChannel.CatoClient client) {
|
||||
if (getSession() == null || !getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.MASTER) {
|
||||
throw new IllegalStateException("CatoSession not ready");
|
||||
@@ -471,4 +511,5 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
@@ -41,8 +40,11 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
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;
|
||||
@@ -130,9 +132,9 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
|
||||
item.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/help/launcher/multiplayer.html"));
|
||||
})
|
||||
.addNavigationDrawerItem(report -> {
|
||||
report.setTitle(i18n("multiplayer.report"));
|
||||
report.setLeftGraphic(wrap(SVG::bug));
|
||||
report.setOnAction(e -> FXUtils.openLink(Metadata.EULA_URL));
|
||||
report.setTitle(i18n("feedback"));
|
||||
report.setLeftGraphic(wrap(SVG::messageAlertOutline));
|
||||
report.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-feedback"));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -256,7 +258,7 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
|
||||
tokenField.setPromptText(i18n("multiplayer.session.create.token.prompt"));
|
||||
|
||||
JFXHyperlink applyLink = new JFXHyperlink(i18n("multiplayer.session.create.token.apply"));
|
||||
applyLink.setOnAction(e -> FXUtils.openLink("https://noin.cn/circle/386.html"));
|
||||
applyLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-static-token"));
|
||||
|
||||
gridPane.addRow(0, new Label(i18n("multiplayer.session.create.token")), tokenField, applyLink);
|
||||
|
||||
@@ -270,7 +272,7 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
|
||||
pane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
JFXHyperlink aboutLink = new JFXHyperlink(i18n("about"));
|
||||
aboutLink.setOnAction(e -> FXUtils.openLink("https://noin.cn/71.html"));
|
||||
aboutLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-about"));
|
||||
|
||||
HBox placeholder = new HBox();
|
||||
HBox.setHgrow(placeholder, Priority.ALWAYS);
|
||||
@@ -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");
|
||||
|
||||
@@ -27,7 +27,9 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import java.io.*;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -36,6 +38,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class MultiplayerServer extends Thread {
|
||||
private ServerSocket socket;
|
||||
private final String sessionName;
|
||||
private final int gamePort;
|
||||
private final boolean allowAllJoinRequests;
|
||||
|
||||
@@ -48,7 +51,8 @@ public class MultiplayerServer extends Thread {
|
||||
private final Map<String, Endpoint> clients = new ConcurrentHashMap<>();
|
||||
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
|
||||
|
||||
public MultiplayerServer(int gamePort, boolean allowAllJoinRequests) {
|
||||
public MultiplayerServer(String sessionName, int gamePort, boolean allowAllJoinRequests) {
|
||||
this.sessionName = sessionName;
|
||||
this.gamePort = gamePort;
|
||||
this.allowAllJoinRequests = allowAllJoinRequests;
|
||||
|
||||
@@ -116,7 +120,7 @@ public class MultiplayerServer extends Thread {
|
||||
|
||||
try {
|
||||
if (client.socket.isConnected()) {
|
||||
client.write(new KickResponse(""));
|
||||
client.write(new KickResponse(KickResponse.KICKED));
|
||||
client.socket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -129,8 +133,8 @@ public class MultiplayerServer extends Thread {
|
||||
String clientName = null;
|
||||
LOG.info("Accepted client " + address);
|
||||
try (Socket clientSocket = targetSocket;
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||
clientSocket.setKeepAlive(true);
|
||||
Endpoint endpoint = new Endpoint(clientSocket, writer);
|
||||
clients.put(address, endpoint);
|
||||
@@ -149,6 +153,17 @@ public class MultiplayerServer extends Thread {
|
||||
LOG.info("Received join request with clientVersion=" + joinRequest.getClientVersion() + ", id=" + joinRequest.getUsername());
|
||||
clientName = joinRequest.getUsername();
|
||||
|
||||
if (!Objects.equals(MultiplayerManager.CATO_VERSION, joinRequest.getClientVersion())) {
|
||||
try {
|
||||
endpoint.write(new KickResponse(KickResponse.VERSION_NOT_MATCHED));
|
||||
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to send kick response.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CatoClient catoClient = new CatoClient(this, clientName);
|
||||
nameClientMap.put(clientName, endpoint);
|
||||
onClientAdded.fireEvent(catoClient);
|
||||
@@ -156,7 +171,7 @@ public class MultiplayerServer extends Thread {
|
||||
if (onClientAdding != null && !allowAllJoinRequests) {
|
||||
onClientAdding.call(catoClient, () -> {
|
||||
try {
|
||||
endpoint.write(new JoinResponse(gamePort));
|
||||
endpoint.write(new JoinResponse(sessionName, gamePort));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to send join response.", e);
|
||||
try {
|
||||
@@ -177,7 +192,7 @@ public class MultiplayerServer extends Thread {
|
||||
});
|
||||
} else {
|
||||
// Allow all join requests.
|
||||
endpoint.write(new JoinResponse(gamePort));
|
||||
endpoint.write(new JoinResponse(sessionName, gamePort));
|
||||
}
|
||||
} else if (request instanceof KeepAliveRequest) {
|
||||
endpoint.write(new KeepAliveResponse(System.currentTimeMillis()));
|
||||
|
||||
@@ -62,6 +62,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -88,6 +89,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
private TaskExecutor executor;
|
||||
protected RemoteModRepository repository;
|
||||
|
||||
private Runnable retrySearch;
|
||||
|
||||
public DownloadListPage(RemoteModRepository repository) {
|
||||
this(repository, null);
|
||||
}
|
||||
@@ -115,7 +118,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
if (!searchInitialized) {
|
||||
searchInitialized = true;
|
||||
search("", null, 0, "", 0);
|
||||
search("", null, 0, "", RemoteModRepository.SortType.DATE_CREATED);
|
||||
}
|
||||
|
||||
if (versionSelection) {
|
||||
@@ -150,7 +153,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this.loading.set(loading);
|
||||
}
|
||||
|
||||
public void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, int sort) {
|
||||
public void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, RemoteModRepository.SortType sort) {
|
||||
retrySearch = null;
|
||||
setLoading(true);
|
||||
setFailed(false);
|
||||
File versionJar = StringUtils.isNotBlank(version.get().getVersion())
|
||||
@@ -177,6 +181,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
failed.set(false);
|
||||
} else {
|
||||
failed.set(true);
|
||||
retrySearch = () -> search(userGameVersion, category, pageOffset, searchFilter, sort);
|
||||
}
|
||||
}).executor(true);
|
||||
}
|
||||
@@ -307,17 +312,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
}).start();
|
||||
|
||||
StackPane sortStackPane = new StackPane();
|
||||
JFXComboBox<String> sortComboBox = new JFXComboBox<>();
|
||||
JFXComboBox<RemoteModRepository.SortType> sortComboBox = new JFXComboBox<>();
|
||||
sortStackPane.getChildren().setAll(sortComboBox);
|
||||
sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty());
|
||||
sortComboBox.getStyleClass().add("fit-width");
|
||||
sortComboBox.getItems().setAll(
|
||||
i18n("curse.sort.date_created"),
|
||||
i18n("curse.sort.popularity"),
|
||||
i18n("curse.sort.last_updated"),
|
||||
i18n("curse.sort.name"),
|
||||
i18n("curse.sort.author"),
|
||||
i18n("curse.sort.total_downloads"));
|
||||
sortComboBox.setConverter(stringConverter(sortType -> i18n("curse.sort." + sortType.name().toLowerCase(Locale.ROOT))));
|
||||
sortComboBox.getSelectionModel().select(0);
|
||||
searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
|
||||
|
||||
@@ -341,7 +340,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
.orElse(null),
|
||||
0,
|
||||
nameField.getText(),
|
||||
sortComboBox.getSelectionModel().getSelectedIndex());
|
||||
sortComboBox.getSelectionModel().getSelectedItem());
|
||||
searchButton.setOnAction(searchAction);
|
||||
nameField.setOnAction(searchAction);
|
||||
gameVersionField.setOnAction(searchAction);
|
||||
@@ -360,6 +359,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
return null;
|
||||
}
|
||||
}, getSkinnable().failedProperty()));
|
||||
spinnerPane.setOnFailedAction(e -> {
|
||||
if (getSkinnable().retrySearch != null) {
|
||||
getSkinnable().retrySearch.run();
|
||||
}
|
||||
});
|
||||
|
||||
JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||
spinnerPane.setContent(listView);
|
||||
|
||||
@@ -84,7 +84,12 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
this.mod = ModTranslations.getModByCurseForgeId(addon.getSlug());
|
||||
this.version = version;
|
||||
this.callback = callback;
|
||||
loadModVersions();
|
||||
|
||||
this.state.set(State.fromTitle(addon.getTitle()));
|
||||
}
|
||||
|
||||
private void loadModVersions() {
|
||||
File versionJar = StringUtils.isNotBlank(version.getVersion())
|
||||
? version.getProfile().getRepository().getVersionJar(version.getVersion())
|
||||
: null;
|
||||
@@ -93,9 +98,9 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
setFailed(false);
|
||||
|
||||
Task.allOf(
|
||||
Task.supplyAsync(() -> addon.getData().loadDependencies()),
|
||||
Task.supplyAsync(() -> {
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions();
|
||||
Task.supplyAsync(() -> addon.getData().loadDependencies()),
|
||||
Task.supplyAsync(() -> {
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions();
|
||||
// if (StringUtils.isNotBlank(version.getVersion())) {
|
||||
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||
// if (gameVersion.isPresent()) {
|
||||
@@ -103,8 +108,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
|
||||
// }
|
||||
// }
|
||||
return sortVersions(versions);
|
||||
}))
|
||||
return sortVersions(versions);
|
||||
}))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -122,8 +127,6 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
setLoading(false);
|
||||
}).start();
|
||||
|
||||
this.state.set(State.fromTitle(addon.getTitle()));
|
||||
}
|
||||
|
||||
private SimpleMultimap<String, RemoteMod.Version> sortVersions(Stream<RemoteMod.Version> versions) {
|
||||
@@ -195,7 +198,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest).executor(true),
|
||||
new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest, file.getFile().getIntegrityCheck()).executor(true),
|
||||
i18n("message.downloading")
|
||||
);
|
||||
}
|
||||
@@ -297,6 +300,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
return null;
|
||||
}
|
||||
}, getSkinnable().failedProperty()));
|
||||
spinnerPane.setOnFailedAction(e -> getSkinnable().loadModVersions());
|
||||
|
||||
ComponentList list = new ComponentList();
|
||||
StackPane.setAlignment(list, Pos.TOP_CENTER);
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class ModDownloadListPage extends DownloadListPage {
|
||||
private class Repository implements RemoteModRepository {
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, int sort) throws IOException {
|
||||
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort) throws IOException {
|
||||
String newSearchFilter;
|
||||
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
|
||||
List<ModTranslations.Mod> mods = ModTranslations.searchMod(searchFilter);
|
||||
|
||||
@@ -95,7 +95,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
synchronized (ModListPage.this) {
|
||||
runInFX(() -> loadingProperty().set(true));
|
||||
modManager.refreshMods();
|
||||
return new LinkedList<>(modManager.getMods());
|
||||
return new ArrayList<>(modManager.getMods());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
@@ -118,10 +118,12 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.mod"), "*.jar", "*.zip", "*.litemod"));
|
||||
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
|
||||
|
||||
// It's guaranteed that succeeded and failed are thread safe here.
|
||||
List<String> succeeded = new LinkedList<>();
|
||||
List<String> failed = new LinkedList<>();
|
||||
if (res == null) return;
|
||||
|
||||
// It's guaranteed that succeeded and failed are thread safe here.
|
||||
List<String> succeeded = new ArrayList<>(res.size());
|
||||
List<String> failed = new ArrayList<>();
|
||||
|
||||
Task.runAsync(() -> {
|
||||
for (File file : res) {
|
||||
try {
|
||||
@@ -135,7 +137,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
}
|
||||
}
|
||||
}).withRunAsync(Schedulers.javafx(), () -> {
|
||||
List<String> prompt = new LinkedList<>();
|
||||
List<String> prompt = new ArrayList<>(1);
|
||||
if (!succeeded.isEmpty())
|
||||
prompt.add(i18n("mods.add.success", String.join(", ", succeeded)));
|
||||
if (!failed.isEmpty())
|
||||
|
||||
@@ -119,7 +119,9 @@ public final class ModTranslations {
|
||||
modIdMap = new HashMap<>();
|
||||
for (Mod mod : mods) {
|
||||
for (String id : mod.getModIds()) {
|
||||
modIdMap.put(id, mod);
|
||||
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) {
|
||||
modIdMap.put(id, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -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,27 +41,25 @@ 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.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
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;
|
||||
|
||||
public final class VersionSettingsPage extends StackPane implements DecoratorPage, VersionPage.VersionLoadable, PageAware {
|
||||
@@ -98,9 +93,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final OptionToggleButton useNativeGLFWPane;
|
||||
private final OptionToggleButton useNativeOpenALPane;
|
||||
private final ComponentSublist javaSublist;
|
||||
private final MultiFileItem<JavaVersion> javaItem;
|
||||
private final MultiFileItem.Option<JavaVersion> javaAutoDeterminedOption;
|
||||
private final MultiFileItem.FileOption<JavaVersion> javaCustomOption;
|
||||
private final MultiFileItem<Pair<JavaVersionType, JavaVersion>> javaItem;
|
||||
private final MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>> javaAutoDeterminedOption;
|
||||
private final MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>> javaCustomOption;
|
||||
private final ComponentSublist gameDirSublist;
|
||||
private final MultiFileItem<GameDirectoryType> gameDirItem;
|
||||
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
|
||||
@@ -115,10 +110,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
private final InvalidationListener javaListener = any -> initJavaSubtitle();
|
||||
|
||||
private boolean uiVisible = false;
|
||||
private final StringProperty selectedVersion = new SimpleStringProperty();
|
||||
private final BooleanProperty navigateToSpecificSettings = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty enableSpecificSettings = new SimpleBooleanProperty(true);
|
||||
private final BooleanProperty enableSpecificSettings = new SimpleBooleanProperty(false);
|
||||
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
|
||||
private final ObjectProperty<OperatingSystem.PhysicalMemoryStatus> memoryStatus = new SimpleObjectProperty<>(OperatingSystem.PhysicalMemoryStatus.INVALID);
|
||||
private final BooleanProperty modpack = new SimpleBooleanProperty();
|
||||
@@ -200,8 +194,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
javaSublist.getContent().add(javaItem);
|
||||
javaSublist.setTitle(i18n("settings.game.java_directory"));
|
||||
javaSublist.setHasSubtitle(true);
|
||||
javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), null);
|
||||
javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null)
|
||||
javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), pair(JavaVersionType.AUTO, null));
|
||||
javaCustomOption = new MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null))
|
||||
.setChooserTitle(i18n("settings.game.java_directory.choose"));
|
||||
|
||||
gameDirItem = new MultiFileItem<>();
|
||||
@@ -211,8 +205,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
gameDirSublist.setHasSubtitle(true);
|
||||
gameDirItem.disableProperty().bind(modpack);
|
||||
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
.setDirectory(true);
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
.setDirectory(true);
|
||||
|
||||
gameDirItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("settings.advanced.game_dir.default"), GameDirectoryType.ROOT_FOLDER),
|
||||
@@ -399,7 +393,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
txtServerIP = new JFXTextField();
|
||||
txtServerIP.setPromptText(i18n("settings.advanced.server_ip.prompt"));
|
||||
txtServerIP.getStyleClass().add("fit-width");
|
||||
FXUtils.setLimitWidth(txtServerIP, 300);
|
||||
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
|
||||
}
|
||||
@@ -545,9 +538,15 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID));
|
||||
|
||||
Task.supplyAsync(JavaVersion::getJavas).thenAcceptAsync(Schedulers.javafx(), list -> {
|
||||
List<MultiFileItem.Option<JavaVersion>> options = list.stream()
|
||||
.map(javaVersion -> new MultiFileItem.Option<>(javaVersion.getVersion() + i18n("settings.game.java_directory.bit",
|
||||
javaVersion.getPlatform().getBit()), javaVersion)
|
||||
boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && list.stream().allMatch(java -> java.getArchitecture().isX86());
|
||||
|
||||
List<MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>>> options = list.stream()
|
||||
.map(javaVersion -> new MultiFileItem.Option<>(
|
||||
i18n("settings.game.java_directory.template",
|
||||
javaVersion.getVersion(),
|
||||
isX86 ? i18n("settings.game.java_directory.bit", javaVersion.getBits().getBit())
|
||||
: javaVersion.getPlatform().getArchitecture().getDisplayName()),
|
||||
pair(JavaVersionType.DETECTED, javaVersion))
|
||||
.setSubtitle(javaVersion.getBinary().toString()))
|
||||
.collect(Collectors.toList());
|
||||
options.add(0, javaAutoDeterminedOption);
|
||||
@@ -558,7 +557,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
}).start();
|
||||
|
||||
javaItem.setSelectedData(null);
|
||||
javaItem.setFallbackData(JavaVersion.fromCurrentEnvironment());
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
|
||||
|
||||
@@ -684,7 +682,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
} else if (javaAutoDeterminedOption.isSelected()) {
|
||||
versionSetting.setJavaAutoSelected();
|
||||
} else {
|
||||
versionSetting.setJavaVersion((JavaVersion) newValue.getUserData());
|
||||
//noinspection unchecked
|
||||
versionSetting.setJavaVersion(((Pair<JavaVersionType, JavaVersion>) newValue.getUserData()).getValue());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -695,7 +694,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
|
||||
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
|
||||
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
|
||||
|
||||
|
||||
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
|
||||
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
|
||||
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
|
||||
@@ -721,7 +720,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
// javaLoading.set(true);
|
||||
lastVersionSetting.getJavaVersion(null, null)
|
||||
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> {
|
||||
javaItem.setSelectedData(javaVersion);
|
||||
javaItem.setSelectedData(pair(JavaVersionType.DETECTED, javaVersion));
|
||||
// javaLoading.set(false);
|
||||
}).start();
|
||||
}
|
||||
@@ -751,7 +750,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
profile.getRepository().getVersion(versionId));
|
||||
}
|
||||
}).thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
|
||||
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
|
||||
.map(JavaVersion::getBinary).map(Path::toString).orElseGet(() ->
|
||||
autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid"))))
|
||||
.start();
|
||||
}
|
||||
|
||||
@@ -767,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() {
|
||||
@@ -788,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();
|
||||
}
|
||||
|
||||
@@ -796,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);
|
||||
}
|
||||
|
||||
@@ -808,4 +797,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private enum JavaVersionType {
|
||||
DETECTED,
|
||||
CUSTOM,
|
||||
AUTO,
|
||||
}
|
||||
}
|
||||
@@ -122,8 +122,13 @@ public final class Versions {
|
||||
return;
|
||||
}
|
||||
if (profile.getRepository().renameVersion(version, newName)) {
|
||||
profile.getRepository().refreshVersionsAsync().start();
|
||||
resolve.run();
|
||||
profile.getRepository().refreshVersionsAsync()
|
||||
.thenRunAsync(Schedulers.javafx(), () -> {
|
||||
if (profile.getRepository().hasVersion(newName)) {
|
||||
profile.setSelectedVersion(newName);
|
||||
}
|
||||
}).start();
|
||||
} else {
|
||||
reject.accept(i18n("version.manage.rename.fail"));
|
||||
}
|
||||
@@ -191,6 +196,7 @@ public final class Versions {
|
||||
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
|
||||
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
|
||||
File file = chooser.showSaveDialog(Controllers.getStage());
|
||||
if (file != null)
|
||||
new LauncherHelper(profile, account, id).makeLaunchScript(file);
|
||||
|
||||
@@ -21,23 +21,18 @@ 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.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -45,28 +40,30 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private static final Map<String, String> SOURCE = new HashMap<String, String>() {
|
||||
{
|
||||
put("javafx.fxml.LoadException", i18n("crash.NoClassDefFound"));
|
||||
put("Location is not set", i18n("crash.NoClassDefFound"));
|
||||
put("UnsatisfiedLinkError", i18n("crash.user_fault"));
|
||||
put("java.lang.NoClassDefFoundError", i18n("crash.NoClassDefFound"));
|
||||
put("org.jackhuang.hmcl.util.ResourceNotFoundError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.VerifyError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound"));
|
||||
put("javax.imageio.IIOException", i18n("crash.NoClassDefFound"));
|
||||
put("netscape.javascript.JSException", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.IncompatibleClassChangeError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.ClassFormatError", i18n("crash.NoClassDefFound"));
|
||||
put("com.sun.javafx.css.StyleManager.findMatchingStyles", i18n("launcher.update_java"));
|
||||
put("NoSuchAlgorithmException", "Has your operating system been installed completely or is a ghost system?");
|
||||
}
|
||||
};
|
||||
// Lazy initialization resources
|
||||
private static final class Hole {
|
||||
@SuppressWarnings("unchecked")
|
||||
static final Pair<String, String>[] SOURCE = (Pair<String, String>[]) new Pair<?, ?>[]{
|
||||
pair("javafx.fxml.LoadException", i18n("crash.NoClassDefFound")),
|
||||
pair("Location is not set", i18n("crash.NoClassDefFound")),
|
||||
pair("UnsatisfiedLinkError", i18n("crash.user_fault")),
|
||||
pair("java.lang.NoClassDefFoundError", i18n("crash.NoClassDefFound")),
|
||||
pair("org.jackhuang.hmcl.util.ResourceNotFoundError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.VerifyError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound")),
|
||||
pair("javax.imageio.IIOException", i18n("crash.NoClassDefFound")),
|
||||
pair("netscape.javascript.JSException", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.IncompatibleClassChangeError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.ClassFormatError", i18n("crash.NoClassDefFound")),
|
||||
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?")
|
||||
};
|
||||
}
|
||||
|
||||
private boolean checkThrowable(Throwable e) {
|
||||
String s = StringUtils.getStackTrace(e);
|
||||
for (HashMap.Entry<String, String> entry : SOURCE.entrySet())
|
||||
for (Pair<String, String> entry : Hole.SOURCE)
|
||||
if (s.contains(entry.getKey())) {
|
||||
if (StringUtils.isNotBlank(entry.getValue())) {
|
||||
String info = entry.getValue();
|
||||
@@ -85,8 +82,6 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private final boolean showCrashWindow;
|
||||
|
||||
public CrashReporter(boolean showCrashWindow) {
|
||||
@@ -98,27 +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 (CAUGHT_EXCEPTIONS.contains(stackTrace))
|
||||
return;
|
||||
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.toString() + "\n" +
|
||||
"\n Content: \n " +
|
||||
stackTrace + "\n\n" +
|
||||
"-- System Details --\n" +
|
||||
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\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(() -> {
|
||||
@@ -127,7 +106,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
new CrashWindow(text).show();
|
||||
}
|
||||
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
|
||||
reportToServer(text);
|
||||
reportToServer(report);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -136,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 {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public final class HMCLService {
|
||||
private HMCLService() {
|
||||
}
|
||||
|
||||
public static void openRedirectLink(String id) {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/" + id);
|
||||
}
|
||||
}
|
||||
@@ -64,11 +64,11 @@ import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static java.lang.Class.forName;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.SelfDependencyPatcher.DependencyDescriptor.JFX_DEPENDENCIES;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA;
|
||||
|
||||
@@ -81,26 +81,37 @@ public final class SelfDependencyPatcher {
|
||||
|
||||
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
|
||||
public static final String CURRENT_ARCH_CLASSIFIER = currentArchClassifier();
|
||||
public static final List<DependencyDescriptor> JFX_DEPENDENCIES = readDependencies();
|
||||
|
||||
private static List<DependencyDescriptor> readDependencies() {
|
||||
String content;
|
||||
try (InputStream in = SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE)) {
|
||||
content = IOUtils.readFullyAsString(in, UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return new Gson().fromJson(content, TypeToken.getParameterized(List.class, DependencyDescriptor.class).getType());
|
||||
}
|
||||
|
||||
private static String currentArchClassifier() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
switch (Architecture.JDK) {
|
||||
switch (Architecture.CURRENT_ARCH) {
|
||||
case X86_64:
|
||||
return "linux";
|
||||
case ARM:
|
||||
case ARM32:
|
||||
return "linux-arm32-monocle";
|
||||
case ARM64:
|
||||
return "linux-aarch64";
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
switch (Architecture.JDK) {
|
||||
switch (Architecture.CURRENT_ARCH) {
|
||||
case X86_64:
|
||||
return "mac";
|
||||
case ARM64:
|
||||
return "mac-aarch64";
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
switch (Architecture.JDK) {
|
||||
switch (Architecture.CURRENT_ARCH) {
|
||||
case X86_64:
|
||||
return "win";
|
||||
case X86:
|
||||
@@ -187,18 +198,6 @@ public final class SelfDependencyPatcher {
|
||||
|
||||
private static final String DEPENDENCIES_LIST_FILE = "/assets/openjfx-dependencies.json";
|
||||
|
||||
private static List<DependencyDescriptor> readDependencies() {
|
||||
String content;
|
||||
try (InputStream in = SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE)) {
|
||||
content = IOUtils.readFullyAsString(in, UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return new Gson().fromJson(content, TypeToken.getParameterized(List.class, DependencyDescriptor.class).getType());
|
||||
}
|
||||
|
||||
private static final List<DependencyDescriptor> JFX_DEPENDENCIES = readDependencies();
|
||||
|
||||
/**
|
||||
* Patch in any missing dependencies, if any.
|
||||
*/
|
||||
@@ -206,7 +205,7 @@ public final class SelfDependencyPatcher {
|
||||
// Do nothing if JavaFX is detected
|
||||
try {
|
||||
try {
|
||||
forName("javafx.application.Application");
|
||||
Class.forName("javafx.application.Application");
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
@@ -419,11 +418,7 @@ public final class SelfDependencyPatcher {
|
||||
}
|
||||
|
||||
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
||||
String expectedHash = dependency.sha1();
|
||||
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependency.localPath()));
|
||||
if (!expectedHash.equalsIgnoreCase(actualHash)) {
|
||||
throw new ChecksumMismatchException("SHA-1", expectedHash, actualHash);
|
||||
}
|
||||
ChecksumMismatchException.verifyChecksum(dependency.localPath(), "SHA-1", dependency.sha1());
|
||||
}
|
||||
|
||||
public static class PatchException extends Exception {
|
||||
|
||||
@@ -65,7 +65,12 @@ public final class Locales {
|
||||
*/
|
||||
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH, ES, RU);
|
||||
/**
|
||||
* Japanese
|
||||
*/
|
||||
public static final SupportedLocale JA = new SupportedLocale(Locale.JAPANESE);
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH, ES, RU, JA);
|
||||
|
||||
public static SupportedLocale getLocaleByName(String name) {
|
||||
if (name == null) return DEFAULT;
|
||||
@@ -80,6 +85,8 @@ public final class Locales {
|
||||
return ES;
|
||||
case "ru":
|
||||
return RU;
|
||||
case "ja":
|
||||
return JA;
|
||||
default:
|
||||
return DEFAULT;
|
||||
}
|
||||
@@ -91,6 +98,7 @@ public final class Locales {
|
||||
else if (locale == ZH_CN) return "zh_CN";
|
||||
else if (locale == ES) return "es";
|
||||
else if (locale == RU) return "ru";
|
||||
else if (locale == JA) return "ja";
|
||||
else if (locale == DEFAULT) return "def";
|
||||
else throw new IllegalArgumentException("Unknown locale: " + locale);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@
|
||||
-fx-pref-width: 200;
|
||||
}
|
||||
|
||||
.bold {
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.memory-label {
|
||||
}
|
||||
|
||||
@@ -151,7 +155,7 @@
|
||||
-fx-padding: 0 0 0 10;
|
||||
}
|
||||
|
||||
.advanced-list-item > .rippler-container > .container > .two-line-list-item > FlowPane > .title {
|
||||
.advanced-list-item > .rippler-container > .container > .two-line-list-item > .first-line > .title {
|
||||
-fx-font-size: 13;
|
||||
-fx-text-alignment: justify;
|
||||
}
|
||||
@@ -165,7 +169,7 @@
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
}
|
||||
|
||||
.advanced-list-item:selected > .rippler-container > .container > .two-line-list-item > FlowPane > .title {
|
||||
.advanced-list-item:selected > .rippler-container > .container > .two-line-list-item > .first-line > .title {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
@@ -178,7 +182,7 @@
|
||||
-fx-padding: 0 0 0 0;
|
||||
}
|
||||
|
||||
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > FlowPane > .title {
|
||||
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
|
||||
-fx-font-size: 13;
|
||||
}
|
||||
|
||||
@@ -194,7 +198,7 @@
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
}
|
||||
|
||||
.profile-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > FlowPane > .title {
|
||||
.profile-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
@@ -257,12 +261,7 @@
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.two-line-list-item > HBox {
|
||||
-fx-spacing: 8;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.two-line-list-item > FlowPane > .title {
|
||||
.two-line-list-item > .first-line > .title {
|
||||
-fx-text-fill: #292929;
|
||||
-fx-font-size: 15px;
|
||||
-fx-padding: 0 8 0 0;
|
||||
@@ -274,7 +273,7 @@
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-list-item > FlowPane > .tag {
|
||||
.two-line-list-item > .first-line > .tag {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
-fx-padding: 2;
|
||||
@@ -286,7 +285,7 @@
|
||||
|
||||
}
|
||||
|
||||
.two-line-item-second-large > FlowPane > .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;
|
||||
@@ -307,7 +306,7 @@
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.bubble > HBox > .two-line-list-item > FlowPane > .title,
|
||||
.bubble > HBox > .two-line-list-item > .first-line > .title,
|
||||
.bubble > HBox > .two-line-list-item > HBox > .subtitle {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
@@ -748,7 +747,7 @@
|
||||
-fx-font-weight: BOLD;
|
||||
-fx-prompt-text-fill: #808080;
|
||||
-fx-alignment: top-left;
|
||||
-fx-pref-width: 300.0;
|
||||
-fx-max-width: 1000000000;
|
||||
-jfx-focus-color: -fx-base-check-color;
|
||||
-fx-padding: 8;
|
||||
-jfx-unfocus-color: transparent;
|
||||
@@ -1219,6 +1218,7 @@
|
||||
-jfx-unfocus-color: transparent;
|
||||
-fx-background-color: #f1f3f4;
|
||||
-fx-padding: 4;
|
||||
-fx-max-width: 1000000000;
|
||||
}
|
||||
|
||||
.jfx-combo-box-warning {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,10 +23,26 @@
|
||||
<JFXSpinner fx:id="spinner" styleClass="first-spinner" />
|
||||
<StackPane fx:id="center" style="-fx-padding: 10;">
|
||||
<ComponentList fx:id="centrePane" styleClass="no-padding">
|
||||
<HBox fx:id="checkPane" spacing="10" style="-fx-padding: 10;">
|
||||
<JFXCheckBox fx:id="chkRelease" text="%version.game.releases" selected="true" />
|
||||
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshots" />
|
||||
<JFXCheckBox fx:id="chkOld" text="%version.game.old" />
|
||||
<HBox fx:id="checkPane" spacing="10">
|
||||
<JFXCheckBox fx:id="chkRelease" text="%version.game.releases" selected="true">
|
||||
<HBox.margin>
|
||||
<Insets top="10" bottom="10" />
|
||||
</HBox.margin>
|
||||
</JFXCheckBox>
|
||||
<JFXCheckBox fx:id="chkSnapshot" text="%version.game.snapshots">
|
||||
<HBox.margin>
|
||||
<Insets top="10" bottom="10" />
|
||||
</HBox.margin>
|
||||
</JFXCheckBox>
|
||||
<JFXCheckBox fx:id="chkOld" text="%version.game.old">
|
||||
<HBox.margin>
|
||||
<Insets top="10" bottom="10" />
|
||||
</HBox.margin>
|
||||
</JFXCheckBox>
|
||||
|
||||
<HBox HBox.hgrow="ALWAYS" />
|
||||
|
||||
<JFXButton fx:id="btnRefresh" styleClass="jfx-tool-bar-button" onMouseClicked="#onRefresh" text="%button.refresh" />
|
||||
</HBox>
|
||||
<JFXListView fx:id="list" styleClass="jfx-list-view-float" VBox.vgrow="ALWAYS">
|
||||
</JFXListView>
|
||||
|
||||
@@ -96,8 +96,8 @@ account.methods.microsoft.error.missing_xbox_account=Your Microsoft account is n
|
||||
account.methods.microsoft.error.no_character=Account is missing a Minecraft Java profile. While the Microsoft account is valid, it does not own the game.
|
||||
account.methods.microsoft.error.unknown=Failed to log in. Microsoft respond with error code %d.
|
||||
account.methods.microsoft.logging_in=Logging in...
|
||||
account.methods.microsoft.hint=You should click "login" button and continue login process in newly opened browser window.
|
||||
account.methods.microsoft.manual=After clicking "login" button, you should finish authorization in the newly opened browser window. If the browser window failed to show, you can click here to copy the URL, and manually open it in your browser.
|
||||
account.methods.microsoft.hint=You should click "login" button, paste the device code shown here later and continue login process in newly opened browser window.
|
||||
account.methods.microsoft.manual=Your device code is <b>%1$s</b>, click here to copy. After clicking "login" button, you should finish authorization in the newly opened browser window. If the browser window failed to show, you can open %2$s manually.
|
||||
account.methods.microsoft.profile=Account Profile...
|
||||
account.methods.microsoft.snapshot=HMCL Snapshot version does not support Microsoft login.
|
||||
account.methods.microsoft.waiting_browser=Waiting for authorization in opened browser window...
|
||||
@@ -182,6 +182,11 @@ curse.category.4472=Tech
|
||||
curse.category.4473=Magic
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5129=Vanilla+
|
||||
curse.category.5189=Utility & QOL
|
||||
curse.category.5190=QoL
|
||||
curse.category.5191=Utility & QoL
|
||||
curse.category.5192=FancyMenu
|
||||
curse.category.423=Map and Information
|
||||
curse.category.426=Addons
|
||||
curse.category.434=Armor, Tools, and Weapons
|
||||
@@ -223,7 +228,8 @@ curse.category.4558=Redstone
|
||||
curse.category.4843=Automation
|
||||
curse.category.4906=MCreator
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
|
||||
curse.category.5193=Data Packs
|
||||
curse.category.399=Steampunk
|
||||
curse.category.396=128x
|
||||
curse.category.398=512x and Higher
|
||||
@@ -289,10 +295,17 @@ download.javafx.notes=Downloading necessary components for HMCL through network.
|
||||
download.javafx.component=Downloading the module %s
|
||||
download.javafx.prepare=Ready to download
|
||||
|
||||
exception.access_denied=It's denied by operating system to access file %s. Maybe we don't have permission to access this file, or this file has already been opened by other program.\n\
|
||||
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
|
||||
extension.png=Image file
|
||||
extension.sh=Bash shell
|
||||
extension.ps1=PowerShell Script
|
||||
extension.sh=Bash Script
|
||||
|
||||
fatal.javafx.incompatible=Application cannot patch JavaFX on current Java environment below 11.\nPlease run this app using JDK 11 or higher or a JDK with JavaFX bundled.
|
||||
fatal.javafx.missing=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Oracle JRE 8, or install BellSoft Liberica Full JRE.\nIf you are using other OpenJDK builds, please ensure OpenJFX is included.
|
||||
@@ -300,6 +313,8 @@ fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the cu
|
||||
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure Hello Minecraft! Launcher has read and write access to "%s" and the files in it.
|
||||
fatal.migration_requires_manual_reboot=The update is complete. Please reopen Hello Minecraft! Launcher.
|
||||
fatal.apply_update_failure=We're sorry, Hello Minecraft! Launcher couldn't finish the update because something went wrong.\nBut you can still manually finish the update by downloading Hello Minecraft! Launcher from %s.\nPlease consider reporting this issue to us.
|
||||
fatal.samba=If you are trying to run HMCL in a shared folder by Samba, HMCL may not working, please try updating your Java or running HMCL in a local folder.
|
||||
fatal.illegal_char=Illegal character '=' in your user folder path. HMCL can work, but some features don't work.\nYou will not be able to use the authlib-injector account or changing skin of offline account, and there may be greater security risks in multiplayer games.
|
||||
|
||||
feedback=Feedback
|
||||
feedback.add=Add Feedback
|
||||
@@ -353,7 +368,11 @@ game.crash.reason.fabric_warnings=Fabric gives some warnings:\n%1$s
|
||||
game.crash.reason.file_already_exists=Game cannot run because file %1$s already exists.\nIf it can be deleted, backup it, delete it and try again.
|
||||
game.crash.reason.file_changed=Game cannot run because file verification failed.\nIf you have modified the Minecraft primary jar, you need roll back the changes, or re-download the game.
|
||||
game.crash.reason.gl_operation_failure=Game crashed due to some mods, shader packs, texture packs.\nPlease disable the mods/shader packs/texture packs you are using and try again.
|
||||
game.crash.reason.graphics_driver=Game crashed due to a problem with your graphics driver.\nPlease update your graphics driver to the latest version.\nIf your computer has a discrete graphics card, you need to check whether the game uses itegrated/core graphics. If so, please run HMCL and game using discrete graphics card. If you still have these problems, you may consider getting a new graphics card or a new computer.
|
||||
game.crash.reason.graphics_driver=Game crashed due to a problem with your graphics driver.\n\
|
||||
Please update your graphics driver to the latest version.\n\
|
||||
If your computer has a discrete graphics card, you need to check whether the game uses integrated/core graphics. If so, please run HMCL and game using discrete graphics card. If you still have these problems, you may consider getting a new graphics card or a new computer.\n\
|
||||
If you do want to continue using the core graphics, please check if you are using Intel(R) Core(TM) 3000 series or older Processor. If so, for Minecraft 1.16.5 or older version, you need to downgrade Java to 1.8.0_51 or older version, or you must buy a dedicated graphics card or new computer.\n\
|
||||
Click <a href="https://hmcl.huangyuhui.net/help/game/graphics-card">here (Chinese website)</a> for more information.
|
||||
game.crash.reason.illegal_access_error=Game crashed because of some mods.\nIf you recognize: %1$s, you may update or delete the mod and try again.
|
||||
game.crash.reason.jdk_9=Game cannot run because the Java version is too high.\nYou need download and install Java 8 and select the newly installed Java in game settings.
|
||||
game.crash.reason.jvm_32bit=Game crashed because memory allocation exceeds the limit of 32-bit Java VM.\nIf your OS is 64-bit, please install and use 64-bit Java. If your OS is 32-bit, you may reinstall a 64-bit OS or change a new computer.\nOr, you can disable automatic memory allocation and set maxmium memory size to 1024MB or below.
|
||||
@@ -375,7 +394,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
|
||||
@@ -396,7 +415,6 @@ install.change_version=Change version
|
||||
install.change_version.confirm=Are you sure you want to update %s from verison %s to %s?
|
||||
install.failed=Version failed to install
|
||||
install.failed.downloading=Failed to install due to some files not downloaded successfully
|
||||
install.failed.downloading.access_denied=Failed to download file: %1$s. Because it's denied by operating system to access file %2$s. Maybe we don't have permission to access this file, or this file has already been opened by other program. You can try to close related program, or restart your computer.
|
||||
install.failed.downloading.detail=Failed to download file: %s
|
||||
install.failed.downloading.timeout=Timed out while downloading the file: %s
|
||||
install.failed.install_online=Unable to recognize the provided installer file. If you are installing a mod, go to "Mods" page.
|
||||
@@ -449,12 +467,18 @@ launch.advice.not_enough_space=You have allocated too much memory, because the p
|
||||
launch.advice.require_newer_java_version=Minecraft %1$s requires Java %2$s or later, are you willing to download one now?
|
||||
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. Shall we continue launching?
|
||||
launch.advice.vanilla_linux_java_8=For Linux x86-64, Minecraft 1.12.2 and below can only run on Java 8.\nJava 9 and above versions cannot load 32-bit native libraries like liblwjgl.so.
|
||||
launch.advice.vanilla_x86=Minecraft currently does not provide official support for architectures other than x86 and x86-64.\nYou can try to download the corresponding native library of the platform and specify its placement path.
|
||||
launch.advice.vanilla_x86.translation=Minecraft currently does not provide official support for architectures other than x86 and x86-64.\nPlease use Java for x86-64 to run minecraft through translator, or download the corresponding native library of the platform and specify its placement path.
|
||||
launch.failed=Unable to launch
|
||||
launch.failed.cannot_create_jvm=Java virtual machine could not be created. Java arguments may cause issues. Please restart without JVM arguments.
|
||||
launch.failed.creating_process=Failed to create process. Check your Java path.
|
||||
launch.failed.command_too_long=The command length exceeds the limit so cannot create bat script. Please export it as PowerShell script.
|
||||
launch.failed.decompressing_natives=Unable to decompress native libraries.
|
||||
launch.failed.download_library=Unable to download library %s.
|
||||
launch.failed.executable_permission=Unable to add permission to the launch script
|
||||
launch.failed.executable_permission=Unable to add permission to the launch script.
|
||||
launch.failed.execution_policy=Set execution policy
|
||||
launch.failed.execution_policy.failed_to_set=Failed to set execution policy
|
||||
launch.failed.execution_policy.hint=The current execution policy prevents running of PowerShell scripts.\nClick 'Ok' to allow the current user to execute PowerShell scripts,or click 'Cancel' to keep the status quo.
|
||||
launch.failed.exited_abnormally=Game exited abnormally, please check the log, or ask someone for help.
|
||||
launch.failed.no_accepted_java=Cannot find the Java installation suitable for current game. If you think you have installed a suitable Java VM, you can manually select it in game settings.
|
||||
launch.state.dependencies=Dependencies
|
||||
@@ -475,6 +499,7 @@ launcher.background.classic=Classic
|
||||
launcher.background.choose=Choose a background image file
|
||||
launcher.background.default=Standard
|
||||
launcher.background.network=Network
|
||||
launcher.background.translucent=Translucent
|
||||
launcher.cache_directory=Directory for caching
|
||||
launcher.cache_directory.clean=Clear
|
||||
launcher.cache_directory.choose=Choose the directory for caching
|
||||
@@ -633,9 +658,11 @@ multiplayer.download.success=Dependencies initialization succeeded
|
||||
multiplayer.download.unsupported=Current operating system or architecure is unsupported.
|
||||
multiplayer.exit.after_ready=Multiplayer session broken. cato exitcode %d
|
||||
multiplayer.exit.before_ready=Multiplayer session failed to create. cato exitcode %d
|
||||
multiplayer.exit.timeout=Failed to connect to multiplayer server.
|
||||
multiplayer.exit.timeout.static_token=Failed to connect to multiplayer server. Please switch to dynamic token and try again.
|
||||
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
|
||||
@@ -679,20 +706,20 @@ 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.
|
||||
multiplayer.session.join.hint=You must obtain the invitation code from the gamer who has already created a multiplayer session.
|
||||
multiplayer.session.join.invitation_code=Invitation code
|
||||
multiplayer.session.join.invitation_code.error=Incorrect invitation code. Please obtain invitation code from the player who creates the multiplayer session.
|
||||
multiplayer.session.join.invitation_code.version=Versions of multiplayer functionalities are not the same among you.
|
||||
multiplayer.session.join.kicked=You have been kicked by the session holder. You will lost connection with the multiplayer session.
|
||||
multiplayer.session.join.kicked=Lost connection: %s.
|
||||
multiplayer.session.join.kicked.join_acceptance_timeout=The session holder does not accept your join request in time.
|
||||
multiplayer.session.join.kicked.kicked=You have been kicked by the session holder.
|
||||
multiplayer.session.join.kicked.version_not_matched=Versions of multiplayer functionalities are not the same among you.
|
||||
multiplayer.session.join.lost_connection=Lost connection with the multiplayer session. Maybe the session is destroyed by the creator, or you cannot establish connection with this session.
|
||||
multiplayer.session.join.port.error=Cannot find available local network port for listening. Please ensure that HMCL has the permission to listen on a port.
|
||||
multiplayer.session.join.rejected=Your connection is rejected by the session holder.
|
||||
multiplayer.session.join.wait=Waiting for acceptation.
|
||||
multiplayer.session.join.wait_timeout=The session holder does not accept your join request in time.
|
||||
multiplayer.session.members=Room Members
|
||||
multiplayer.session.quit=Quit Room
|
||||
multiplayer.session.quit.warning=After quiting room, you will lost the connection with the server. Continue?
|
||||
@@ -829,8 +856,11 @@ settings.game.exploration=Explore
|
||||
settings.game.fullscreen=Fullscreen
|
||||
settings.game.java_directory=Java Directory
|
||||
settings.game.java_directory.auto=Automatically selected
|
||||
settings.game.java_directory.bit=, %s-Bit
|
||||
settings.game.java_directory.auto.not_found=No suitable Java
|
||||
settings.game.java_directory.bit=%s-Bit
|
||||
settings.game.java_directory.choose=Choose Java Directory.
|
||||
settings.game.java_directory.invalid=Invalid Java Path
|
||||
settings.game.java_directory.template=%s (%s)
|
||||
settings.game.management=Manage
|
||||
settings.game.working_directory=Working Directory
|
||||
settings.game.working_directory.choose=Choose Working Directory
|
||||
@@ -920,7 +950,7 @@ update.found=Update Available!
|
||||
update.newest_version=Latest version: %s
|
||||
update.bubble.title=Update Available: %s
|
||||
update.bubble.subtitle=Click here to update
|
||||
update.note=Warning: Beta versions may have more functionality and bug fixes but also more possible errors.
|
||||
update.note=Warning: Beta versions and nightly versions may have more functionality and bug fixes but also more possible errors.
|
||||
update.latest=This is the latest version.
|
||||
update.no_browser=Cannot open a browser. The link has been copied to the clipboard. Paste it into the address bar of browser to update.
|
||||
update.tooltip=Update
|
||||
@@ -948,7 +978,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
|
||||
|
||||
@@ -342,7 +342,7 @@ settings.advanced.launcher_visible=Visibilidad de launcher
|
||||
settings.advanced.minecraft_arguments=Minecraft Arguments
|
||||
settings.advanced.natives_directory=Loka biblioteko- vojo (LWJGL)
|
||||
settings.advanced.natives_directory.choose=Elektu lokan bibliotekvojn
|
||||
settings.advanced.natives_directory.default=Por defecto (.minecraft/versions/<version name>/natives/)
|
||||
settings.advanced.natives_directory.default=Por defecto (El lanzador proporciona la biblioteca local del juego)
|
||||
settings.advanced.no_jvm_args=No JVM Args por defecto
|
||||
settings.advanced.precall_command=Comando pre-inicio (será ejecutado antes de inicio del juego)
|
||||
settings.advanced.server_ip=Host de servidor
|
||||
@@ -355,7 +355,7 @@ settings.game.dimension=Dimensiones de ventana de juego
|
||||
settings.game.exploration=Explorar
|
||||
settings.game.fullscreen=Pantalla completa
|
||||
settings.game.java_directory=Directorio Java
|
||||
settings.game.java_directory.bit=, %s-Bit
|
||||
settings.game.java_directory.bit=%s-Bit
|
||||
settings.game.java_directory.choose=Escoja directorio Java.
|
||||
settings.game.management=Gestionar
|
||||
settings.game.working_directory=Directorio de función
|
||||
|
||||
@@ -0,0 +1,991 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Contributors: zhixuan2333
|
||||
about=About
|
||||
about.copyright=著作権
|
||||
about.copyright.statement=Copyright(c)2021huangyuhui。
|
||||
about.author=Author
|
||||
about.author.statement=Bilibili: @huanghongxun
|
||||
about.claim=EULA
|
||||
about.claim.statement=テキスト全体のリンクをクリックします。
|
||||
about.community=コミュニティ
|
||||
about.community.discord=Discord
|
||||
about.community.discord.statement=チャットに参加しよう!
|
||||
about.community.kaiheila=KaiHeiLa
|
||||
about.community.kaiheila.statement=チャットに参加しよう!
|
||||
about.dependency=Dependencies
|
||||
about.legal=法的承認
|
||||
about.thanks_to=Thanks to
|
||||
about.thanks_to.bangbang93.statement=BMCLAPIダウンロードソースプロバイダー。
|
||||
about.thanks_to.contributors=問題、プルリクエストなどを介してこのプロジェクトに参加したすべての貢献者。
|
||||
about.thanks_to.contributors.statement=Hello Minecraftをサポートしてくれたオープンソースコミュニティに感謝します!ランチャー
|
||||
about.thanks_to.gamerteam.statement=デフォルトの背景画像プロバイダー。
|
||||
about.thanks_to.mcbbs=MCBBS
|
||||
about.thanks_to.mcbbs.statement=MCBBSダウンロードソースプロバイダー。
|
||||
about.thanks_to.mcmod=mcmod.cn
|
||||
about.thanks_to.mcmod.statement=Mod中国語の名前翻訳プロバイダー。
|
||||
about.thanks_to.noin=noin.cn
|
||||
about.thanks_to.noin.statement=マルチプレイヤーサービスプロバイダー。
|
||||
about.thanks_to.red_lnn.statement=デフォルトの背景画像プロバイダー。
|
||||
about.thanks_to.users=HMCLユーザーグループのメンバー
|
||||
about.thanks_to.users.statement=寄付、バグレポートなどに感謝します。
|
||||
about.thanks_to.yushijinhun.statement=authlib-インジェクターのサポート
|
||||
about.open_source=オープンソース
|
||||
about.open_source.statement=GPL v3(https://github.com/huanghongxun/HMCL/)
|
||||
|
||||
account=アカウント
|
||||
account.cape=マント
|
||||
account.character=キャラクター
|
||||
account.choose=キャラクターを選択する
|
||||
account.create=アカウントを追加
|
||||
account.create.microsoft=Microsoft Login
|
||||
account.create.yggdrasil=Mojang Login
|
||||
account.create.offline=Offline
|
||||
account.create.authlibInjector=サードパーティーアカウントを追加
|
||||
account.email=メールアドレス
|
||||
account.failed=エラーコード:%d
|
||||
account.failed.character_deleted=キャラクターが削除されました。
|
||||
account.failed.connect_authentication_server=認証サーバーに接続できません。インターネット接続を確認してください。
|
||||
account.failed.connect_injector_server=認証サーバーに接続できません。ネットワークをチェックして、URLが正しいことを確認してください。
|
||||
account.failed.injector_download_failure=authlib-injectorのダウンロードに失敗しました。ネットワークを確認して、別のダウンロードソースに切り替えてみてください。
|
||||
account.failed.invalid_credentials=パスワードが正しくないか、一時的にログインできません。
|
||||
account.failed.invalid_password=無効なパスワード
|
||||
account.failed.invalid_token=ログアウトし、パスワードを再入力してログインしてください。
|
||||
account.failed.migration=アカウントをMicrosoftアカウントに移行する必要があります。すでに移行されている場合は、代わりに移行されたMicrosoftアカウントにログインする必要があります。
|
||||
account.failed.no_character=このアカウントに文字がありません。
|
||||
account.failed.server_response_malformed=無効なサーバー応答。認証サーバーでエラーが発生している可能性があります。
|
||||
account.failed.wrong_account=不一致のアカウントでログインしました。
|
||||
account.injector.add=認証サーバーを追加します
|
||||
account.injector.empty=Empty(追加するには右側のプラスボタンをクリックしてください)
|
||||
account.injector.http=警告:このサーバーはHTTPを使用するため、パスワードはクリアテキストで送信されます。
|
||||
account.injector.link.register=登録
|
||||
account.injector.server=Authサーバー
|
||||
account.injector.server_url=サーバーのURL
|
||||
account.injector.server_name=サーバー名
|
||||
account.login=ログイン
|
||||
account.login.hint=パスワードは保存されません。
|
||||
account.login.refresh=再ログイン
|
||||
account.logout=ログアウト
|
||||
account.register=登録
|
||||
account.manage=アカウントリスト
|
||||
account.methods=ログインタイプ
|
||||
account.methods.authlib_injector=authlib-インジェクター
|
||||
account.methods.microsoft=Microsoft Account
|
||||
account.methods.microsoft.birth=誕生日の設定を編集する方法...
|
||||
account.methods.microsoft.close_page=Microsoftアカウントの認証が終了しました。後で終了するログイン手順がいくつか残っています。このページを今すぐ閉じることができます。
|
||||
account.methods.microsoft.error.add_family=まだ18歳ではないため、Minecraftをプレイするには、大人があなたを家族に追加する必要があります。
|
||||
account.methods.microsoft.error.add_family_probably=すでに18歳であるかどうかにかかわらず、アカウント設定を確認してください。そうでない場合は、アカウントを家族に追加する必要があります。
|
||||
account.methods.microsoft.error.country_unavailable=XBoxLiveはお住まいの国/地域ではご利用いただけません。
|
||||
account.methods.microsoft.error.missing_xbox_account=MicrosoftアカウントがXboxアカウントに接続されていません。続行する前に作成してください。
|
||||
account.methods.microsoft.error.no_character=アカウントにMinecraftJavaプロファイルがありません。 Microsoftアカウントは有効ですが、ゲームを所有していません。
|
||||
account.methods.microsoft.error.unknown=ログインに失敗しました。Microsoftはエラーコード%dで応答します。
|
||||
account.methods.microsoft.logging_in=ログイン...
|
||||
account.methods.microsoft.hint=「ログイン」ボタンをクリックして、新しく開いたブラウザウィンドウでログインプロセスを続行する必要があります。
|
||||
account.methods.microsoft.manual=「ログイン」ボタンをクリックした後、新しく開いたブラウザウィンドウで認証を完了する必要があります。ブラウザウィンドウが表示されない場合は、ここをクリックしてURLをコピーし、ブラウザで手動で開くことができます。
|
||||
account.methods.microsoft.profile=アカウントプロファイル..
|
||||
account.methods.microsoft.snapshot=HMCLスナップショットバージョンはMicrosoftログインをサポートしていません。
|
||||
account.methods.microsoft.waiting_browser=開いているブラウザウィンドウで認証を待機しています...
|
||||
account.methods.offline=オフライン
|
||||
account.methods.offline.uuid=UUID
|
||||
account.methods.offline.uuid.hint=UUIDは、Minecraftのゲームキャラクターの一意の識別子です。 UUIDの生成方法は、ゲームランチャーによって異なります。 UUIDを他のランチャーによって生成されたものに変更すると、オフラインアカウントのバックパック内のゲームブロック/アイテムが残ることが約束されます。このオプションは専門家向けです。何をしているのかわからない限り、このオプションを変更することはお勧めしません。
|
||||
account.methods.offline.uuid.malformed=Malformed
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.methods.yggdrasil.migration=Microsoftアカウントに移行する
|
||||
account.methods.yggdrasil.migration.how=Microsoftアカウントに移行する方法は?
|
||||
account.methods.yggdrasil.purchase=Minecraftを購入する
|
||||
account.missing=アカウントなし
|
||||
account.missing.add=追加するにはここをクリック
|
||||
account.not_logged_in=ログインしていません
|
||||
account.password=パスワード
|
||||
account.skin=スキン
|
||||
account.skin.file=スキンイメージファイル
|
||||
account.skin.model=モデル
|
||||
account.skin.model.default=Classic (Steve)
|
||||
account.skin.model.slim=Slim (Alex)
|
||||
account.skin.type.csl_api=Blessing Skin
|
||||
account.skin.type.csl_api.location=アドレス
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI
|
||||
account.skin.type.little_skin=LittleSkin
|
||||
account.skin.type.little_skin.hint=スキンのWebサイトでキャラクターを作成する必要があります。そして、このオフラインアカウントのスキンは、同じ名前のスキンWebサイトのキャラクターのスキンにバインドされます。
|
||||
account.skin.type.local_file=ローカルスキン画像ファイル
|
||||
account.skin.upload=スキンをアップロードする
|
||||
account.skin.upload.failed=スキンのアップロードに失敗しました
|
||||
account.skin.invalid_skin=認識されないスキンファイル
|
||||
account.username=ユーザー名
|
||||
|
||||
archive.author=作成者
|
||||
archive.date=公開日
|
||||
archive.game_version=ゲームバージョン
|
||||
archive.name=名前
|
||||
archive.version=バージョン
|
||||
|
||||
Assets.download=アセットのダウンロード
|
||||
Assets.download_all=アセットの整合性チェック
|
||||
Assets.index.malformed=アセットインデックスの形式が正しくありません。バージョン設定の[ゲームアセットファイルの更新]で再試行できます。
|
||||
|
||||
button.cancel=キャンセル
|
||||
button.change_source=ダウンロードソースの変更
|
||||
button.clear=クリア
|
||||
button.delete=削除
|
||||
button.edit=編集
|
||||
button.install=インストール
|
||||
button.export=エクスポート
|
||||
button.no=いいえ
|
||||
button.ok=OK
|
||||
button.refresh=更新
|
||||
button.remove=削除
|
||||
button.remove.confirm=削除してもよろしいですか?この操作をロールバックすることはできません!
|
||||
button.save=保存
|
||||
button.save_as=名前を付けて保存
|
||||
button.select_all=すべて選択
|
||||
button.yes=はい
|
||||
|
||||
color.recent=推奨
|
||||
color.custom=カスタムカラー
|
||||
|
||||
crack.NoClassDefFound=「HelloMinecraft!Launcher」ソフトウェアが破損していないことを確認してください。
|
||||
crack.user_fault=OSまたはJava環境が正しくインストールされていない可能性があり、クラッシュする可能性があります。Javaランタイム環境またはコンピュータを確認してください。
|
||||
|
||||
curse.category.0=すべて
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/4471
|
||||
curse.category.4474=Sci-Fi
|
||||
curse.category.4481=小さい/軽い
|
||||
curse.category.4483=戦闘
|
||||
curse.category.4477=ミニゲーム
|
||||
curse.category.4478=クエスト
|
||||
curse.category.4484=マルチプレイヤー
|
||||
curse.category.4476=探索
|
||||
curse.category.4736=Skyblock
|
||||
curse.category.4475=アドベンチャーとRPG
|
||||
curse.category.4487=FTB
|
||||
curse.category.4480=マップベース
|
||||
curse.category.4479=ハードコア
|
||||
curse.category.4482=特大
|
||||
curse.category.4472=Tech
|
||||
curse.category.4473=魔法
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5129=Vanilla+
|
||||
curse.category.5189=Utility & QOL
|
||||
curse.category.5190=QoL
|
||||
curse.category.5191=Utility & QoL
|
||||
curse.category.5192=FancyMenu
|
||||
curse.category.423=地図と情報
|
||||
curse.category.426=Addons
|
||||
curse.category.434=武器、道具、武器
|
||||
curse.category.409=構造
|
||||
curse.category.4485=ブラッドマジック
|
||||
curse.category.420=ストレージ
|
||||
curse.category.429=インダストリアルクラフト
|
||||
curse.category.419=魔法
|
||||
curse.category.412=テクノロジー
|
||||
curse.category.4557=レッドストーン
|
||||
curse.category.428=Tinkerの構成
|
||||
curse.category.414=プレイヤートランスポート
|
||||
curse.category.4486=ラッキーブロック
|
||||
curse.category.432=Buildcraft
|
||||
curse.category.418=遺伝学
|
||||
curse.category.4671=Twitch統合
|
||||
curse.category.408=鉱石と資源
|
||||
curse.category.4773=CraftTweaker
|
||||
curse.category.430=Thaumcraft
|
||||
curse.category.422=アドベンチャーとRPG
|
||||
curse.category.413=処理中
|
||||
curse.category.417=エネルギー
|
||||
curse.category.415=エネルギー、流体、およびアイテムの輸送
|
||||
curse.category.433=林業
|
||||
curse.category.425=その他
|
||||
curse.category.4545=Applied Energistics 2
|
||||
curse.category.416=農業
|
||||
curse.category.421=APIとライブラリ
|
||||
curse.category.4780=ファブリック
|
||||
curse.category.424=Cosmetic
|
||||
curse.category.406=世界の世代
|
||||
curse.category.435=サーバーユーティリティ
|
||||
curse.category.411=モブ
|
||||
curse.category.407=バイオーム
|
||||
curse.category.427=熱膨張
|
||||
curse.category.410=ディメンション
|
||||
curse.category.436=食品
|
||||
curse.category.4558=レッドストーン
|
||||
curse.category.4843=自動化
|
||||
curse.category.4906=MCreator
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.399=スチームパンク
|
||||
curse.category.396=128x
|
||||
curse.category.398=512x以上
|
||||
curse.category.397=256x
|
||||
curse.category.405=その他
|
||||
curse.category.395=64x
|
||||
curse.category.400=フォトリアリスティック
|
||||
curse.category.393=16x
|
||||
curse.category.403=従来型
|
||||
curse.category.394=32x
|
||||
curse.category.404=アニメーション
|
||||
curse.category.4465=Modサポート
|
||||
curse.category.402=中世
|
||||
curse.category.401=モダン
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/17
|
||||
curse.category.4464=変更された世界
|
||||
curse.category.250=ゲームマップ
|
||||
curse.category.249=作成
|
||||
curse.category.251=パルクール
|
||||
curse.category.253=サバイバル
|
||||
curse.category.248=アドベンチャー
|
||||
curse.category.252=パズル
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/4546
|
||||
curse.category.4551=ハードコアクエストモード
|
||||
curse.category.4548=ラッキーブロック
|
||||
curse.category.4556=進行
|
||||
curse.category.4752=ガジェットの構築
|
||||
curse.category.4553=CraftTweaker
|
||||
curse.category.4554=レシピ
|
||||
curse.category.4549=ガイドブック
|
||||
curse.category.4547=構成
|
||||
curse.category.4550=クエスト
|
||||
curse.category.4555=World Gen
|
||||
curse.category.4552=スクリプト
|
||||
|
||||
curse.sort.author=Author
|
||||
curse.sort.date_created=作成日
|
||||
curse.sort.last_updated=最終更新
|
||||
curse.sort.name=名前
|
||||
curse.sort.popularity=人気
|
||||
curse.sort.total_downloads=合計ダウンロード数
|
||||
|
||||
download=ダウンロード
|
||||
download.code.404=リモートサーバーにファイルが見つかりません:%s
|
||||
download.content=ゲームコンテンツ
|
||||
download.curseforge.customization=光と影、およびゲームのカスタマイズ
|
||||
download.existing=ファイルは既に存在するため、保存できません。 「名前を付けて保存」を選択して、ファイルを別の場所に保存できます。
|
||||
download.external_link=ダウンロードサイトを開く
|
||||
download.failed=%1$sのダウンロードに失敗しました、応答コード:%2 $ d
|
||||
download.failed.empty=候補者なし。戻るにはここをクリックしてください。
|
||||
download.failed.refresh=バージョンリストをダウンロードできません。ここをクリックして再試行してください。
|
||||
download.game=ゲームのダウンロード
|
||||
download.provider.mcbbs=MCBBS(https://www.mcbbs.net/)
|
||||
download.provider.bmclapi=BMCLAPI(ミラーソース)
|
||||
download.provider.mojang=Mojang(OptiFineはBMCLAPIにより提供されます) 推奨
|
||||
download.provider.official=公式ソースからロード
|
||||
download.provider.balanced=mcbbsのソースからロード
|
||||
download.provider.mirror=ミラーソースからロード
|
||||
download.javafx=必要なランタイムコンポーネントのダウンロード
|
||||
download.javafx.notes=ネットワークを介したHMCLに必要なコンポーネントのダウンロード。\n [ダウンロードソースの変更]ボタンをクリックして詳細を表示し、ダウンロードソースを選択するか、[キャンセル]ボタンをクリックして停止して終了します。\n注:ダウンロード速度が遅すぎます。ダウンロードソースを変更してみてください。
|
||||
download.javafx.component=モジュール%sをダウンロードしています
|
||||
download.javafx.prepare=ダウンロードする準備ができました
|
||||
|
||||
exception.access_denied=It's denied by operating system to access file %s. Maybe we don't have permission to access this file, or this file has already been opened by other program.\n\
|
||||
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=WindowsBatファイル
|
||||
extension.mod=Modファイル
|
||||
extension.png=画像ファイル
|
||||
extension.ps1=PowerShellスクリプト
|
||||
extension.sh=Bashスクリプト
|
||||
|
||||
fatal.javafx.incompatible=アプリケーションは11未満の現在のJava環境でJavaFXにパッチを適用できません。\nこのアプリは、JDK11以降またはJavaFXがバンドルされたJDKを使用して実行してください。
|
||||
fatal.javafx.missing=JavaFXがありません。\nJava11以降を使用している場合は、Oracle JRE 8にダウングレードするか、BellSoft Liberica FullJREをインストールしてください。\n他のOpenJDKビルドを使用している場合は、OpenJFXが含まれていることを確認してください。
|
||||
fatal.missing_dst_root_ca_x3=現在のJavaプラットフォームにDSTルートCAX3証明書がありません。\n引き続きHelloMinecraftを使用できます。ランチャーですが、一部のサイト(Let's Encryptによって発行された証明書を使用するサイトなど)に接続できず、ランチャーが正しく機能しない可能性があります。\n問題を解決するには、Javaランタイムを8u101以降に更新してください。
|
||||
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。 Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。
|
||||
fatal.migration_requires_manual_reboot=更新が完了しました。 Hello Minecraftを再開してください!ランチャー。
|
||||
fatal.apply_update_failure=ごめんなさい、Hello Minecraft! Launcher 何か問題が発生したため、ランチャーは更新を完了できませんでした。\nただし、Hello Minecraftをダウンロードすることで、手動で更新を終了できます。 %sからのランチャー。\nこの問題を報告することを検討してください。
|
||||
fatal.samba=If you are trying to run HMCL in a shared folder by Samba, HMCL may not working, please try updating your Java or running HMCL in a local folder.
|
||||
|
||||
feedback=フィードバック
|
||||
Feedback.add=フィードバックを追加
|
||||
Feedback.add.hint.search_before_add=新しいフィードバックを追加する前に、キーワードを検索して、報告する内容がすでに報告されているかどうかを確認する必要があります。持っている場合は、それを賛成して優先順位を上げることができます
|
||||
Feedback.add.hint.title=フィードバックのタイトルは、ニーズを簡潔に要約できる必要があります。 「問題がある」、「アイデアがある」、「ゲームが開かない」など、一般的な問題を一目で確認できないタイトルは使用できません。
|
||||
Feedback.add.hint.content=フィードバックコンテンツは、ニーズを完全かつ簡潔に表現する必要があります。問題が発生した場合は、ランチャーを開いた後にクリックされたボタンや、操作が実行された後にトリガーされる問題など、繰り返しパスを詳細に説明する必要があります。新しい機能を追加する場合は、プレーヤーがその機能を必要とする理由、その機能で解決できる問題、およびその機能の実装方法について詳しく説明する必要があります。
|
||||
Feedback.add.login=フィードバックの許可を得るには、HMCLアカウントにログイン/登録する必要があります。
|
||||
Feedback.add.permission=新しいフィードバックを追加するには、フィードバック権限を取得する必要があります。
|
||||
feedback.author=作成者
|
||||
feedback.content=コンテンツ
|
||||
Feedback.empty=条件を満たすアイテムはありません。
|
||||
Feedback.failed=ロードに失敗しました
|
||||
Feedback.failed.permission=フィードバックを送信する権限を持つアカウントにログインしている場合にのみ、キーワードで検索できます。
|
||||
Feedback.failed.too_frequently=頻繁すぎる。あとでもう一度試してみてください。
|
||||
Feedback.like=Like
|
||||
Feedback.login=HMCLアカウントにログインします
|
||||
feedback.response=応答
|
||||
Feedback.response.empty=応答なし
|
||||
feedback.state.accepted=承認済み
|
||||
Feedback.state.open=保留中
|
||||
Feedback.state.rejected=拒否されました
|
||||
feedback.title=タイトル
|
||||
feedback.type=タイプ
|
||||
Feedback.type.bug=バグレポート
|
||||
Feedback.type.feature=機能リクエスト
|
||||
Feedback.unlike=Unlike
|
||||
Feedback.version=ランチャーバージョン
|
||||
|
||||
file=ファイル
|
||||
|
||||
folder.config=Config
|
||||
folder.game=ゲームディレクトリ
|
||||
folder.mod=Mod
|
||||
folder.resourcepacks=リソースパック
|
||||
folder.saves=マップ
|
||||
folder.screenshots=スクリーンショット
|
||||
|
||||
game=ゲーム
|
||||
game.crash.feedback=問題は非常に一般的ですが、理由がわかりませんでしたか?フィードバックページでフィードバックをお寄せください。
|
||||
game.crash.info=ゲームステータス
|
||||
game.crash.reason=クラッシュアナライザー
|
||||
game.crash.reason.analyzing=分析中..
|
||||
game.crash.reason.block=ブロックが原因でゲームを実行できません。\nMCEditでこのブロックを削除するか、関連するModを直接削除できます。\nブロックタイプ:%1$s \nブロックの場所:%2 $ s
|
||||
game.crash.reason.bootstrap_failed=mod%1$sがクラッシュしたため、ゲームを実行できません。\n削除または更新を試みることができます。
|
||||
game.crash.reason.config=modが構成ファイルを解析できないため、ゲームを実行できません。\nMod%1$sは構成ファイル%2 $ sを解析できません。
|
||||
game.crash.reason.debug_crash=クラッシュが手動でトリガーされるため、ゲームを実行できません。\n実際、ゲームは無実であり、すべてあなたの責任です。
|
||||
game.crash.reason.duplicated_mod=modが重複しているためゲームを実行できません:%s。\n%s \n各Modは1つしかインストールできません。重複したmodを削除して、再試行してください。
|
||||
game.crash.reason.entity=エンティティが原因でゲームを実行できません。\nMCEditでこのエンティティを削除するか、関連するModを直接削除できます。\nエンティティタイプ:%1$s \nエンティティの場所:%2 $ s
|
||||
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
|
||||
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
|
||||
game.crash.reason.file_already_exists=ファイル%1$sが既に存在するため、ゲームを実行できません。\n削除できる場合は、バックアップして削除し、再試行してください。
|
||||
game.crash.reason.file_changed=ファイルの検証に失敗したため、ゲームを実行できません。\nMinecraftプライマリjarを変更した場合は、変更をロールバックするか、ゲームを再ダウンロードする必要があります。
|
||||
game.crash.reason.gl_operation_failure=一部のモッド、シェーダーパック、テクスチャパックが原因でゲームがクラッシュしました。\n使用しているモッド/シェーダーパック/テクスチャパックを無効にして、再試行してください。
|
||||
game.crash.reason.graphics_driver=グラフィックスドライバーの問題によりゲームがクラッシュしました。\nグラフィックスドライバーを最新バージョンに更新してください。\nコンピューターにディスクリートグラフィックスカードがある場合は、ゲームでitegrated /を使用しているかどうかを確認する必要があります。コアグラフィックス。その場合は、ディスクリートグラフィックカードを使用してHMCLとゲームを実行してください。それでもこれらの問題が発生する場合は、新しいグラフィックカードまたは新しいコンピュータの入手を検討してください。
|
||||
game.crash.reason.illegal_access_error=一部のmodが原因でゲームがクラッシュしました。\n認識している場合:%1$s、modを更新または削除して、再試行してください。
|
||||
game.crash.reason.jdk_9=Javaのバージョンが高すぎるため、ゲームを実行できません。\nJava 8をダウンロードしてインストールし、ゲーム設定で新しくインストールしたJavaを選択する必要があります。
|
||||
game.crash.reason.jvm_32bit=メモリ割り当てが32ビットJavaVMの制限を超えたため、ゲームがクラッシュしました。\nOSが64ビットの場合は、64ビットJavaをインストールして使用してください。 OSが32ビットの場合は、64ビットOSを再インストールするか、新しいコンピュータを変更できます。\nまたは、自動メモリ割り当てを無効にして、最大メモリサイズを1024MB以下に設定できます。
|
||||
game.crash.reason.loading_crashed_forge=mod%1$s(%2 $ s)がクラッシュしたため、ゲームがクラッシュしました。\n削除または更新を試みることができます。
|
||||
game.crash.reason.loading_crashed_fabric=mod%1$sがクラッシュしたため、ゲームがクラッシュしました。\n削除または更新を試みることができます。
|
||||
game.crash.reason.memory_exceeded=JVMが割り当てるのに十分なメモリがないため、ゲームがクラッシュしました。\nこの問題は、ページサイズが小さすぎることが原因です。\nゲーム設定で自動メモリ割り当てをオフにし、メモリ割り当てを次のように調整する必要があります。システムが処理できる値。\nシステムのページサイズを十分な大きさに調整することもできます。
|
||||
game.crash.reason.mod=modが原因でゲームがクラッシュしました:%1$s。\nmodを更新または削除して、再試行できます。
|
||||
game.crash.reason.mod_resolution=modの解決に失敗したため、ゲームがクラッシュしました。\nFabricは次の情報を提供します:\n%s
|
||||
game.crash.reason.mod_resolution_collection=必要なmodバージョンに互換性がないため、ゲームがクラッシュしました。\n%1$sには%2 $ sが必要です。\nこれは、%2 $ sをアップグレードまたはダウングレードする必要があることを意味します。 %3 $ sをダウンロードする必要があります。
|
||||
game.crash.reason.mod_resolution_conflict=modが競合しているため、ゲームがクラッシュしました。\n%1$sが%2 $ sと競合しています。
|
||||
game.crash.reason.mod_resolution_missing=一部のmodが依存するmodがインストールされていないため、ゲームがクラッシュしました。\n%1$sではmodをインストールする必要があります:%2 $ s。\nこれは、%2 $をダウンロードする必要があることを意味します。 ■ダウンロードページまたはオンラインWebサイトから。
|
||||
game.crash.reason.mod_resolution_missing_minecraft=modが現在のMinecraftバージョンと互換性がないため、ゲームがクラッシュしました。\n%1$sにはMinecraftバージョン%2 $ sが必要です。\nインストールされているバージョンのmodを保持する場合は、変更する必要があります。ゲームバージョン。\n現在のゲームバージョンを引き続き使用する場合は、適切なmodを再インストールする必要があります。
|
||||
game.crash.reason.mod_resolution_mod_version=%1$s(バージョン:%2 $ s)
|
||||
game.crash.reason.mod_resolution_mod_version.any=%1$s(任意のバージョン)
|
||||
game.crash.reason.no_class_def_found_error=コードが不完全なためゲームを実行できません。\nゲームにmodがないか、一部のmodファイルが不完全であるか、一部のmodが現在のゲームと互換性がない可能性があります。\nゲームを再インストールする必要がある場合があります。およびmod、またはヘルプを要求します。\n%1$sがありません
|
||||
game.crash.reason.no_such_method_error=コードが不完全なためゲームを実行できません。\nゲームにmodがないか、一部のmodファイルが不完全であるか、一部のmodが現在のゲームと互換性がない可能性があります。\nゲームを再インストールする必要がある場合があります。と改造、または助けを求める。
|
||||
game.crash.reason.opengl_not_supported=グラフィックスドライバに問題があるため、ゲームを実行できません。\nOpenGLがサポートされていないため、リモートデスクトップモードですか、それともストリーミングモードですか。はいの場合は、元のコンピューターを使用してゲームを開始してください。\nまたは、グラフィックスドライバーを最新バージョンに更新して、ゲームを再起動してください。コンピューターに個別のグラフィックがある場合は、ゲームが統合/コアグラフィックを使用しているかどうかを確認する必要があります。その場合は、ディスクリートグラフィックカードを使用してHMCLとゲームを実行してみてください。それでもこれらの問題が発生する場合は、新しいグラフィックカードまたは新しいコンピュータの入手を検討してください。
|
||||
game.crash.reason.openj9=ゲームはOpenJ9仮想マシンでは実行できません。ゲーム設定でHotspotJava仮想マシンに切り替えて、ゲームを再起動してください。そうでない場合は、オンラインでダウンロードできます。
|
||||
game.crash.reason.out_of_memory=メモリ不足のためゲームを実行できません。\nこれは、メモリ割り当てが少なすぎるか、modが多すぎることが原因である可能性があります。\nゲーム設定でゲームメモリ割り当てを増やして、ゲームを実行できるようにすることができます。より大きなメモリ。\nそれでもこれらの問題が発生する場合は、より良いコンピュータを変更する必要があるかもしれません。
|
||||
game.crash.reason.resolution_too_high=解像度が高すぎるリソースパックを使用しているため、ゲームを実行できません\n解像度の低いリソースパックを使用するか、メモリの大きいグラフィックカードを購入する必要があります。
|
||||
game.crash.reason.stacktrace=不明。 [ログ]ボタンをクリックすると詳細を確認できます。\nModIDを表すキーワードがいくつかあります。オンラインで検索して理由を見つけることができます。\n%s
|
||||
game.crash.reason.too_old_java=Java仮想マシンのバージョンが低すぎるため、ゲームを実行できません。\nゲーム設定でJava仮想マシンの新しいバージョン(%1$s)に切り替えて、ゲームを再起動する必要があります。そうでない場合は、オンラインでダウンロードできます。
|
||||
game.crash.reason.unknown=不明。 「ログ」ボタンをクリックすると詳細を確認できます。
|
||||
game.crash.reason.unsatisfied_link_error=必要なネイティブライブラリがないため、ゲームを実行できません。\n不足しているネイティブライブラリ:%1$s。\nゲーム設定でネイティブライブラリパスオプションを変更した場合は、元に戻すことをお勧めします。標準モードに切り替えます。\nすでに標準モードになっている場合は、不足しているネイティブライブラリがmodまたはゲームに属しているかどうかを確認してください。 HMCLが原因であることが確実な場合は、フィードバックをお送りください。\nネイティブライブラリパスを本当にカスタマイズする必要がある場合は、ゲームに必要なすべてのネイティブライブラリをディレクトリに配置する必要があります。
|
||||
game.crash.title=ゲームがクラッシュしました
|
||||
game.directory=ゲームディレクトリ
|
||||
game.version=ゲームバージョン
|
||||
|
||||
help=ヘルプ
|
||||
help.doc=ドキュメント
|
||||
help.detail=データパック、modpackなどのメーカー向け。
|
||||
|
||||
input.email=ユーザー名はメールである必要があります。
|
||||
input.number=数値である必要があります。
|
||||
input.not_empty=必須フィールド
|
||||
input.url=有効なURLである必要があります。
|
||||
|
||||
install=新規作成
|
||||
install.change_version=バージョンの変更
|
||||
install.change_version.confirm=%sをバージョン%sから%sに更新してもよろしいですか?
|
||||
install.failed=バージョンのインストールに失敗しました
|
||||
install.failed.downloading=一部のファイルが正常にダウンロードされなかったため、インストールに失敗しました
|
||||
install.failed.downloading.detail=ファイルのダウンロードに失敗しました:%s
|
||||
install.failed.downloading.timeout=ファイルのダウンロード中にタイムアウトしました:%s
|
||||
install.failed.install_online=提供されたインストーラーファイルを認識できません。 modをインストールする場合は、「Mods」ページに移動します。
|
||||
install.failed.malformed=少し前にダウンロードしたファイルの形式が正しくありません。この問題を解決するには、他のダウンロードプロバイダーに切り替えることができます。
|
||||
install.failed.optifine_conflict=Fabric、OptiFine、ForgeがMinecraft1.13に同時にインストールされます
|
||||
install.failed.optifine_forge_1.17=Minecraft 1.17.1の場合、OptiFine H1 Pre2以降のバージョンのみがForgeと互換性があり、スナップショットバージョンでインストールできます。
|
||||
install.failed.version_mismatch=ライブラリにはゲームバージョン%sが必要ですが、実際のバージョンは%sです。
|
||||
install.installer.change_version=%s、このバージョンは現在のゲームバージョンと互換性がありません。別のものを選択するには、ここをクリックしてください。
|
||||
install.installer.choose=%sバージョンを選択してください
|
||||
install.installer.depend=%sに依存
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.fabric-api.warning=警告:Fabric APIはmodであり、新しいゲームのmodディレクトリにインストールされます。新しいゲームの実行ディレクトリを変更しないでください。変更すると、FabricAPIのインストールが失われます。その設定を変更したい場合は、FabricAPIを再インストールする必要があります。
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.incompatible=%sと互換性がありません
|
||||
install.installer.install=インストール%s
|
||||
install.installer.install_offline=ファイルからのインストール/更新
|
||||
install.installer.install_offline.extension=Forge / OptiFineインストーラー
|
||||
install.installer.install_offline.tooltip=Forge / OptiFineインストールファイルのインポートをサポートします
|
||||
install.installer.install_online=オンラインでインストール
|
||||
install.installer.install_online.tooltip=Fabric、Forge、OptiFine、LiteLoaderのインストールをサポートします。
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=インストールされていません
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.version=%s
|
||||
install.modpack=modpackを導入
|
||||
install.new_game=新規作成
|
||||
install.new_game.already_exists=このバージョンはすでに存在します。
|
||||
install.new_game.current_game_version=現在のゲームバージョン
|
||||
install.new_game.malformed=無効な名前
|
||||
install.select=操作を選択します
|
||||
install.success=正常にインストールされました
|
||||
|
||||
lang=日本語
|
||||
lang.default=システム言語を使用する
|
||||
|
||||
launch.advice.java.auto=現在選択されているJavaVMは、ゲームの要件を満たしていません。適切なJavaVMを選択することを許可しますか?または、ゲーム設定で適切なJavaVMを選択することもできます。
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2以前では、Java7以前のバージョンが必要です。
|
||||
launch.advice.corrected=JVMの選択はすでに修正されています。選択したJavaバージョンを維持したい場合は、ゲーム設定でJavaVMチェックを無効にすることができます。
|
||||
launch.advice.uncorrected=ゲームを正常に開始できることが確実な場合は、ゲーム設定でJavaVMチェックを無効にすることができます。
|
||||
launch.advice.different_platform=OSは64ビットですが、Javaは32ビットです。 64ビットJavaをお勧めします。
|
||||
launch.advice.forge2760_liteloader=Forge 2760以降はLiteLoaderと互換性がないため、Forgeを2773以降にアップグレードすることを検討してください。打ち上げを続けましょうか。
|
||||
launch.advice.forge28_2_2_optifine=Forge28.2.2以降のバージョンはOptiFineと互換性がありません。 Forgeを28.2.1以前のバージョンにダウングレードすることを検討してください。打ち上げを続けましょうか。
|
||||
launch.advice.java8_1_13=Minecraft 1.13以降は、Java8以降でのみ実行できます。
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13は、1.8.0_51より前のJava8でクラッシュする可能性があります。最新バージョンのJava8をインストールしてください。
|
||||
launch.advice.java9=Java9以降のバージョンのJavaでMinecraft1.12以前を起動することはできません。
|
||||
ゲームを高速化するには、launch.advice.newer_java=Java8をお勧めします。多くのMinecraft1.12以降、およびほとんどのModには、Java8が必要です。
|
||||
launch.advice.not_enough_space=割り当てたメモリが多すぎます。物理メモリサイズが%dMBであるため、ゲームがクラッシュする可能性があります。打ち上げを続けましょうか。
|
||||
launch.advice.require_newer_java_version=Minecraft%1$sにはJava%2 $ s以降が必要です。今すぐダウンロードしますか?
|
||||
launch.advice.too_large_memory_for_32bit=32ビットJavaランタイム環境が原因で、割り当てたメモリが多すぎるため、ゲームがクラッシュする可能性があります。 32ビットシステムの最大メモリ容量は1024MBです。打ち上げを続けましょうか。
|
||||
launch.advice.vanilla_linux_java_8=Linux x86-64の場合、Minecraft1.12.2以下はJava8でのみ実行できます。\nJava9以降のバージョンでは、liblwjgl.soなどの32ビットネイティブライブラリをロードできません。
|
||||
launch.advice.vanilla_x86=Minecraftは現在、x86およびx86-64以外のアーキテクチャの公式サポートを提供していません。\nプラットフォームの対応するネイティブライブラリをダウンロードして、その配置パスを指定してみてください。
|
||||
launch.advice.vanilla_x86.translation=Minecraftは現在、x86およびx86-64以外のアーキテクチャの公式サポートを提供していません。\nJava for x86-64を使用して、トランスレータを介してminecraftを実行するか、プラットフォームの対応するネイティブライブラリをダウンロードして指定してくださいその配置パス。
|
||||
launch.failed=起動できません
|
||||
launch.failed.cannot_create_jvm=Java仮想マシンを作成できませんでした。 Java引数は問題を引き起こす可能性があります。 JVM引数なしで再起動してください。
|
||||
launch.failed.creating_process=プロセスの作成に失敗しました。 Javaパスを確認してください。
|
||||
launch.failed.command_too_long=コマンドの長さが制限を超えているため、batスクリプトを作成できません。 PowerShellスクリプトとしてエクスポートしてください。
|
||||
launch.failed.decompressing_natives=ネイティブライブラリを解凍できません。
|
||||
launch.failed.download_library=ライブラリ%sをダウンロードできません。
|
||||
launch.failed.executable_permission=起動スクリプトにアクセス許可を追加できません。
|
||||
launch.failed.execution_policy=実行ポリシーを設定します
|
||||
launch.failed.execution_policy.failed_to_set=実行ポリシーの設定に失敗しました
|
||||
launch.failed.execution_policy.hint=現在の実行ポリシーにより、PowerShellスクリプトの実行が禁止されています。\n現在のユーザーがPowerShellスクリプトを実行できるようにするには、[OK]をクリックするか、現状を維持するには[キャンセル]をクリックします。
|
||||
launch.failed.exited_abnormally=ゲームが異常終了しました。ログを確認するか、誰かに助けを求めてください。
|
||||
launch.failed.no_accepted_java=現在のゲームに適したJavaインストールが見つかりません。適切なJavaVMがインストールされていると思われる場合は、ゲーム設定で手動で選択できます。
|
||||
launch.state.dependencies=依存関係
|
||||
launch.state.done=完了
|
||||
launch.state.java=Javaバージョンの検出
|
||||
launch.state.logging_in=ログイン
|
||||
launch.state.modpack=modpackを読み込んでいます
|
||||
launch.state.waiting_launching=ゲームの起動
|
||||
launch.wrong_javadir=無効なJavaディレクトリ、デフォルトのJavaパスが適用されます。
|
||||
|
||||
launcher=ランチャー
|
||||
launcher.agreement=EULA
|
||||
launcher.agreement.accept=同意します
|
||||
launcher.agreement.decline=同意しません
|
||||
launcher.agreement.hint=このソフトウェアを使用するには、EULAに同意する必要があります。
|
||||
launcher.background=背景画像
|
||||
launcher.background.classic=クラシック
|
||||
launcher.background.choose=背景画像ファイルを選択してください
|
||||
launcher.background.default=標準
|
||||
launcher.background.network=ネットワーク
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=キャッシュ用のディレクトリ
|
||||
launcher.cache_directory.clean=Clear
|
||||
launcher.cache_directory.choose=キャッシュするディレクトリを選択します
|
||||
launcher.cache_directory.default=標準
|
||||
launcher.cache_directory.disabled=無効
|
||||
launcher.cache_directory.invalid=無効なディレクトリ。デフォルト設定の復元。
|
||||
launcher.contact=お問い合わせ
|
||||
launcher.crash=Hello Minecraft!ランチャーがクラッシュしました!次のコンテンツをコピーして、MCBBS、Baidu Tieba、GitHub、またはMinecraftForumを介してフィードバックを送信してください。
|
||||
launcher.crash_out_dated=Hello Minecraft!ランチャーがクラッシュしました!ランチャーが古くなっています。ランチャーを更新してください!
|
||||
launcher.update_java=Javaを更新してください。
|
||||
|
||||
login.empty_username=ユーザー名を設定していません!
|
||||
login.enter_password=パスワードを入力してください。
|
||||
|
||||
logwindow.show_lines=行を表示
|
||||
logwindow.terminate_game=ゲームを終了する
|
||||
logwindow.title=ログ
|
||||
logwindow.autoscroll=Autoscroll
|
||||
logwindow.export_game_crash_logs=ゲームのクラッシュ情報をエクスポートする
|
||||
|
||||
main_page=ホーム
|
||||
|
||||
message.cancelled=操作がキャンセルされました
|
||||
message.confirm=確認
|
||||
message.copied=クリップボードにコピーされました
|
||||
message.default=Default
|
||||
message.doing=お待ちください
|
||||
message.downloading=ダウンロード中...
|
||||
message.error=エラー
|
||||
message.failed=操作に失敗しました
|
||||
message.info=Info
|
||||
message.success=ジョブは正常に完了しました
|
||||
message.unknown=不明
|
||||
message.warning=警告
|
||||
|
||||
modpack=Modpack
|
||||
modpack.choose=modpackを選択(zip)。
|
||||
modpack.choose.local=ローカルからmodpackファイルをインポート。
|
||||
modpack.choose.local.detail=modpackファイルをこのページにドラッグしてインストールできます
|
||||
modpack.choose.remote=インターネットからmodpackをインポート。
|
||||
modpack.choose.remote.detail=リモートmodpackファイルへの直接ダウンロードリンクが必要です
|
||||
modpack.choose.remote.tooltip=リモートmodpackファイルへの直接ダウンロードリンク
|
||||
modpack.desc=注意事項と変更ログを含めてmodpackを説明してください。マークダウンとオンライン写真がサポートされています。
|
||||
modpack.description=説明
|
||||
modpack.download=Modpackをダウンロード
|
||||
modpack.enter_name=modpackの名前を入力。
|
||||
modpack.export=Modpackをエクスポート
|
||||
modpack.export.as=Export Modpack As...
|
||||
modpack.file_api=Modpackダウンロードリンクプレフィックス
|
||||
modpack.files.blueprints=BuildCraftブループリント
|
||||
modpack.files.config=Modの構成
|
||||
modpack.files.dumps=NEIデバッグ出力
|
||||
modpack.files.hmclversion_cfg=ランチャー構成ファイル
|
||||
modpack.files.liteconfig=Mod構成ファイル
|
||||
modpack.files.mods=Mod
|
||||
modpack.files.mods.voxelmods=VoxelMods(VoxelMapを含む)オプション
|
||||
modpack.files.options_txt=ゲームオプション
|
||||
modpack.files.optionsshaders_txt=シェーダーオプション
|
||||
modpack.files.resourcepacks=リソース/テクスチャパック
|
||||
modpack.files.saves=保存されたマップ
|
||||
modpack.files.scripts=MineTweaker構成
|
||||
modpack.files.servers_dat=サーバーリスト
|
||||
modpack.install=%smodpackをインストールします
|
||||
modpack.installing=modpackのインストール
|
||||
modpack.introduction=Curse、MultiMC、HMCLmodpackがサポートされています。
|
||||
modpack.invalid=無効なmodpackファイル。
|
||||
modpack.mismatched_type=不適切なmodpackタイプ、現在のゲームは%s modpackですが、更新ファイルは%smodpackです。
|
||||
modpack.name=Modpack名
|
||||
modpack.not_a_valid_name=無効なmodpack名
|
||||
modpack.origin=Origin
|
||||
modpack.origin.url=公式ウェブサイト
|
||||
modpack.origin.mcbbs=MCBBS
|
||||
modpack.origin.mcbbs.prompt=スレッドID
|
||||
modpack.scan=このmodpackをスキャンしています
|
||||
modpack.task.install=Modpackのインポート
|
||||
modpack.task.install.error=このmodpackファイルは認識できません。 CurseおよびMultiMCmodpackのみがサポートされています。
|
||||
modpack.task.install.will=modpackをインストールします:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=Cursemodpackに関連するファイルをインストールします
|
||||
modpack.type.curse.tolerable_error=このCursemodpackのすべてのファイルのダウンロードを完了できません。対応するゲームバージョンを開始するときに、ダウンロードを再試行できます。ネットワークの問題により、数回再試行する場合があります。
|
||||
modpack.type.curse.error=このCursemodpackをインストールできません。再試行してください。
|
||||
modpack.type.curse.not_found=必要なリソースの一部が欠落しているため、ダウンロードできませんでした。最新バージョンまたは他のmodpackを検討してください。
|
||||
modpack.type.manual.warning=このmodpackをインポートする代わりに、おそらくこのmodpackファイルを直接解凍する必要があります。そして、バンドルされたランチャーを使用してゲームを起動します。このmodpackは、ランチャーによってエクスポートされるのではなく、.minecraftディレクトリを圧縮することによって手動で作成されます。 HMCLはこのmodpackのインポートを試みることができます、続行しますか?
|
||||
modpack.type.mcbbs=MCBBS標準
|
||||
modpack.type.mcbbs.export=Hello Minecraftでインポートできます!ランチャーとMultiMC
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=Hello Minecraftでインポートできます!ランチャーとMultiMC
|
||||
modpack.type.server=サーバー自動更新Modpack
|
||||
modpack.type.server.export=サーバーマネージャーがゲームクライアントをリモートで更新できるようにする
|
||||
modpack.type.server.malformed=不正なサーバーのmodpackマニフェスト
|
||||
modpack.unsupported=サポートされていないmodpack、HMCL、MultiMC、およびCursemodpackのみがサポートされています。
|
||||
modpack.update=ゲームアップデート
|
||||
modpack.wizard=modpackウィザードのエクスポート
|
||||
modpack.wizard.step.1=基本オプション
|
||||
modpack.wizard.step.1.title=modpackの基本オプションを設定します。
|
||||
modpack.wizard.step.2=ファイルを選択します
|
||||
modpack.wizard.step.2.title=modpackにファイルを追加します。
|
||||
modpack.wizard.step.3=Modpackタイプ
|
||||
modpack.wizard.step.3.title=modpackの形式を選択します。
|
||||
modpack.wizard.step.initialization.exported_version=エクスポートされたゲームバージョン
|
||||
modpack.wizard.step.initialization.force_update=可能であればmodpackを強制的に更新します
|
||||
modpack.wizard.step.initialization.include_launcher=ランチャーを含める
|
||||
modpack.wizard.step.initialization.save=エクスポート先...
|
||||
modpack.wizard.step.initialization.warning=modpackを作成する前に、ゲームが正常に起動できること、および\nMinecraftがリリースバージョンであることを確認する必要があります。\n再配布できないmodを追加しないでください。
|
||||
modpack.wizard.step.initialization.server=サーバーの自動更新modpackの詳細については、ここをクリックしてください
|
||||
|
||||
modrinth.category.adventure=Adventure
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=装飾
|
||||
modrinth.category.equipment=機器
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.food=Food
|
||||
modrinth.category.library=Library
|
||||
modrinth.category.magic=Magic
|
||||
modrinth.category.misc=Misc
|
||||
modrinth.category.storage=ストレージ
|
||||
modrinth.category.technology=テクノロジー
|
||||
modrinth.category.utility=ユーティリティ
|
||||
modrinth.category.worldgen=Worldgen
|
||||
|
||||
mods=Mods
|
||||
mods.add=modを追加
|
||||
mods.add.failed=mods%sの追加に失敗しました。
|
||||
mods.add.success=mods%sが正常に追加されました。
|
||||
mods.category=Category
|
||||
mods.check_updates=更新を確認
|
||||
mods.check_updates.current_version=Current
|
||||
mods.check_updates.failed=一部のファイルのダウンロードに失敗しました
|
||||
mods.check_updates.file=ファイル
|
||||
mods.check_updates.source=Source
|
||||
mods.check_updates.target_version=Target
|
||||
mods.check_updates.update=更新
|
||||
mods.choose_mod=modを選択してください
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependencies=Dependencies
|
||||
mods.disable=無効にする
|
||||
mods.download=Modのダウンロード
|
||||
mods.download.title=Modダウンロード-%1s
|
||||
mods.enable=有効にする
|
||||
mods.manage=Mods
|
||||
mods.mcbbs=MCBBS
|
||||
mods.mcmod=MCMOD
|
||||
mods.mcmod.page=MCMOD
|
||||
mods.mcmod.search=MCMODで検索
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Name
|
||||
mods.not_modded=最初にmodloaderをインストールする必要があります(Fabric、Forge、またはLiteLoader)
|
||||
mods.restore=Restore
|
||||
mods.url=公式ページ
|
||||
|
||||
multiplayer=マルチプレイ
|
||||
multiplayer.agreement.prompt=マルチプレイヤーサービスの使用を開始する前に、まずEULAに同意する必要があります。
|
||||
multiplayer.download=マルチプレイヤーの依存関係をダウンロードする
|
||||
multiplayer.download.failed=マルチプレイヤーの初期化に失敗しました。一部のファイルはダウンロードできません
|
||||
multiplayer.download.success=依存関係の初期化に成功しました
|
||||
multiplayer.download.unsupported=現在のオペレーティングシステムまたはアーキテクチャはサポートされていません。
|
||||
multiplayer.exit.after_ready=マルチプレイヤーセッションが壊れています。 cato exitcode%d
|
||||
multiplayer.exit.before_ready=マルチプレイヤーセッションの作成に失敗しました。 cato exitcode%d
|
||||
multiplayer.exit.timeout=マルチプレイヤーサーバーへの接続に失敗しました。
|
||||
multiplayer.exit.timeout.dynamic_token=マルチプレイヤーサーバーへの接続に失敗しました。
|
||||
multiplayer.hint=マルチプレイヤー機能は実験的なものです。フィードバックをお願いします。
|
||||
multiplayer.nat=ネットワークタイプの検出
|
||||
multiplayer.nat.failed=ネットワークタイプの検出に失敗しました。
|
||||
multiplayer.nat.hint=ネットワークタイプの検出により、ネットワークがマルチプレイヤーモードの要件を満たしているかどうかが明確になります。
|
||||
multiplayer.nat.latency=レイテンシー
|
||||
multiplayer.nat.not_yet_tested=まだテストされていません
|
||||
multiplayer.nat.packet_loss_ratio=パケット損失率
|
||||
multiplayer.nat.testing=テスト
|
||||
multiplayer.nat.type=NATタイプ
|
||||
multiplayer.nat.type.blocked_udp=非常に悪い(ブロックされたUDP)
|
||||
multiplayer.nat.type.full_cone=Good(フルコーン)
|
||||
multiplayer.nat.type.open_access=Good(オープンアクセス)
|
||||
multiplayer.nat.type.port_restricted_cone=Medium(ポート制限付きコーン)
|
||||
multiplayer.nat.type.restricted_cone=Medium(制限付きコーン)
|
||||
multiplayer.nat.type.symmetric=Bad(対称)
|
||||
multiplayer.nat.type.symmetric_udp_firewall=Bad(UDPファイアウォールと対称)
|
||||
multiplayer.nat.type.unknown=不明`
|
||||
multiplayer.powered_by=マルチプレイヤーサービスは<a href="https://noin.cn">noin.cn</a>によって提供されます。 <a href="https://noin.cn/agreement">EULA</a>
|
||||
multiplayer.report=レポート
|
||||
multiplayer.relay=リレーモード
|
||||
multiplayer.relay.hint=静的トークンを持ち、テストされたNATタイプが不良対称であるユーザーの場合
|
||||
multiplayer.session=セッション
|
||||
multiplayer.session.name.format=%1$sの部屋
|
||||
multiplayer.session.name.motd=HMCLマルチプレイヤーセッション-%s
|
||||
multiplayer.session.close=部屋を閉じる
|
||||
multiplayer.session.close.warning=部屋を閉じた後、部屋に参加したすべてのプレイヤーは接続を失います。継続する?
|
||||
multiplayer.session.copy_room_code=招待コードをコピーする
|
||||
multiplayer.session.create=セッションの作成
|
||||
multiplayer.session.create.error.dynamic_token=マルチプレイヤーセッションの作成に失敗しました。後でもう一度やり直してください。
|
||||
multiplayer.session.create.error.static_token=マルチプレイヤーセッションの作成に失敗しました。動的トークンで再試行できます。
|
||||
multiplayer.session.create.hint=マルチプレイヤーセッションを作成する前に、実行中のゲームで[LANサーバーを開く]をクリックし、ゲームに表示されているポートを下の空白に入力する必要があります。
|
||||
multiplayer.session.create.join=接続リクエスト
|
||||
multiplayer.session.create.join.allow=すべての参加リクエストを受け入れます(無効にした場合、攻撃を防ぐために、参加リクエストを手動で受け入れる必要があります)。
|
||||
multiplayer.session.create.join.prompt=Player%sがマルチプレイヤーセッションに参加したいと考えています。承認?
|
||||
multiplayer.session.create.members=メンバー
|
||||
multiplayer.session.create.members.kick=キック
|
||||
multiplayer.session.create.members.kick.prompt=このプレーヤーをキックした後、このプレーヤーとの確立された接続は切断されます。
|
||||
multiplayer.session.create.name=セッション名
|
||||
multiplayer.session.create.port=ポート
|
||||
multiplayer.session.create.port.error=ゲームポートを検出できません。マルチプレイヤー機能を有効にするには、ゲームで[LANサーバーを開く]をクリックする必要があります。
|
||||
multiplayer.session.create.token=トークン
|
||||
multiplayer.session.create.token.apply=静的トークンを申請する
|
||||
multiplayer.session.create.token.prompt=デフォルトはランダム化されています。 noin.cn(中国のウェブサイト)で自分のトークンを申請することができます。
|
||||
multiplayer.session.error.already_started=Catoサービスはすでにローカルで開始されています。他のマルチプレイヤーサービスが提供されているかどうかを確認してください。または、タスクマネージャーでcatoプロセスを強制終了できます。
|
||||
multiplayer.session.error.file_not_found=cato実行可能ファイルが見つかりません。このページに入るときに、このファイルをダウンロードする必要があります。ウイルス対策ソフトウェアがファイルを分離したかどうかを確認してください。 trueの場合、ロールバックしてください。
|
||||
multiplayer.session.expired=マルチプレイヤーセッションの有効期限が切れています。続行するには、部屋を再作成または再参加する必要があります。
|
||||
multiplayer.session.hint=マルチプレイヤー機能を有効にするには、ゲームで[LANサーバーを開く]をクリックする必要があります。
|
||||
multiplayer.session.join=セッションに参加する
|
||||
multiplayer.session.join.error=マルチプレイヤーセッションへの参加に失敗しました
|
||||
multiplayer.session.join.error.connection=マルチプレイヤーセッションへの参加に失敗しました。接続を確立できません。
|
||||
multiplayer.session.join.hint=マルチプレイヤーセッションをすでに作成しているゲーマーから招待コードを取得する必要があります。
|
||||
multiplayer.session.join.invitation_code=招待コード
|
||||
multiplayer.session.join.invitation_code.error=不正な招待コード。マルチプレイヤーセッションを作成したプレイヤーから招待コードを入手してください。
|
||||
multiplayer.session.join.invitation_code.version=マルチプレイヤー機能のバージョンはあなたの間で同じではありません。
|
||||
multiplayer.session.join.kicked=セッションホルダーによってキックされました。マルチプレイヤーセッションとの接続が失われます。
|
||||
multiplayer.session.join.lost_connection=マルチプレイヤーセッションとの接続が失われました。セッションが作成者によって破棄されたか、このセッションとの接続を確立できない可能性があります。
|
||||
multiplayer.session.join.port.error=リスニングに使用できるローカルネットワークポートが見つかりません。 HMCLがポートでリッスンする権限を持っていることを確認してください。
|
||||
multiplayer.session.join.rejected=接続はセッション所有者によって拒否されます。
|
||||
multiplayer.session.join.wait=受け入れを待っています。
|
||||
multiplayer.session.join.wait_timeout=セッション所有者は時間内にあなたの参加リクエストを受け入れません。
|
||||
multiplayer.session.members=ルームメンバー
|
||||
multiplayer.session.quit=Quit Room
|
||||
multiplayer.session.quit.warning=部屋を終了した後、サーバーとの接続が失われます。継続する?
|
||||
multiplayer.session.username=ユーザー名
|
||||
multiplayer.state.connecting=接続中
|
||||
multiplayer.state.disconnected=マルチプレイヤーセッションが作成/開始されていません
|
||||
multiplayer.state.disconnected.hint=誰かがマルチプレイヤーセッションを作成し、他の人がセッションに参加して一緒にゲームをプレイする必要があります。
|
||||
multiplayer.state.master=作成された部屋:%1$s、ポート:%2 $ d
|
||||
multiplayer.state.master.hint=マルチプレイヤールームを作成したら、[招待コードをコピー]ボタンをクリックして、招待したいプレイヤーに招待コードを送信します。これらのプレーヤーは、会議室に参加するときに招待コードを貼り付けることができます。
|
||||
multiplayer.state.slave=参加ルーム:%1$s、アドレス:%2 $ s
|
||||
multiplayer.state.slave.copy=コピーアドレス
|
||||
multiplayer.state.slave.hint=マルチプレイヤールームに参加した後、Minecraftのマルチプレイヤーページにアクセスして「HMCLマルチプレイヤーセッション」サーバーに接続するか、以下に示すアドレスのサーバーを手動で追加する必要があります。
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=データパックをインストールします
|
||||
datapack.choose_datapack=インポートするデータパックのzipを選択してください
|
||||
datapack.extension=Datapack
|
||||
datapack.title=World%s-データパック
|
||||
|
||||
world=マップ
|
||||
world.add=マップを追加(.zip)
|
||||
world.datapack=データパックの管理
|
||||
world.datapack.1_13=Minecraft1.13以降のバージョンのみがデータパックをサポートします。
|
||||
world.description=%s。最終ゲーム時刻:%s。ゲームバージョン:%s。
|
||||
world.download=ダウンロード
|
||||
world.export=このマップをエクスポートする
|
||||
world.export.title=保存する場所を選択してください
|
||||
world.export.location=エクスポート先
|
||||
world.export.wizard=エクスポートワールド%s
|
||||
world.extension=World zip
|
||||
world.game_version=ゲームバージョン
|
||||
world.import.already_exists=このマップはすでに存在しています。
|
||||
world.import.choose=インポートするzipファイルを選択してください
|
||||
world.import.failed=このマップをインポートできません:%s
|
||||
world.import.invalid=無効なワールドzipファイル
|
||||
world.manage=マップ / データパック
|
||||
world.name=マップ名
|
||||
world.name.enter=マップ名を入力してください
|
||||
world.reveal=フォルダを表示
|
||||
world.show_all=すべて表示
|
||||
world.time=EEE、MMM d、yyyy HH:mm:ss
|
||||
|
||||
profile=ゲームディレクトリ
|
||||
profile.already_exists=この名前はすでに存在します。別の名前を使用してください。
|
||||
profile.default=現在のディレクトリ
|
||||
profile.home=標準ディレクトリ
|
||||
profile.instance_directory=ゲームディレクトリ
|
||||
profile.instance_directory.choose=ゲームディレクトリを選択してください
|
||||
profile.manage=ゲームディレクトリリスト
|
||||
profile.name=名前
|
||||
profile.new=新規作成
|
||||
profile.title=ゲームディレクトリ
|
||||
profile.selected=選択済み
|
||||
profile.use_relative_path=可能であれば、ゲームディレクトリの相対パスを使用します
|
||||
|
||||
repositories.custom=カスタムMavenリポジトリ(%s)
|
||||
repositories.maven_central=Universal(Maven Central)
|
||||
repositories.aliyun_mirror=中国本土(Aliyun Mavenリポジトリ)
|
||||
repositories.chooser=JavaFXがないため、HMCLが機能するにはJavaFXが必要です。\n [OK]をクリックして指定したダウンロードソースからJavaFXランタイムコンポーネントをダウンロードしてHMCLを起動するか、[キャンセル]をクリックして終了します。\nダウンロードソースを選択:
|
||||
repositories.chooser.title=ダウンロードソースを選択してJavaFXをダウンロードします
|
||||
|
||||
resourcepack=リソースパック
|
||||
|
||||
search=検索
|
||||
search.hint.chinese=中国語と英語での検索をサポート
|
||||
search.hint.english=英語でのみ検索をサポート
|
||||
search.sort=Sort
|
||||
|
||||
selector.choose=選択
|
||||
selector.choose_file=ファイルを選択します
|
||||
selector.custom=カスタム
|
||||
|
||||
settings=設定
|
||||
|
||||
settings.advanced=詳細設定
|
||||
settings.advanced.custom_commands=カスタムコマンド
|
||||
settings.advanced.custom_commands.hint=カスタムコマンドは、次の環境変数を使用して実行されます。\n \
|
||||
\-$ INST_NAME:バージョンの名前\n \
|
||||
\-$ INST_ID:バージョンの名前\n \
|
||||
\-$ INST_DIR:バージョンの絶対パス\n \
|
||||
\-$ INST_MC_DIR:Minecraftの絶対パス\n \
|
||||
\-$ INST_JAVA:起動に使用されるJavaバイナリ\n \
|
||||
\-$ INST_FORGE:Forgeがインストールされている場合に設定\n \
|
||||
\-$ INST_LITELOADER:LiteLoaderがインストールされている場合に設定\n \
|
||||
\-$ INST_OPTIFINE:OptiFineがインストールされている場合に設定\n \
|
||||
\-$ INST_FABRIC:ファブリックがインストールされている場合に設定
|
||||
settings.advanced.dont_check_game_completeness=ゲームファイルをスキャンしない
|
||||
settings.advanced.dont_check_jvm_validity=JVMがゲームを起動できるかどうかを確認しないでください
|
||||
settings.advanced.game_dir.default=標準(.minecraft /)
|
||||
settings.advanced.game_dir.independent=バージョン独立(.minecraft / versions / <バージョン名> /、アセットとライブラリを除く)
|
||||
settings.advanced.java_permanent_generation_space=PermGen Space / MB
|
||||
settings.advanced.java_permanent_generation_space.prompt=Java 8以降のメタスペース、MB
|
||||
settings.advanced.jvm=Java仮想マシンの設定
|
||||
settings.advanced.jvm_args=JavaVM引数
|
||||
settings.advanced.jvm_args.prompt=ここに入力して、デフォルト設定を上書きします
|
||||
settings.advanced.launcher_visibility.close=ゲームの起動時にランチャーを閉じます。
|
||||
settings.advanced.launcher_visibility.hide=ゲームの起動時にランチャーを非表示にします。
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=ランチャーを非表示にし、ゲームが終了したら再度開きます。
|
||||
settings.advanced.launcher_visibility.keep=ランチャーを表示したままにします。
|
||||
settings.advanced.launcher_visible=ランチャーの設定
|
||||
settings.advanced.minecraft_arguments=Minecraftの引数
|
||||
settings.advanced.minecraft_arguments.prompt=デフォルト
|
||||
settings.advanced.natives_directory=ネイティブライブラリパス(例: LWJGL)
|
||||
settings.advanced.natives_directory.choose=ネイティブライブラリパスを選択してください
|
||||
settings.advanced.natives_directory.custom=Custom(ゲームが実行時に必要とするネイティブライブラリを提供する必要があります)
|
||||
settings.advanced.natives_directory.default=Standard(ネイティブライブラリは当社が提供します)
|
||||
settings.advanced.natives_directory.hint=このオプションは、AppleM1またはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。 、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。 lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。 HMCLは、不足しているネイティブライブラリを提供しません。
|
||||
settings.advanced.no_jvm_args=デフォルトのJVM引数なし
|
||||
settings.advanced.precall_command=起動前コマンド
|
||||
settings.advanced.precall_command.prompt=ゲーム開始前に実行されます
|
||||
settings.advanced.process_priority=プロセスの優先度
|
||||
settings.advanced.process_priority.low=低
|
||||
settings.advanced.process_priority.below_normal=通常以下
|
||||
settings.advanced.process_priority.normal=中
|
||||
settings.advanced.process_priority.above_normal=通常より上
|
||||
settings.advanced.process_priority.high=高
|
||||
settings.advanced.post_exit_command=終了後のコマンド
|
||||
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
|
||||
settings.advanced.server_ip=サーバーアドレス
|
||||
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
|
||||
settings.advanced.use_native_glfw=[Linux]システムGLFWを使用する
|
||||
settings.advanced.use_native_openal=[Linux]システムOpenALを使用する
|
||||
settings.advanced.workaround=Workarounds
|
||||
settings.advanced.workaround.warning=Workaround settings are only for professional users. Modifying workaround settings may result in game crash. Unless you know what you are doing, please do not modify these options.
|
||||
settings.advanced.wrapper_launcher=Wrapper command
|
||||
settings.advanced.wrapper_launcher.prompt=Allows launching using an extra wrapper program like 'optirun' on Linux.
|
||||
|
||||
settings.custom=カスタム
|
||||
|
||||
settings.game=ゲーム
|
||||
settings.game.current=ゲーム
|
||||
settings.game.dimension=ゲームウィンドウのサイズ
|
||||
settings.game.exploration=フォルダ
|
||||
settings.game.fullscreen=フルスクリーン
|
||||
settings.game.java_directory=Javaディレクトリ
|
||||
settings.game.java_directory.auto=自動的に選択されます
|
||||
settings.game.java_directory.bit=%s-ビット
|
||||
settings.game.java_directory.choose=Javaディレクトリを選択します。
|
||||
settings.game.java_directory.template=%s(%s)
|
||||
settings.game.management=管理
|
||||
settings.game.working_directory=作業ディレクトリ
|
||||
settings.game.working_directory.choose=作業ディレクトリを選択してください
|
||||
|
||||
settings.icon=ゲームアイコン
|
||||
|
||||
settings.launcher=設定
|
||||
settings.launcher.appearance=外観
|
||||
settings.launcher.common_path.tooltip=このアプリは、すべてのダウンロードをここにキャッシュします。
|
||||
settings.launcher.debug=デバッグ
|
||||
settings.launcher.download=ダウンロード
|
||||
settings.launcher.download.threads=スレッド
|
||||
settings.launcher.download.threads.auto=自動
|
||||
settings.launcher.download.threads.hint=同時実行性が大きすぎると、システムがフリーズする可能性があります。ダウンロード速度は、ICPと宛先サーバーの影響を受ける可能性があります。
|
||||
settings.launcher.download_source=ソースのダウンロード
|
||||
settings.launcher.download_source.auto=ダウンロードソースの自動選択
|
||||
settings.launcher.enable_game_list=メインページにバージョンリストを表示する
|
||||
settings.launcher.font=フォント
|
||||
settings.launcher.general=全般的
|
||||
settings.launcher.language=言語
|
||||
settings.launcher.launcher_log.export=ランチャーログのエクスポート
|
||||
settings.launcher.launcher_log.export.failed=ログのエクスポートに失敗しました
|
||||
settings.launcher.launcher_log.export.success=ログが%sにエクスポートされました
|
||||
settings.launcher.log=ログ
|
||||
settings.launcher.log.font=ログフォント
|
||||
settings.launcher.proxy=プロキシ
|
||||
settings.launcher.proxy.authentication=プロキシ認証
|
||||
settings.launcher.proxy.disable=システムプロキシを使用する
|
||||
settings.launcher.proxy.host=Host
|
||||
settings.launcher.proxy.http=HTTP
|
||||
settings.launcher.proxy.none=プロキシなし
|
||||
settings.launcher.proxy.password=パスワード
|
||||
settings.launcher.proxy.port=ポート
|
||||
settings.launcher.proxy.socks=Socks
|
||||
settings.launcher.proxy.username=アカウント
|
||||
settings.launcher.theme=テーマ
|
||||
settings.launcher.title_transparent=タイトルの透明性
|
||||
settings.launcher.version_list_source=バージョンリストソース
|
||||
|
||||
settings.memory=ゲームメモリ
|
||||
settings.memory.allocate.auto=最小%1 $ .1f GB /実際の%2 $ .1f GB
|
||||
settings.memory.allocate.auto.exceeded=最小%1 $ .1f GB /実際の%2 $ .1f GB(%3 $ .1f GB使用可能)
|
||||
settings.memory.allocate.manual=割り当て%1 $ .1f GB
|
||||
settings.memory.allocate.manual.exceeded=割り当て%1 $ .1f GB(%3 $ .1f GB使用可能)
|
||||
settings.memory.auto_allocate=メモリサイズの自動割り当て
|
||||
settings.memory.lower_bound=最小メモリ
|
||||
settings.memory.used_per_total=%1 $ .1f GB使用済み/%2 $ .1fGB合計
|
||||
settings.physical_memory=物理メモリサイズ
|
||||
settings.show_log=ログを表示
|
||||
settings.skin=オフラインアカウントのスキンの変更がサポートされるようになりました!アカウントページに移動して、今すぐ肌を変えてください!
|
||||
settings.tabs.installers=Installers
|
||||
settings.take_effect_after_restart=再起動後に有効になります
|
||||
settings.type=バージョン設定タイプ
|
||||
settings.type.global=グローバルゲーム設定(ゲーム間で共有されるすべての設定)
|
||||
settings.type.global.manage=グローバルゲーム設定
|
||||
settings.type.global.edit=グローバルゲーム設定を構成します
|
||||
settings.type.special.enable=このゲームの特殊な設定を有効にします
|
||||
settings.type.special.edit=現在のゲーム設定を構成します
|
||||
settings.type.special.edit.hint=現在のゲームバージョン%sで特殊な設定が有効になっているため、現在のページのオプションはそのゲームバージョンに適用されません。リンクをクリックして、現在のゲーム設定を構成します。
|
||||
|
||||
sponsor=寄付
|
||||
sponsor.bmclapi=ダウンロードサービスはBMCLAPIとMCBBSによって提供されます。詳細については、ここをクリックしてください。
|
||||
sponsor.hmcl=こんにちはマインクラフト! Launcherは、無料のオープンソースのMinecraftランチャーであり、ユーザーは複数の個別のMinecraftインストールを簡単に管理できます。私たちはAfdianを使用して、ホスティングとプロジェクト開発の費用を引き続き支払います。詳細については、ここをクリックしてください。
|
||||
|
||||
system.architecture=Arch
|
||||
system.operating_system=OS
|
||||
|
||||
update=更新
|
||||
update.accept=更新
|
||||
update.changelog=変更
|
||||
update.channel.dev=ベータ
|
||||
update.channel.dev.hint=ベータ版を使用しています。ベータ版には、リリースバージョンと比較していくつかの追加機能が含まれている可能性があり、テストにのみ使用されます。\n \
|
||||
ベータ版は不安定なはずです!\n \
|
||||
問題が発生した場合は、<a href="hmcl://settings/feedback">フィードバックページ</a>にアクセスして報告するか、チャット<a href="https://discord.gg/jVvC7HfM6U">Discord</a>または<a href="https://kaihei.co/Kx7n3t">KaiHeiLa</a>で問題を報告します。\n \n \
|
||||
このヒントを非表示にするには、<a href="https://hmcl.huangyuhui.net/api/redirect/sponsor">寄付</a>するか、フィードバックに貢献をする必要があります。
|
||||
update.channel.dev.title=ベータ版のヒント
|
||||
update.channel.nightly=アルファ
|
||||
update.channel.nightly.hint=アルファ版を使用しています。これには、ベータ版およびリリース版と比較していくつかの追加機能が含まれている可能性があり、テストにのみ使用されます。\n \
|
||||
アルファ版は不安定です!\n \
|
||||
問題が発生した場合は、<a href="hmcl://settings/feedback">フィードバックページ</a>にアクセスして報告するか、チャット<a href="https://discord.gg/jVvC7HfM6U">Discord</a>または<a href="https://kaihei.co/Kx7n3t">KaiHeiLa</a>で問題を報告します。\n \n \
|
||||
このヒントを非表示にするには、<a href="https://hmcl.huangyuhui.net/api/redirect/sponsor">寄付</a>するか、フィードバックに印象的な貢献をする必要があります。
|
||||
update.channel.nightly.title=アルファ版のヒント
|
||||
update.channel.stable=リリース
|
||||
update.checking=更新の確認
|
||||
update.failed=更新の実行に失敗しました
|
||||
update.found=アップデートが利用可能です!
|
||||
update.newest_version=最新バージョン:%s
|
||||
update.bubble.title=利用可能な更新:%s
|
||||
update.bubble.subtitle=更新するにはここをクリックしてください
|
||||
update.note=警告:ベータ版には、より多くの機能とバグ修正が含まれている可能性がありますが、エラーが発生する可能性もあります。
|
||||
update.latest=これは最新バージョンです。
|
||||
update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。
|
||||
update.tooltip=更新
|
||||
|
||||
version=ゲーム
|
||||
version.cannot_read=ゲームのバージョンが見つかりません。自動インストールを続行できません。
|
||||
version.empty=ゲームなし
|
||||
version.empty.add=起動構成を作成
|
||||
version.empty.launch=起動するバージョンがありません。ダウンロードページからバージョンをインストールしてください。
|
||||
version.empty.hint=インストールされているゲームバージョンはありません。新しいゲームをダウンロードするには、ここをクリックしてください。
|
||||
version.game.old=Old
|
||||
version.game.release=リリース
|
||||
version.game.releases=リリース
|
||||
version.game.snapshot=スナップショット
|
||||
version.game.snapshots=Snapshots
|
||||
version.launch=プレイ
|
||||
version.launch.test=テストプレイ
|
||||
version.launch_script=起動スクリプトを作成する
|
||||
version.launch_script.failed=起動スクリプトを作成できません。
|
||||
version.launch_script.save=起動スクリプトを保存する
|
||||
version.launch_script.success=作成されたスクリプト: %s。
|
||||
version.manage=起動構成
|
||||
version.manage.clean=ゲームディレクトリをクリアする
|
||||
version.manage.clean.tooltip=ログのクリア、クラッシュレポート
|
||||
version.manage.duplicate=ゲームインスタンスをコピーする
|
||||
version.manage.duplicate.duplicate_save=コピー保存
|
||||
version.manage.duplicate.prompt=新しいバージョン名を入力してください
|
||||
version.manage.duplicate.confirm=
|
||||
version.manage.manage=バージョンの管理
|
||||
version.manage.manage.title=バージョンの管理-%1s
|
||||
version.manage.redownload_assets_index=ゲームアセットファイルの更新
|
||||
version.manage.remove=このバージョンを削除します
|
||||
version.manage.remove.confirm=このバージョン%sを削除してもよろしいですか?このバージョンを再度復元することはできません。
|
||||
version.manage.remove.confirm.trash=このバージョン%sを削除してもよろしいですか?このバージョンは、システムのゴミ箱に%sという名前で復元できます。
|
||||
version.manage.remove.confirm.independent=このバージョンは独立モードであるため、このバージョンを削除すると、このバージョンに属するすべての保存済みワールドも削除されます。このバージョン%sを削除しますか?
|
||||
version.manage.remove_libraries=ライブラリファイルを削除します
|
||||
version.manage.rename=このバージョンの名前を変更します
|
||||
version.manage.rename.message=このバージョンの新しい名前を入力してください
|
||||
version.manage.rename.fail=バージョンの名前を変更できませんでした。
|
||||
version.settings=設定
|
||||
version.update=modpackを更新する
|
||||
|
||||
wizard.prev=< 前へ
|
||||
wizard.failed=失敗
|
||||
wizard.finish=終了
|
||||
wizard.next=次へ >
|
||||
File diff suppressed because it is too large.
Load diff
@@ -96,8 +96,8 @@ account.methods.microsoft.error.missing_xbox_account=你的微軟帳號尚未關
|
||||
account.methods.microsoft.error.no_character=該帳號沒有包含 Minecraft Java 版購買記錄
|
||||
account.methods.microsoft.error.unknown=登入失敗,錯誤碼:%d
|
||||
account.methods.microsoft.logging_in=登入中...
|
||||
account.methods.microsoft.hint=您需要點擊登入按鈕,並在新打開的瀏覽器窗口中完成登入。
|
||||
account.methods.microsoft.manual=若登入頁面未能打開,您可以點擊此處複製連結,並手動在瀏覽器中打開網頁。
|
||||
account.methods.microsoft.hint=您需要點擊登錄按鈕,並在新打開的瀏覽器窗口中完成登錄,並輸入待會在此處顯示的設備代碼。
|
||||
account.methods.microsoft.manual=設備代碼為:<b>%1$s</b>,已經幫你拷貝到剪切板中,如果未能黏貼,你可以點擊此處再複製一次。你需要在新打開的瀏覽器窗口中完成登錄,若登錄頁面未能打開,您手動在瀏覽器中打開網頁:%2$s。
|
||||
account.methods.microsoft.profile=帳戶設置頁
|
||||
account.methods.microsoft.snapshot=HMCL 快照版不支持微软登录
|
||||
account.methods.microsoft.waiting_browser=等待在新打開的瀏覽器窗口中完成登入...
|
||||
@@ -182,6 +182,11 @@ curse.category.4472=科技
|
||||
curse.category.4473=魔法
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5129=原版增強
|
||||
curse.category.5189=實用與QOL
|
||||
curse.category.5190=QoL
|
||||
curse.category.5191=實用與QoL
|
||||
curse.category.5192=夢幻菜單
|
||||
curse.category.423=訊息展示
|
||||
curse.category.426=模組擴展
|
||||
curse.category.434=裝備武器
|
||||
@@ -224,6 +229,7 @@ curse.category.4843=自動化
|
||||
curse.category.4906=MCreator
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5193=封包
|
||||
curse.category.399=蒸汽朋克
|
||||
curse.category.396=128x
|
||||
curse.category.398=512x 及更高
|
||||
@@ -289,9 +295,16 @@ download.javafx.notes=正在通過網絡下載 HMCL 必要的運行時組件。\
|
||||
download.javafx.component=正在下載模塊 %s
|
||||
download.javafx.prepare=準備開始下載
|
||||
|
||||
exception.access_denied=因為無法訪問文件 %s,HMCL 沒有對該文件的訪問權限,或者該文件被其他程序打開。\n\
|
||||
請你檢查當前操作系統帳戶是否能訪問該文件,比如非管理員用戶可能不能訪問其他帳戶的個人文件夾內的文件。\n\
|
||||
對於 Windows 用戶,你還可以嘗試通過資源監視器查看是否有程序占用了該文件,如果是,你可以關閉占用此文件相關程序,或者重啟電腦再試。
|
||||
exception.artifact_malformed=下載的文件正確,無法通過校驗。
|
||||
exception.ssl_handshake=無法建立 SSL 連接,因為當前 Java 虛擬機缺少相關的 SSL 證書。你可以嘗試使用其他的 Java 虛擬機啟動 HMCL 再試。
|
||||
|
||||
extension.bat=Windows 指令碼
|
||||
extension.mod=模組檔案
|
||||
extension.png=圖片檔案
|
||||
extension.ps1=PowerShell 指令碼
|
||||
extension.sh=Bash 指令碼
|
||||
|
||||
fatal.javafx.incompatible=缺少 JavaFX 運行環境。\nHMCL 無法在低於 Java 11 且缺少 JavaFX 的 Java 環境上自行補全 JavaFX 運行環境。\n請更換如 Oracle Java 8 等包含 JavaFX 的 Java 運行環境,或者更新到 Java 11 或更高版本。
|
||||
@@ -300,6 +313,8 @@ fatal.missing_dst_root_ca_x3=目前的 Java 平台缺少 DST Root CA X3 憑證
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 Hello Minecraft! Launcher 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 Hello Minecraft! Launcher。
|
||||
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 Hello Minecraft! Launcher 來完成升級。\n請考慮向我們回報該問題。
|
||||
fatal.samba=如果您正在通過 Samba 共亯的資料夾中運行 HMCL,HMCL 可能無法正常工作,請嘗試更新您的 Java 或在本地資料夾內運行 HMCL。
|
||||
fatal.illegal_char=您的用戶資料夾路徑中存在非法字元‘=’。HMCL 能够運行,但一些功能無法正常使用。 \n您將無法使用外置登入帳戶以及離線登入更換皮膚功能,並且在進行多人遊戲時可能存在更大的安全隱患。
|
||||
|
||||
feedback=回饋
|
||||
feedback.add=新增回饋
|
||||
@@ -353,7 +368,11 @@ game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
|
||||
game.crash.reason.file_already_exists=當前遊戲因為文件 %1$s 已經存在,無法繼續運行。\n如果你認為這個文件可以刪除,你可以在備份這個文件後嘗試刪除它,並重新啟動遊戲。
|
||||
game.crash.reason.file_changed=當前遊戲因為檔案校驗失敗,無法繼續運行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。
|
||||
game.crash.reason.gl_operation_failure=當前遊戲因為你使用的某些 Mod、光影包、材質包,無法繼續運行。\n請先嘗試禁用你所使用的Mod/光影包/材質包再試。
|
||||
game.crash.reason.graphics_driver=當前遊戲因為你的顯示卡驅動存在問題崩潰了。\n請嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。\n如果你的電腦存在獨立顯示卡,你需要檢查遊戲是否使用集成/核心顯示卡啟動,如果是,請嘗試使用獨立顯示卡啟動 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。
|
||||
game.crash.reason.graphics_driver=當前遊戲因為你的顯示卡驅動存在問題崩潰了。\n\
|
||||
請嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。\n\
|
||||
如果你的電腦存在獨立顯示卡,你需要檢查遊戲是否使用集成/核芯顯示卡啟動。如果是,請嘗試使用獨立顯示卡啟動 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。\n\
|
||||
如果你確實需要使用核芯顯示卡,請檢查你的電腦的 CPU 是否是 Intel(R) Core(TM) 3000 系列或更舊的處理器,如果是,對於 Minecraft 1.16.5 及更舊版本,請你將遊戲所使用的 Java 版本降級至 1.8.0_51 及以下版本,否則你必須更換獨立顯示卡或新電腦。\n\
|
||||
詳情請點擊 <a href="https://hmcl.huangyuhui.net/help/game/graphics-card">此處</a> 查看
|
||||
game.crash.reason.illegal_access_error=當前遊戲因為某些 Mod 的問題,無法繼續運行。\n如果你認識:%1$s,你可以更新或刪除對應 Mod 再試。
|
||||
game.crash.reason.jdk_9=當前遊戲因為 Java 版本過高,無法繼續運行。\n你需要下載安裝 Java 8,並在遊戲設置中將 Java 設置為 1.8 的版本。
|
||||
game.crash.reason.jvm_32bit=當前遊戲因為記憶體分配過大,超過了 32 位 Java 記憶體限制,無法繼續運行。\n如果你的電腦是 64 位系統,請下載安裝並更換 64 位 Java。如果你的電腦室 32 位系統,你或許可以重新安裝 64 位系統,或換一台新電腦。\n或者,你可以關閉遊戲記憶體的自動分配,並且把記憶體限制調節為 1024 MB 或以下。
|
||||
@@ -375,8 +394,8 @@ 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.unknown=原因未知,請點擊日誌按鈕查看詳細訊息。
|
||||
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=遊戲意外退出
|
||||
game.directory=遊戲路徑
|
||||
@@ -396,7 +415,6 @@ install.change_version=變更版本
|
||||
install.change_version.confirm=你確定要 %s 從 %s 更新到 %s 嗎?
|
||||
install.failed=安裝失敗
|
||||
install.failed.downloading=安裝失敗,部分檔案未能完成下載
|
||||
install.failed.downloading.access_denied=未能下載文件:%1$s。因為無法訪問文件 %2$s,HMCL 沒有對該文件的訪問權限,或者該文件被其他程序打開。你可以嘗試關閉相關程序,或者重啟電腦再試。
|
||||
install.failed.downloading.detail=未能下載檔案: %s
|
||||
install.failed.downloading.timeout=下載逾時: %s
|
||||
install.failed.install_online=無法識別要安裝的軟體。如果你要安裝 Mod,你需要在模組管理頁面安裝模組。
|
||||
@@ -449,12 +467,18 @@ launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過
|
||||
launch.advice.require_newer_java_version=Minecraft %1$s 僅能運行在 Java %2$s 或更高版本上,是否下載?
|
||||
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。是否繼續啟動?
|
||||
launch.advice.vanilla_linux_java_8=對於 Linux x86-64 平台,Minecraft 1.12.2 及以下版本僅支持 Java 8。\nJava 9+ 版本會無法載入 32 位的 liblwjgl.so。
|
||||
launch.advice.vanilla_x86=Minecraft 官方尚未提供對非 x86-64 平臺的支持。\n您可以嘗試下載您所用平臺對應的本機庫,在遊戲設定的調試選項中指定其路徑後再嘗試運行。
|
||||
launch.advice.vanilla_x86.translation=Minecraft 官方尚未提供對非 x86-64 平臺的支持。\n請使用 x86-64 平臺的 Java 通過轉譯執行,或者嘗試下載您所用平臺對應的本機庫,在遊戲設定的調試選項中指定其路徑後再嘗試運行。
|
||||
launch.failed=啟動失敗
|
||||
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
|
||||
launch.failed.creating_process=啟動失敗,在建立新處理程式時發生錯誤。可能是 Java 路徑錯誤。
|
||||
launch.failed.command_too_long=命令長度超過限制,無法創建 bat 腳本,請匯出為 PowerShell 腳本。
|
||||
launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。
|
||||
launch.failed.download_library=無法下載遊戲相依元件 %s。
|
||||
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
|
||||
launch.failed.execution_policy=設定執行策略
|
||||
launch.failed.execution_policy.failed_to_set=設定執行策略失敗
|
||||
launch.failed.execution_policy.hint=當前執行策略封锁您執行 PowerShell 腳本。\n點擊“確定”允許當前用戶執行本地 PowerShell 腳本,或點擊“取消”保持現狀。
|
||||
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
|
||||
launch.failed.no_accepted_java=找不到適合當前遊戲使用的 Java。如果您認為實際存在合適的 Java,您可以在遊戲設置中手動設置 Java。
|
||||
launch.state.dependencies=處理遊戲相依元件
|
||||
@@ -475,6 +499,7 @@ launcher.background.choose=選擇背景路徑
|
||||
launcher.background.classic=經典
|
||||
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
|
||||
launcher.background.network=網路
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=檔案下載快取目錄
|
||||
launcher.cache_directory.clean=清理
|
||||
launcher.cache_directory.choose=選擇檔案下載快取目錄
|
||||
@@ -633,9 +658,11 @@ multiplayer.download.success=多人聯機初始化完成
|
||||
multiplayer.download.unsupported=多人聯機依賴不支持當前系統或平台
|
||||
multiplayer.exit.after_ready=多人聯機會話意外退出,退出碼 %d
|
||||
multiplayer.exit.before_ready=多人聯機房間創建失敗,cato 退出碼 %d
|
||||
multiplayer.exit.timeout=無法連接多人聯機服務
|
||||
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=尚未檢測
|
||||
@@ -652,8 +679,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
|
||||
@@ -662,7 +689,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=自動接受所有連接申請(不啟用此選項時,你需要手動同意申請,以避免不相關人士誤連你的伺服器)
|
||||
@@ -673,9 +700,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 小時,你需要重新創建/加入房間以繼續聯機。
|
||||
@@ -685,13 +712,14 @@ multiplayer.session.join.error.connection=加入房間失敗。無法與對方
|
||||
multiplayer.session.join.hint=你需要向已經創建好房間的玩家索要邀請碼以便加入多人聯機房間
|
||||
multiplayer.session.join.invitation_code=邀請碼
|
||||
multiplayer.session.join.invitation_code.error=邀請碼不正確,請向開服玩家獲取邀請碼
|
||||
multiplayer.session.join.invitation_code.version=多人聯機功能版本號不一致,請保證連接多人聯機功能版本號一致。
|
||||
multiplayer.session.join.kicked=你已經被房主踢出房間,你將與房間失去連接。
|
||||
multiplayer.session.join.kicked=你與房間失去連接:%s。
|
||||
multiplayer.session.join.kicked.join_acceptance_timeout=對方未能即時同意你的加入申請
|
||||
multiplayer.session.join.kicked.kicked=你被房主踢出房間。
|
||||
multiplayer.session.join.kicked.version_not_matched=多人聯機功能版本號不一致,請保證連接多人聯機功能版本號一致。
|
||||
multiplayer.session.join.lost_connection=你已與房間失去連接。這可能意味著房主已經解散房間,或者你無法連接至房間。
|
||||
multiplayer.session.join.port.error=無法找到可用的本地網路埠,請確保 HMCL 擁有綁定本地埠的權限。
|
||||
multiplayer.session.join.rejected=你被房主拒絕連接。
|
||||
multiplayer.session.join.wait=等待對方同意加入申請。
|
||||
multiplayer.session.join.wait_timeout=對方未能即時同意你的加入申請
|
||||
multiplayer.session.members=房間成員
|
||||
multiplayer.session.quit=退出房間
|
||||
multiplayer.session.quit.warning=退出房間後,你將會與伺服器斷開連接,是否繼續?
|
||||
@@ -828,8 +856,11 @@ settings.game.exploration=瀏覽
|
||||
settings.game.fullscreen=全螢幕
|
||||
settings.game.java_directory=Java 路徑
|
||||
settings.game.java_directory.auto=自動選擇合適的 Java
|
||||
settings.game.java_directory.bit=,%s 位
|
||||
settings.game.java_directory.auto.not_found=沒有合適的 Java
|
||||
settings.game.java_directory.bit=%s 位
|
||||
settings.game.java_directory.choose=選擇 Java 路徑
|
||||
settings.game.java_directory.invalid=Java 路徑不正確
|
||||
settings.game.java_directory.template=%s(%s)
|
||||
settings.game.management=管理
|
||||
settings.game.working_directory=執行路徑(版本隔離,修改後請自行移動相關遊戲檔案,如存檔模組設定等)
|
||||
settings.game.working_directory.choose=選擇執行路徑
|
||||
@@ -919,7 +950,7 @@ update.found=發現到更新
|
||||
update.newest_version=最新版本為: %s
|
||||
update.bubble.title=發現更新: %s
|
||||
update.bubble.subtitle=點選此處進行升級
|
||||
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
|
||||
update.note=測試版與開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
|
||||
update.latest=目前版本為最新版本
|
||||
update.no_browser=無法開啟瀏覽器,網址已經複製到剪貼簿了,您可以手動複製網址開啟頁面
|
||||
update.tooltip=更新
|
||||
|
||||
@@ -96,8 +96,8 @@ account.methods.microsoft.error.missing_xbox_account=你的微软帐户尚未关
|
||||
account.methods.microsoft.error.no_character=该帐户没有包含 Minecraft Java 版购买记录
|
||||
account.methods.microsoft.error.unknown=登录失败,错误码:%d
|
||||
account.methods.microsoft.logging_in=登录中...
|
||||
account.methods.microsoft.hint=您需要点击登录按钮,并在新打开的浏览器窗口中完成登录。
|
||||
account.methods.microsoft.manual=若登录页面未能打开,您可以点击此处复制链接,并手动在浏览器中打开网页。
|
||||
account.methods.microsoft.hint=您需要点击登录按钮,并在新打开的浏览器窗口中完成登录,并输入待会在此处显示的设备代码。
|
||||
account.methods.microsoft.manual=设备代码为:<b>%1$s</b>,已经帮你拷贝到剪切板中,如果未能粘贴,你可以点击此处再复制一次。你需要在新打开的浏览器窗口中完成登录,若登录页面未能打开,您手动在浏览器中打开网页:%2$s。
|
||||
account.methods.microsoft.profile=帐户设置页
|
||||
account.methods.microsoft.snapshot=HMCL 快照版不支持微软登录
|
||||
account.methods.microsoft.waiting_browser=等待在新打开的浏览器窗口中完成登录...
|
||||
@@ -182,6 +182,11 @@ curse.category.4472=科技
|
||||
curse.category.4473=魔法
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5129=原版增强
|
||||
curse.category.5189=实用与QOL
|
||||
curse.category.5190=QoL
|
||||
curse.category.5191=实用与QoL
|
||||
curse.category.5192=梦幻菜单
|
||||
curse.category.423=信息展示
|
||||
curse.category.426=模组扩展
|
||||
curse.category.434=装备武器
|
||||
@@ -223,7 +228,8 @@ curse.category.4558=红石
|
||||
curse.category.4843=自动化
|
||||
curse.category.4906=MCreator
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
|
||||
curse.category.5193=数据包
|
||||
curse.category.399=蒸汽朋克
|
||||
curse.category.396=128x
|
||||
curse.category.398=512x 及更高
|
||||
@@ -289,9 +295,16 @@ download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\
|
||||
download.javafx.component=正在下载模块 %s
|
||||
download.javafx.prepare=准备开始下载
|
||||
|
||||
exception.access_denied=因为无法访问文件 %s,HMCL 没有对该文件的访问权限,或者该文件被其他程序打开。\n\
|
||||
请你检查当前操作系统帐户是否能访问该文件,比如非管理员用户可能不能访问其他帐户的个人文件夹内的文件。\n\
|
||||
对于 Windows 用户,你还可以尝试通过资源监视器查看是否有程序占用了该文件,如果是,你可以关闭占用此文件相关程序,或者重启电脑再试。
|
||||
exception.artifact_malformed=下载的文件无法通过校验。
|
||||
exception.ssl_handshake=无法建立 SSL 连接,因为当前 Java 虚拟机缺少相关的 SSL 证书。你可以尝试使用其他的 Java 虚拟机启动 HMCL 再试。
|
||||
|
||||
extension.bat=Windows 脚本
|
||||
extension.mod=模组文件
|
||||
extension.png=图片文件
|
||||
extension.ps1=PowerShell 脚本
|
||||
extension.sh=Bash 脚本
|
||||
|
||||
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHMCL 无法在低于 Java 11 且缺少 JavaFX 的 Java 环境上自行补全 JavaFX 运行环境。\n请更换如 Oracle Java 8 等包含 JavaFX 的 Java 运行环境,或者更新到 Java 11 或更高版本。
|
||||
@@ -300,6 +313,8 @@ fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 Hello Minecraft! Launcher 对 "%s" 目录及该目录下的文件拥有读写权限。
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 Hello Minecraft! Launcher。
|
||||
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 Hello Minecraft! Launcher 来完成升级。\n请考虑向我们反馈该问题。
|
||||
fatal.samba=如果您正在通过 Samba 共享的文件夹中运行 HMCL,HMCL 可能无法正常工作,请尝试更新您的 Java 或在本地文件夹内运行 HMCL。
|
||||
fatal.illegal_char=您的用户文件夹路径中存在非法字符‘=’。HMCL 能够运行,但一些功能无法正常使用。\n您将无法使用外置登陆账户以及离线登陆更换皮肤功能,并且在进行多人游戏时可能存在更大的安全隐患。
|
||||
|
||||
feedback=反馈
|
||||
feedback.add=新增反馈
|
||||
@@ -353,10 +368,14 @@ game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
|
||||
game.crash.reason.file_already_exists=当前游戏因为文件 %1$s 已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
|
||||
game.crash.reason.file_changed=当前游戏因为文件校验失败,无法继续运行。\n如果你手动修改了 Minecraft.jar 文件,你需要回退修改,或者重新下载游戏。
|
||||
game.crash.reason.gl_operation_failure=当前游戏因为你使用的某些 Mod、光影包、材质包,无法继续运行。\n请先尝试禁用你所使用的Mod/光影包/材质包再试。
|
||||
game.crash.reason.graphics_driver=当前游戏因为你的显卡驱动存在问题崩溃了。\n请尝试升级你的显卡驱动到最新版本后再尝试启动游戏。\n如果你的电脑存在独立显卡,你需要检查游戏是否使用集成/核心显卡启动。如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑。
|
||||
game.crash.reason.graphics_driver=当前游戏因为你的显卡驱动存在问题崩溃了。\n\
|
||||
请尝试升级你的显卡驱动到最新版本后再尝试启动游戏。\n\
|
||||
如果你的电脑存在独立显卡,你需要检查游戏是否使用集成/核芯显卡启动。如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑。\n\
|
||||
如果你确实需要使用核芯显卡,请检查你的电脑的 CPU 是否是 Intel(R) Core(TM) 3000 系列或更旧的处理器,如果是,对于 Minecraft 1.16.5 及更旧版本,请你将游戏所使用的 Java 版本降级至 1.8.0_51 及以下版本,否则你必须更换独立显卡或新电脑。\n\
|
||||
详情请点击 <a href="https://hmcl.huangyuhui.net/help/game/graphics-card">此处</a> 查看
|
||||
game.crash.reason.illegal_access_error=当前游戏因为某些 Mod 的问题,无法继续运行。\n如果你认识:%1$s,你可以更新或删除对应 Mod 再试。
|
||||
game.crash.reason.jdk_9=当前游戏因为 Java 版本过高,无法继续运行。\n你需要下载安装 Java 8,并在游戏设置中将 Java 设置为 1.8 的版本。
|
||||
game.crash.reason.jvm_32bit=当前游戏因为内存分配过大,超过了 32 位 Java 内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。如果你的电脑室 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以关闭游戏内存的自动分配,并且把内存限制调节为 1024 MB 或以下。
|
||||
game.crash.reason.jvm_32bit=当前游戏因为内存分配过大,超过了 32 位 Java 内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。如果你的电脑是 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以关闭游戏内存的自动分配,并且把内存限制调节为 1024 MB 或以下。
|
||||
game.crash.reason.loading_crashed_forge=当前游戏因为模组 %1$s (%2$s) 错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.loading_crashed_fabric=当前游戏因为模组 %1$s 错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.memory_exceeded=当前游戏因为分配的内存过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在游戏设置中关闭游戏内存的自动分配,并将游戏内存调低至游戏能正常启动为止。\n你还可以尝试调大系统的页面大小。
|
||||
@@ -375,8 +394,8 @@ 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.unknown=原因未知,请点击日志按钮查看详细信息。
|
||||
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=游戏意外退出
|
||||
game.directory=游戏路径
|
||||
@@ -396,7 +415,6 @@ install.change_version=更换版本
|
||||
install.change_version.confirm=你确定要将 %s 从 %s 更新到 %s 吗?
|
||||
install.failed=安装失败
|
||||
install.failed.downloading=安装失败,部分文件未能完成下载
|
||||
install.failed.downloading.access_denied=未能下载文件:%1$s。因为无法访问文件 %2$s,HMCL 没有对该文件的访问权限,或者该文件被其他程序打开。你可以尝试关闭相关程序,或者重启电脑再试。
|
||||
install.failed.downloading.detail=未能下载文件:%s
|
||||
install.failed.downloading.timeout=下载超时:%s
|
||||
install.failed.install_online=无法识别要安装的软件。如果你要安装 Mod,你需要在模组管理页面安装模组。
|
||||
@@ -448,13 +466,19 @@ launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8
|
||||
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏。是否继续启动?
|
||||
launch.advice.require_newer_java_version=Minecraft %1$s 仅能运行在 Java %2$s 或更高版本上,是否下载?
|
||||
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小。是否继续启动?
|
||||
launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2 及以下版本仅支持 Java 8。\nJava 9+ 版本会无法加载 32 位的 liblwjgl.so。
|
||||
launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2 及以下版本仅支持 Java 8。\nJava 9+ 版本会无法加载 32 位的 liblwjgl.so。
|
||||
launch.advice.vanilla_x86=Minecraft 官方尚未提供对非 x86-64 平台的支持。\n您可以尝试下载您所用平台对应的本机库,在游戏设置的调试选项中指定其路径后再尝试运行。
|
||||
launch.advice.vanilla_x86.translation=Minecraft 官方尚未提供对非 x86-64 平台的支持。\n请使用 x86-64 平台的 Java 通过转译执行,或者尝试下载您所用平台对应的本机库,在游戏设置的调试选项中指定其路径后再尝试运行。
|
||||
launch.failed=启动失败
|
||||
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
|
||||
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
|
||||
launch.failed.command_too_long=命令长度超过限制,无法创建 bat 脚本,请导出为 PowerShell 脚本。
|
||||
launch.failed.decompressing_natives=未能解压游戏本地库。
|
||||
launch.failed.download_library=未能下载游戏依赖 %s.
|
||||
launch.failed.executable_permission=未能为启动文件添加执行权限。
|
||||
launch.failed.execution_policy=设置执行策略
|
||||
launch.failed.execution_policy.failed_to_set=设置执行策略失败
|
||||
launch.failed.execution_policy.hint=当前执行策略阻止您执行 PowerShell 脚本。\n点击“确定”允许当前用户执行本地 PowerShell 脚本,或点击“取消”保持现状。
|
||||
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
|
||||
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。如果您认为实际存在合适的 Java,您可以在游戏设置中手动设置 Java。
|
||||
launch.state.dependencies=处理游戏依赖
|
||||
@@ -475,6 +499,7 @@ launcher.background.choose=选择背景路径
|
||||
launcher.background.classic=经典
|
||||
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
|
||||
launcher.background.network=网络
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=文件下载缓存目录
|
||||
launcher.cache_directory.clean=清理
|
||||
launcher.cache_directory.choose=选择文件下载缓存目录
|
||||
@@ -633,9 +658,11 @@ multiplayer.download.success=多人联机初始化完成
|
||||
multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台
|
||||
multiplayer.exit.after_ready=多人联机会话意外退出,退出码 %d
|
||||
multiplayer.exit.before_ready=多人联机房间创建失败,cato 退出码 %d
|
||||
multiplayer.exit.timeout=无法连接多人联机服务
|
||||
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=尚未检测
|
||||
@@ -651,8 +678,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 的房间
|
||||
@@ -662,7 +689,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=自动接受所有连接申请(不启用此选项时,你需要手动同意申请,以避免不相关人士误连你的服务器)
|
||||
@@ -673,9 +700,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 小时,你需要重新创建/加入房间以继续联机。
|
||||
@@ -685,13 +712,14 @@ multiplayer.session.join.error.connection=加入房间失败。无法与对方
|
||||
multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间
|
||||
multiplayer.session.join.invitation_code=邀请码
|
||||
multiplayer.session.join.invitation_code.error=邀请码不正确,请向开服玩家获取邀请码
|
||||
multiplayer.session.join.invitation_code.version=多人联机功能版本号不一致,请保证连接多人联机功能版本号一致。
|
||||
multiplayer.session.join.kicked=你已经被房主踢出房间,你将与房间失去连接。
|
||||
multiplayer.session.join.kicked=你与房间失去连接:%s。
|
||||
multiplayer.session.join.kicked.join_acceptance_timeout=对方未能即时同意你的加入申请
|
||||
multiplayer.session.join.kicked.kicked=你被房主踢出房间。
|
||||
multiplayer.session.join.kicked.version_not_matched=多人联机功能版本号不一致,请保证连接多人联机功能版本号一致。
|
||||
multiplayer.session.join.lost_connection=你已与房间失去连接。这可能意味着房主已经解散房间,或者你无法连接至房间。
|
||||
multiplayer.session.join.port.error=无法找到可用的本地网络端口,请确保 HMCL 拥有绑定本地端口的权限。
|
||||
multiplayer.session.join.rejected=你被房主拒绝连接。
|
||||
multiplayer.session.join.wait=等待对方同意加入申请。
|
||||
multiplayer.session.join.wait_timeout=对方未能即时同意你的加入申请
|
||||
multiplayer.session.members=房间成员
|
||||
multiplayer.session.quit=退出房间
|
||||
multiplayer.session.quit.warning=退出房间后,你将会与服务器断开连接,是否继续?
|
||||
@@ -828,8 +856,11 @@ settings.game.exploration=浏览
|
||||
settings.game.fullscreen=全屏
|
||||
settings.game.java_directory=Java 路径
|
||||
settings.game.java_directory.auto=自动选择合适的 Java
|
||||
settings.game.java_directory.bit=,%s 位
|
||||
settings.game.java_directory.auto.not_found=没有合适的 Java
|
||||
settings.game.java_directory.bit=%s 位
|
||||
settings.game.java_directory.choose=选择 Java 路径
|
||||
settings.game.java_directory.invalid=Java 路径不正确
|
||||
settings.game.java_directory.template=%s(%s)
|
||||
settings.game.management=管理
|
||||
settings.game.working_directory=版本隔离(修改后请自行移动相关游戏文件,如存档模组配置等)
|
||||
settings.game.working_directory.choose=选择运行路径
|
||||
@@ -919,7 +950,7 @@ update.found=发现更新
|
||||
update.newest_version=最新版本为:%s
|
||||
update.bubble.title=发现更新:%s
|
||||
update.bubble.subtitle=点击此处进行升级
|
||||
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
|
||||
update.note=测试版与开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
|
||||
update.latest=当前版本为最新版本
|
||||
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
|
||||
update.tooltip=更新
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
public final class JavaFXLauncher {
|
||||
|
||||
private JavaFXLauncher() {
|
||||
}
|
||||
|
||||
public static void start() {
|
||||
// init JavaFX Toolkit
|
||||
try {
|
||||
// Java 9 or Latter
|
||||
final MethodHandle startup =
|
||||
MethodHandles.publicLookup().findStatic(
|
||||
javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class));
|
||||
startup.invokeExact((Runnable) () -> {
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
// Java 8
|
||||
try {
|
||||
Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.jackhuang.hmcl.JavaFXLauncher;
|
||||
import org.jackhuang.hmcl.game.ClassicVersion;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class GameCrashWindowTest {
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void test() throws Exception {
|
||||
JavaFXLauncher.start();
|
||||
|
||||
ManagedProcess process = new ManagedProcess(null, Arrays.asList("commands", "2"));
|
||||
|
||||
String logs = FileUtils.readText(new File("../HMCLCore/src/test/resources/logs/too_old_java.txt"));
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FXUtils.runInFX(() -> {
|
||||
GameCrashWindow window = new GameCrashWindow(process, ProcessListener.ExitType.APPLICATION_ERROR, null,
|
||||
new ClassicVersion(),
|
||||
new LaunchOptions.Builder()
|
||||
.setJava(new JavaVersion(Paths.get("."), "16", Platform.SYSTEM_PLATFORM))
|
||||
.setGameDir(new File("."))
|
||||
.create(),
|
||||
Arrays.stream(logs.split("\\n"))
|
||||
.map(log -> pair(log, Log4jLevel.guessLevel(log)))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
window.showAndWait();
|
||||
|
||||
latch.countDown();
|
||||
});
|
||||
latch.await();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -31,7 +31,7 @@ public class MultiplayerClientServerTest {
|
||||
public void startServer() throws Exception {
|
||||
Logging.initForTest();
|
||||
int localPort = MultiplayerManager.findAvailablePort();
|
||||
MultiplayerServer server = new MultiplayerServer(1000, true);
|
||||
MultiplayerServer server = new MultiplayerServer("SessionName", 1000, true);
|
||||
server.startServer(localPort);
|
||||
|
||||
MultiplayerClient client = new MultiplayerClient("username", localPort);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api group: 'com.google.code.gson', name: 'gson', version: '2.8.1'
|
||||
api group: 'com.moandjiezana.toml', name: 'toml4j', version: '0.7.2'
|
||||
api group: 'org.tukaani', name: 'xz', version: '1.8'
|
||||
api(group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0') {
|
||||
exclude group: 'org.jetbrains', module: 'annotations'
|
||||
}
|
||||
api group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
|
||||
api group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
|
||||
api group: 'com.nqzero', name: 'permit-reflect', version: '0.3'
|
||||
api group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
||||
compileOnlyApi group: 'org.jetbrains', name: 'annotations', version: '16.0.3'
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("com.google.code.gson:gson:2.8.1")
|
||||
api("com.moandjiezana.toml:toml4j:0.7.2")
|
||||
api("org.tukaani:xz:1.8")
|
||||
api("org.hildan.fxgson:fx-gson:3.1.0") {
|
||||
exclude(group = "org.jetbrains", module = "annotations")
|
||||
}
|
||||
api("org.jenkins-ci:constant-pool-scanner:1.2")
|
||||
api("com.github.steveice10:opennbt:1.1")
|
||||
api("com.nqzero:permit-reflect:0.3")
|
||||
api("org.nanohttpd:nanohttpd:2.3.1")
|
||||
api("org.apache.commons:commons-compress:1.21")
|
||||
api("org.apache.commons:commons-lang3:3.12.0")
|
||||
compileOnlyApi("org.jetbrains:annotations:16.0.3")
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn(":log4j-patch:jar")
|
||||
|
||||
into("assets/game") {
|
||||
from(project(":log4j-patch").file("build/libs"))
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,10 @@
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -27,26 +29,18 @@ import java.util.UUID;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class AuthInfo implements AutoCloseable {
|
||||
public class AuthInfo implements AutoCloseable {
|
||||
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
private final String accessToken;
|
||||
private final String userProperties;
|
||||
private final Arguments arguments;
|
||||
private final AutoCloseable closeable;
|
||||
|
||||
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties) {
|
||||
this(username, uuid, accessToken, userProperties, null, null);
|
||||
}
|
||||
|
||||
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties, Arguments arguments, AutoCloseable closeable) {
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
this.accessToken = accessToken;
|
||||
this.userProperties = userProperties;
|
||||
this.arguments = arguments;
|
||||
this.closeable = closeable;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
@@ -72,24 +66,14 @@ public final class AuthInfo implements AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when launching game.
|
||||
* @return null if no argument is specified
|
||||
*/
|
||||
public Arguments getArguments() {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public AuthInfo withArguments(Arguments arguments) {
|
||||
return new AuthInfo(username, uuid, accessToken, userProperties, arguments, closeable);
|
||||
}
|
||||
|
||||
public AuthInfo withCloseable(AutoCloseable closeable) {
|
||||
return new AuthInfo(username, uuid, accessToken, userProperties, arguments, closeable);
|
||||
public Arguments getLaunchArguments(LaunchOptions options) throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (closeable != null) {
|
||||
closeable.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* 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.auth;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
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;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class OAuth {
|
||||
public static final OAuth MICROSOFT = new OAuth(
|
||||
"https://login.live.com/oauth20_authorize.srf",
|
||||
"https://login.live.com/oauth20_token.srf",
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode",
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
|
||||
|
||||
private final String authorizationURL;
|
||||
private final String accessTokenURL;
|
||||
private final String deviceCodeURL;
|
||||
private final String tokenURL;
|
||||
|
||||
public OAuth(String authorizationURL, String accessTokenURL, String deviceCodeURL, String tokenURL) {
|
||||
this.authorizationURL = authorizationURL;
|
||||
this.accessTokenURL = accessTokenURL;
|
||||
this.deviceCodeURL = deviceCodeURL;
|
||||
this.tokenURL = tokenURL;
|
||||
}
|
||||
|
||||
public Result authenticate(GrantFlow grantFlow, Options options) throws AuthenticationException {
|
||||
try {
|
||||
switch (grantFlow) {
|
||||
case AUTHORIZATION_CODE:
|
||||
return authenticateAuthorizationCode(options);
|
||||
case DEVICE:
|
||||
return authenticateDevice(options);
|
||||
default:
|
||||
throw new UnsupportedOperationException("grant flow " + grantFlow);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof InterruptedException) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} else {
|
||||
throw new ServerDisconnectException(e);
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
||||
Session session = options.callback.startServer();
|
||||
options.callback.openBrowser(NetworkUtils.withQuery(authorizationURL,
|
||||
mapOf(pair("client_id", options.callback.getClientId()), pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope),
|
||||
pair("prompt", "select_account"))));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
|
||||
.form(pair("client_id", options.callback.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", options.callback.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.getJson(AuthorizationResponse.class);
|
||||
handleErrorResponse(response);
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
}
|
||||
|
||||
private Result authenticateDevice(Options options) throws IOException, InterruptedException, JsonParseException, AuthenticationException {
|
||||
DeviceTokenResponse deviceTokenResponse = HttpRequest.POST(deviceCodeURL)
|
||||
.form(pair("client_id", options.callback.getClientId()), pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.getJson(DeviceTokenResponse.class);
|
||||
handleErrorResponse(deviceTokenResponse);
|
||||
|
||||
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
|
||||
|
||||
// Microsoft OAuth Flow
|
||||
options.callback.openBrowser(deviceTokenResponse.verificationURI);
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
int interval = deviceTokenResponse.interval;
|
||||
|
||||
while (true) {
|
||||
Thread.sleep(Math.max(interval, 1));
|
||||
|
||||
// We stop waiting if user does not respond our authentication request in 15 minutes.
|
||||
long estimatedTime = System.nanoTime() - startTime;
|
||||
if (TimeUnit.SECONDS.convert(estimatedTime, TimeUnit.NANOSECONDS) >= Math.min(deviceTokenResponse.expiresIn, 900)) {
|
||||
throw new NoSelectedCharacterException();
|
||||
}
|
||||
|
||||
TokenResponse tokenResponse = HttpRequest.POST(tokenURL)
|
||||
.form(
|
||||
pair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
pair("code", deviceTokenResponse.deviceCode),
|
||||
pair("client_id", options.callback.getClientId()))
|
||||
.ignoreHttpCode()
|
||||
.getJson(TokenResponse.class);
|
||||
|
||||
if ("authorization_pending".equals(tokenResponse.error)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("expired_token".equals(tokenResponse.error)) {
|
||||
throw new NoSelectedCharacterException();
|
||||
}
|
||||
|
||||
if ("slow_down".equals(tokenResponse.error)) {
|
||||
interval += 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Result(tokenResponse.accessToken, tokenResponse.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
public Result refresh(String refreshToken, Options options) throws AuthenticationException {
|
||||
try {
|
||||
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);
|
||||
|
||||
handleErrorResponse(response);
|
||||
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException {
|
||||
if (response.error == null || response.errorDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.error) {
|
||||
case "invalid_grant":
|
||||
if (response.errorDescription.contains("The user must sign in again and if needed grant the client application access to the requested scope")) {
|
||||
throw new CredentialExpiredException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new RemoteAuthenticationException(response.error, response.errorDescription, "");
|
||||
}
|
||||
|
||||
public static class Options {
|
||||
private String userAgent;
|
||||
private final String scope;
|
||||
private final Callback callback;
|
||||
|
||||
public Options(String scope, Callback callback) {
|
||||
this.scope = scope;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public Options setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Session {
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
/**
|
||||
* Wait for authentication
|
||||
*
|
||||
* @return authentication code
|
||||
* @throws InterruptedException if interrupted
|
||||
* @throws ExecutionException if an I/O error occurred.
|
||||
*/
|
||||
String waitFor() throws InterruptedException, ExecutionException;
|
||||
|
||||
default String getIdToken() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
/**
|
||||
* Start OAuth callback server at localhost.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
Session startServer() throws IOException, AuthenticationException;
|
||||
|
||||
void grantDeviceCode(String userCode, String verificationURI);
|
||||
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
|
||||
String getClientId();
|
||||
|
||||
String getClientSecret();
|
||||
|
||||
boolean isPublicClient();
|
||||
}
|
||||
|
||||
public enum GrantFlow {
|
||||
AUTHORIZATION_CODE,
|
||||
DEVICE,
|
||||
}
|
||||
|
||||
public class Result {
|
||||
private final String accessToken;
|
||||
private final String refreshToken;
|
||||
|
||||
public Result(String accessToken, String refreshToken) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeviceTokenResponse extends ErrorResponse {
|
||||
@SerializedName("user_code")
|
||||
public String userCode;
|
||||
|
||||
@SerializedName("device_code")
|
||||
public String deviceCode;
|
||||
|
||||
// The URI to be visited for user.
|
||||
@SerializedName("verification_uri")
|
||||
public String verificationURI;
|
||||
|
||||
// Life time in seconds for device_code and user_code
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
// Polling interval
|
||||
@SerializedName("interval")
|
||||
public int interval;
|
||||
}
|
||||
|
||||
private static class TokenResponse extends ErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("ext_expires_in")
|
||||
public int extExpiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
}
|
||||
|
||||
private static class ErrorResponse {
|
||||
@SerializedName("error")
|
||||
public String error;
|
||||
|
||||
@SerializedName("error_description")
|
||||
public String errorDescription;
|
||||
|
||||
@SerializedName("correlation_id")
|
||||
public String correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response: {"error":"invalid_grant","error_description":"The provided
|
||||
* value for the 'redirect_uri' is not valid. The value must exactly match the
|
||||
* redirect URI used to obtain the authorization
|
||||
* code.","correlation_id":"??????"}
|
||||
*/
|
||||
public static class AuthorizationResponse extends ErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
@SerializedName("user_id")
|
||||
public String userId;
|
||||
|
||||
@SerializedName("foci")
|
||||
public String foci;
|
||||
}
|
||||
|
||||
private static class RefreshResponse extends ErrorResponse {
|
||||
@SerializedName("expires_in")
|
||||
int expiresIn;
|
||||
|
||||
@SerializedName("access_token")
|
||||
String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
String refreshToken;
|
||||
}
|
||||
}
|
||||
+24
-7
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import java.io.IOException;
|
||||
@@ -71,7 +72,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
Optional<String> prefetchedMeta = server.getMetadataResponse();
|
||||
|
||||
if (auth.isPresent() && artifact.isPresent() && prefetchedMeta.isPresent()) {
|
||||
return Optional.of(auth.get().withArguments(generateArguments(artifact.get(), server, prefetchedMeta.get())));
|
||||
return Optional.of(new AuthlibInjectorAuthInfo(auth.get(), artifact.get(), server, prefetchedMeta.get()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -112,14 +113,30 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
}
|
||||
}
|
||||
|
||||
return auth.withArguments(generateArguments(artifact, server, prefetchedMeta));
|
||||
return new AuthlibInjectorAuthInfo(auth, artifact, server, prefetchedMeta);
|
||||
}
|
||||
|
||||
private static Arguments generateArguments(AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
|
||||
return new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
|
||||
"-Dauthlibinjector.side=client",
|
||||
"-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
|
||||
private static class AuthlibInjectorAuthInfo extends AuthInfo {
|
||||
|
||||
private final AuthlibInjectorArtifactInfo artifact;
|
||||
private final AuthlibInjectorServer server;
|
||||
private final String prefetchedMeta;
|
||||
|
||||
public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
|
||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
|
||||
|
||||
this.artifact = artifact;
|
||||
this.server = server;
|
||||
this.prefetchedMeta = prefetchedMeta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Arguments getLaunchArguments(LaunchOptions options) {
|
||||
return new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
|
||||
"-Dauthlibinjector.side=client",
|
||||
"-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
@@ -36,11 +37,9 @@ import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
@@ -50,20 +49,15 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class MicrosoftService {
|
||||
private static final String AUTHORIZATION_URL = "https://login.live.com/oauth20_authorize.srf";
|
||||
private static final String ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf";
|
||||
private static final String SCOPE = "XboxLive.signin offline_access";
|
||||
private static final int[] PORTS = {29111, 29112, 29113, 29114, 29115};
|
||||
private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10,
|
||||
TimeUnit.SECONDS);
|
||||
private static final Pattern OAUTH_URL_PATTERN = Pattern
|
||||
.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$");
|
||||
|
||||
private final OAuthCallback callback;
|
||||
private final OAuth.Callback callback;
|
||||
|
||||
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
|
||||
|
||||
public MicrosoftService(OAuthCallback callback) {
|
||||
public MicrosoftService(OAuth.Callback callback) {
|
||||
this.callback = requireNonNull(callback);
|
||||
this.profileRepository = new ObservableOptionalCache<>(uuid -> {
|
||||
LOG.info("Fetching properties of " + uuid);
|
||||
@@ -76,37 +70,11 @@ public class MicrosoftService {
|
||||
}
|
||||
|
||||
public MicrosoftSession authenticate() throws AuthenticationException {
|
||||
// Example URL:
|
||||
// https://login.live.com/oauth20_authorize.srf?response_type=code&client_id=6a3728d6-27a3-4180-99bb-479895b8f88e&redirect_uri=http://localhost:29111/auth-response&scope=XboxLive.signin+offline_access&state=612fd24a2447427383e8b222b597db66&prompt=select_account
|
||||
try {
|
||||
// Microsoft OAuth Flow
|
||||
OAuthSession session = callback.startServer();
|
||||
callback.openBrowser(NetworkUtils.withQuery(AUTHORIZATION_URL,
|
||||
mapOf(pair("client_id", callback.getClientId()), pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE),
|
||||
pair("prompt", "select_account"))));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST(ACCESS_TOKEN_URL)
|
||||
.form(mapOf(pair("client_id", callback.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", callback.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE)))
|
||||
.getString();
|
||||
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
LiveAuthorizationResponse.class);
|
||||
|
||||
return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken);
|
||||
OAuth.Result result = OAuth.MICROSOFT.authenticate(OAuth.GrantFlow.DEVICE, new OAuth.Options(SCOPE, callback));
|
||||
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof InterruptedException) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} else {
|
||||
throw new ServerDisconnectException(e);
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
@@ -114,19 +82,8 @@ public class MicrosoftService {
|
||||
|
||||
public MicrosoftSession refresh(MicrosoftSession oldSession) throws AuthenticationException {
|
||||
try {
|
||||
LiveRefreshResponse response = HttpRequest.POST(ACCESS_TOKEN_URL)
|
||||
.form(pair("client_id", callback.getClientId()),
|
||||
pair("client_secret", callback.getClientSecret()),
|
||||
pair("refresh_token", oldSession.getRefreshToken()),
|
||||
pair("grant_type", "refresh_token"))
|
||||
.accept("application/json")
|
||||
.ignoreHttpErrorCode(400)
|
||||
.ignoreHttpErrorCode(401)
|
||||
.getJson(LiveRefreshResponse.class);
|
||||
|
||||
handleLiveErrorMessage(response);
|
||||
|
||||
return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken);
|
||||
OAuth.Result result = OAuth.MICROSOFT.refresh(oldSession.getRefreshToken(), new OAuth.Options(SCOPE, callback));
|
||||
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
@@ -134,22 +91,6 @@ public class MicrosoftService {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLiveErrorMessage(LiveErrorResponse response) throws AuthenticationException {
|
||||
if (response.error == null || response.errorDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.error) {
|
||||
case "invalid_grant":
|
||||
if (response.errorDescription.contains("The user must sign in again and if needed grant the client application access to the requested scope")) {
|
||||
throw new CredentialExpiredException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new RemoteAuthenticationException(response.error, response.errorDescription, "");
|
||||
}
|
||||
|
||||
private String getUhs(XBoxLiveAuthenticationResponse response, String existingUhs) throws AuthenticationException {
|
||||
if (response.errorCode != 0) {
|
||||
throw new XboxAuthorizationException(response.errorCode, response.redirectUrl);
|
||||
@@ -195,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")
|
||||
@@ -357,57 +284,6 @@ public class MicrosoftService {
|
||||
public static class NoXuiException extends AuthenticationException {
|
||||
}
|
||||
|
||||
public static class LiveErrorResponse {
|
||||
@SerializedName("error")
|
||||
public String error;
|
||||
|
||||
@SerializedName("error_description")
|
||||
public String errorDescription;
|
||||
|
||||
@SerializedName("correlation_id")
|
||||
public String correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response: {"error":"invalid_grant","error_description":"The provided
|
||||
* value for the 'redirect_uri' is not valid. The value must exactly match the
|
||||
* redirect URI used to obtain the authorization
|
||||
* code.","correlation_id":"??????"}
|
||||
*/
|
||||
public static class LiveAuthorizationResponse extends LiveErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
@SerializedName("user_id")
|
||||
public String userId;
|
||||
|
||||
@SerializedName("foci")
|
||||
public String foci;
|
||||
}
|
||||
|
||||
private static class LiveRefreshResponse extends LiveErrorResponse {
|
||||
@SerializedName("expires_in")
|
||||
int expiresIn;
|
||||
|
||||
@SerializedName("access_token")
|
||||
String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
String refreshToken;
|
||||
}
|
||||
|
||||
private static class XBoxLiveAuthenticationResponseDisplayClaims {
|
||||
List<Map<Object, Object>> xui;
|
||||
}
|
||||
@@ -530,44 +406,6 @@ public class MicrosoftService {
|
||||
public String developerMessage;
|
||||
}
|
||||
|
||||
public interface OAuthCallback {
|
||||
/**
|
||||
* Start OAuth callback server at localhost.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
OAuthSession startServer() throws IOException, AuthenticationException;
|
||||
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
|
||||
String getClientId();
|
||||
|
||||
String getClientSecret();
|
||||
}
|
||||
|
||||
public interface OAuthSession {
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
/**
|
||||
* Wait for authentication
|
||||
*
|
||||
* @return authentication code
|
||||
* @throws InterruptedException if interrupted
|
||||
* @throws ExecutionException if an I/O error occurred.
|
||||
*/
|
||||
String waitFor() throws InterruptedException, ExecutionException;
|
||||
|
||||
default String getIdToken() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||
|
||||
@@ -25,8 +25,10 @@ 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.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
@@ -89,11 +91,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();
|
||||
@@ -117,15 +130,7 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
|
||||
try {
|
||||
YggdrasilServer server = new YggdrasilServer(0);
|
||||
server.start();
|
||||
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
|
||||
|
||||
return authInfo.withArguments(new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
|
||||
"-Dauthlibinjector.side=client"
|
||||
))
|
||||
.withCloseable(server::stop);
|
||||
return new OfflineAuthInfo(authInfo, artifact);
|
||||
} catch (Exception e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
@@ -134,6 +139,46 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
}
|
||||
|
||||
private class OfflineAuthInfo extends AuthInfo {
|
||||
private final AuthlibInjectorArtifactInfo artifact;
|
||||
private YggdrasilServer server;
|
||||
|
||||
public OfflineAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact) {
|
||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
|
||||
|
||||
this.artifact = artifact;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Arguments getLaunchArguments(LaunchOptions options) throws IOException {
|
||||
if (!options.isDaemon()) return null;
|
||||
|
||||
server = new YggdrasilServer(0);
|
||||
server.start();
|
||||
|
||||
try {
|
||||
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
return new Arguments().addJVMArguments(
|
||||
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
|
||||
"-Dauthlibinjector.side=client"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
super.close();
|
||||
|
||||
if (server != null)
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() throws AuthenticationException {
|
||||
return Optional.of(logIn());
|
||||
|
||||
@@ -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) {
|
||||
@@ -175,7 +188,7 @@ public class YggdrasilServer extends HttpServer {
|
||||
}
|
||||
}
|
||||
if (skin != null && skin.getCape() != null) {
|
||||
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
|
||||
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getCape().getHash())));
|
||||
}
|
||||
|
||||
Map<String, Object> textureResponse = mapOf(
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
@@ -78,7 +79,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
Version version = original.resolve(repository);
|
||||
return Task.allOf(
|
||||
Task.composeAsync(() -> {
|
||||
if (!repository.getVersionJar(version).exists())
|
||||
File versionJar = repository.getVersionJar(version);
|
||||
if (!versionJar.exists() || versionJar.length() == 0)
|
||||
return new GameDownloadTask(this, null, version);
|
||||
else
|
||||
return null;
|
||||
@@ -102,19 +104,27 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
if (!gameVersion.isPresent()) return null;
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version.resolvePreservingPatches(getGameRepository()));
|
||||
version.resolve(getGameRepository()).getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
Optional<String> libraryVersion = analyzer.getVersion(OPTIFINE);
|
||||
if (OPTIFINE.matchLibrary(library) && libraryVersion.isPresent()) {
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version, library, integrityCheck)) {
|
||||
tasks.add(installLibraryAsync(gameVersion.get(), version, OPTIFINE.getPatchId(), libraryVersion.get()));
|
||||
}
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
Optional<Library> library = analyzer.getLibrary(type);
|
||||
if (library.isPresent() && GameLibrariesTask.shouldDownloadLibrary(repository, version, library.get(), integrityCheck)) {
|
||||
tasks.add(downloadMissingLibraryAsync(gameVersion.get(), version, type, library.get()));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
return Task.allOf(tasks);
|
||||
});
|
||||
}
|
||||
|
||||
private Task<?> downloadMissingLibraryAsync(String gameVersion, Version version, LibraryAnalyzer.LibraryType libraryType, Library library) {
|
||||
switch (libraryType) {
|
||||
case OPTIFINE:
|
||||
if (library.hasDownloadURL())
|
||||
break;
|
||||
|
||||
return installLibraryAsync(gameVersion, version, libraryType.getPatchId(), library.getVersion());
|
||||
}
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<Version> installLibraryAsync(String gameVersion, Version baseVersion, String libraryId, String libraryVersion) {
|
||||
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
|
||||
|
||||
@@ -48,6 +48,10 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
return Optional.ofNullable(libraries.get(type)).map(Pair::getValue);
|
||||
}
|
||||
|
||||
public Optional<Library> getLibrary(LibraryType type) {
|
||||
return Optional.ofNullable(libraries.get(type.getPatchId())).map(Pair::getKey);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Iterator<LibraryMark> iterator() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -62,18 +63,31 @@ public class MaintainTask extends Task<Version> {
|
||||
String mainClass = version.resolve(null).getMainClass();
|
||||
|
||||
if (mainClass != null && mainClass.equals(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN)) {
|
||||
return maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version), true), false);
|
||||
version = maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version), true), false);
|
||||
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.MOD_LAUNCHER_MAIN)) {
|
||||
// Forge 1.13 and OptiFine
|
||||
return maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true);
|
||||
version = maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true);
|
||||
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN)) {
|
||||
// Forge 1.17
|
||||
return maintainGameWithCpwBoostrapLauncher(repository, unique(version));
|
||||
version = maintainGameWithCpwBoostrapLauncher(repository, unique(version));
|
||||
} else {
|
||||
// Vanilla Minecraft does not need maintain
|
||||
// Fabric does not need maintain, nothing compatible with fabric now.
|
||||
return maintainOptiFineLibrary(repository, unique(version), false);
|
||||
version = maintainOptiFineLibrary(repository, unique(version), false);
|
||||
}
|
||||
|
||||
List<Library> libraries = version.getLibraries();
|
||||
if (libraries.size() > 0) {
|
||||
Library library = libraries.get(0);
|
||||
if ("org.glavo".equals(library.getGroupId())
|
||||
&& ("log4j-patch".equals(library.getArtifactId()) || "log4j-patch-beta9".equals(library.getArtifactId()))
|
||||
&& "1.0".equals(library.getVersion())
|
||||
&& library.getDownload() == null) {
|
||||
version = version.setLibraries(libraries.subList(1, libraries.size()));
|
||||
}
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
public static Version maintainPreservingPatches(GameRepository repository, Version version) {
|
||||
@@ -136,10 +150,9 @@ public class MaintainTask extends Task<Version> {
|
||||
builder.addJvmArgument("-Dhmcl.transformer.candidates=${library_directory}/" + library.getPath());
|
||||
if (!libraryExisting) builder.addLibrary(hmclTransformerDiscoveryService);
|
||||
Path libraryPath = repository.getLibraryFile(version, hmclTransformerDiscoveryService).toPath();
|
||||
try {
|
||||
try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/HMCLTransformerDiscoveryService-1.0.jar")) {
|
||||
Files.createDirectories(libraryPath.getParent());
|
||||
Files.copy(MaintainTask.class.getResourceAsStream("/assets/game/HMCLTransformerDiscoveryService-1.0.jar"),
|
||||
libraryPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(input, libraryPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to unpack HMCLTransformerDiscoveryService", e);
|
||||
}
|
||||
@@ -270,7 +283,7 @@ public class MaintainTask extends Task<Version> {
|
||||
public static Version unique(Version version) {
|
||||
List<Library> libraries = new ArrayList<>();
|
||||
|
||||
SimpleMultimap<String, Integer> multimap = new SimpleMultimap<String, Integer>(HashMap::new, LinkedList::new);
|
||||
SimpleMultimap<String, Integer> multimap = new SimpleMultimap<String, Integer>(HashMap::new, ArrayList::new);
|
||||
|
||||
for (Library library : version.getLibraries()) {
|
||||
String id = library.getGroupId() + ":" + library.getArtifactId();
|
||||
|
||||
@@ -96,7 +96,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
|
||||
}
|
||||
|
||||
public Task<Version> getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) {
|
||||
throw new UnsupportedOperationException(toString() + " cannot be installed yet");
|
||||
throw new UnsupportedOperationException(this + " cannot be installed yet");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loaded 100 of 199 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user