Compare commits
166
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f176fbd1fd | ||
|
|
23811dad7f | ||
|
|
e792bfc554 | ||
|
|
d6b4af2d96 | ||
|
|
2a915460ae | ||
|
|
e9b577f66f | ||
|
|
e12c7e53f6 | ||
|
|
61caf8a925 | ||
|
|
a7123a389e | ||
|
|
3a6e80da0e | ||
|
|
2c8446fa33 | ||
|
|
21fcac1b0c | ||
|
|
095d71fea7 | ||
|
|
07ff244cd2 | ||
|
|
0269e54b95 | ||
|
|
06ef27f0e0 | ||
|
|
b3f649c026 | ||
|
|
0531d0cbc2 | ||
|
|
84b6d50421 | ||
|
|
dfdad64e93 | ||
|
|
323d09b104 | ||
|
|
0f85782312 | ||
|
|
7b79945391 | ||
|
|
441ce7220f | ||
|
|
e8df66dcc0 | ||
|
|
f7420e284d | ||
|
|
a2fa7ed5ab | ||
|
|
3f67b6bda0 | ||
|
|
4cd783d6bb | ||
|
|
4118030d9c | ||
|
|
bcc5cda5c0 | ||
|
|
1147892830 | ||
|
|
a4fff896ea | ||
|
|
5e2da98061 | ||
|
|
2b76d44ab0 | ||
|
|
cf44b2e536 | ||
|
|
895481ce63 | ||
|
|
1e45711006 | ||
|
|
f4893d9f75 | ||
|
|
3e172608bf | ||
|
|
f0fcd28bd0 | ||
|
|
5736ea8a24 | ||
|
|
b510352154 | ||
|
|
4efd3a1e7d | ||
|
|
c892caeba3 | ||
|
|
3b57add252 | ||
|
|
e757b6832f | ||
|
|
a45ccde0a1 | ||
|
|
eb14c66e1a | ||
|
|
6fa48981d5 | ||
|
|
3c89a012b2 | ||
|
|
a543216e67 | ||
|
|
86b0b855a7 | ||
|
|
13d5d6b322 | ||
|
|
cfb0a8ce68 | ||
|
|
fe000fb94f | ||
|
|
29acd01155 | ||
|
|
7cf696e2a0 | ||
|
|
09ccf73285 | ||
|
|
7403c3ff79 | ||
|
|
b46a07cd45 | ||
|
|
a262a95408 | ||
|
|
88222af99a | ||
|
|
f1d90fe0a1 | ||
|
|
52c84ceb29 | ||
|
|
087b2b678d | ||
|
|
3f6e7fcffd | ||
|
|
ed59611772 | ||
|
|
c100f46b9d | ||
|
|
00b1b34f98 | ||
|
|
f411840199 | ||
|
|
8d44411302 | ||
|
|
85540268bd | ||
|
|
e64a7b45b5 | ||
|
|
7d89e87dca | ||
|
|
8f002e433f | ||
|
|
a639fbb1c8 | ||
|
|
70e6b6ade7 | ||
|
|
d3b91d439c | ||
|
|
8912ee2661 | ||
|
|
5dd6ab5908 | ||
|
|
57eca838e2 | ||
|
|
75d070d8c9 | ||
|
|
f81ba320da | ||
|
|
8047775bf8 | ||
|
|
740d3c5d80 | ||
|
|
967c326219 | ||
|
|
fdc3500cc4 | ||
|
|
2f17995955 | ||
|
|
ef311560fd | ||
|
|
440b42a8b7 | ||
|
|
8b706466e7 | ||
|
|
8e16127845 | ||
|
|
bdbf5773a8 | ||
|
|
1ca146c615 | ||
|
|
de8ae88ee4 | ||
|
|
ba62160c76 | ||
|
|
8e7f36c681 | ||
|
|
b08f3869cf | ||
|
|
f05647fc82 | ||
|
|
67e9826309 | ||
|
|
f1c9b77df5 | ||
|
|
c3abfcf1d9 | ||
|
|
6632a91426 | ||
|
|
ac378ad46a | ||
|
|
6bbb1d157c | ||
|
|
efa1012a1c | ||
|
|
5058c68be8 | ||
|
|
3612f04657 | ||
|
|
a2d3bb3b42 | ||
|
|
d3efc81332 | ||
|
|
6b1799838f | ||
|
|
5ad4880182 | ||
|
|
a3781141fe | ||
|
|
ed168e14d6 | ||
|
|
ee81d7cc66 | ||
|
|
299077d1cb | ||
|
|
7448ea90b2 | ||
|
|
ac578c044f | ||
|
|
fc68029adc | ||
|
|
f6a92c1806 | ||
|
|
58eb452e90 | ||
|
|
fb8a96a170 | ||
|
|
ced441a830 | ||
|
|
c5693c3450 | ||
|
|
bc27eda220 | ||
|
|
0c40dcdeed | ||
|
|
4282147042 | ||
|
|
0874c98d21 | ||
|
|
0c3c7dd62a | ||
|
|
f663e5568c | ||
|
|
b8c1738f96 | ||
|
|
41c61645e7 | ||
|
|
9fc697751c | ||
|
|
a4db0532b9 | ||
|
|
8954139b5e | ||
|
|
b2f7e724e4 | ||
|
|
0d20910ea3 | ||
|
|
5c90299073 | ||
|
|
fc66cbcf96 | ||
|
|
e7b9a95df0 | ||
|
|
acc9740fa9 | ||
|
|
5789f82617 | ||
|
|
8827fd06d8 | ||
|
|
583e004d71 | ||
|
|
63207affc6 | ||
|
|
8b147bf20a | ||
|
|
15754119d5 | ||
|
|
8119324a92 | ||
|
|
c0ad7764b0 | ||
|
|
3ea054ae31 | ||
|
|
d03ad38e24 | ||
|
|
93eaee317c | ||
|
|
68b3a9f076 | ||
|
|
c943e4c74f | ||
|
|
4dba96e635 | ||
|
|
e521a1b91a | ||
|
|
a7e0fe7499 | ||
|
|
4e2d5cac9e | ||
|
|
452d030817 | ||
|
|
da9ec2c1e1 | ||
|
|
4ebbb400ab | ||
|
|
0cfc2471a0 | ||
|
|
413a7291ec | ||
|
|
e4893a6df9 | ||
|
|
bb16a0b6c0 |
No files matched your search
@@ -6,3 +6,6 @@ contact_links:
|
|||||||
- name: Discord 服务器
|
- name: Discord 服务器
|
||||||
url: https://discord.gg/jVvC7HfM6U
|
url: https://discord.gg/jVvC7HfM6U
|
||||||
about: Hello Minecraft! Launcher 的官方 Discord 服务器。
|
about: Hello Minecraft! Launcher 的官方 Discord 服务器。
|
||||||
|
- name: GitHub Issue
|
||||||
|
url: https://github.com/HMCL-dev/HMCL/issues/new/choose
|
||||||
|
about: 本仓库为镜像仓库,Bug 报告与功能请求请在 GitHub 上提交。
|
||||||
@@ -59,6 +59,9 @@ Jenkinsfile text eol=lf
|
|||||||
|
|
||||||
# Minecraft schematic
|
# Minecraft schematic
|
||||||
*.litematic binary
|
*.litematic binary
|
||||||
|
*.nbt binary
|
||||||
|
*.schem binary
|
||||||
|
*.schematic binary
|
||||||
|
|
||||||
# Compiled / intermediate build artefacts
|
# Compiled / intermediate build artefacts
|
||||||
*.bin binary
|
*.bin binary
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ contact_links:
|
|||||||
- name: QQ 群
|
- name: QQ 群
|
||||||
url: https://docs.hmcl.net/groups.html
|
url: https://docs.hmcl.net/groups.html
|
||||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
||||||
|
- name: GitHub Issue
|
||||||
|
url: https://github.com/HMCL-dev/HMCL/issues/new/choose
|
||||||
|
about: 本仓库为镜像仓库,Bug 报告与功能请求请在 GitHub 上提交。
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
name: Check Codes Comment
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Check Codes"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
|
|
||||||
steps:
|
|
||||||
- name: Checkout Script
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
with:
|
|
||||||
sparse-checkout: |
|
|
||||||
.github/scripts/check-codes-comment.js
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
- name: Download Artifact
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: checkstyle-artifact
|
|
||||||
path: ${{ runner.temp }}/checkstyle-artifact
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Generate Review Comment
|
|
||||||
uses: actions/github-script@v9
|
|
||||||
env:
|
|
||||||
REPORTS_PATH: ${{ runner.temp }}/checkstyle-artifact/ALL_REPORTS.jsonl
|
|
||||||
PULL_REQUEST_NUMBER_PATH: ${{ runner.temp }}/checkstyle-artifact/PULL_REQUEST_NUMBER
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const script = require("./.github/scripts/check-codes-comment.js");
|
|
||||||
await script({ github, context, core });
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Check Codes
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- '**.java'
|
|
||||||
- '**.properties'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**.java'
|
|
||||||
- '**.properties'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Setup JDK 17
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
java-package: 'jdk+fx'
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v5
|
|
||||||
with:
|
|
||||||
cache-cleanup: never
|
|
||||||
- name: Check Codes
|
|
||||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
|
||||||
- if: failure() && github.event_name == 'pull_request'
|
|
||||||
name: Setup reviewdog
|
|
||||||
uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0
|
|
||||||
with:
|
|
||||||
reviewdog_version: v0.21.0
|
|
||||||
- if: failure() && github.event_name == 'pull_request'
|
|
||||||
name: Generate Artifact
|
|
||||||
run: |
|
|
||||||
echo "${{ github.event.number }}" > PULL_REQUEST_NUMBER
|
|
||||||
> ALL_REPORTS.jsonl
|
|
||||||
find . -path "*/build/reports/checkstyle/*.xml" | while read -r xml_file; do
|
|
||||||
if [ -s "$xml_file" ]; then
|
|
||||||
echo "Processing $xml_file..."
|
|
||||||
reviewdog -f=checkstyle -name="checkstyle" -reporter=rdjsonl < "$xml_file" >> ALL_REPORTS.jsonl
|
|
||||||
else
|
|
||||||
echo "Skipping empty or missing report: $xml_file"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
- if: failure() && github.event_name == 'pull_request'
|
|
||||||
name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: checkstyle-artifact
|
|
||||||
path: |
|
|
||||||
ALL_REPORTS.jsonl
|
|
||||||
PULL_REQUEST_NUMBER
|
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
name: Java CI
|
name: Java CI
|
||||||
|
|
||||||
|
# Pushes to main and pull requests only. A branch with an open pull request used to build twice
|
||||||
|
# (once for the push, once for the pull request), which kept the runners queued.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
|
|
||||||
|
# A newer run for the same pull request or branch replaces the older one.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
@@ -14,17 +25,35 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v6
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'liberica'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
java-package: 'jdk+fx'
|
java-package: 'jdk+fx'
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v5
|
uses: gradle/actions/setup-gradle@v5
|
||||||
with:
|
with:
|
||||||
cache-cleanup: never
|
cache-cleanup: never
|
||||||
- name: Build with Gradle
|
# Only main writes the dependency cache; pull requests read it.
|
||||||
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel
|
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
# The repository's wrapper downloads Gradle itself, so cache that download too.
|
||||||
|
- name: Cache Gradle distribution
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/wrapper
|
||||||
|
.gradle/wrapper-neo
|
||||||
|
key: gradle-dist-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }}
|
||||||
|
- name: Install Xvfb
|
||||||
|
run: |
|
||||||
|
# Installing is the slowest setup step, so skip it when the runner already has xvfb, and
|
||||||
|
# only refresh the package index when a plain install fails.
|
||||||
|
command -v xvfb-run >/dev/null || sudo apt-get install -y --no-install-recommends xvfb \
|
||||||
|
|| { sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb; }
|
||||||
|
# One Gradle run builds, tests and runs the code and translation checks, so the
|
||||||
|
# project is configured and compiled once instead of in two workflows.
|
||||||
|
- name: Build and check with Gradle
|
||||||
|
run: xvfb-run --auto-servernum ./gradlew build checkstyle checkTranslations --no-daemon --parallel
|
||||||
env:
|
env:
|
||||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
|
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
|
||||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
name: PR Size Label
|
name: PR Size Label
|
||||||
|
|
||||||
|
# Disabled on this Gitea fork: act resolves actions/github-script from
|
||||||
|
# git.nest.potatolab.uk and that mirror 404s. Re-enable after mirroring
|
||||||
|
# actions/github-script or replacing the labeler with a plain shell step.
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- reopened
|
|
||||||
- synchronize
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -16,16 +14,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
label:
|
label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout scripts
|
- name: Disabled
|
||||||
uses: actions/checkout@v7
|
run: echo "pr-size-label is disabled on this fork"
|
||||||
with:
|
|
||||||
sparse-checkout: |
|
|
||||||
.github/scripts/pr-size-label.js
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
- name: Label PR by filtered changed lines
|
|
||||||
uses: actions/github-script@v9
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const script = require("./.github/scripts/pr-size-label.js");
|
|
||||||
await script({ github, context, core });
|
|
||||||
@@ -77,3 +77,6 @@ language-subtag-registry
|
|||||||
!/.gemini/config.yaml
|
!/.gemini/config.yaml
|
||||||
.codex
|
.codex
|
||||||
/.gradle-*/
|
/.gradle-*/
|
||||||
|
|
||||||
|
# Managed Java downloaded by the launcher when it is run from the repository root
|
||||||
|
/java/
|
||||||
Generated
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
<component name="CopyrightManager">
|
<component name="CopyrightManager">
|
||||||
<copyright>
|
<copyright>
|
||||||
<option name="notice" value="Hello Minecraft! Launcher Copyright (C) &#36;today.year 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/>." />
|
<option name="notice" value="SPDX-FileCopyrightText: Copyright (C) &#36;today.year The HMCL Authors SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL" />
|
||||||
<option name="myName" value="HMCL" />
|
<option name="myName" value="HMCL" />
|
||||||
</copyright>
|
</copyright>
|
||||||
</component>
|
</component>
|
||||||
Generated
+1
@@ -3,6 +3,7 @@
|
|||||||
<LanguageOptions name="JAVA">
|
<LanguageOptions name="JAVA">
|
||||||
<option name="fileTypeOverride" value="3" />
|
<option name="fileTypeOverride" value="3" />
|
||||||
<option name="addBlankAfter" value="false" />
|
<option name="addBlankAfter" value="false" />
|
||||||
|
<option name="block" value="false" />
|
||||||
</LanguageOptions>
|
</LanguageOptions>
|
||||||
</settings>
|
</settings>
|
||||||
</component>
|
</component>
|
||||||
+53
-67
@@ -1,4 +1,5 @@
|
|||||||
import org.jackhuang.hmcl.gradle.TerracottaConfigUpgradeTask
|
import org.jackhuang.hmcl.gradle.addon.FormatJsonFile
|
||||||
|
import org.jackhuang.hmcl.gradle.addon.UpdateAddonProjectInfos
|
||||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||||
@@ -6,10 +7,10 @@ import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
|||||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||||
import org.jackhuang.hmcl.gradle.l10n.SyncTranslations
|
import org.jackhuang.hmcl.gradle.l10n.SyncTranslations
|
||||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
|
||||||
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
||||||
import org.jackhuang.hmcl.gradle.pack.ReleaseType
|
import org.jackhuang.hmcl.gradle.pack.ReleaseType
|
||||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||||
|
import org.jackhuang.hmcl.gradle.utils.OfficialBuildValueSource
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.FileSystems
|
import java.nio.file.FileSystems
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
@@ -154,33 +155,28 @@ tasks.compileJava {
|
|||||||
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
|
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
|
||||||
}
|
}
|
||||||
|
|
||||||
val hmclProperties = buildList {
|
val createPropertiesFile = tasks.register<WriteProperties>("createPropertiesFile") {
|
||||||
add("hmcl.version" to project.version.toString())
|
destinationFile.set(layout.buildDirectory.file("hmcl.properties"))
|
||||||
add("hmcl.add-opens" to addOpens.joinToString(" "))
|
encoding = "UTF-8"
|
||||||
System.getenv("GITHUB_SHA")?.let {
|
|
||||||
add("hmcl.version.hash" to it)
|
|
||||||
}
|
|
||||||
add("hmcl.version.type" to versionType)
|
|
||||||
add("hmcl.microsoft.auth.id" to microsoftAuthId)
|
|
||||||
add("hmcl.curseforge.apikey" to curseForgeApiKey)
|
|
||||||
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
|
|
||||||
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
property("hmcl.version", project.version.toString())
|
||||||
val createPropertiesFile = tasks.register("createPropertiesFile") {
|
property("hmcl.add-opens", addOpens.joinToString(" "))
|
||||||
outputs.file(hmclPropertiesFile)
|
System.getenv("GITHUB_SHA")?.let { property("hmcl.version.hash", it) }
|
||||||
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
property("hmcl.version.type", versionType)
|
||||||
|
property("hmcl.microsoft.auth.id", microsoftAuthId)
|
||||||
doLast {
|
property("hmcl.curseforge.apikey", curseForgeApiKey)
|
||||||
val targetFile = hmclPropertiesFile.get().asFile
|
property("hmcl.authlib-injector.version", libs.authlib.injector.get().version!!)
|
||||||
targetFile.parentFile.mkdir()
|
property("hmcl.lwjgl-unsafe-agent.version", libs.lwjgl.unsafe.agent.get().version!!)
|
||||||
targetFile.bufferedWriter().use {
|
property("hmcl.official", providers.of(OfficialBuildValueSource::class) {
|
||||||
for ((k, v) in hmclProperties) {
|
parameters.publicKeyFile.set(layout.projectDirectory.file("src/main/resources/assets/hmcl_signature_publickey.der"))
|
||||||
it.write("$k=$v\n")
|
parameters.privateKeyLocation.set(providers.environmentVariable("HMCL_SIGNATURE_KEY"))
|
||||||
}
|
// Pin the official public key independently of the replaceable resource file.
|
||||||
}
|
// Under HMCL’s license agreement, you **MUST** change the software name or the version number in
|
||||||
}
|
// a reasonable way to distinguish it from the original version.
|
||||||
|
// You may modify this hash if and only if you have changed the software name or version number
|
||||||
|
// in accordance with the license requirements.
|
||||||
|
parameters.expectedSha256.set("36aadcfde8626b9d4c7a74facb3fdfda47e703c205a673fbe6a56b1d419f8b8e")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.jar {
|
tasks.jar {
|
||||||
@@ -214,7 +210,7 @@ tasks.shadowJar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
manifest.attributes(
|
manifest.attributes(
|
||||||
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
|
"Created-By" to "Copyright(c) 2013-2026 The HMCL Authors",
|
||||||
"Implementation-Version" to project.version.toString(),
|
"Implementation-Version" to project.version.toString(),
|
||||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||||
"Multi-Release" to "true",
|
"Multi-Release" to "true",
|
||||||
@@ -236,13 +232,12 @@ tasks.shadowJar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.processResources {
|
tasks.processResources {
|
||||||
dependsOn(createPropertiesFile)
|
|
||||||
dependsOn(upsideDownTranslate)
|
dependsOn(upsideDownTranslate)
|
||||||
dependsOn(createLocaleNamesResourceBundle)
|
dependsOn(createLocaleNamesResourceBundle)
|
||||||
dependsOn(createLanguageList)
|
dependsOn(createLanguageList)
|
||||||
|
|
||||||
into("assets/") {
|
into("assets/") {
|
||||||
from(hmclPropertiesFile)
|
from(createPropertiesFile.flatMap { it.destinationFile })
|
||||||
from(embedResources)
|
from(embedResources)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,10 +247,6 @@ tasks.processResources {
|
|||||||
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs.property("terracotta_version", libs.versions.terracotta)
|
|
||||||
doLast {
|
|
||||||
upgradeTerracottaConfig.get().checkValid()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
|
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
|
||||||
@@ -366,6 +357,11 @@ fun parseToolOptions(options: String?): MutableList<String> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests use the same JavaFX reflective access as the packaged application.
|
||||||
|
tasks.withType<Test> {
|
||||||
|
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
|
||||||
|
}
|
||||||
|
|
||||||
// For IntelliJ IDEA
|
// For IntelliJ IDEA
|
||||||
tasks.withType<JavaExec> {
|
tasks.withType<JavaExec> {
|
||||||
if (name != "run") {
|
if (name != "run") {
|
||||||
@@ -404,27 +400,6 @@ tasks.register<JavaExec>("run") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// terracotta
|
|
||||||
|
|
||||||
val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgradeTerracottaConfig") {
|
|
||||||
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
|
|
||||||
val source = layout.projectDirectory.file("terracotta-template.json");
|
|
||||||
|
|
||||||
classifiers.set(
|
|
||||||
listOf(
|
|
||||||
"windows-x86_64", "windows-arm64",
|
|
||||||
"macos-x86_64", "macos-arm64",
|
|
||||||
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
|
|
||||||
"freebsd-x86_64"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
version.set(libs.versions.terracotta)
|
|
||||||
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
|
|
||||||
|
|
||||||
templateFile.set(source)
|
|
||||||
outputFile.set(destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Translations
|
// Check Translations
|
||||||
|
|
||||||
@@ -462,21 +437,32 @@ val createLanguageList = tasks.register<CreateLanguageList>("createLanguageList"
|
|||||||
outputFile.set(generatedDir.map { it.file("languages.json") })
|
outputFile.set(generatedDir.map { it.file("languages.json") })
|
||||||
}
|
}
|
||||||
|
|
||||||
val createLocaleNamesResourceBundle = tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
|
val createLocaleNamesResourceBundle =
|
||||||
dependsOn(createLanguageList)
|
tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
|
||||||
|
dependsOn(createLanguageList)
|
||||||
|
|
||||||
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
languagesFile.set(createLanguageList.flatMap { it.outputFile })
|
||||||
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
|
||||||
}
|
}
|
||||||
|
|
||||||
// mcmod data
|
// mcmod data
|
||||||
|
|
||||||
tasks.register<ParseModDataTask>("parseModData") {
|
tasks.register<FormatJsonFile>("formatModJson") {
|
||||||
inputFile.set(layout.projectDirectory.file("mod.json"))
|
file.set(layout.projectDirectory.file("mod.json"))
|
||||||
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/mod_data.txt"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<ParseModDataTask>("parseModPackData") {
|
tasks.register<FormatJsonFile>("formatModpackJson") {
|
||||||
inputFile.set(layout.projectDirectory.file("modpack.json"))
|
file.set(layout.projectDirectory.file("modpack.json"))
|
||||||
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/modpack_data.txt"))
|
}
|
||||||
|
|
||||||
|
tasks.register<UpdateAddonProjectInfos>("updateModProjectInfos") {
|
||||||
|
inputFile.set(layout.projectDirectory.file("mod.json"))
|
||||||
|
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/addon/mod.csv"))
|
||||||
|
projectType.set(UpdateAddonProjectInfos.AddonProjectType.MOD)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<UpdateAddonProjectInfos>("updateModpackProjectInfos") {
|
||||||
|
inputFile.set(layout.projectDirectory.file("modpack.json"))
|
||||||
|
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/addon/modpack.csv"))
|
||||||
|
projectType.set(UpdateAddonProjectInfos.AddonProjectType.MODPACK)
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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 com.jfoenix.controls;
|
package com.jfoenix.controls;
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXCheckBoxSkin;
|
import com.jfoenix.skins.JFXCheckBoxSkin;
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
|
package com.jfoenix.controls;
|
||||||
|
|
||||||
|
import com.jfoenix.skins.JFXSwitchSkin;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.css.CssMetaData;
|
||||||
|
import javafx.css.PseudoClass;
|
||||||
|
import javafx.css.SimpleStyleableDoubleProperty;
|
||||||
|
import javafx.css.Styleable;
|
||||||
|
import javafx.css.StyleableDoubleProperty;
|
||||||
|
import javafx.css.StyleablePropertyFactory;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.scene.AccessibleAction;
|
||||||
|
import javafx.scene.AccessibleAttribute;
|
||||||
|
import javafx.scene.AccessibleRole;
|
||||||
|
import javafx.scene.control.Control;
|
||||||
|
import javafx.scene.control.Skin;
|
||||||
|
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||||
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/// A Material Design 3 switch for a boolean setting.
|
||||||
|
///
|
||||||
|
/// Supports clicking, dragging, and Space-key activation. Labels belong to the surrounding layout;
|
||||||
|
/// supply an accessible text description. Size and colors are configurable through CSS.
|
||||||
|
@NotNullByDefault
|
||||||
|
public class JFXSwitch extends Control {
|
||||||
|
|
||||||
|
/// Creates an unselected switch.
|
||||||
|
public JFXSwitch() {
|
||||||
|
getStyleClass().setAll("jfx-switch");
|
||||||
|
setFocusTraversable(true);
|
||||||
|
setAccessibleRole(AccessibleRole.TOGGLE_BUTTON);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selection state.
|
||||||
|
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected") {
|
||||||
|
/// Updates visual and accessible selection without firing an action.
|
||||||
|
@Override
|
||||||
|
protected void invalidated() {
|
||||||
|
pseudoClassStateChanged(SELECTED, get());
|
||||||
|
notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Changes selection without firing an action event.
|
||||||
|
public final void setSelected(boolean value) {
|
||||||
|
selected.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the switch is selected.
|
||||||
|
public final boolean isSelected() {
|
||||||
|
return selected.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bindable selection state; the default is false.
|
||||||
|
public final BooleanProperty selectedProperty() {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only activation state.
|
||||||
|
private final ReadOnlyBooleanWrapper armed = new ReadOnlyBooleanWrapper(this, "armed") {
|
||||||
|
/// Reflects input feedback in CSS.
|
||||||
|
@Override
|
||||||
|
protected void invalidated() {
|
||||||
|
pseudoClassStateChanged(ARMED, get());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns whether the switch is armed.
|
||||||
|
public final boolean isArmed() {
|
||||||
|
return armed.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the read-only state indicating a pending activation.
|
||||||
|
public final ReadOnlyBooleanProperty armedProperty() {
|
||||||
|
return armed.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional action handler.
|
||||||
|
private @Nullable ObjectProperty<@Nullable EventHandler<ActionEvent>> onAction;
|
||||||
|
|
||||||
|
/// Sets or clears the action handler.
|
||||||
|
public final void setOnAction(@Nullable EventHandler<ActionEvent> handler) {
|
||||||
|
if (onAction != null || handler != null)
|
||||||
|
onActionProperty().set(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the action handler, or null when none is installed.
|
||||||
|
public final @Nullable EventHandler<ActionEvent> getOnAction() {
|
||||||
|
return onAction == null ? null : onAction.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the handler property for activation events.
|
||||||
|
public final ObjectProperty<@Nullable EventHandler<ActionEvent>> onActionProperty() {
|
||||||
|
if (onAction == null) {
|
||||||
|
onAction = new SimpleObjectProperty<>(this, "onAction") {
|
||||||
|
/// Updates the action handler.
|
||||||
|
@Override
|
||||||
|
protected void invalidated() {
|
||||||
|
setEventHandler(ActionEvent.ACTION, get());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return onAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The requested track height in logical pixels.
|
||||||
|
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(SIZE, this, "size", 32.0) {
|
||||||
|
/// Validates the size and requests layout.
|
||||||
|
@Override
|
||||||
|
protected void invalidated() {
|
||||||
|
get();
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sets the track height; see [#sizeProperty()].
|
||||||
|
public final void setSize(double value) {
|
||||||
|
size.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the requested track height in logical pixels.
|
||||||
|
public final double getSize() {
|
||||||
|
return size.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track height in logical pixels, styled by `-jfx-size`; the default is 32.
|
||||||
|
/// Scales the artwork and interaction target proportionally; nonpositive or nonfinite sizes render at the default size.
|
||||||
|
public final StyleableDoubleProperty sizeProperty() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional selected-icon property.
|
||||||
|
private @Nullable BooleanProperty showSelectedIcon;
|
||||||
|
|
||||||
|
/// Sets the value of [#showSelectedIconProperty()].
|
||||||
|
public final void setShowSelectedIcon(boolean value) {
|
||||||
|
if (showSelectedIcon != null || value)
|
||||||
|
showSelectedIconProperty().set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the selected icon is shown.
|
||||||
|
public final boolean isShowSelectedIcon() {
|
||||||
|
return showSelectedIcon != null && showSelectedIcon.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to show a check mark when selected; defaults to false.
|
||||||
|
public final BooleanProperty showSelectedIconProperty() {
|
||||||
|
if (showSelectedIcon == null)
|
||||||
|
showSelectedIcon = createOptionProperty("showSelectedIcon", false, false);
|
||||||
|
return showSelectedIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional unselected-icon property.
|
||||||
|
private @Nullable BooleanProperty showUnselectedIcon;
|
||||||
|
|
||||||
|
/// Sets the value of [#showUnselectedIconProperty()].
|
||||||
|
public final void setShowUnselectedIcon(boolean value) {
|
||||||
|
if (showUnselectedIcon != null || value)
|
||||||
|
showUnselectedIconProperty().set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the unselected icon is shown.
|
||||||
|
public final boolean isShowUnselectedIcon() {
|
||||||
|
return showUnselectedIcon != null && showUnselectedIcon.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to show a cross when unselected; defaults to false.
|
||||||
|
public final BooleanProperty showUnselectedIconProperty() {
|
||||||
|
if (showUnselectedIcon == null)
|
||||||
|
showUnselectedIcon = createOptionProperty("showUnselectedIcon", false, false);
|
||||||
|
return showUnselectedIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional animation policy override.
|
||||||
|
private @Nullable BooleanProperty disableAnimation;
|
||||||
|
|
||||||
|
/// Sets the value of [#disableAnimationProperty()].
|
||||||
|
public final void setDisableAnimation(boolean value) {
|
||||||
|
if (disableAnimation != null || value == AnimationUtils.isAnimationEnabled())
|
||||||
|
disableAnimationProperty().set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether animation is disabled.
|
||||||
|
public final boolean isDisableAnimation() {
|
||||||
|
return disableAnimation == null ? !AnimationUtils.isAnimationEnabled() : disableAnimation.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables animation, defaulting to the inverse of the global animation-enabled setting.
|
||||||
|
public final BooleanProperty disableAnimationProperty() {
|
||||||
|
if (disableAnimation == null)
|
||||||
|
disableAnimation = createOptionProperty("disableAnimation", !AnimationUtils.isAnimationEnabled(), true);
|
||||||
|
return disableAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arms input feedback unless the switch is disabled or its selection is bound.
|
||||||
|
public final void arm() {
|
||||||
|
if (!isDisabled() && !selected.isBound())
|
||||||
|
armed.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears input feedback without firing an action.
|
||||||
|
public final void disarm() {
|
||||||
|
armed.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggles selection and fires one action; disabled or bound switches ignore activation.
|
||||||
|
public void fire() {
|
||||||
|
if (!isDisabled() && !selected.isBound()) {
|
||||||
|
setSelected(!isSelected());
|
||||||
|
fireEvent(new ActionEvent(this, this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the default switch skin.
|
||||||
|
@Override
|
||||||
|
protected Skin<?> createDefaultSkin() {
|
||||||
|
return new JFXSwitchSkin(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a property that updates the skin, optionally settling active animations.
|
||||||
|
private BooleanProperty createOptionProperty(String name, boolean initialValue, boolean finishAnimation) {
|
||||||
|
return new SimpleBooleanProperty(this, name, initialValue) {
|
||||||
|
/// Validates the value even without a skin, so subsequent changes also reach this callback.
|
||||||
|
@Override
|
||||||
|
protected void invalidated() {
|
||||||
|
get();
|
||||||
|
if (getSkin() instanceof JFXSwitchSkin skin)
|
||||||
|
skin.updateOptions(finishAnimation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicates that the switch is selected.
|
||||||
|
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||||
|
|
||||||
|
/// Indicates a pending activation.
|
||||||
|
private static final PseudoClass ARMED = PseudoClass.getPseudoClass("armed");
|
||||||
|
|
||||||
|
/// Factory for switch CSS metadata.
|
||||||
|
private static final StyleablePropertyFactory<JFXSwitch> STYLEABLES =
|
||||||
|
new StyleablePropertyFactory<>(Control.getClassCssMetaData());
|
||||||
|
|
||||||
|
/// CSS metadata for track height.
|
||||||
|
private static final CssMetaData<JFXSwitch, Number> SIZE =
|
||||||
|
STYLEABLES.createSizeCssMetaData("-jfx-size", control -> control.size, 32.0);
|
||||||
|
|
||||||
|
/// Switch and inherited CSS metadata.
|
||||||
|
private static final @Unmodifiable List<CssMetaData<? extends Styleable, ?>> CSS_META_DATA =
|
||||||
|
List.copyOf(STYLEABLES.getCssMetaData());
|
||||||
|
|
||||||
|
/// Returns immutable CSS metadata, including inherited control properties.
|
||||||
|
public static @Unmodifiable List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||||
|
return CSS_META_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the metadata used to style this switch.
|
||||||
|
@Override
|
||||||
|
public @Unmodifiable List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
||||||
|
return getClassCssMetaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exposes selection to assistive technology.
|
||||||
|
@Override
|
||||||
|
public @Nullable Object queryAccessibleAttribute(AccessibleAttribute attribute, @Nullable Object... parameters) {
|
||||||
|
return attribute == AccessibleAttribute.SELECTED ? isSelected() : super.queryAccessibleAttribute(attribute, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles accessible activation.
|
||||||
|
@Override
|
||||||
|
public void executeAccessibleAction(AccessibleAction action, @Nullable Object... parameters) {
|
||||||
|
if (action == AccessibleAction.FIRE)
|
||||||
|
fire();
|
||||||
|
else
|
||||||
|
super.executeAccessibleAction(action, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.controls;
|
|
||||||
|
|
||||||
import com.jfoenix.skins.JFXToggleButtonSkin;
|
|
||||||
import javafx.css.*;
|
|
||||||
import javafx.css.converter.BooleanConverter;
|
|
||||||
import javafx.css.converter.PaintConverter;
|
|
||||||
import javafx.scene.control.Skin;
|
|
||||||
import javafx.scene.control.ToggleButton;
|
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import javafx.scene.paint.Paint;
|
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JFXToggleButton is the material design implementation of a toggle button.
|
|
||||||
* important CSS Selectors:
|
|
||||||
* <p>
|
|
||||||
* .jfx-toggle-button{
|
|
||||||
* -fx-toggle-color: color-value;
|
|
||||||
* -fx-untoggle-color: color-value;
|
|
||||||
* -fx-toggle-line-color: color-value;
|
|
||||||
* -fx-untoggle-line-color: color-value;
|
|
||||||
* }
|
|
||||||
* <p>
|
|
||||||
* To change the rippler color when toggled:
|
|
||||||
* <p>
|
|
||||||
* .jfx-toggle-button .jfx-rippler{
|
|
||||||
* -fx-rippler-fill: color-value;
|
|
||||||
* }
|
|
||||||
* <p>
|
|
||||||
* .jfx-toggle-button:selected .jfx-rippler{
|
|
||||||
* -fx-rippler-fill: color-value;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public class JFXToggleButton extends ToggleButton {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public JFXToggleButton() {
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Skin<?> createDefaultSkin() {
|
|
||||||
return new JFXToggleButtonSkin(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initialize() {
|
|
||||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
|
||||||
// it's up for the user to add this behavior
|
|
||||||
// toggleColor.addListener((o, oldVal, newVal) -> {
|
|
||||||
// // update line color in case not set by the user
|
|
||||||
// if(newVal instanceof Color)
|
|
||||||
// toggleLineColor.set(((Color)newVal).desaturate().desaturate().brighter());
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
* *
|
|
||||||
* styleable Properties *
|
|
||||||
* *
|
|
||||||
**************************************************************************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the style class to 'jfx-toggle-button'.
|
|
||||||
* <p>
|
|
||||||
* This is the selector class from which CSS can be used to style
|
|
||||||
* this control.
|
|
||||||
*/
|
|
||||||
private static final String DEFAULT_STYLE_CLASS = "jfx-toggle-button";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the button is toggled
|
|
||||||
*/
|
|
||||||
private final StyleableObjectProperty<Paint> toggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.TOGGLE_COLOR,
|
|
||||||
JFXToggleButton.this,
|
|
||||||
"toggleColor",
|
|
||||||
Color.valueOf(
|
|
||||||
"#009688"));
|
|
||||||
|
|
||||||
public Paint getToggleColor() {
|
|
||||||
return toggleColor == null ? Color.valueOf("#009688") : toggleColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> toggleColorProperty() {
|
|
||||||
return this.toggleColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setToggleColor(Paint color) {
|
|
||||||
this.toggleColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default color used when the button is not toggled
|
|
||||||
*/
|
|
||||||
private StyleableObjectProperty<Paint> untoggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNTOGGLE_COLOR,
|
|
||||||
JFXToggleButton.this,
|
|
||||||
"unToggleColor",
|
|
||||||
Color.valueOf(
|
|
||||||
"#FAFAFA"));
|
|
||||||
|
|
||||||
public Paint getUnToggleColor() {
|
|
||||||
return untoggleColor == null ? Color.valueOf("#FAFAFA") : untoggleColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> unToggleColorProperty() {
|
|
||||||
return this.untoggleColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnToggleColor(Paint color) {
|
|
||||||
this.untoggleColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default line color used when the button is toggled
|
|
||||||
*/
|
|
||||||
private final StyleableObjectProperty<Paint> toggleLineColor = new SimpleStyleableObjectProperty<>(
|
|
||||||
StyleableProperties.TOGGLE_LINE_COLOR,
|
|
||||||
JFXToggleButton.this,
|
|
||||||
"toggleLineColor",
|
|
||||||
Color.valueOf("#77C2BB"));
|
|
||||||
|
|
||||||
public Paint getToggleLineColor() {
|
|
||||||
return toggleLineColor == null ? Color.valueOf("#77C2BB") : toggleLineColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> toggleLineColorProperty() {
|
|
||||||
return this.toggleLineColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setToggleLineColor(Paint color) {
|
|
||||||
this.toggleLineColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default line color used when the button is not toggled
|
|
||||||
*/
|
|
||||||
private final StyleableObjectProperty<Paint> untoggleLineColor = new SimpleStyleableObjectProperty<>(
|
|
||||||
StyleableProperties.UNTOGGLE_LINE_COLOR,
|
|
||||||
JFXToggleButton.this,
|
|
||||||
"unToggleLineColor",
|
|
||||||
Color.valueOf("#999999"));
|
|
||||||
|
|
||||||
public Paint getUnToggleLineColor() {
|
|
||||||
return untoggleLineColor == null ? Color.valueOf("#999999") : untoggleLineColor.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableObjectProperty<Paint> unToggleLineColorProperty() {
|
|
||||||
return this.untoggleLineColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUnToggleLineColor(Paint color) {
|
|
||||||
this.untoggleLineColor.set(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default size of the toggle button.
|
|
||||||
*/
|
|
||||||
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
|
|
||||||
StyleableProperties.SIZE,
|
|
||||||
JFXToggleButton.this,
|
|
||||||
"size",
|
|
||||||
10.0);
|
|
||||||
|
|
||||||
public double getSize() {
|
|
||||||
return size.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public StyleableDoubleProperty sizeProperty() {
|
|
||||||
return this.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSize(double size) {
|
|
||||||
this.size.set(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable the visual indicator for focus
|
|
||||||
*/
|
|
||||||
private final StyleableBooleanProperty disableVisualFocus = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_VISUAL_FOCUS,
|
|
||||||
JFXToggleButton.this,
|
|
||||||
"disableVisualFocus",
|
|
||||||
false);
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty disableVisualFocusProperty() {
|
|
||||||
return this.disableVisualFocus;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Boolean isDisableVisualFocus() {
|
|
||||||
return disableVisualFocus != null && this.disableVisualFocusProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setDisableVisualFocus(final Boolean disabled) {
|
|
||||||
this.disableVisualFocusProperty().set(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* disable animation on button action
|
|
||||||
*/
|
|
||||||
private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
|
|
||||||
JFXToggleButton.this,
|
|
||||||
"disableAnimation",
|
|
||||||
!AnimationUtils.isAnimationEnabled());
|
|
||||||
|
|
||||||
public final StyleableBooleanProperty disableAnimationProperty() {
|
|
||||||
return this.disableAnimation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Boolean isDisableAnimation() {
|
|
||||||
return disableAnimation != null && this.disableAnimationProperty().get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final void setDisableAnimation(final Boolean disabled) {
|
|
||||||
this.disableAnimationProperty().set(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StyleableProperties {
|
|
||||||
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_COLOR =
|
|
||||||
new CssMetaData<>("-jfx-toggle-color",
|
|
||||||
PaintConverter.getInstance(), Color.valueOf("#009688")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXToggleButton control) {
|
|
||||||
return control.toggleColor == null || !control.toggleColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
|
||||||
return control.toggleColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_COLOR =
|
|
||||||
new CssMetaData<>("-jfx-untoggle-color",
|
|
||||||
PaintConverter.getInstance(), Color.valueOf("#FAFAFA")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXToggleButton control) {
|
|
||||||
return control.untoggleColor == null || !control.untoggleColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
|
||||||
return control.unToggleColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_LINE_COLOR =
|
|
||||||
new CssMetaData<>("-jfx-toggle-line-color",
|
|
||||||
PaintConverter.getInstance(), Color.valueOf("#77C2BB")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXToggleButton control) {
|
|
||||||
return control.toggleLineColor == null || !control.toggleLineColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
|
||||||
return control.toggleLineColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_LINE_COLOR =
|
|
||||||
new CssMetaData<>("-jfx-untoggle-line-color",
|
|
||||||
PaintConverter.getInstance(), Color.valueOf("#999999")) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXToggleButton control) {
|
|
||||||
return control.untoggleLineColor == null || !control.untoggleLineColor.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
|
|
||||||
return control.unToggleLineColorProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXToggleButton, Number> SIZE =
|
|
||||||
new CssMetaData<>("-jfx-size",
|
|
||||||
StyleConverter.getSizeConverter(), 10.0) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXToggleButton control) {
|
|
||||||
return !control.size.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableProperty<Number> getStyleableProperty(JFXToggleButton control) {
|
|
||||||
return control.sizeProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_VISUAL_FOCUS =
|
|
||||||
new CssMetaData<>("-jfx-disable-visual-focus",
|
|
||||||
BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXToggleButton control) {
|
|
||||||
return control.disableVisualFocus == null || !control.disableVisualFocus.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
|
|
||||||
return control.disableVisualFocusProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_ANIMATION =
|
|
||||||
new CssMetaData<>("-jfx-disable-animation",
|
|
||||||
BooleanConverter.getInstance(), false) {
|
|
||||||
@Override
|
|
||||||
public boolean isSettable(JFXToggleButton control) {
|
|
||||||
return control.disableAnimation == null || !control.disableAnimation.isBound();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
|
|
||||||
return control.disableAnimationProperty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
|
||||||
new ArrayList<>(ToggleButton.getClassCssMetaData());
|
|
||||||
Collections.addAll(styleables,
|
|
||||||
SIZE,
|
|
||||||
TOGGLE_COLOR,
|
|
||||||
UNTOGGLE_COLOR,
|
|
||||||
TOGGLE_LINE_COLOR,
|
|
||||||
UNTOGGLE_LINE_COLOR,
|
|
||||||
DISABLE_VISUAL_FOCUS,
|
|
||||||
DISABLE_ANIMATION
|
|
||||||
);
|
|
||||||
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
|
|
||||||
return getClassCssMetaData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
|
||||||
return StyleableProperties.CHILD_STYLEABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
|||||||
public class JFXCheckBoxSkin extends CheckBoxSkin {
|
public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||||
private final StackPane box = new StackPane();
|
private final StackPane box = new StackPane();
|
||||||
private final StackPane mark = new StackPane();
|
private final StackPane mark = new StackPane();
|
||||||
|
private final SVGPath markShape = new SVGPath();
|
||||||
private final double lineThick = 2.0;
|
private final double lineThick = 2.0;
|
||||||
private final double padding = 10.0;
|
private final double padding = 10.0;
|
||||||
private final JFXRippler rippler;
|
private final JFXRippler rippler;
|
||||||
@@ -37,7 +38,11 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
|||||||
private final double labelOffset = -8.0;
|
private final double labelOffset = -8.0;
|
||||||
private final Transition transition;
|
private final Transition transition;
|
||||||
private boolean invalid = true;
|
private boolean invalid = true;
|
||||||
private JFXFillTransition select;
|
private boolean selected;
|
||||||
|
private boolean indeterminate;
|
||||||
|
|
||||||
|
/// The checkbox fill animation, initialized with the configured checked color.
|
||||||
|
private JFXFillTransition fillTransition;
|
||||||
|
|
||||||
public JFXCheckBoxSkin(JFXCheckBox control) {
|
public JFXCheckBoxSkin(JFXCheckBox control) {
|
||||||
super(control);
|
super(control);
|
||||||
@@ -50,22 +55,17 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
|||||||
boxContainer.getChildren().add(this.box);
|
boxContainer.getChildren().add(this.box);
|
||||||
boxContainer.setPadding(new Insets(this.padding));
|
boxContainer.setPadding(new Insets(this.padding));
|
||||||
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE, RipplerPos.BACK);
|
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE, RipplerPos.BACK);
|
||||||
this.updateRippleColor();
|
this.updateRippleColor(control.isSelected(), control.isIndeterminate());
|
||||||
SVGPath shape = new SVGPath();
|
this.mark.setShape(this.markShape);
|
||||||
shape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
|
this.updateMarkShape(true, false);
|
||||||
this.mark.setShape(shape);
|
|
||||||
this.mark.setMaxSize(15.0, 12.0);
|
|
||||||
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
|
|
||||||
this.mark.setVisible(false);
|
this.mark.setVisible(false);
|
||||||
this.mark.setScaleX(0.0);
|
this.mark.setScaleX(0.0);
|
||||||
this.mark.setScaleY(0.0);
|
this.mark.setScaleY(0.0);
|
||||||
boxContainer.getChildren().add(this.mark);
|
boxContainer.getChildren().add(this.mark);
|
||||||
this.container.getChildren().add(this.rippler);
|
this.container.getChildren().add(this.rippler);
|
||||||
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
|
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
|
||||||
control.selectedProperty().addListener((o, oldVal, newVal) -> {
|
this.registerChangeListener(control.selectedProperty(), ignored -> requestSelectionUpdate());
|
||||||
this.updateRippleColor();
|
this.registerChangeListener(control.indeterminateProperty(), ignored -> requestSelectionUpdate());
|
||||||
this.playSelectAnimation(newVal);
|
|
||||||
});
|
|
||||||
|
|
||||||
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
|
||||||
if (focusVisibleProperty == null)
|
if (focusVisibleProperty == null)
|
||||||
@@ -82,37 +82,63 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
|||||||
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
|
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
|
||||||
this.updateChildren();
|
this.updateChildren();
|
||||||
this.registerChangeListener(control.checkedColorProperty(), ignored -> {
|
this.registerChangeListener(control.checkedColorProperty(), ignored -> {
|
||||||
if (select != null) {
|
if (fillTransition != null) {
|
||||||
select.stop();
|
fillTransition.stop();
|
||||||
}
|
}
|
||||||
this.createFillTransition();
|
boolean selected = control.isSelected();
|
||||||
updateColors();
|
boolean indeterminate = control.isIndeterminate();
|
||||||
|
JFXFillTransition fillTransition = this.createFillTransition();
|
||||||
|
this.fillTransition = fillTransition;
|
||||||
|
fillTransition.jumpTo(selected || indeterminate ? fillTransition.getCycleDuration() : Duration.ZERO);
|
||||||
|
updateColors(selected, indeterminate);
|
||||||
});
|
});
|
||||||
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors());
|
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors(control.isSelected(), control.isIndeterminate()));
|
||||||
this.transition = new CheckBoxTransition();
|
this.transition = new CheckBoxTransition();
|
||||||
this.createFillTransition();
|
this.fillTransition = this.createFillTransition();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateRippleColor() {
|
private void requestSelectionUpdate() {
|
||||||
var control = (JFXCheckBox) this.getSkinnable();
|
this.invalid = true;
|
||||||
this.rippler.setRipplerFill(control.isSelected()
|
this.getSkinnable().requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRippleColor(boolean selected, boolean indeterminate) {
|
||||||
|
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
||||||
|
this.rippler.setRipplerFill(selected || indeterminate
|
||||||
? control.getCheckedColor()
|
? control.getCheckedColor()
|
||||||
: control.getUnCheckedColor());
|
: control.getUnCheckedColor());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateColors() {
|
private void updateColors(boolean selected, boolean indeterminate) {
|
||||||
var control = (JFXCheckBox) getSkinnable();
|
var control = (JFXCheckBox) getSkinnable();
|
||||||
boolean isSelected = control.isSelected();
|
boolean marked = selected || indeterminate;
|
||||||
JFXNodeUtils.updateBackground(box.getBackground(), box, isSelected ? control.getCheckedColor() : Color.TRANSPARENT);
|
JFXNodeUtils.updateBackground(box.getBackground(), box, marked ? control.getCheckedColor() : Color.TRANSPARENT);
|
||||||
rippler.setRipplerFill(isSelected ? control.getCheckedColor() : control.getUnCheckedColor());
|
rippler.setRipplerFill(marked ? control.getCheckedColor() : control.getUnCheckedColor());
|
||||||
|
this.updateBorder(selected, indeterminate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBorder(boolean selected, boolean indeterminate) {
|
||||||
|
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
||||||
final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
|
final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
|
||||||
box.setBorder(new Border(new BorderStroke(
|
box.setBorder(new Border(new BorderStroke(
|
||||||
isSelected ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
|
selected || indeterminate ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
|
||||||
borderStroke.getTopStyle(),
|
borderStroke.getTopStyle(),
|
||||||
borderStroke.getRadii(),
|
borderStroke.getRadii(),
|
||||||
borderStroke.getWidths())));
|
borderStroke.getWidths())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateMarkShape(boolean selected, boolean indeterminate) {
|
||||||
|
if (indeterminate) {
|
||||||
|
this.markShape.setContent("M1 0 H9 A1 1 0 0 1 10 1 A1 1 0 0 1 9 2 H1 A1 1 0 0 1 0 1 A1 1 0 0 1 1 0 Z");
|
||||||
|
this.mark.setMaxSize(10.0, 2.0);
|
||||||
|
this.mark.setStyle("-fx-background-color:-monet-on-primary;");
|
||||||
|
} else if (selected) {
|
||||||
|
this.markShape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
|
||||||
|
this.mark.setMaxSize(15.0, 12.0);
|
||||||
|
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void updateChildren() {
|
protected void updateChildren() {
|
||||||
super.updateChildren();
|
super.updateChildren();
|
||||||
if (this.container != null) {
|
if (this.container != null) {
|
||||||
@@ -140,9 +166,10 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
|||||||
double xOffset = computeXOffset(w, labelWidth + boxWidth, checkBox.getAlignment().getHpos()) + x;
|
double xOffset = computeXOffset(w, labelWidth + boxWidth, checkBox.getAlignment().getHpos()) + x;
|
||||||
double yOffset = computeYOffset(h, maxHeight, checkBox.getAlignment().getVpos()) + x;
|
double yOffset = computeYOffset(h, maxHeight, checkBox.getAlignment().getVpos()) + x;
|
||||||
if (this.invalid) {
|
if (this.invalid) {
|
||||||
if (this.getSkinnable().isSelected()) {
|
boolean selected = checkBox.isSelected();
|
||||||
this.playSelectAnimation(true);
|
boolean indeterminate = checkBox.isIndeterminate();
|
||||||
}
|
this.updateRippleColor(selected, indeterminate);
|
||||||
|
this.playSelectAnimation(selected, indeterminate);
|
||||||
|
|
||||||
this.invalid = false;
|
this.invalid = false;
|
||||||
}
|
}
|
||||||
@@ -169,47 +196,85 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Animates a selection change, or applies it immediately when animations are disabled.
|
private void playSelectAnimation(boolean selected, boolean indeterminate) {
|
||||||
private void playSelectAnimation(boolean selection) {
|
boolean previousSelected = this.selected;
|
||||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
boolean previousIndeterminate = this.indeterminate;
|
||||||
|
this.selected = selected;
|
||||||
|
this.indeterminate = indeterminate;
|
||||||
|
boolean previousMarked = previousSelected || previousIndeterminate;
|
||||||
|
boolean marked = selected || indeterminate;
|
||||||
|
boolean sameVisualState = previousIndeterminate == indeterminate
|
||||||
|
&& (indeterminate || previousSelected == selected);
|
||||||
|
this.updateBorder(selected, indeterminate);
|
||||||
|
|
||||||
this.box.setBorder(new Border(new BorderStroke(
|
if (sameVisualState) {
|
||||||
selection ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
|
return;
|
||||||
BorderStrokeStyle.SOLID,
|
}
|
||||||
new CornerRadii(2.0),
|
this.invalid = false;
|
||||||
new BorderWidths(this.lineThick))));
|
|
||||||
|
|
||||||
if (!AnimationUtils.isAnimationEnabled()) {
|
if (!AnimationUtils.isAnimationEnabled()) {
|
||||||
finishSelectionAnimation();
|
finishSelectionAnimation(selected, indeterminate);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.transition.setRate(selection ? 1.0 : -1.0);
|
if (previousMarked && marked) {
|
||||||
this.select.setRate(selection ? 1.0 : -1.0);
|
this.transition.stop();
|
||||||
|
this.transition.setOnFinished(null);
|
||||||
|
this.transition.jumpTo(this.transition.getCycleDuration());
|
||||||
|
this.transition.setRate(-1.0);
|
||||||
|
this.transition.setOnFinished(event -> {
|
||||||
|
this.transition.setOnFinished(null);
|
||||||
|
this.updateMarkShape(selected, indeterminate);
|
||||||
|
this.transition.jumpTo(Duration.ZERO);
|
||||||
|
this.transition.setRate(1.0);
|
||||||
|
this.transition.play();
|
||||||
|
});
|
||||||
|
this.transition.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transition.setOnFinished(null);
|
||||||
|
if (marked) {
|
||||||
|
this.updateMarkShape(selected, indeterminate);
|
||||||
|
}
|
||||||
|
this.transition.setRate(marked ? 1.0 : -1.0);
|
||||||
|
this.fillTransition.setRate(marked ? 1.0 : -1.0);
|
||||||
|
this.fillTransition.play();
|
||||||
this.transition.play();
|
this.transition.play();
|
||||||
this.select.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops selection animations and immediately displays the checkbox's current selected state.
|
/// Stops selection animations and immediately displays the checkbox's current state.
|
||||||
///
|
///
|
||||||
/// The check mark, fill, and border are updated even if no animation is running.
|
/// The state indicator, fill, and border are updated even if no animation is running.
|
||||||
/// This also suppresses the initial selection animation on the first layout.
|
/// This also suppresses pending selection updates and the initial selection animation.
|
||||||
/// The skin must not have been disposed.
|
/// The skin must not have been disposed.
|
||||||
public void finishSelectionAnimation() {
|
public void finishSelectionAnimation() {
|
||||||
transition.stop();
|
CheckBox checkBox = this.getSkinnable();
|
||||||
select.stop();
|
this.finishSelectionAnimation(checkBox.isSelected(), checkBox.isIndeterminate());
|
||||||
invalid = false;
|
|
||||||
boolean selected = getSkinnable().isSelected();
|
|
||||||
mark.setVisible(selected);
|
|
||||||
mark.setScaleX(selected ? 1.0 : 0.0);
|
|
||||||
mark.setScaleY(selected ? 1.0 : 0.0);
|
|
||||||
updateColors();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createFillTransition() {
|
private void finishSelectionAnimation(boolean selected, boolean indeterminate) {
|
||||||
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
|
this.selected = selected;
|
||||||
|
this.indeterminate = indeterminate;
|
||||||
|
boolean marked = selected || indeterminate;
|
||||||
|
transition.stop();
|
||||||
|
transition.setOnFinished(null);
|
||||||
|
transition.jumpTo(marked ? transition.getCycleDuration() : Duration.ZERO);
|
||||||
|
fillTransition.stop();
|
||||||
|
fillTransition.jumpTo(marked ? fillTransition.getCycleDuration() : Duration.ZERO);
|
||||||
|
invalid = false;
|
||||||
|
updateMarkShape(selected, indeterminate);
|
||||||
|
mark.setVisible(marked);
|
||||||
|
mark.setScaleX(marked ? 1.0 : 0.0);
|
||||||
|
mark.setScaleY(marked ? 1.0 : 0.0);
|
||||||
|
updateColors(selected, indeterminate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JFXFillTransition createFillTransition() {
|
||||||
|
JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
|
||||||
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
|
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
|
||||||
this.select.setInterpolator(Interpolator.EASE_OUT);
|
fillTransition.setInterpolator(Interpolator.EASE_OUT);
|
||||||
|
return fillTransition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class CheckBoxTransition extends CachedTransition {
|
private final class CheckBoxTransition extends CachedTransition {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public class JFXDatePickerContent extends VBox {
|
|||||||
setText(null);
|
setText(null);
|
||||||
|
|
||||||
if (Objects.equals(oldItem, item) && oldEmpty == empty) return;
|
if (Objects.equals(oldItem, item) && oldEmpty == empty) return;
|
||||||
ripplerContainer.releaseRippleImmediately();
|
ripplerContainer.resetFeedback();
|
||||||
|
|
||||||
if (!empty && item != null) {
|
if (!empty && item != null) {
|
||||||
setGraphic(root);
|
setGraphic(root);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
|||||||
|
|
||||||
arc = new Arc();
|
arc = new Arc();
|
||||||
arc.setManaged(false);
|
arc.setManaged(false);
|
||||||
arc.setLength(180);
|
arc.setLength(-180);
|
||||||
arc.getStyleClass().setAll("arc");
|
arc.getStyleClass().setAll("arc");
|
||||||
arc.setFill(Color.TRANSPARENT);
|
arc.setFill(Color.TRANSPARENT);
|
||||||
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||||
@@ -192,24 +192,24 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
|||||||
|
|
||||||
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
|
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration),
|
frames.add(new KeyFrame(Duration.seconds(duration),
|
||||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
|
||||||
new KeyValue(arc.startAngleProperty(),
|
new KeyValue(arc.startAngleProperty(),
|
||||||
angle + 45 + startingAngle,
|
-(angle + 45 + startingAngle),
|
||||||
Interpolator.LINEAR)));
|
Interpolator.LINEAR)));
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
|
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
|
||||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
new KeyValue(arc.lengthProperty(), -250, Interpolator.LINEAR),
|
||||||
new KeyValue(arc.startAngleProperty(),
|
new KeyValue(arc.startAngleProperty(),
|
||||||
angle + 90 + startingAngle,
|
-(angle + 90 + startingAngle),
|
||||||
Interpolator.LINEAR)));
|
Interpolator.LINEAR)));
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
|
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
|
||||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
new KeyValue(arc.lengthProperty(), -250, Interpolator.LINEAR),
|
||||||
new KeyValue(arc.startAngleProperty(),
|
new KeyValue(arc.startAngleProperty(),
|
||||||
angle + 135 + startingAngle,
|
-(angle + 135 + startingAngle),
|
||||||
Interpolator.LINEAR)));
|
Interpolator.LINEAR)));
|
||||||
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
|
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
|
||||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
|
||||||
new KeyValue(arc.startAngleProperty(),
|
new KeyValue(arc.startAngleProperty(),
|
||||||
angle + 435 + startingAngle,
|
-(angle + 435 + startingAngle),
|
||||||
Interpolator.LINEAR)));
|
Interpolator.LINEAR)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,21 +222,21 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
|||||||
addKeyFrames(keyFrames, 900, 2.8);
|
addKeyFrames(keyFrames, 900, 2.8);
|
||||||
addKeyFrames(keyFrames, 1350, 4.2);
|
addKeyFrames(keyFrames, 1350, 4.2);
|
||||||
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
|
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
|
||||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
|
||||||
new KeyValue(arc.startAngleProperty(),
|
new KeyValue(arc.startAngleProperty(),
|
||||||
1845 + startingAngle,
|
-(1845 + startingAngle),
|
||||||
Interpolator.LINEAR)));
|
Interpolator.LINEAR)));
|
||||||
|
|
||||||
timeline = new Timeline();
|
timeline = new Timeline();
|
||||||
timeline.getKeyFrames().setAll(keyFrames);
|
timeline.getKeyFrames().setAll(keyFrames);
|
||||||
} else {
|
} else {
|
||||||
final double arcLength = 250;
|
final double arcLength = -250;
|
||||||
timeline = new Timeline(
|
timeline = new Timeline(
|
||||||
new KeyFrame(Duration.ZERO,
|
new KeyFrame(Duration.ZERO,
|
||||||
new KeyValue(arc.startAngleProperty(), 45 + startingAngle, Interpolator.LINEAR),
|
new KeyValue(arc.startAngleProperty(), -(45 + startingAngle), Interpolator.LINEAR),
|
||||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
|
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
|
||||||
new KeyFrame(Duration.seconds(1.2),
|
new KeyFrame(Duration.seconds(1.2),
|
||||||
new KeyValue(arc.startAngleProperty(), 45 + 360 + startingAngle, Interpolator.LINEAR),
|
new KeyValue(arc.startAngleProperty(), -(45 + 360 + startingAngle), Interpolator.LINEAR),
|
||||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
|
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,587 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
|
package com.jfoenix.skins;
|
||||||
|
|
||||||
|
import com.jfoenix.controls.JFXSwitch;
|
||||||
|
import javafx.animation.KeyFrame;
|
||||||
|
import javafx.animation.KeyValue;
|
||||||
|
import javafx.animation.Timeline;
|
||||||
|
import javafx.animation.Transition;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.css.PseudoClass;
|
||||||
|
import javafx.css.StyleOrigin;
|
||||||
|
import javafx.css.StyleableProperty;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Point2D;
|
||||||
|
import javafx.scene.control.SkinBase;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.paint.Paint;
|
||||||
|
import javafx.scene.shape.Circle;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import javafx.scene.shape.SVGPath;
|
||||||
|
import javafx.scene.transform.Scale;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||||
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/// Renders a Material Design 3 switch and handles input.
|
||||||
|
/// Geometry uses unscaled MD3 units and is mirrored in RTL layouts.
|
||||||
|
@NotNullByDefault
|
||||||
|
public final class JFXSwitchSkin extends SkinBase<JFXSwitch> {
|
||||||
|
/// Track width in unscaled MD3 artwork coordinates.
|
||||||
|
private static final double TRACK_WIDTH = 52;
|
||||||
|
|
||||||
|
/// Track height corresponding to the control's default `-jfx-size`.
|
||||||
|
private static final double TRACK_HEIGHT = 32;
|
||||||
|
|
||||||
|
/// Unscaled interaction target height.
|
||||||
|
private static final double TARGET_HEIGHT = 48;
|
||||||
|
|
||||||
|
/// Vertical center shared by the track, handle, state layer, and icon.
|
||||||
|
private static final double CENTER_Y = TARGET_HEIGHT / 2;
|
||||||
|
|
||||||
|
/// Handle center at the leading end of the track.
|
||||||
|
private static final double OFF_CENTER = TRACK_HEIGHT / 2;
|
||||||
|
|
||||||
|
/// Handle center at the trailing end of the track.
|
||||||
|
private static final double ON_CENTER = TRACK_WIDTH - OFF_CENTER;
|
||||||
|
|
||||||
|
/// Diameter of the unselected handle without an icon.
|
||||||
|
private static final double OFF_DIAMETER = 16;
|
||||||
|
|
||||||
|
/// Diameter of a selected handle or a handle containing an icon.
|
||||||
|
private static final double ON_DIAMETER = 24;
|
||||||
|
|
||||||
|
/// Handle diameter while pressed or dragged.
|
||||||
|
private static final double PRESSED_DIAMETER = 28;
|
||||||
|
|
||||||
|
/// Radius of the hover, focus, and pressed overlay.
|
||||||
|
private static final double STATE_LAYER_RADIUS = 20;
|
||||||
|
|
||||||
|
/// Minimum pointer movement in control pixels before a press becomes a drag.
|
||||||
|
private static final double DRAG_THRESHOLD = 3;
|
||||||
|
|
||||||
|
/// Indicates visible keyboard focus.
|
||||||
|
private static final PseudoClass VISUAL_FOCUSED = PseudoClass.getPseudoClass("visual-focused");
|
||||||
|
|
||||||
|
/// The rendered switch.
|
||||||
|
private final JFXSwitch control;
|
||||||
|
|
||||||
|
/// Unmanaged switch artwork.
|
||||||
|
private final Pane artwork = new Pane();
|
||||||
|
|
||||||
|
/// Scales artwork from its leading top corner.
|
||||||
|
private final Scale artworkScale = new Scale();
|
||||||
|
|
||||||
|
/// Rounded track with its optional outline.
|
||||||
|
private final Rectangle track = new Rectangle(0, (TARGET_HEIGHT - TRACK_HEIGHT) / 2, TRACK_WIDTH, TRACK_HEIGHT);
|
||||||
|
|
||||||
|
/// Interaction feedback behind the handle.
|
||||||
|
private final Circle stateLayer = new Circle(OFF_CENTER, CENTER_Y, STATE_LAYER_RADIUS);
|
||||||
|
|
||||||
|
/// Switch handle.
|
||||||
|
private final Circle handle = new Circle(OFF_CENTER, CENTER_Y, OFF_DIAMETER / 2);
|
||||||
|
|
||||||
|
/// Optional handle icon.
|
||||||
|
private @Nullable SVGPath icon;
|
||||||
|
|
||||||
|
/// Current logical handle center measured from the track's leading edge.
|
||||||
|
private final DoubleProperty center = new SimpleDoubleProperty(OFF_CENTER) {
|
||||||
|
/// Updates handle position.
|
||||||
|
@Override
|
||||||
|
protected void invalidated() {
|
||||||
|
layoutHandle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Unscaled handle diameter.
|
||||||
|
private final DoubleProperty diameter = new SimpleDoubleProperty(OFF_DIAMETER) {
|
||||||
|
/// Updates handle size.
|
||||||
|
@Override
|
||||||
|
protected void invalidated() {
|
||||||
|
layoutHandle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Optional movement and resize animation.
|
||||||
|
private @Nullable Timeline geometryTransition;
|
||||||
|
|
||||||
|
/// Animated CSS paints.
|
||||||
|
private final List<CssColor> colors = new ArrayList<>();
|
||||||
|
|
||||||
|
/// Optional shared color animation.
|
||||||
|
private @Nullable Transition colorTransition;
|
||||||
|
|
||||||
|
/// Whether a selection color update is pending.
|
||||||
|
private boolean animateColors;
|
||||||
|
|
||||||
|
/// Prevents animation writes from being interpreted as new CSS targets.
|
||||||
|
private boolean applyingColors;
|
||||||
|
|
||||||
|
/// Pointer event handler.
|
||||||
|
private final EventHandler<MouseEvent> mouseHandler = this::handleMouse;
|
||||||
|
|
||||||
|
/// Keyboard event handler.
|
||||||
|
private final EventHandler<KeyEvent> keyHandler = this::handleKey;
|
||||||
|
|
||||||
|
/// Whether a primary press is awaiting click or drag completion.
|
||||||
|
private boolean mouseDown;
|
||||||
|
|
||||||
|
/// Whether the handle is following the pointer instead of an animation.
|
||||||
|
private boolean dragging;
|
||||||
|
|
||||||
|
/// Whether a Space press is awaiting release.
|
||||||
|
private boolean keyDown;
|
||||||
|
|
||||||
|
/// Pointer position in unscaled artwork coordinates at press time.
|
||||||
|
private double pressX;
|
||||||
|
|
||||||
|
/// Handle center at press time, preserving the pointer's grab offset.
|
||||||
|
private double pressCenter;
|
||||||
|
|
||||||
|
/// Creates a skin for the switch.
|
||||||
|
public JFXSwitchSkin(JFXSwitch control) {
|
||||||
|
super(control);
|
||||||
|
this.control = control;
|
||||||
|
artwork.getStyleClass().setAll("switch-artwork");
|
||||||
|
track.getStyleClass().setAll("switch-track");
|
||||||
|
stateLayer.getStyleClass().setAll("switch-state-layer");
|
||||||
|
handle.getStyleClass().setAll("switch-handle");
|
||||||
|
track.setArcWidth(TRACK_HEIGHT);
|
||||||
|
track.setArcHeight(TRACK_HEIGHT);
|
||||||
|
artwork.setManaged(false);
|
||||||
|
artwork.setMouseTransparent(true);
|
||||||
|
artwork.getTransforms().add(artworkScale);
|
||||||
|
artwork.getChildren().setAll(track, stateLayer, handle);
|
||||||
|
for (var node : artwork.getChildren())
|
||||||
|
node.setManaged(false);
|
||||||
|
getChildren().add(artwork);
|
||||||
|
registerChangeListener(control.selectedProperty(), value -> updateSelection());
|
||||||
|
registerChangeListener(control.armedProperty(), value -> update(false));
|
||||||
|
@Nullable ReadOnlyBooleanProperty focusVisible = FXUtils.focusVisibleProperty(control);
|
||||||
|
ReadOnlyBooleanProperty focusIndicator = focusVisible != null ? focusVisible : control.focusedProperty();
|
||||||
|
registerChangeListener(focusIndicator, value ->
|
||||||
|
control.pseudoClassStateChanged(VISUAL_FOCUSED, focusIndicator.get() && !control.isPressed()));
|
||||||
|
// Match the previous toggle skin: pointer press and release both dismiss focus feedback.
|
||||||
|
registerChangeListener(control.pressedProperty(), value ->
|
||||||
|
control.pseudoClassStateChanged(VISUAL_FOCUSED, false));
|
||||||
|
Consumer<ObservableValue<?>> cancelGesture = value -> cancelGesture();
|
||||||
|
registerChangeListener(control.disabledProperty(), cancelGesture);
|
||||||
|
registerChangeListener(control.sceneProperty(), cancelGesture);
|
||||||
|
registerChangeListener(control.focusedProperty(), value -> {
|
||||||
|
if (!control.isFocused())
|
||||||
|
cancelGesture();
|
||||||
|
});
|
||||||
|
control.addEventFilter(MouseEvent.ANY, mouseHandler);
|
||||||
|
control.addEventFilter(KeyEvent.ANY, keyHandler);
|
||||||
|
control.pseudoClassStateChanged(VISUAL_FOCUSED, focusIndicator.get() && !control.isPressed());
|
||||||
|
update(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates icon and animation options, optionally settling active animations.
|
||||||
|
public void updateOptions(boolean finishAnimation) {
|
||||||
|
if (finishAnimation)
|
||||||
|
finishColors();
|
||||||
|
update(finishAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops animations and removes artwork, event handlers, and listeners.
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
control.removeEventFilter(MouseEvent.ANY, mouseHandler);
|
||||||
|
control.removeEventFilter(KeyEvent.ANY, keyHandler);
|
||||||
|
cancelGesture();
|
||||||
|
for (CssColor color : colors)
|
||||||
|
color.dispose();
|
||||||
|
control.pseudoClassStateChanged(VISUAL_FOCUSED, false);
|
||||||
|
getChildren().remove(artwork);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Centers the scaled artwork within the control and starts pending color transitions.
|
||||||
|
@Override
|
||||||
|
protected void layoutChildren(double x, double y, double w, double h) {
|
||||||
|
double scale = sizeScale();
|
||||||
|
artworkScale.setX(scale);
|
||||||
|
artworkScale.setY(scale);
|
||||||
|
// Keep animation and CSS geometry in MD3 units; scale the entire artwork consistently.
|
||||||
|
artwork.resizeRelocate(x + (w - TRACK_WIDTH * scale) / 2,
|
||||||
|
y + (h - TARGET_HEIGHT * scale) / 2, TRACK_WIDTH, TARGET_HEIGHT);
|
||||||
|
layoutHandle();
|
||||||
|
// JavaFX applies CSS before layout, so all changed paints now share the same start time.
|
||||||
|
if (animateColors) {
|
||||||
|
animateColors = false;
|
||||||
|
for (CssColor color : colors) {
|
||||||
|
if (color.target != null) {
|
||||||
|
colorTransition().playFromStart();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserves the scaled track width and control insets.
|
||||||
|
@Override
|
||||||
|
protected double computeMinWidth(double h, double top, double right, double bottom, double left) {
|
||||||
|
return left + TRACK_WIDTH * sizeScale() + right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserves the scaled interaction target height and control insets.
|
||||||
|
@Override
|
||||||
|
protected double computeMinHeight(double w, double top, double right, double bottom, double left) {
|
||||||
|
return top + TARGET_HEIGHT * sizeScale() + bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses the same fixed width as the minimum size.
|
||||||
|
@Override
|
||||||
|
protected double computePrefWidth(double h, double top, double right, double bottom, double left) {
|
||||||
|
return computeMinWidth(h, top, right, bottom, left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses the same fixed height as the minimum size.
|
||||||
|
@Override
|
||||||
|
protected double computePrefHeight(double w, double top, double right, double bottom, double left) {
|
||||||
|
return computeMinHeight(w, top, right, bottom, left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limits width to the preferred size.
|
||||||
|
@Override
|
||||||
|
protected double computeMaxWidth(double h, double top, double right, double bottom, double left) {
|
||||||
|
return computePrefWidth(h, top, right, bottom, left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Limits height to the preferred size.
|
||||||
|
@Override
|
||||||
|
protected double computeMaxHeight(double w, double top, double right, double bottom, double left) {
|
||||||
|
return computePrefHeight(w, top, right, bottom, left);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles clicking and dragging.
|
||||||
|
private void handleMouse(MouseEvent event) {
|
||||||
|
if (event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY) {
|
||||||
|
// Activation happens on release; do not also activate the surrounding settings row.
|
||||||
|
event.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
|
||||||
|
if (event.getButton() != MouseButton.PRIMARY || keyDown
|
||||||
|
|| event.isShiftDown() || event.isControlDown() || event.isAltDown() || event.isMetaDown()
|
||||||
|
|| event.isMiddleButtonDown() || event.isSecondaryButtonDown())
|
||||||
|
return;
|
||||||
|
if (control.isDisabled() || control.selectedProperty().isBound())
|
||||||
|
return;
|
||||||
|
if (control.isFocusTraversable())
|
||||||
|
control.requestFocus();
|
||||||
|
@Nullable Point2D point = artwork.sceneToLocal(event.getSceneX(), event.getSceneY());
|
||||||
|
if (point != null) {
|
||||||
|
mouseDown = true;
|
||||||
|
pressX = point.getX();
|
||||||
|
pressCenter = center.get();
|
||||||
|
control.arm();
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
} else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED && mouseDown && event.isPrimaryButtonDown()) {
|
||||||
|
@Nullable Point2D point = artwork.sceneToLocal(event.getSceneX(), event.getSceneY());
|
||||||
|
if (point == null)
|
||||||
|
return;
|
||||||
|
if (!dragging && Math.abs(point.getX() - pressX) * sizeScale() < DRAG_THRESHOLD)
|
||||||
|
return;
|
||||||
|
dragging = true;
|
||||||
|
if (geometryTransition != null)
|
||||||
|
geometryTransition.stop();
|
||||||
|
control.arm();
|
||||||
|
diameter.set(PRESSED_DIAMETER);
|
||||||
|
center.set(dragCenter(point.getX()));
|
||||||
|
event.consume();
|
||||||
|
} else if (event.getEventType() == MouseEvent.MOUSE_RELEASED && event.getButton() == MouseButton.PRIMARY) {
|
||||||
|
if (!mouseDown) {
|
||||||
|
event.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
@Nullable Point2D point = dragging
|
||||||
|
? artwork.sceneToLocal(event.getSceneX(), event.getSceneY())
|
||||||
|
: control.sceneToLocal(event.getSceneX(), event.getSceneY());
|
||||||
|
if (dragging && point != null)
|
||||||
|
center.set(dragCenter(point.getX()));
|
||||||
|
boolean activate = point != null && (dragging
|
||||||
|
? (center.get() >= (OFF_CENTER + ON_CENTER) / 2) != control.isSelected()
|
||||||
|
: control.isArmed() && control.contains(point));
|
||||||
|
mouseDown = false;
|
||||||
|
dragging = false;
|
||||||
|
control.disarm();
|
||||||
|
event.consume();
|
||||||
|
if (activate)
|
||||||
|
control.fire();
|
||||||
|
// Action handlers may replace or remove the skin synchronously.
|
||||||
|
if (control.getSkin() == this)
|
||||||
|
update(false);
|
||||||
|
} else if (mouseDown && !dragging) {
|
||||||
|
if (event.getEventType() == MouseEvent.MOUSE_ENTERED)
|
||||||
|
control.arm();
|
||||||
|
else if (event.getEventType() == MouseEvent.MOUSE_EXITED)
|
||||||
|
control.disarm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamps pointer displacement to the track while preserving the initial grab offset.
|
||||||
|
private double dragCenter(double pointerX) {
|
||||||
|
return Math.max(OFF_CENTER, Math.min(ON_CENTER, pressCenter + pointerX - pressX));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles Space activation and Escape cancellation.
|
||||||
|
private void handleKey(KeyEvent event) {
|
||||||
|
if (event.getCode() == KeyCode.ESCAPE && event.getEventType() == KeyEvent.KEY_PRESSED) {
|
||||||
|
if (mouseDown || keyDown) {
|
||||||
|
boolean awaitingRelease = keyDown;
|
||||||
|
cancelGesture();
|
||||||
|
// Ignore auto-repeat until the canceled Space press is released.
|
||||||
|
keyDown = awaitingRelease;
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
} else if (event.getCode() == KeyCode.SPACE) {
|
||||||
|
if (event.getEventType() == KeyEvent.KEY_PRESSED) {
|
||||||
|
if (event.isShiftDown() || event.isControlDown() || event.isAltDown() || event.isMetaDown())
|
||||||
|
return;
|
||||||
|
if (!keyDown) {
|
||||||
|
if (mouseDown || control.isDisabled() || control.selectedProperty().isBound())
|
||||||
|
return;
|
||||||
|
keyDown = true;
|
||||||
|
control.arm();
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
} else if (event.getEventType() == KeyEvent.KEY_RELEASED && keyDown) {
|
||||||
|
keyDown = false;
|
||||||
|
boolean activate = control.isArmed();
|
||||||
|
control.disarm();
|
||||||
|
event.consume();
|
||||||
|
if (activate)
|
||||||
|
control.fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancels input without changing selection.
|
||||||
|
private void cancelGesture() {
|
||||||
|
finishColors();
|
||||||
|
mouseDown = false;
|
||||||
|
keyDown = false;
|
||||||
|
dragging = false;
|
||||||
|
control.disarm();
|
||||||
|
update(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates geometry and schedules selection colors for the next CSS pass.
|
||||||
|
private void updateSelection() {
|
||||||
|
update(false);
|
||||||
|
if (control.isDisableAnimation() || control.isDisabled() || control.getScene() == null)
|
||||||
|
return;
|
||||||
|
if (colors.isEmpty()) {
|
||||||
|
colors.add(new CssColor(track.fillProperty()));
|
||||||
|
colors.add(new CssColor(handle.fillProperty()));
|
||||||
|
colors.add(new CssColor(stateLayer.fillProperty()));
|
||||||
|
colors.add(new CssColor(track.strokeProperty()));
|
||||||
|
if (icon != null)
|
||||||
|
colors.add(new CssColor(icon.fillProperty()));
|
||||||
|
}
|
||||||
|
if (colorTransition != null)
|
||||||
|
colorTransition.stop();
|
||||||
|
// A reversal before the first frame may reapply the visible paint without emitting a change event.
|
||||||
|
for (CssColor color : colors)
|
||||||
|
color.target = null;
|
||||||
|
animateColors = true;
|
||||||
|
control.requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates handle geometry, with optional animation.
|
||||||
|
private void update(boolean immediate) {
|
||||||
|
if (dragging)
|
||||||
|
return;
|
||||||
|
double currentCenter = center.get();
|
||||||
|
double currentDiameter = diameter.get();
|
||||||
|
if (geometryTransition != null)
|
||||||
|
geometryTransition.stop();
|
||||||
|
boolean selected = control.isSelected();
|
||||||
|
boolean pressed = control.isArmed() && !control.isDisabled();
|
||||||
|
boolean hasIcon = selected ? control.isShowSelectedIcon() : control.isShowUnselectedIcon();
|
||||||
|
double targetDiameter = pressed ? PRESSED_DIAMETER : selected || hasIcon ? ON_DIAMETER : OFF_DIAMETER;
|
||||||
|
double targetCenter = selected ? ON_CENTER : OFF_CENTER;
|
||||||
|
updateIcon(selected, hasIcon);
|
||||||
|
if (immediate || control.isDisableAnimation() || control.getScene() == null
|
||||||
|
|| currentCenter == targetCenter && currentDiameter == targetDiameter) {
|
||||||
|
center.set(targetCenter);
|
||||||
|
diameter.set(targetDiameter);
|
||||||
|
} else {
|
||||||
|
if (geometryTransition == null)
|
||||||
|
geometryTransition = new Timeline();
|
||||||
|
// Preserve the visible geometry when a press or reversal interrupts a transition.
|
||||||
|
center.set(currentCenter);
|
||||||
|
diameter.set(currentDiameter);
|
||||||
|
geometryTransition.getKeyFrames().setAll(
|
||||||
|
new KeyFrame(Duration.ZERO,
|
||||||
|
new KeyValue(center, currentCenter),
|
||||||
|
new KeyValue(diameter, currentDiameter)),
|
||||||
|
new KeyFrame(pressed ? Motion.SHORT2 : Motion.MEDIUM1,
|
||||||
|
new KeyValue(diameter, targetDiameter, pressed ? Motion.LINEAR : Motion.STANDARD)),
|
||||||
|
new KeyFrame(Motion.MEDIUM2,
|
||||||
|
new KeyValue(center, targetCenter, Motion.EASE_OUT_BACK)));
|
||||||
|
geometryTransition.play();
|
||||||
|
}
|
||||||
|
layoutHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the handle icon, creating it if needed.
|
||||||
|
private void updateIcon(boolean selected, boolean visible) {
|
||||||
|
if (icon == null && visible) {
|
||||||
|
icon = new SVGPath();
|
||||||
|
icon.getStyleClass().setAll("switch-icon");
|
||||||
|
icon.setManaged(false);
|
||||||
|
// The first CSS fill should appear immediately, without a transition from default black.
|
||||||
|
icon.setFill(null);
|
||||||
|
if (!colors.isEmpty())
|
||||||
|
colors.add(new CssColor(icon.fillProperty()));
|
||||||
|
artwork.getChildren().add(icon);
|
||||||
|
}
|
||||||
|
if (icon != null) {
|
||||||
|
icon.setVisible(visible);
|
||||||
|
if (visible)
|
||||||
|
icon.setContent(selected
|
||||||
|
? "M2 8l4 4 8-8-1.4-1.4L6 9.2 3.4 6.6Z"
|
||||||
|
: "M4 2.6l4 4 4-4L13.4 4l-4 4 4 4-1.4 1.4-4-4-4 4L2.6 12l4-4-4-4Z");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Positions and sizes the handle and its overlays.
|
||||||
|
private void layoutHandle() {
|
||||||
|
double cx = center.get();
|
||||||
|
stateLayer.setCenterX(cx);
|
||||||
|
handle.setCenterX(cx);
|
||||||
|
handle.setRadius(diameter.get() / 2);
|
||||||
|
if (icon != null && icon.isVisible())
|
||||||
|
icon.relocate(cx - icon.getLayoutBounds().getWidth() / 2,
|
||||||
|
CENTER_Y - icon.getLayoutBounds().getHeight() / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the size scale, or 1 for an invalid size.
|
||||||
|
private double sizeScale() {
|
||||||
|
double size = control.getSize();
|
||||||
|
return Double.isFinite(size) && size > 0 ? size / TRACK_HEIGHT : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the shared color animation, creating it if needed.
|
||||||
|
private Transition colorTransition() {
|
||||||
|
if (colorTransition == null) {
|
||||||
|
colorTransition = new Transition() {
|
||||||
|
{
|
||||||
|
setCycleDuration(Motion.SHORT4);
|
||||||
|
setInterpolator(Motion.STANDARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates only paints with a pending CSS target.
|
||||||
|
@Override
|
||||||
|
protected void interpolate(double fraction) {
|
||||||
|
for (CssColor color : colors)
|
||||||
|
color.interpolate(fraction);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
colorTransition.setOnFinished(event -> finishColors());
|
||||||
|
}
|
||||||
|
return colorTransition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops color animation and applies pending targets.
|
||||||
|
private void finishColors() {
|
||||||
|
animateColors = false;
|
||||||
|
if (colorTransition != null)
|
||||||
|
colorTransition.stop();
|
||||||
|
for (CssColor color : colors)
|
||||||
|
color.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animates a solid paint while preserving CSS precedence.
|
||||||
|
private final class CssColor implements ChangeListener<@Nullable Paint> {
|
||||||
|
|
||||||
|
/// The animated paint property.
|
||||||
|
private final ObjectProperty<@Nullable Paint> property;
|
||||||
|
|
||||||
|
/// Most recent solid CSS target; null means no transition is pending.
|
||||||
|
private @Nullable Color target;
|
||||||
|
|
||||||
|
/// CSS origin of the target paint.
|
||||||
|
private @Nullable StyleOrigin origin;
|
||||||
|
|
||||||
|
/// Starting color.
|
||||||
|
private Color from = Color.TRANSPARENT;
|
||||||
|
|
||||||
|
/// Observes a styleable paint property.
|
||||||
|
private CssColor(ObjectProperty<@Nullable Paint> property) {
|
||||||
|
this.property = property;
|
||||||
|
property.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animates selection colors and applies other paint changes immediately.
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends @Nullable Paint> observable,
|
||||||
|
@Nullable Paint previous, @Nullable Paint next) {
|
||||||
|
if (applyingColors)
|
||||||
|
return;
|
||||||
|
// CSS may reapply the same target while the displayed value is still interpolating.
|
||||||
|
if (target != null && target.equals(next) && previous != null) {
|
||||||
|
apply(previous);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target = null;
|
||||||
|
if (!animateColors || !(previous instanceof Color start) || !(next instanceof Color end) || start.equals(end))
|
||||||
|
return;
|
||||||
|
from = start;
|
||||||
|
target = end;
|
||||||
|
origin = ((StyleableProperty<?>) property).getStyleOrigin();
|
||||||
|
interpolate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies an animation frame.
|
||||||
|
private void interpolate(double fraction) {
|
||||||
|
if (target != null)
|
||||||
|
apply(fraction == 1 ? target : from.interpolate(target, fraction));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a CSS value while suppressing this transition's own listener.
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void apply(Paint value) {
|
||||||
|
applyingColors = true;
|
||||||
|
try {
|
||||||
|
((StyleableProperty<Paint>) property).applyStyle(origin, value);
|
||||||
|
} finally {
|
||||||
|
applyingColors = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies and clears the pending target.
|
||||||
|
private void finish() {
|
||||||
|
if (target != null) {
|
||||||
|
apply(target);
|
||||||
|
target = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the paint listener.
|
||||||
|
private void dispose() {
|
||||||
|
property.removeListener(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.jfoenix.skins;
|
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXRippler;
|
|
||||||
import com.jfoenix.controls.JFXRippler.RipplerMask;
|
|
||||||
import com.jfoenix.controls.JFXRippler.RipplerPos;
|
|
||||||
import com.jfoenix.controls.JFXToggleButton;
|
|
||||||
import com.jfoenix.effects.JFXDepthManager;
|
|
||||||
import com.jfoenix.transitions.JFXAnimationTimer;
|
|
||||||
import com.jfoenix.transitions.JFXKeyFrame;
|
|
||||||
import com.jfoenix.transitions.JFXKeyValue;
|
|
||||||
import javafx.animation.Interpolator;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Cursor;
|
|
||||||
import javafx.scene.control.skin.ToggleButtonSkin;
|
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.shape.Circle;
|
|
||||||
import javafx.scene.shape.Line;
|
|
||||||
import javafx.scene.shape.StrokeLineCap;
|
|
||||||
import javafx.util.Duration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <h1>Material Design ToggleButton Skin</h1>
|
|
||||||
*
|
|
||||||
* @author Shadi Shaheen
|
|
||||||
* @version 1.0
|
|
||||||
* @since 2016-03-09
|
|
||||||
*/
|
|
||||||
public class JFXToggleButtonSkin extends ToggleButtonSkin {
|
|
||||||
|
|
||||||
|
|
||||||
private Runnable releaseManualRippler = null;
|
|
||||||
|
|
||||||
private JFXAnimationTimer timer;
|
|
||||||
private final Circle circle;
|
|
||||||
private final Line line;
|
|
||||||
|
|
||||||
public JFXToggleButtonSkin(JFXToggleButton toggleButton) {
|
|
||||||
super(toggleButton);
|
|
||||||
|
|
||||||
double circleRadius = toggleButton.getSize();
|
|
||||||
|
|
||||||
line = new Line();
|
|
||||||
line.setStroke(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
|
|
||||||
line.setStartX(0);
|
|
||||||
line.setStartY(0);
|
|
||||||
line.setEndX(circleRadius * 2 + 2);
|
|
||||||
line.setEndY(0);
|
|
||||||
line.setStrokeWidth(circleRadius * 1.5);
|
|
||||||
line.setStrokeLineCap(StrokeLineCap.ROUND);
|
|
||||||
line.setSmooth(true);
|
|
||||||
|
|
||||||
circle = new Circle();
|
|
||||||
circle.setFill(getSkinnable().isSelected() ? toggleButton.getToggleColor() : toggleButton.getUnToggleColor());
|
|
||||||
circle.setCenterX(-circleRadius);
|
|
||||||
circle.setCenterY(0);
|
|
||||||
circle.setRadius(circleRadius);
|
|
||||||
circle.setSmooth(true);
|
|
||||||
JFXDepthManager.setDepth(circle, 1);
|
|
||||||
|
|
||||||
StackPane circlePane = new StackPane();
|
|
||||||
circlePane.getChildren().add(circle);
|
|
||||||
circlePane.setPadding(new Insets(circleRadius * 1.5));
|
|
||||||
|
|
||||||
JFXRippler rippler = new JFXRippler(circlePane, RipplerMask.CIRCLE, RipplerPos.BACK);
|
|
||||||
rippler.setRipplerFill(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
|
|
||||||
rippler.setTranslateX(computeTranslation(circleRadius, line));
|
|
||||||
|
|
||||||
final StackPane main = new StackPane();
|
|
||||||
main.getChildren().setAll(line, rippler);
|
|
||||||
main.setCursor(Cursor.HAND);
|
|
||||||
|
|
||||||
// show focus traversal effect
|
|
||||||
getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (newVal) {
|
|
||||||
releaseManualRippler = rippler.createManualRipple();
|
|
||||||
} else if (releaseManualRippler != null) {
|
|
||||||
releaseManualRippler.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
toggleButton.focusedProperty().addListener((o, oldVal, newVal) -> {
|
|
||||||
if (!toggleButton.isDisableVisualFocus()) {
|
|
||||||
if (newVal) {
|
|
||||||
if (!getSkinnable().isPressed()) {
|
|
||||||
rippler.setOverlayVisible(true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rippler.setOverlayVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
toggleButton.pressedProperty().addListener(observable -> rippler.setOverlayVisible(false));
|
|
||||||
|
|
||||||
// add change listener to selected property
|
|
||||||
getSkinnable().selectedProperty().addListener(observable -> {
|
|
||||||
rippler.setRipplerFill(toggleButton.isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
|
|
||||||
if (!toggleButton.isDisableAnimation()) {
|
|
||||||
timer.reverseAndContinue();
|
|
||||||
} else {
|
|
||||||
rippler.setTranslateX(computeTranslation(circleRadius, line));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
getSkinnable().setGraphic(main);
|
|
||||||
|
|
||||||
timer = new JFXAnimationTimer(
|
|
||||||
new JFXKeyFrame(Duration.millis(100),
|
|
||||||
JFXKeyValue.builder()
|
|
||||||
.setTarget(rippler.translateXProperty())
|
|
||||||
.setEndValueSupplier(() -> computeTranslation(circleRadius, line))
|
|
||||||
.setInterpolator(Interpolator.EASE_BOTH)
|
|
||||||
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
|
|
||||||
.build(),
|
|
||||||
|
|
||||||
JFXKeyValue.builder()
|
|
||||||
.setTarget(line.strokeProperty())
|
|
||||||
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
|
|
||||||
((JFXToggleButton) getSkinnable()).getToggleLineColor()
|
|
||||||
: ((JFXToggleButton) getSkinnable()).getUnToggleLineColor())
|
|
||||||
.setInterpolator(Interpolator.EASE_BOTH)
|
|
||||||
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
|
|
||||||
.build(),
|
|
||||||
|
|
||||||
JFXKeyValue.builder()
|
|
||||||
.setTarget(circle.fillProperty())
|
|
||||||
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
|
|
||||||
((JFXToggleButton) getSkinnable()).getToggleColor()
|
|
||||||
: ((JFXToggleButton) getSkinnable()).getUnToggleColor())
|
|
||||||
.setInterpolator(Interpolator.EASE_BOTH)
|
|
||||||
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
timer.setCacheNodes(circle, line);
|
|
||||||
|
|
||||||
registerChangeListener(toggleButton.toggleColorProperty(), observableValue -> {
|
|
||||||
if (getSkinnable().isSelected()) {
|
|
||||||
circle.setFill(((JFXToggleButton) getSkinnable()).getToggleColor());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
|
|
||||||
if (!getSkinnable().isSelected()) {
|
|
||||||
circle.setFill(((JFXToggleButton) getSkinnable()).getUnToggleColor());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerChangeListener(toggleButton.toggleLineColorProperty(), observableValue -> {
|
|
||||||
if (getSkinnable().isSelected()) {
|
|
||||||
line.setStroke(((JFXToggleButton) getSkinnable()).getToggleLineColor());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
|
|
||||||
if (!getSkinnable().isSelected()) {
|
|
||||||
line.setStroke(((JFXToggleButton) getSkinnable()).getUnToggleLineColor());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private double computeTranslation(double circleRadius, Line line) {
|
|
||||||
return (getSkinnable().isSelected() ? 1 : -1) * ((line.getLayoutBounds().getWidth() / 2) - circleRadius + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
timer.dispose();
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.FileSaver;
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
@@ -23,6 +8,7 @@ import org.jackhuang.hmcl.util.SwingUtils;
|
|||||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.TLSSecurity;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
@@ -42,6 +28,7 @@ public final class EntryPoint {
|
|||||||
private EntryPoint() {
|
private EntryPoint() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initializes TLS protection before downloading dependencies and starting the launcher.
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||||
@@ -49,6 +36,7 @@ public final class EntryPoint {
|
|||||||
|
|
||||||
createHMCLDirectories();
|
createHMCLDirectories();
|
||||||
LOG.start(Metadata.HMCL_LOCAL_HOME.resolve("logs"));
|
LOG.start(Metadata.HMCL_LOCAL_HOME.resolve("logs"));
|
||||||
|
TLSSecurity.install();
|
||||||
|
|
||||||
checkWine();
|
checkWine();
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl;
|
||||||
|
|
||||||
import com.sun.jna.Pointer;
|
import com.sun.jna.Pointer;
|
||||||
@@ -41,8 +26,6 @@ import org.jackhuang.hmcl.task.Schedulers;
|
|||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
|
||||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
|
||||||
import org.jackhuang.hmcl.util.*;
|
import org.jackhuang.hmcl.util.*;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||||
@@ -142,7 +125,6 @@ public final class Launcher extends Application {
|
|||||||
Platform.setImplicitExit(false);
|
Platform.setImplicitExit(false);
|
||||||
Controllers.initialize(primaryStage);
|
Controllers.initialize(primaryStage);
|
||||||
|
|
||||||
UpdateChecker.init();
|
|
||||||
|
|
||||||
primaryStage.show();
|
primaryStage.show();
|
||||||
});
|
});
|
||||||
@@ -280,11 +262,6 @@ public final class Launcher extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (UpdateHandler.processArguments(args)) {
|
|
||||||
LOG.shutdown();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||||
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
|
||||||
|
|
||||||
@@ -297,6 +274,10 @@ public final class Launcher extends Application {
|
|||||||
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
|
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
|
||||||
}
|
}
|
||||||
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
|
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
|
||||||
|
try {
|
||||||
|
LOG.info("Process PID: " + ProcessHandle.current().pid());
|
||||||
|
} catch (UnsupportedOperationException ignored) {
|
||||||
|
}
|
||||||
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
|
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
|
||||||
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
|
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
@@ -42,22 +27,16 @@ public final class Metadata {
|
|||||||
public static final String WINDOWS_APP_USER_MODEL_ID = "org.jackhuang.hmcl";
|
public static final String WINDOWS_APP_USER_MODEL_ID = "org.jackhuang.hmcl";
|
||||||
|
|
||||||
public static final String TITLE = NAME + " " + VERSION;
|
public static final String TITLE = NAME + " " + VERSION;
|
||||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
|
||||||
|
/// Whether the build metadata identifies this launcher as an official build.
|
||||||
|
public static final boolean OFFICIAL_BUILD = Boolean.parseBoolean(JarUtils.getAttribute("hmcl.official", "false"));
|
||||||
|
|
||||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
|
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
|
||||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
||||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
public static final int RECOMMENDED_JAVA_VERSION = 25;
|
||||||
|
|
||||||
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
|
|
||||||
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
|
|
||||||
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
|
|
||||||
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
|
|
||||||
|
|
||||||
public static final String DOCS_URL = "https://docs.hmcl.net";
|
public static final String DOCS_URL = "https://docs.hmcl.net";
|
||||||
public static final String CONTACT_URL = DOCS_URL + "/help.html";
|
|
||||||
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
|
|
||||||
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
|
||||||
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
|
|
||||||
|
|
||||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||||
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
||||||
@@ -110,7 +89,7 @@ public final class Metadata {
|
|||||||
|
|
||||||
public static @Nullable String getSuggestedJavaDownloadLink() {
|
public static @Nullable String getSuggestedJavaDownloadLink() {
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
|
||||||
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
|
return "https://www.loongnix.cn/zh/api/java/downloads-jdk25/index.html";
|
||||||
else {
|
else {
|
||||||
EnumSet<Architecture> supportedArchitectures;
|
EnumSet<Architecture> supportedArchitectures;
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||||
@@ -162,4 +141,18 @@ public final class Metadata {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Server address used when the installed modpack manifest does not provide one.
|
||||||
|
///
|
||||||
|
/// Can be set at build or run time with the `hmcl.server.address` system property; empty by default.
|
||||||
|
public static String getDefaultServerAddress() {
|
||||||
|
return System.getProperty("hmcl.server.address", "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The server name shown in the title bar.
|
||||||
|
///
|
||||||
|
/// Can be set with the `hmcl.server.name` system property; empty by default.
|
||||||
|
public static String getServerName() {
|
||||||
|
return System.getProperty("hmcl.server.name", "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
|
package org.jackhuang.hmcl.addon;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.addon.mod.ModProjectInfo;
|
||||||
|
import org.jackhuang.hmcl.modpack.ModpackProjectInfo;
|
||||||
|
import org.jackhuang.hmcl.util.Pair;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
|
/// @author Glavo
|
||||||
|
@NotNullByDefault
|
||||||
|
public abstract class AddonProjectInfoRegistry<I extends AddonProjectInfo> {
|
||||||
|
public static final AddonProjectInfoRegistry<ModProjectInfo> MOD = new AddonProjectInfoRegistry<>(
|
||||||
|
"/assets/addon/mod.csv",
|
||||||
|
32768
|
||||||
|
) {
|
||||||
|
@Override
|
||||||
|
protected int getFieldsCount() {
|
||||||
|
return MOD_IDS_INDEX + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ModProjectInfo parseAddonProjectInfo(String[] fields) throws Exception {
|
||||||
|
return new ModProjectInfo(
|
||||||
|
Integer.parseInt(fields[MCMOD_ID_INDEX]),
|
||||||
|
fields[CURSEFORGE_ID_INDEX],
|
||||||
|
fields[MODRINTH_ID_INDEX],
|
||||||
|
fields[MAIN_NAME_INDEX],
|
||||||
|
fields[SUB_NAME_INDEX],
|
||||||
|
fields[ABBR_NAME_INDEX],
|
||||||
|
fields[MOD_IDS_INDEX]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Provides metadata for modpacks.
|
||||||
|
public static final AddonProjectInfoRegistry<ModpackProjectInfo> MODPACK = new AddonProjectInfoRegistry<>(
|
||||||
|
"/assets/addon/modpack.csv", 2048
|
||||||
|
) {
|
||||||
|
@Override
|
||||||
|
protected int getFieldsCount() {
|
||||||
|
return ABBR_NAME_INDEX + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ModpackProjectInfo parseAddonProjectInfo(String[] fields) {
|
||||||
|
return new ModpackProjectInfo(Integer.parseInt(fields[MCMOD_ID_INDEX]),
|
||||||
|
fields[CURSEFORGE_ID_INDEX], fields[MODRINTH_ID_INDEX],
|
||||||
|
fields[MAIN_NAME_INDEX], fields[SUB_NAME_INDEX], fields[ABBR_NAME_INDEX]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns the registry for the addon type, or `null` for null or unsupported types.
|
||||||
|
public static @Nullable AddonProjectInfoRegistry<?> getByAddonType(@Nullable RemoteAddon.Type type) {
|
||||||
|
if (type == null) return null;
|
||||||
|
return switch (type) {
|
||||||
|
case MOD -> MOD;
|
||||||
|
case MODPACK -> MODPACK;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static final int MCMOD_ID_INDEX = 0;
|
||||||
|
protected static final int CURSEFORGE_ID_INDEX = 1;
|
||||||
|
protected static final int MODRINTH_ID_INDEX = 2;
|
||||||
|
protected static final int MAIN_NAME_INDEX = 3;
|
||||||
|
protected static final int SUB_NAME_INDEX = 4;
|
||||||
|
protected static final int ABBR_NAME_INDEX = 5;
|
||||||
|
protected static final int MOD_IDS_INDEX = 6;
|
||||||
|
|
||||||
|
private final ReentrantLock lock = new ReentrantLock();
|
||||||
|
private final String assetsPath;
|
||||||
|
private final int initialCapacity;
|
||||||
|
|
||||||
|
private volatile @Nullable SoftReference<@Unmodifiable List<I>> infosRef;
|
||||||
|
|
||||||
|
/// Softly caches projects indexed by mod ID.
|
||||||
|
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> modIdMapRef;
|
||||||
|
|
||||||
|
/// Softly caches projects indexed by cleaned secondary name.
|
||||||
|
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> subnameMapRef;
|
||||||
|
|
||||||
|
/// Softly caches projects indexed by CurseForge identifier.
|
||||||
|
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> curseForgeMapRef;
|
||||||
|
|
||||||
|
/// Softly caches searchable names in resource and field order.
|
||||||
|
private volatile @Nullable SoftReference<@Unmodifiable List<Pair<String, I>>> keywordsRef;
|
||||||
|
|
||||||
|
/// Creates a registry reading records from the given classpath resource.
|
||||||
|
AddonProjectInfoRegistry(String assetsPath, int initialCapacity) {
|
||||||
|
this.assetsPath = assetsPath;
|
||||||
|
this.initialCapacity = initialCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract int getFieldsCount();
|
||||||
|
|
||||||
|
protected abstract I parseAddonProjectInfo(String[] fields) throws Exception;
|
||||||
|
|
||||||
|
private @Unmodifiable List<I> loadInfos() throws IOException {
|
||||||
|
@Nullable InputStream inputStream = AddonProjectInfoRegistry.class.getResourceAsStream(assetsPath);
|
||||||
|
if (inputStream == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||||
|
ArrayList<I> infos = new ArrayList<>(initialCapacity);
|
||||||
|
@Unmodifiable List<I> result = Collections.unmodifiableList(infos);
|
||||||
|
|
||||||
|
int fieldsCount = getFieldsCount();
|
||||||
|
String[] fields = new String[fieldsCount];
|
||||||
|
|
||||||
|
@Nullable StringBuilder fieldBuilder = null;
|
||||||
|
|
||||||
|
@Nullable String line;
|
||||||
|
loop:
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
for (int fieldIndex = 0; fieldIndex < fieldsCount; fieldIndex++) {
|
||||||
|
if (offset > line.length()) {
|
||||||
|
LOG.warning("Invalid line in " + assetsPath + ": " + line);
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
int end = line.indexOf(',', offset);
|
||||||
|
if (end < 0)
|
||||||
|
end = line.length();
|
||||||
|
|
||||||
|
|
||||||
|
String field;
|
||||||
|
if (offset == end) {
|
||||||
|
field = "";
|
||||||
|
} else {
|
||||||
|
int cursor = offset;
|
||||||
|
while (cursor < end) {
|
||||||
|
if (line.charAt(cursor) == '\\') {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor == end) {
|
||||||
|
field = line.substring(offset, end);
|
||||||
|
} else {
|
||||||
|
if (fieldBuilder == null) {
|
||||||
|
fieldBuilder = new StringBuilder(32);
|
||||||
|
} else {
|
||||||
|
fieldBuilder.setLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldBuilder.append(line, offset, cursor);
|
||||||
|
while (cursor < end) {
|
||||||
|
char c = line.charAt(cursor);
|
||||||
|
if (c == '\\') {
|
||||||
|
if (cursor < end - 1) {
|
||||||
|
char next = line.charAt(cursor + 1);
|
||||||
|
switch (next) {
|
||||||
|
case '0' -> {
|
||||||
|
fieldBuilder.append('\0');
|
||||||
|
cursor += 2;
|
||||||
|
}
|
||||||
|
case 'n' -> {
|
||||||
|
fieldBuilder.append('\n');
|
||||||
|
cursor += 2;
|
||||||
|
}
|
||||||
|
case 'r' -> {
|
||||||
|
fieldBuilder.append('\r');
|
||||||
|
cursor += 2;
|
||||||
|
}
|
||||||
|
case 't' -> {
|
||||||
|
fieldBuilder.append('\t');
|
||||||
|
cursor += 2;
|
||||||
|
}
|
||||||
|
case '\\' -> {
|
||||||
|
fieldBuilder.append('\\');
|
||||||
|
cursor += 2;
|
||||||
|
}
|
||||||
|
case 'c' -> {
|
||||||
|
fieldBuilder.append(',');
|
||||||
|
cursor += 2;
|
||||||
|
}
|
||||||
|
case 'u' -> {
|
||||||
|
if (cursor < end - 5) {
|
||||||
|
try {
|
||||||
|
int codePoint = Integer.parseInt(line, cursor + 2, cursor + 6, 16);
|
||||||
|
fieldBuilder.append((char) codePoint);
|
||||||
|
cursor += 6;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LOG.warning("Invalid line in " + assetsPath + ": " + line);
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldBuilder.append(c);
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
LOG.warning("Invalid line in " + assetsPath + ": " + line);
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldBuilder.append(c);
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
fieldBuilder.append(c);
|
||||||
|
cursor++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
field = fieldBuilder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fields[fieldIndex] = field;
|
||||||
|
offset = end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
I info = parseAddonProjectInfo(fields);
|
||||||
|
info.setKeepAlive(result);
|
||||||
|
infos.add(info);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warning("Failed to parse line in " + assetsPath + ": " + line, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
infos.trimToSize();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
|
||||||
|
/// Duplicate keys retain the first project in resource order.
|
||||||
|
private @Unmodifiable Map<String, I> getModIdMap() {
|
||||||
|
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.modIdMapRef;
|
||||||
|
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
reference = this.modIdMapRef;
|
||||||
|
cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
Map<String, I> map = new HashMap<>();
|
||||||
|
for (I info : getInfos()) {
|
||||||
|
if (info instanceof ModProjectInfo mod) {
|
||||||
|
for (String id : mod.getModIds()) {
|
||||||
|
if (!id.isEmpty())
|
||||||
|
map.putIfAbsent(id, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cached = Collections.unmodifiableMap(map);
|
||||||
|
this.modIdMapRef = new SoftReference<>(cached);
|
||||||
|
return cached;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
|
||||||
|
/// Duplicate keys retain the first project in resource order.
|
||||||
|
private @Unmodifiable Map<String, I> getSubnameMap() {
|
||||||
|
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.subnameMapRef;
|
||||||
|
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
reference = this.subnameMapRef;
|
||||||
|
cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
Map<String, I> map = new HashMap<>();
|
||||||
|
for (I info : getInfos()) {
|
||||||
|
String name = cleanSubname(info.subName());
|
||||||
|
if (!name.isEmpty())
|
||||||
|
map.putIfAbsent(name, info);
|
||||||
|
}
|
||||||
|
cached = Collections.unmodifiableMap(map);
|
||||||
|
this.subnameMapRef = new SoftReference<>(cached);
|
||||||
|
return cached;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
|
||||||
|
/// Duplicate keys retain the first project in resource order.
|
||||||
|
private @Unmodifiable Map<String, I> getCurseForgeMap() {
|
||||||
|
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.curseForgeMapRef;
|
||||||
|
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
reference = this.curseForgeMapRef;
|
||||||
|
cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
Map<String, I> map = new HashMap<>();
|
||||||
|
for (I info : getInfos()) {
|
||||||
|
String id = info.curseforgeId();
|
||||||
|
if (StringUtils.isNotBlank(id))
|
||||||
|
map.putIfAbsent(id, info);
|
||||||
|
}
|
||||||
|
cached = Collections.unmodifiableMap(map);
|
||||||
|
this.curseForgeMapRef = new SoftReference<>(cached);
|
||||||
|
return cached;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first project with the exact CurseForge identifier, or `null` if the
|
||||||
|
/// identifier is null, blank or unknown.
|
||||||
|
public @Nullable I getInfoByCurseForgeId(@Nullable String id) {
|
||||||
|
return StringUtils.isBlank(id) ? null : getCurseForgeMap().get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a project by its cleaned secondary name, falling back to its exact mod ID.
|
||||||
|
/// A name match is accepted only when the ID is blank or belongs to that mod project.
|
||||||
|
/// Null and blank inputs are absent; returns `null` if neither lookup matches.
|
||||||
|
/// Duplicate keys resolve to the first project in resource order.
|
||||||
|
public @Nullable I getInfo(@Nullable String id, @Nullable String subname) {
|
||||||
|
String name = cleanSubname(subname);
|
||||||
|
if (StringUtils.isNotBlank(name)) {
|
||||||
|
@Nullable I info = getSubnameMap().get(name);
|
||||||
|
if (info != null && (StringUtils.isBlank(id)
|
||||||
|
|| info instanceof ModProjectInfo mod && mod.getModIds().contains(id)))
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return StringUtils.isBlank(id) ? null : getModIdMap().get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSupportedSubnameChar(int ch) {
|
||||||
|
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|
||||||
|
|| ".+\\".indexOf(ch) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes supported separators from a secondary name.
|
||||||
|
/// Returns an empty string for null, blank or unsupported names; matching is case-sensitive.
|
||||||
|
private static String cleanSubname(@Nullable String subname) {
|
||||||
|
if (StringUtils.isBlank(subname))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
while (offset < subname.length()) {
|
||||||
|
int ch = subname.charAt(offset);
|
||||||
|
if (!isSupportedSubnameChar(ch))
|
||||||
|
break;
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset == subname.length())
|
||||||
|
return subname;
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder(subname.length());
|
||||||
|
builder.append(subname, 0, offset);
|
||||||
|
|
||||||
|
while (offset < subname.length()) {
|
||||||
|
int ch = subname.codePointAt(offset);
|
||||||
|
if (isSupportedSubnameChar(ch)) {
|
||||||
|
builder.appendCodePoint(ch);
|
||||||
|
} else if (!(Character.isWhitespace(ch)
|
||||||
|
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
offset += Character.charCount(ch);
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns searchable names, rebuilding the index after its soft reference is cleared.
|
||||||
|
private @Unmodifiable List<Pair<String, I>> getKeywords() {
|
||||||
|
@Nullable SoftReference<@Unmodifiable List<Pair<String, I>>> reference = keywordsRef;
|
||||||
|
@Unmodifiable List<Pair<String, I>> cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
reference = keywordsRef;
|
||||||
|
cached = reference != null ? reference.get() : null;
|
||||||
|
if (cached != null)
|
||||||
|
return cached;
|
||||||
|
List<Pair<String, I>> keywords = new ArrayList<>();
|
||||||
|
for (I info : getInfos()) {
|
||||||
|
if (!info.mainName().isEmpty())
|
||||||
|
keywords.add(Pair.pair(info.mainName(), info));
|
||||||
|
if (!info.subName().isEmpty())
|
||||||
|
keywords.add(Pair.pair(info.subName(), info));
|
||||||
|
if (!info.abbrName().isEmpty())
|
||||||
|
keywords.add(Pair.pair(info.abbrName(), info));
|
||||||
|
}
|
||||||
|
cached = List.copyOf(keywords);
|
||||||
|
keywordsRef = new SoftReference<>(cached);
|
||||||
|
return cached;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns projects ranked by descending longest common subsequence length after removing
|
||||||
|
/// space characters from the query. Comparison is case-sensitive and uses UTF-16 units.
|
||||||
|
/// Matches require at least one unit and at least query length minus three units.
|
||||||
|
/// Each matching name contributes a result, so projects may occur more than once.
|
||||||
|
/// Ties preserve resource order, then primary name, secondary name and abbreviation order.
|
||||||
|
public @Unmodifiable List<I> search(String query) {
|
||||||
|
query = query.chars().filter(ch -> !Character.isSpaceChar(ch))
|
||||||
|
.collect(StringBuilder::new, (sb, ch) -> sb.append((char) ch), StringBuilder::append).toString();
|
||||||
|
@Unmodifiable List<Pair<String, I>> keywords = getKeywords();
|
||||||
|
int maxLength = keywords.stream().mapToInt(keyword -> keyword.getKey().length()).max().orElse(0);
|
||||||
|
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxLength);
|
||||||
|
List<Pair<Integer, I>> matches = new ArrayList<>();
|
||||||
|
for (Pair<String, I> keyword : keywords) {
|
||||||
|
int score = lcs.calc(query, keyword.getKey());
|
||||||
|
if (score >= Math.max(1, query.length() - 3))
|
||||||
|
matches.add(Pair.pair(score, keyword.getValue()));
|
||||||
|
}
|
||||||
|
return matches.stream().sorted((a, b) -> Integer.compare(b.getKey(), a.getKey()))
|
||||||
|
.map(Pair::getValue).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns cached project metadata, loading it on demand. Missing resources and I/O failures
|
||||||
|
/// produce an empty list; malformed records are skipped. The cache is softly referenced.
|
||||||
|
public @Unmodifiable List<I> getInfos() {
|
||||||
|
@Nullable SoftReference<@Unmodifiable List<I>> infosRef = this.infosRef;
|
||||||
|
@Unmodifiable List<I> infos = infosRef != null ? infosRef.get() : null;
|
||||||
|
if (infos != null)
|
||||||
|
return infos;
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
infosRef = this.infosRef;
|
||||||
|
infos = infosRef != null ? infosRef.get() : null;
|
||||||
|
if (infos != null)
|
||||||
|
return infos;
|
||||||
|
|
||||||
|
try {
|
||||||
|
infos = loadInfos();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warning("Failed to load addon project info from " + assetsPath, e);
|
||||||
|
infos = List.of();
|
||||||
|
}
|
||||||
|
this.infosRef = new SoftReference<>(infos);
|
||||||
|
return infos;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.addon.update;
|
package org.jackhuang.hmcl.addon.update;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
@@ -36,7 +21,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|||||||
public class AddonCheckUpdatesTask extends Task<AddonUpdates> {
|
public class AddonCheckUpdatesTask extends Task<AddonUpdates> {
|
||||||
private final List<Task<Pair<AddonUpdate, @Nullable AddonUpdate>>> dependents;
|
private final List<Task<Pair<AddonUpdate, @Nullable AddonUpdate>>> dependents;
|
||||||
|
|
||||||
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<? extends LocalAddonFile> addons) {
|
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<? extends LocalAddonFile<?>> addons) {
|
||||||
dependents = addons.stream().map(addon ->
|
dependents = addons.stream().map(addon ->
|
||||||
Task.supplyAsync(Schedulers.io(), () -> {
|
Task.supplyAsync(Schedulers.io(), () -> {
|
||||||
AddonUpdate candidate = null;
|
AddonUpdate candidate = null;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.addon.update;
|
package org.jackhuang.hmcl.addon.update;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||||
@@ -37,14 +22,14 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|||||||
|
|
||||||
public class AddonUpdateTask extends Task<Void> {
|
public class AddonUpdateTask extends Task<Void> {
|
||||||
private final Collection<Task<?>> dependents = new ArrayList<>();
|
private final Collection<Task<?>> dependents = new ArrayList<>();
|
||||||
private final List<LocalAddonFile> failedAddons = new ArrayList<>();
|
private final List<LocalAddonFile<?>> failedAddons = new ArrayList<>();
|
||||||
|
|
||||||
public AddonUpdateTask(DownloadProvider downloadProvider, Path addonDirectory, List<AddonUpdate> addons) {
|
public AddonUpdateTask(DownloadProvider downloadProvider, Path addonDirectory, List<AddonUpdate> addons) {
|
||||||
setStage("addon.check_update.confirm");
|
setStage("addon.check_update.confirm");
|
||||||
getProperties().put("total", addons.size());
|
getProperties().put("total", addons.size());
|
||||||
|
|
||||||
for (AddonUpdate addon : addons) {
|
for (AddonUpdate addon : addons) {
|
||||||
LocalAddonFile local = addon.localAddonFile();
|
LocalAddonFile<?> local = addon.localAddonFile();
|
||||||
RemoteAddon.Version remote = addon.targetVersion();
|
RemoteAddon.Version remote = addon.targetVersion();
|
||||||
boolean isDisabled = local.isDisabled();
|
boolean isDisabled = local.isDisabled();
|
||||||
String fileName = remote.file().filename();
|
String fileName = remote.file().filename();
|
||||||
@@ -91,7 +76,7 @@ public class AddonUpdateTask extends Task<Void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<LocalAddonFile> getFailedAddons() {
|
public List<LocalAddonFile<?>> getFailedAddons() {
|
||||||
return failedAddons;
|
return failedAddons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.download;
|
package org.jackhuang.hmcl.download;
|
||||||
|
|
||||||
import org.glavo.url.WebURL;
|
import org.glavo.url.WebURL;
|
||||||
@@ -29,36 +14,69 @@ import org.jackhuang.hmcl.game.GameComponentType;
|
|||||||
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
import org.jackhuang.hmcl.game.HMCLCacheRepository;
|
||||||
import org.jackhuang.hmcl.setting.DownloadSource;
|
import org.jackhuang.hmcl.setting.DownloadSource;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
import org.jackhuang.hmcl.util.RegionUtils;
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/// @author Glavo
|
/// @author Glavo
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public final class HMCLDownloadProvider extends DownloadProvider {
|
public final class HMCLDownloadProvider extends DownloadProvider {
|
||||||
private volatile DownloadSource versionListSource = DownloadSource.DEFAULT;
|
private volatile DownloadSource versionListSource = DownloadSource.MIRROR;
|
||||||
private volatile DownloadSource fileSource = DownloadSource.DEFAULT;
|
private volatile DownloadSource fileSource = DownloadSource.MIRROR;
|
||||||
|
|
||||||
|
private static boolean preferMirror(DownloadSource source) {
|
||||||
|
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
|
||||||
|
return source != DownloadSource.OFFICIAL;
|
||||||
|
}
|
||||||
|
return source == DownloadSource.MIRROR;
|
||||||
|
}
|
||||||
|
|
||||||
private static DownloadCandidates getCandidates(
|
private static DownloadCandidates getCandidates(
|
||||||
DownloadSource source,
|
DownloadSource source,
|
||||||
WebURL defaultUrl, WebURL bmclapiUrl
|
WebURL officialUrl,
|
||||||
|
List<WebURL> mirrorUrls
|
||||||
) {
|
) {
|
||||||
if (LocaleUtils.IS_CHINA_MAINLAND) {
|
if (preferMirror(source)) {
|
||||||
return switch (source) {
|
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
|
||||||
case DEFAULT, OFFICIAL -> DownloadCandidates.of(defaultUrl, bmclapiUrl);
|
candidates.addAll(spreadPrimary(mirrorUrls.stream().map(DownloadCandidate::of).toList(), officialUrl.href()));
|
||||||
case MIRROR -> DownloadCandidates.of(bmclapiUrl, defaultUrl);
|
candidates.add(DownloadCandidate.of(officialUrl));
|
||||||
};
|
return DownloadCandidates.of(List.copyOf(candidates));
|
||||||
} else {
|
} else {
|
||||||
return switch (source) {
|
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
|
||||||
case DEFAULT, OFFICIAL -> DownloadCandidates.of(defaultUrl);
|
candidates.add(DownloadCandidate.of(officialUrl));
|
||||||
case MIRROR -> DownloadCandidates.of(bmclapiUrl, defaultUrl);
|
for (WebURL mirrorUrl : mirrorUrls) {
|
||||||
};
|
candidates.add(DownloadCandidate.of(mirrorUrl));
|
||||||
|
}
|
||||||
|
return DownloadCandidates.of(List.copyOf(candidates));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rotates the first mirror among the top entries using a stable hash of the URL, so concurrent
|
||||||
|
/// file downloads spread across healthy mirrors instead of stampeding one entry point.
|
||||||
|
private static List<DownloadCandidate> spreadPrimary(List<DownloadCandidate> mirrors, String key) {
|
||||||
|
if (mirrors.size() <= 1)
|
||||||
|
return mirrors;
|
||||||
|
|
||||||
|
int top = Math.min(2, mirrors.size());
|
||||||
|
int primary = Math.floorMod(key.hashCode(), top);
|
||||||
|
if (primary == 0)
|
||||||
|
return mirrors;
|
||||||
|
|
||||||
|
var rotated = new ArrayList<DownloadCandidate>(mirrors.size());
|
||||||
|
rotated.add(mirrors.get(primary));
|
||||||
|
for (int i = 0; i < mirrors.size(); i++) {
|
||||||
|
if (i != primary)
|
||||||
|
rotated.add(mirrors.get(i));
|
||||||
|
}
|
||||||
|
return rotated;
|
||||||
|
}
|
||||||
|
|
||||||
private static DownloadCandidates getCandidates(DownloadSource source, List<MirrorRule> rules, List<String> urls) {
|
private static DownloadCandidates getCandidates(DownloadSource source, List<MirrorRule> rules, List<String> urls) {
|
||||||
if (urls.isEmpty()) {
|
if (urls.isEmpty()) {
|
||||||
throw new IllegalArgumentException("urls cannot be empty");
|
throw new IllegalArgumentException("urls cannot be empty");
|
||||||
@@ -66,7 +84,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
|
|
||||||
// true means prefer mirror, false means prefer official, null means do not use mirror
|
// true means prefer mirror, false means prefer official, null means do not use mirror
|
||||||
@Nullable Boolean mirrorFirst;
|
@Nullable Boolean mirrorFirst;
|
||||||
if (LocaleUtils.IS_CHINA_MAINLAND) {
|
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
|
||||||
mirrorFirst = switch (source) {
|
mirrorFirst = switch (source) {
|
||||||
case DEFAULT, MIRROR -> true;
|
case DEFAULT, MIRROR -> true;
|
||||||
case OFFICIAL -> false;
|
case OFFICIAL -> false;
|
||||||
@@ -82,7 +100,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
return DownloadCandidates.of(urls.stream().map(DownloadCandidate::of).toArray(DownloadCandidate[]::new));
|
return DownloadCandidates.of(urls.stream().map(DownloadCandidate::of).toArray(DownloadCandidate[]::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
var candidates = new ArrayList<DownloadCandidate>(urls.size() * 2);
|
var candidates = new ArrayList<DownloadCandidate>(urls.size() * 3);
|
||||||
for (String url : urls) {
|
for (String url : urls) {
|
||||||
DownloadCandidate candidate = DownloadCandidate.of(url);
|
DownloadCandidate candidate = DownloadCandidate.of(url);
|
||||||
if (candidate.url() == null) {
|
if (candidate.url() == null) {
|
||||||
@@ -91,79 +109,114 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable DownloadCandidate mirrorCandidate = null;
|
boolean added = false;
|
||||||
boolean fallback = false;
|
|
||||||
for (MirrorRule rule : rules) {
|
for (MirrorRule rule : rules) {
|
||||||
if (url.startsWith(rule.source)) {
|
@Nullable List<DownloadCandidate> list = rule.getCandidates(candidate, mirrorFirst);
|
||||||
mirrorCandidate = DownloadCandidate.of(rule.target + url.substring(rule.source.length()));
|
if (list != null) {
|
||||||
fallback = rule.fallback;
|
candidates.addAll(list);
|
||||||
|
added = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mirrorCandidate == null) {
|
if (!added)
|
||||||
candidates.add(candidate);
|
candidates.add(candidate);
|
||||||
} else if (mirrorFirst && !fallback) {
|
|
||||||
candidates.add(mirrorCandidate);
|
|
||||||
candidates.add(candidate);
|
|
||||||
} else {
|
|
||||||
candidates.add(candidate);
|
|
||||||
candidates.add(mirrorCandidate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return DownloadCandidates.of(List.copyOf(candidates));
|
return DownloadCandidates.of(List.copyOf(candidates));
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String bmclRoot;
|
private final Supplier<@Unmodifiable List<String>> mirrorRoots;
|
||||||
private final List<MirrorRule> rules;
|
private final List<MirrorRule> rules;
|
||||||
|
|
||||||
public HMCLDownloadProvider(String bmclRoot) {
|
public HMCLDownloadProvider(Supplier<@Unmodifiable List<String>> mirrorRoots) {
|
||||||
this.bmclRoot = bmclRoot;
|
this.mirrorRoots = Objects.requireNonNull(mirrorRoots);
|
||||||
this.rules = List.of(
|
this.rules = List.of(
|
||||||
new MirrorRule("https://bmclapi2.bangbang93.com", bmclRoot),
|
new MultiRootRule("https://bmclapi2.bangbang93.com", Function.identity()),
|
||||||
new MirrorRule("https://launchermeta.mojang.com", bmclRoot),
|
new MultiRootRule("https://bmclapi.bangbang93.com", Function.identity()),
|
||||||
new MirrorRule("https://piston-meta.mojang.com", bmclRoot),
|
new MultiRootRule("https://launchermeta.mojang.com", Function.identity()),
|
||||||
new MirrorRule("https://piston-data.mojang.com", bmclRoot),
|
new MultiRootRule("https://piston-meta.mojang.com", Function.identity()),
|
||||||
new MirrorRule("https://launcher.mojang.com", bmclRoot),
|
new MultiRootRule("https://piston-data.mojang.com", Function.identity()),
|
||||||
new MirrorRule("https://libraries.minecraft.net", bmclRoot + "/libraries"),
|
new MultiRootRule("https://launcher.mojang.com", Function.identity()),
|
||||||
new MirrorRule("http://files.minecraftforge.net/maven", bmclRoot + "/maven"),
|
new MultiRootRule("https://libraries.minecraft.net", root -> root + "/libraries"),
|
||||||
new MirrorRule("https://files.minecraftforge.net/maven", bmclRoot + "/maven"),
|
new MultiRootRule("http://files.minecraftforge.net/maven", root -> root + "/maven"),
|
||||||
new MirrorRule("https://maven.minecraftforge.net", bmclRoot + "/maven"),
|
new MultiRootRule("https://files.minecraftforge.net/maven", root -> root + "/maven"),
|
||||||
new MirrorRule("https://maven.neoforged.net/releases/", bmclRoot + "/maven/"),
|
new MultiRootRule("https://maven.minecraftforge.net", root -> root + "/maven"),
|
||||||
new MirrorRule("http://dl.liteloader.com/versions", bmclRoot + "/maven"),
|
(source, mirrorFirst) -> {
|
||||||
new MirrorRule("https://dl.liteloader.com/versions", bmclRoot + "/maven"),
|
String neoforgedMaven = "https://maven.neoforged.net/";
|
||||||
new MirrorRule("https://meta.fabricmc.net", bmclRoot + "/fabric-meta"),
|
String neoforgedMavenRelease = "https://maven.neoforged.net/releases/";
|
||||||
new MirrorRule("https://maven.fabricmc.net", bmclRoot + "/maven"),
|
|
||||||
new MirrorRule("https://authlib-injector.yushi.moe", bmclRoot + "/mirrors/authlib-injector"),
|
if (source.url() == null || !source.url().href().startsWith(neoforgedMaven))
|
||||||
new MirrorRule("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
return null;
|
||||||
new MirrorRule("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
|
||||||
new MirrorRule("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"),
|
var candidates = new ArrayList<DownloadCandidate>();
|
||||||
new MirrorRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
List<String> roots = mirrorRoots.get();
|
||||||
new MirrorRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
|
||||||
|
if (source.url().href().startsWith(neoforgedMavenRelease)) {
|
||||||
|
String suffix = source.url().href().substring(neoforgedMavenRelease.length());
|
||||||
|
List<DownloadCandidate> mirrors = roots.stream()
|
||||||
|
.map(root -> DownloadCandidate.of(root + "/maven/" + suffix))
|
||||||
|
.toList();
|
||||||
|
if (mirrorFirst) {
|
||||||
|
candidates.addAll(spreadPrimary(mirrors, source.url().href()));
|
||||||
|
candidates.add(source);
|
||||||
|
} else {
|
||||||
|
candidates.add(source);
|
||||||
|
candidates.addAll(mirrors);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
candidates.add(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://neoforged.forgecdn.net
|
||||||
|
candidates.add(DownloadCandidate.of("https://neoforged.forgecdn.net/" + source.url().href().substring(neoforgedMaven.length())));
|
||||||
|
return candidates;
|
||||||
|
},
|
||||||
|
new MultiRootRule("http://dl.liteloader.com/versions", root -> root + "/maven"),
|
||||||
|
new MultiRootRule("https://dl.liteloader.com/versions", root -> root + "/maven"),
|
||||||
|
new MultiRootRule("https://meta.fabricmc.net", root -> root + "/fabric-meta"),
|
||||||
|
new MultiRootRule("https://maven.fabricmc.net", root -> root + "/maven"),
|
||||||
|
new MultiRootRule("https://authlib-injector.yushi.moe", root -> root + "/mirrors/authlib-injector"),
|
||||||
|
new SimpleRule("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||||
|
new SimpleRule("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||||
|
new SimpleRule("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"),
|
||||||
|
new SimpleRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
||||||
|
new SimpleRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||||
|
|
||||||
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||||
new MirrorRule("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth", true),
|
new SimpleRule("https://api.modrinth.com", null, "https://mod.mcimirror.top/modrinth"),
|
||||||
new MirrorRule("https://cdn.modrinth.com", "https://mod.mcimirror.top", true),
|
new SimpleRule("https://cdn.modrinth.com", null, "https://mod.mcimirror.top"),
|
||||||
new MirrorRule("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge", true),
|
new SimpleRule("https://api.curseforge.com", null, "https://mod.mcimirror.top/curseforge"),
|
||||||
new MirrorRule("https://edge.forgecdn.net", "https://mod.mcimirror.top", true)
|
new SimpleRule("https://edge.forgecdn.net", null, "https://mod.mcimirror.top")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String primaryMirrorRoot() {
|
||||||
|
List<String> roots = mirrorRoots.get();
|
||||||
|
return roots.isEmpty() ? "https://bmclapi2.bangbang93.com" : roots.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WebURL> mirrorUrls(String suffix) {
|
||||||
|
return mirrorRoots.get().stream()
|
||||||
|
.map(root -> WebURL.parse(root + suffix))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Task<? extends ComponentRemoteVersionList<?>> fetchVersionsAsync(GameComponentType type, @Nullable GameVersionNumber gameVersion) {
|
protected Task<? extends ComponentRemoteVersionList<?>> fetchVersionsAsync(GameComponentType type, @Nullable GameVersionNumber gameVersion) {
|
||||||
assert (gameVersion == null) == (type == GameComponentType.GAME);
|
assert (gameVersion == null) == (type == GameComponentType.GAME);
|
||||||
|
String bmclRoot = primaryMirrorRoot();
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case FORGE -> {
|
case FORGE -> {
|
||||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchOfficial = ForgeRemoteVersion.fetchAsync(this, gameVersion);
|
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchOfficial = ForgeRemoteVersion.fetchAsync(this, gameVersion);
|
||||||
|
|
||||||
DownloadSource source = versionListSource;
|
DownloadSource source = versionListSource;
|
||||||
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
|
if (source == DownloadSource.OFFICIAL) {
|
||||||
return fetchOfficial;
|
return fetchOfficial;
|
||||||
}
|
}
|
||||||
|
|
||||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchBMCL = ForgeRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
|
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchBMCL = ForgeRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
|
||||||
if (source == DownloadSource.MIRROR) {
|
if (preferMirror(source)) {
|
||||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
||||||
} else {
|
} else {
|
||||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
||||||
@@ -173,11 +226,11 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchOfficial = NeoForgeRemoteVersion.fetchAsync(this, gameVersion);
|
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchOfficial = NeoForgeRemoteVersion.fetchAsync(this, gameVersion);
|
||||||
|
|
||||||
DownloadSource source = versionListSource;
|
DownloadSource source = versionListSource;
|
||||||
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
|
if (source == DownloadSource.OFFICIAL) {
|
||||||
return fetchOfficial;
|
return fetchOfficial;
|
||||||
}
|
}
|
||||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchBMCL = NeoForgeRemoteVersion.fetchBMCLAsync(bmclRoot, gameVersion);
|
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchBMCL = NeoForgeRemoteVersion.fetchBMCLAsync(bmclRoot, gameVersion);
|
||||||
if (source == DownloadSource.MIRROR || source == DownloadSource.DEFAULT) {
|
if (preferMirror(source)) {
|
||||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
||||||
} else {
|
} else {
|
||||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
||||||
@@ -209,7 +262,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
return getCandidates(
|
return getCandidates(
|
||||||
versionListSource,
|
versionListSource,
|
||||||
GameRemoteVersion.VERSION_MANIFEST_URL,
|
GameRemoteVersion.VERSION_MANIFEST_URL,
|
||||||
WebURL.parse(bmclRoot + "/mc/game/version_manifest.json")
|
mirrorUrls("/mc/game/version_manifest.json")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,14 +270,14 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
public FabricLikeVersionListCandidates getFabricVersionListCandidates() {
|
public FabricLikeVersionListCandidates getFabricVersionListCandidates() {
|
||||||
DownloadSource source = versionListSource;
|
DownloadSource source = versionListSource;
|
||||||
return new FabricLikeVersionListCandidates(
|
return new FabricLikeVersionListCandidates(
|
||||||
getCandidates(source, FabricRemoteVersion.LOADER_META_URL, WebURL.parse(bmclRoot + "/fabric-meta/v2/versions/loader")),
|
getCandidates(source, FabricRemoteVersion.LOADER_META_URL, mirrorUrls("/fabric-meta/v2/versions/loader")),
|
||||||
getCandidates(source, FabricRemoteVersion.GAME_META_URL, WebURL.parse(bmclRoot + "/fabric-meta/v2/versions/game"))
|
getCandidates(source, FabricRemoteVersion.GAME_META_URL, mirrorUrls("/fabric-meta/v2/versions/game"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DownloadCandidates getCleanroomVersionListCandidates() {
|
public DownloadCandidates getCleanroomVersionListCandidates() {
|
||||||
return LocaleUtils.IS_CHINA_MAINLAND
|
return "CN".equals(RegionUtils.MACHINE_REGION)
|
||||||
? DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL, WebURL.parse("https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom/index.json"))
|
? DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL, WebURL.parse("https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom/index.json"))
|
||||||
: DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL);
|
: DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL);
|
||||||
}
|
}
|
||||||
@@ -241,10 +294,11 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
|
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
|
||||||
|
String location = assetObject.getLocation();
|
||||||
return getCandidates(
|
return getCandidates(
|
||||||
fileSource,
|
fileSource,
|
||||||
WebURL.parse("https://resources.download.minecraft.net/" + assetObject.getLocation()),
|
WebURL.parse("https://resources.download.minecraft.net/" + location),
|
||||||
WebURL.parse(bmclRoot + "/assets/" + assetObject.getLocation())
|
mirrorUrls("/assets/" + location)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,10 +341,73 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record MirrorRule(String source, String target, boolean fallback) {
|
@NotNullByDefault
|
||||||
public MirrorRule(String source, String target) {
|
@FunctionalInterface
|
||||||
this(source, target, false);
|
private interface MirrorRule {
|
||||||
|
@Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
private final class MultiRootRule implements MirrorRule {
|
||||||
|
private final String source;
|
||||||
|
private final Function<String, String> rootMapper;
|
||||||
|
|
||||||
|
private MultiRootRule(String source, Function<String, String> rootMapper) {
|
||||||
|
this.source = source;
|
||||||
|
this.rootMapper = rootMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst) {
|
||||||
|
if (source.url() == null || !source.url().href().startsWith(this.source))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
String urlWithoutPrefix = source.url().href().substring(this.source.length());
|
||||||
|
List<DownloadCandidate> mirrors = mirrorRoots.get().stream()
|
||||||
|
.map(root -> DownloadCandidate.of(rootMapper.apply(root) + urlWithoutPrefix))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
var candidates = new ArrayList<DownloadCandidate>(mirrors.size() + 1);
|
||||||
|
if (mirrorFirst) {
|
||||||
|
candidates.addAll(spreadPrimary(mirrors, source.url().href()));
|
||||||
|
candidates.add(source);
|
||||||
|
} else {
|
||||||
|
candidates.add(source);
|
||||||
|
candidates.addAll(mirrors);
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
private record SimpleRule(String source, @Nullable String first, @Nullable String fallback) implements MirrorRule {
|
||||||
|
public SimpleRule(String source, String target) {
|
||||||
|
this(source, target, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst) {
|
||||||
|
if (source.url() == null || !source.url().href().startsWith(this.source))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
boolean addSourceFirst = !mirrorFirst || first == null;
|
||||||
|
String urlWithoutPrefix = source.url().href().substring(this.source.length());
|
||||||
|
|
||||||
|
var candidates = new ArrayList<DownloadCandidate>();
|
||||||
|
|
||||||
|
if (addSourceFirst)
|
||||||
|
candidates.add(source);
|
||||||
|
|
||||||
|
if (first != null)
|
||||||
|
candidates.add(DownloadCandidate.of(first + urlWithoutPrefix));
|
||||||
|
|
||||||
|
if (!addSourceFirst)
|
||||||
|
candidates.add(source);
|
||||||
|
|
||||||
|
if (fallback != null)
|
||||||
|
candidates.add(DownloadCandidate.of(fallback + urlWithoutPrefix));
|
||||||
|
|
||||||
|
return candidates;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DefaultGameBuilder;
|
import org.jackhuang.hmcl.download.DefaultGameBuilder;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
@@ -470,9 +455,9 @@ public class HMCLGameInstance extends DefaultGameInstance {
|
|||||||
return GameInstanceIconType.OPTIFINE.getIcon();
|
return GameInstanceIconType.OPTIFINE.getIcon();
|
||||||
|
|
||||||
GameVersionNumber version = getVersion();
|
GameVersionNumber version = getVersion();
|
||||||
if (version.isAprilFools())
|
if (version instanceof GameVersionNumber.LegacySnapshot
|
||||||
return GameInstanceIconType.APRIL_FOOLS.getIcon();
|
|| version.isAprilFools()
|
||||||
else if (version instanceof GameVersionNumber.LegacySnapshot || version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
|
|| version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
|
||||||
return GameInstanceIconType.COMMAND.getIcon();
|
return GameInstanceIconType.COMMAND.getIcon();
|
||||||
else if (version instanceof GameVersionNumber.Old)
|
else if (version instanceof GameVersionNumber.Old)
|
||||||
return GameInstanceIconType.CRAFT_TABLE.getIcon();
|
return GameInstanceIconType.CRAFT_TABLE.getIcon();
|
||||||
@@ -565,7 +550,13 @@ public class HMCLGameInstance extends DefaultGameInstance {
|
|||||||
return getLayout().getInstanceGameSettingsFile(id);
|
return getLayout().getInstanceGameSettingsFile(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LaunchOptions.Builder getLaunchOptions(JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
|
public LaunchOptions.Builder getLaunchOptions(
|
||||||
|
JavaRuntime javaRuntime,
|
||||||
|
Path javaExecutable,
|
||||||
|
Path gameDir,
|
||||||
|
List<String> javaAgents,
|
||||||
|
List<String> javaArguments,
|
||||||
|
boolean makeLaunchScript) {
|
||||||
GameSettings.Effective vs = getEffectiveSettings();
|
GameSettings.Effective vs = getEffectiveSettings();
|
||||||
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
|
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
|
||||||
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
|
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
|
||||||
@@ -576,8 +567,8 @@ public class HMCLGameInstance extends DefaultGameInstance {
|
|||||||
maxMemory = noJVMOptions
|
maxMemory = noJVMOptions
|
||||||
? null
|
? null
|
||||||
: Math.toIntExact(HMCLGameRepository.getAutoAllocatedMemory(
|
: Math.toIntExact(HMCLGameRepository.getAutoAllocatedMemory(
|
||||||
SystemInfo.getPhysicalMemoryStatus().available(),
|
SystemInfo.getPhysicalMemoryStatus(),
|
||||||
javaVersion.getPlatform()) / 1024L / 1024L);
|
javaRuntime.getPlatform()) / 1024L / 1024L);
|
||||||
} else {
|
} else {
|
||||||
maxMemory = vs.getMaxMemory();
|
maxMemory = vs.getMaxMemory();
|
||||||
}
|
}
|
||||||
@@ -585,7 +576,8 @@ public class HMCLGameInstance extends DefaultGameInstance {
|
|||||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||||
.setInstanceId(getId())
|
.setInstanceId(getId())
|
||||||
.setGameDir(gameDir)
|
.setGameDir(gameDir)
|
||||||
.setJava(javaVersion)
|
.setJava(javaRuntime)
|
||||||
|
.setJavaExecutable(javaExecutable)
|
||||||
.setVersionType(Metadata.TITLE)
|
.setVersionType(Metadata.TITLE)
|
||||||
.setVersionName(getId().id())
|
.setVersionName(getId().id())
|
||||||
.setProfileName(Metadata.TITLE)
|
.setProfileName(Metadata.TITLE)
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.Metadata;
|
import org.jackhuang.hmcl.Metadata;
|
||||||
@@ -106,6 +91,12 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
|||||||
if (gameVersion == GameVersionNumber.unknown() || gameVersion.compareTo("1.1") < 0)
|
if (gameVersion == GameVersionNumber.unknown() || gameVersion.compareTo("1.1") < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// These snapshots lack the external language resources needed for non-English locales.
|
||||||
|
// Selecting an unavailable language causes a NullPointerException during startup.
|
||||||
|
if (gameVersion instanceof GameVersionNumber.LegacySnapshot
|
||||||
|
&& gameVersion.compareTo("13w16a") >= 0 && gameVersion.compareTo("13w23b") <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
String lang = normalizedLanguageTag(locale, gameVersion);
|
String lang = normalizedLanguageTag(locale, gameVersion);
|
||||||
if (lang.isEmpty())
|
if (lang.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
@@ -34,11 +19,13 @@ import org.jackhuang.hmcl.setting.LegacyGameSettingsMigrator;
|
|||||||
import org.jackhuang.hmcl.setting.GameSettingsPresetID;
|
import org.jackhuang.hmcl.setting.GameSettingsPresetID;
|
||||||
import org.jackhuang.hmcl.util.FileSaver;
|
import org.jackhuang.hmcl.util.FileSaver;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
|
import org.jackhuang.hmcl.util.MathUtils;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.Bits;
|
import org.jackhuang.hmcl.util.platform.Bits;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
import org.jackhuang.hmcl.util.platform.Platform;
|
import org.jackhuang.hmcl.util.platform.Platform;
|
||||||
|
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@@ -511,23 +498,43 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getAutoAllocatedMemory(long available, Platform platform) {
|
public static long getAutoAllocatedMemory(PhysicalMemoryStatus memoryStatus, Platform platform) {
|
||||||
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
|
final long minHeapSize;
|
||||||
if (usable <= 0) {
|
final long maxHeapSize;
|
||||||
return available;
|
|
||||||
|
if (platform.getBits() == Bits.BIT_32) {
|
||||||
|
// https://github.com/HMCL-dev/HMCL/issues/6638
|
||||||
|
maxHeapSize = 768 * 1024 * 1024; // 768 MiB
|
||||||
|
minHeapSize = 256 * 1024 * 1024; // 128 MiB
|
||||||
|
} else {
|
||||||
|
maxHeapSize = 24L * 1024 * 1024 * 1024; // 24 GiB
|
||||||
|
minHeapSize = 512 * 1024 * 1024; // 512 MiB
|
||||||
}
|
}
|
||||||
|
|
||||||
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
|
// Reserve memory for off-heap memory and HMCL itself
|
||||||
final long suggested;
|
final long reserved = 256 * 1024 * 1024; // 256 MiB
|
||||||
if (usable <= threshold)
|
|
||||||
|
final long threshold1 = 8L * 1024 * 1024 * 1024; // 8 GiB
|
||||||
|
final long threshold2 = 16L * 1024 * 1024 * 1024; // 16 GiB
|
||||||
|
|
||||||
|
if (memoryStatus.total() <= minHeapSize || memoryStatus.available() < 0) {
|
||||||
|
return -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long usable = Math.max(
|
||||||
|
Math.max(memoryStatus.available(), memoryStatus.total() / 16) - reserved,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
long suggested;
|
||||||
|
if (usable <= threshold1)
|
||||||
suggested = (long) (usable * 0.8);
|
suggested = (long) (usable * 0.8);
|
||||||
|
else if (usable <= threshold2)
|
||||||
|
suggested = (long) (threshold1 * 0.8 + (usable - threshold1) * 0.4);
|
||||||
else
|
else
|
||||||
suggested = Math.min(
|
suggested = (long) (threshold1 * 0.8 + (threshold2 - threshold1) * 0.4 + (usable - threshold2) * 0.2);
|
||||||
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
|
|
||||||
16L * 1024 * 1024 * 1024);
|
return MathUtils.clamp(suggested & ~0xFFFFFL, minHeapSize, maxHeapSize);
|
||||||
return platform.getBits() == Bits.BIT_32
|
|
||||||
? Math.min(suggested, 768 * 1024 * 1024) // https://github.com/HMCL-dev/HMCL/issues/6638
|
|
||||||
: suggested;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2022 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
@@ -57,11 +42,11 @@ import java.io.IOException;
|
|||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.nio.file.AccessDeniedException;
|
import java.nio.file.AccessDeniedException;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -148,29 +133,46 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds and executes the launch pipeline for the captured game instance.
|
/// Builds and executes the launch pipeline for the captured game instance.
|
||||||
|
@SuppressWarnings("NonAtomicOperationOnVolatileField")
|
||||||
private void launch0() {
|
private void launch0() {
|
||||||
// https://github.com/HMCL-dev/HMCL/pull/4121
|
// https://github.com/HMCL-dev/HMCL/pull/4121
|
||||||
PROCESSES.removeIf(it -> it.get() == null);
|
PROCESSES.removeIf(it -> it.get() == null);
|
||||||
|
|
||||||
|
final var context = new Object() {
|
||||||
|
volatile GameInstanceManifest launchManifest;
|
||||||
|
volatile JavaRuntime javaRuntime;
|
||||||
|
volatile Path javaExecutable;
|
||||||
|
};
|
||||||
|
|
||||||
HMCLGameRepository repository = repository();
|
HMCLGameRepository repository = repository();
|
||||||
DefaultDependencyManager dependencyManager = repository.getDependency();
|
DefaultDependencyManager dependencyManager = repository.getDependency();
|
||||||
// Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch.
|
|
||||||
var launchManifest = new AtomicReference<>(LaunchManifestNormalizer.repairForLaunch(gameInstance.getResolvedManifest()));
|
|
||||||
boolean integrityCheck = gameInstance.unmarkLaunchedAbnormally();
|
boolean integrityCheck = gameInstance.unmarkLaunchedAbnormally();
|
||||||
CountDownLatch launchingLatch = new CountDownLatch(1);
|
CountDownLatch launchingLatch = new CountDownLatch(1);
|
||||||
List<String> javaAgents = new ArrayList<>(0);
|
List<String> javaAgents = new ArrayList<>(0);
|
||||||
List<String> javaArguments = new ArrayList<>(0);
|
List<String> javaArguments = new ArrayList<>(0);
|
||||||
|
|
||||||
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
|
// Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch.
|
||||||
|
context.launchManifest = LaunchManifestNormalizer.repairForLaunch(gameInstance.getResolvedManifest());
|
||||||
|
|
||||||
TaskExecutor executor = checkGameState(gameInstance, setting, launchManifest.get())
|
TaskExecutor executor = checkGameState(gameInstance, setting, context.launchManifest)
|
||||||
.thenComposeAsync(java -> {
|
.thenComposeAsync(java -> {
|
||||||
javaVersionRef.set(Objects.requireNonNull(java));
|
context.javaRuntime = Objects.requireNonNull(java);
|
||||||
launchManifest.set(NativePatcher.patchNative(gameInstance, launchManifest.get(), java, setting, javaArguments));
|
context.javaExecutable = java.getBinary();
|
||||||
|
if (java.getPlatform().os() == OperatingSystem.WINDOWS
|
||||||
|
&& scriptFile == null
|
||||||
|
&& !setting.getInheritable(GameSettings::notUseJavawProperty)
|
||||||
|
&& FileUtils.getName(java.getBinary()).equalsIgnoreCase("java.exe")) {
|
||||||
|
Path javawBinary = java.getBinary().resolveSibling("javaw.exe");
|
||||||
|
if (Files.isRegularFile(javawBinary)) {
|
||||||
|
context.javaExecutable = javawBinary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.launchManifest = NativePatcher.patchNative(gameInstance, context.launchManifest, java, setting, javaArguments);
|
||||||
if (setting.getInheritable(GameSettings::notCheckGameProperty))
|
if (setting.getInheritable(GameSettings::notCheckGameProperty))
|
||||||
return null;
|
return null;
|
||||||
return Task.allOf(
|
return Task.allOf(
|
||||||
dependencyManager.checkGameCompletionAsync(gameInstance, launchManifest.get(), integrityCheck),
|
dependencyManager.checkGameCompletionAsync(gameInstance, context.launchManifest, integrityCheck),
|
||||||
Task.composeAsync(() -> {
|
Task.composeAsync(() -> {
|
||||||
try {
|
try {
|
||||||
@Nullable ModpackConfiguration<?> configuration =
|
@Nullable ModpackConfiguration<?> configuration =
|
||||||
@@ -203,7 +205,7 @@ public final class LauncherHelper {
|
|||||||
|
|
||||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
|
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
|
||||||
|
|
||||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, launchManifest.get(), lib, integrityCheck)) {
|
if (GameLibrariesTask.shouldDownloadLibrary(repository, context.launchManifest, lib, integrityCheck)) {
|
||||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||||
.thenRunAsync(() -> javaAgents.add(agent));
|
.thenRunAsync(() -> javaAgents.add(agent));
|
||||||
} else {
|
} else {
|
||||||
@@ -213,13 +215,13 @@ public final class LauncherHelper {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}).withStage("launch.state.dependencies")
|
}).withStage("launch.state.dependencies")
|
||||||
.thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion(), launchManifest.get()))
|
.thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion(), context.launchManifest))
|
||||||
.thenComposeAsync(() -> {
|
.thenComposeAsync(() -> {
|
||||||
if (setting.getInheritable(GameSettings::allowAutoAgentProperty)
|
if (setting.getInheritable(GameSettings::allowAutoAgentProperty)
|
||||||
|| setting.getInheritable(GameSettings::noJVMOptionsProperty)
|
|| setting.getInheritable(GameSettings::noJVMOptionsProperty)
|
||||||
|| setting.getInheritable(GameSettings::noOptimizingJVMOptionsProperty)
|
|| setting.getInheritable(GameSettings::noOptimizingJVMOptionsProperty)
|
||||||
|| Boolean.TRUE.equals(state().getShownTips().get(LWJGL_3_4_1_TIP))
|
|| Boolean.TRUE.equals(state().getShownTips().get(LWJGL_3_4_1_TIP))
|
||||||
|| !NativePatcher.needPatchMemoryUtil(launchManifest.get(), javaVersionRef.get().getParsedVersion())) {
|
|| !NativePatcher.needPatchMemoryUtil(context.launchManifest, context.javaRuntime.getParsedVersion())) {
|
||||||
return Task.completed(null);
|
return Task.completed(null);
|
||||||
} else {
|
} else {
|
||||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
@@ -239,7 +241,12 @@ public final class LauncherHelper {
|
|||||||
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
|
||||||
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
|
||||||
LaunchOptions.Builder launchOptionsBuilder = gameInstance.getLaunchOptions(
|
LaunchOptions.Builder launchOptionsBuilder = gameInstance.getLaunchOptions(
|
||||||
javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null);
|
context.javaRuntime,
|
||||||
|
context.javaExecutable,
|
||||||
|
repository.getBaseDirectory(),
|
||||||
|
javaAgents,
|
||||||
|
javaArguments,
|
||||||
|
scriptFile != null);
|
||||||
if (disableOfflineSkin) {
|
if (disableOfflineSkin) {
|
||||||
launchOptionsBuilder.setDaemon(false);
|
launchOptionsBuilder.setDaemon(false);
|
||||||
}
|
}
|
||||||
@@ -248,7 +255,7 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) {
|
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) {
|
||||||
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(javaVersionRef.get().getBinary());
|
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(context.javaExecutable);
|
||||||
try {
|
try {
|
||||||
WinReg reg = WinReg.INSTANCE;
|
WinReg reg = WinReg.INSTANCE;
|
||||||
if (reg != null) {
|
if (reg != null) {
|
||||||
@@ -283,7 +290,7 @@ public final class LauncherHelper {
|
|||||||
|
|
||||||
return new HMCLGameLauncher(
|
return new HMCLGameLauncher(
|
||||||
gameInstance,
|
gameInstance,
|
||||||
launchManifest.get(),
|
context.launchManifest,
|
||||||
authInfo,
|
authInfo,
|
||||||
launchOptions,
|
launchOptions,
|
||||||
launcherVisibility == LauncherVisibility.CLOSE
|
launcherVisibility == LauncherVisibility.CLOSE
|
||||||
@@ -305,10 +312,10 @@ public final class LauncherHelper {
|
|||||||
if (launcherVisibility == LauncherVisibility.CLOSE)
|
if (launcherVisibility == LauncherVisibility.CLOSE)
|
||||||
Launcher.stopApplication();
|
Launcher.stopApplication();
|
||||||
else
|
else
|
||||||
launchingStepsPane.setCancel(new TaskCancellationAction(it -> {
|
launchingStepsPane.setCancel(it -> {
|
||||||
process.stop();
|
process.stop();
|
||||||
it.fireEvent(new DialogCloseEvent());
|
it.fireEvent(new DialogCloseEvent());
|
||||||
}));
|
});
|
||||||
} else {
|
} else {
|
||||||
runLater(() -> {
|
runLater(() -> {
|
||||||
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
launchingStepsPane.fireEvent(new DialogCloseEvent());
|
||||||
@@ -421,10 +428,11 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
executor.start();
|
launchingStepsPane.startExecutor();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<JavaRuntime> checkGameState(HMCLGameInstance gameInstance, GameSettings.Effective setting, GameInstanceManifest manifest) {
|
/// Checks launch requirements, allowing required Java downloads to share this launch's execution slot.
|
||||||
|
private Task<JavaRuntime> checkGameState(HMCLGameInstance gameInstance, GameSettings.Effective setting, GameInstanceManifest manifest) {
|
||||||
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, gameInstance.getVersion());
|
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, gameInstance.getVersion());
|
||||||
GameVersionNumber gameVersion = gameInstance.getVersion();
|
GameVersionNumber gameVersion = gameInstance.getVersion();
|
||||||
|
|
||||||
@@ -726,13 +734,18 @@ public final class LauncherHelper {
|
|||||||
return task.withStage("launch.state.java");
|
return task.withStage("launch.state.java");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, HMCLGameRepository repository) {
|
/// Prompts for and downloads Java as a required child of this launch operation.
|
||||||
|
private CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, HMCLGameRepository repository) {
|
||||||
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
|
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
|
||||||
Controllers.dialog(new MessageDialogPane.Builder(
|
Controllers.dialog(new MessageDialogPane.Builder(
|
||||||
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
|
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
|
||||||
i18n("message.warning"),
|
i18n("message.warning"),
|
||||||
MessageType.QUESTION)
|
MessageType.QUESTION)
|
||||||
.yesOrNo(() -> {
|
.yesOrNo(() -> {
|
||||||
|
if (!launchingStepsPane.canStartChild()) {
|
||||||
|
future.completeExceptionally(new CancellationException());
|
||||||
|
return;
|
||||||
|
}
|
||||||
DownloadProvider downloadProvider = repository.getDependency().getDownloadProvider();
|
DownloadProvider downloadProvider = repository.getDependency().getDownloadProvider();
|
||||||
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
|
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
|
||||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
@@ -746,7 +759,7 @@ public final class LauncherHelper {
|
|||||||
}
|
}
|
||||||
future.completeExceptionally(new CancellationException());
|
future.completeExceptionally(new CancellationException());
|
||||||
}
|
}
|
||||||
}), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())));
|
}), i18n("download.java"), it -> future.completeExceptionally(new CancellationException()), launchingStepsPane);
|
||||||
}, () -> future.completeExceptionally(new CancellationException())).build());
|
}, () -> future.completeExceptionally(new CancellationException())).build());
|
||||||
|
|
||||||
return future;
|
return future;
|
||||||
|
|||||||
@@ -1,29 +1,16 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2022 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||||
import org.jackhuang.hmcl.ui.instances.ModTranslations;
|
import org.jackhuang.hmcl.addon.AddonProjectInfo;
|
||||||
|
import org.jackhuang.hmcl.addon.AddonProjectInfoRegistry;
|
||||||
import org.jackhuang.hmcl.util.Pair;
|
import org.jackhuang.hmcl.util.Pair;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -51,15 +38,16 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SearchResult search(DownloadProvider downloadProvider, RemoteAddon.Type type, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
public SearchResult search(DownloadProvider downloadProvider, RemoteAddon.Type type, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||||
if (!StringUtils.containsChinese(searchFilter)) {
|
@Nullable AddonProjectInfoRegistry<?> registry = AddonProjectInfoRegistry.getByAddonType(type);
|
||||||
|
if (registry == null || !StringUtils.containsChinese(searchFilter)) {
|
||||||
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> englishSearchFiltersSet = new LinkedHashSet<>(INITIAL_CAPACITY);
|
Set<String> englishSearchFiltersSet = new LinkedHashSet<>(INITIAL_CAPACITY);
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByAddonType(type).searchMod(searchFilter)) {
|
for (AddonProjectInfo mod : registry.search(searchFilter)) {
|
||||||
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName()));
|
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.subName()) ? mod.subName() : mod.mainName()));
|
||||||
if (StringUtils.isNotBlank(englishSearchFilter)) {
|
if (StringUtils.isNotBlank(englishSearchFilter)) {
|
||||||
englishSearchFiltersSet.add(englishSearchFilter);
|
englishSearchFiltersSet.add(englishSearchFilter);
|
||||||
}
|
}
|
||||||
@@ -91,8 +79,8 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(remoteAddon.slug());
|
@Nullable AddonProjectInfo chineseTranslation = registry.getInfoByCurseForgeId(remoteAddon.slug());
|
||||||
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
|
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.mainName()) && StringUtils.containsChinese(chineseTranslation.mainName())) {
|
||||||
searchResultArray[chineseIndex++] = remoteAddon;
|
searchResultArray[chineseIndex++] = remoteAddon;
|
||||||
} else {
|
} else {
|
||||||
searchResultArray[englishIndex--] = remoteAddon;
|
searchResultArray[englishIndex--] = remoteAddon;
|
||||||
@@ -103,11 +91,11 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
|||||||
|
|
||||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||||
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
|
||||||
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByAddonType(type).getModByCurseForgeId(remoteMod.slug());
|
@Nullable AddonProjectInfo chineseRemoteMod = registry.getInfoByCurseForgeId(remoteMod.slug());
|
||||||
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
|
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.mainName()) || !StringUtils.containsChinese(chineseRemoteMod.mainName())) {
|
||||||
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
return Pair.pair(remoteMod, Integer.MAX_VALUE);
|
||||||
}
|
}
|
||||||
String chineseRemoteModName = chineseRemoteMod.getName();
|
String chineseRemoteModName = chineseRemoteMod.mainName();
|
||||||
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
|
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
|
||||||
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
|
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2024 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
@@ -76,7 +61,7 @@ public final class ModpackHelper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
|
MultiMCComponents.setImplementation(Metadata.FULL_NAME + " v" + Metadata.VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import fi.iki.elonen.NanoHTTPD;
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
@@ -146,7 +131,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
|||||||
String html;
|
String html;
|
||||||
try {
|
try {
|
||||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||||
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
|
.replace("%style%", Themes.getColorScheme().toStyleSheet().replace("-monet", "--monet"))
|
||||||
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
.replace("%lang%", Locale.getDefault().toLanguageTag())
|
||||||
.replace("%success%", i18n("message.success"))
|
.replace("%success%", i18n("message.success"))
|
||||||
.replace("%ok%", i18n("button.ok"))
|
.replace("%ok%", i18n("button.ok"))
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.Metadata;
|
||||||
|
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||||
|
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||||
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
|
/// Resolves the routes that the one-click join button can connect through.
|
||||||
|
@NotNullByDefault
|
||||||
|
public final class ServerEndpoint {
|
||||||
|
private ServerEndpoint() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One selectable connection route.
|
||||||
|
///
|
||||||
|
/// @param name the display name
|
||||||
|
/// @param address the `host[:port]` or `ws(s)://` address handed to the game
|
||||||
|
public record Route(String name, String address) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists the routes configured for an instance.
|
||||||
|
///
|
||||||
|
/// Routes in the installed modpack manifest win, so they change with modpack updates. Without
|
||||||
|
/// them the manifest's single `serverAddress` and then [Metadata#getDefaultServerAddress()] are
|
||||||
|
/// used, each as a single route.
|
||||||
|
///
|
||||||
|
/// @param configurationFile the instance's `modpack.json` path
|
||||||
|
/// @return the routes in display order; empty when nothing is configured
|
||||||
|
public static @Unmodifiable List<Route> routes(Path configurationFile) {
|
||||||
|
List<Route> result = new ArrayList<>();
|
||||||
|
@Nullable ServerModpackManifest manifest = readManifest(configurationFile);
|
||||||
|
if (manifest != null) {
|
||||||
|
for (ServerModpackManifest.Route route : manifest.getRoutes()) {
|
||||||
|
if (StringUtils.isNotBlank(route.getAddress())) {
|
||||||
|
result.add(new Route(StringUtils.isBlank(route.getName()) ? route.getAddress().trim() : route.getName(), route.getAddress().trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.isEmpty() && StringUtils.isNotBlank(manifest.getServerAddress())) {
|
||||||
|
result.add(new Route("default", manifest.getServerAddress().trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
String fallback = Metadata.getDefaultServerAddress();
|
||||||
|
if (!fallback.isEmpty()) {
|
||||||
|
result.add(new Route("default", fallback));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Picks the route to join: the one the player chose last if it still exists, otherwise the first.
|
||||||
|
///
|
||||||
|
/// @param configurationFile the instance's `modpack.json` path
|
||||||
|
/// @return the route to connect through, or `null` when nothing is configured
|
||||||
|
public static @Nullable Route selected(Path configurationFile) {
|
||||||
|
List<Route> routes = routes(configurationFile);
|
||||||
|
if (routes.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
@Nullable String saved = readSelectedName(configurationFile);
|
||||||
|
for (Route route : routes) {
|
||||||
|
if (route.name().equals(saved)) {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routes.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remembers the player's route choice next to the instance's modpack configuration.
|
||||||
|
///
|
||||||
|
/// @param configurationFile the instance's `modpack.json` path
|
||||||
|
/// @param route the chosen route
|
||||||
|
public static void select(Path configurationFile, Route route) {
|
||||||
|
try {
|
||||||
|
Files.writeString(selectionFile(configurationFile), route.name(), StandardCharsets.UTF_8);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warning("Unable to save the selected route", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path selectionFile(Path configurationFile) {
|
||||||
|
return configurationFile.resolveSibling("server-route.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable String readSelectedName(Path configurationFile) {
|
||||||
|
try {
|
||||||
|
Path file = selectionFile(configurationFile);
|
||||||
|
return Files.isRegularFile(file) ? Files.readString(file, StandardCharsets.UTF_8).trim() : null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable ServerModpackManifest readManifest(Path configurationFile) {
|
||||||
|
if (!Files.isRegularFile(configurationFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ModpackConfiguration<ServerModpackManifest> configuration =
|
||||||
|
JsonUtils.fromJsonFile(configurationFile, ModpackConfiguration.typeOf(ServerModpackManifest.class));
|
||||||
|
return configuration != null ? configuration.getManifest() : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warning("Unable to read server routes from " + configurationFile, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2024 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.java;
|
package org.jackhuang.hmcl.java;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2025 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.java;
|
package org.jackhuang.hmcl.java;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2024 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.java;
|
package org.jackhuang.hmcl.java;
|
||||||
|
|
||||||
import kala.compress.archivers.ArchiveEntry;
|
import kala.compress.archivers.ArchiveEntry;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.java;
|
package org.jackhuang.hmcl.java;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2024 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.java;
|
package org.jackhuang.hmcl.java;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
@@ -76,8 +61,12 @@ public final class JavaManager {
|
|||||||
"Semeru"
|
"Semeru"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_USER_HOME.resolve("java"));
|
/// Managed Mojang/Disco Java installs for this workspace (beside `.minecraft`), not the user-global HMCL profile.
|
||||||
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java"));
|
public static final HMCLJavaRepository REPOSITORY =
|
||||||
|
new HMCLJavaRepository(Metadata.CURRENT_DIRECTORY.resolve("java"));
|
||||||
|
/// Optional portable installs under the launcher-local `.hmcl` directory.
|
||||||
|
public static final HMCLJavaRepository LOCAL_REPOSITORY =
|
||||||
|
new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java"));
|
||||||
|
|
||||||
public static String getMojangJavaPlatform(Platform platform) {
|
public static String getMojangJavaPlatform(Platform platform) {
|
||||||
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2024 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.java;
|
package org.jackhuang.hmcl.java;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
@@ -68,7 +53,14 @@ public final class Accounts {
|
|||||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||||
public static final List<AccountFactory<?>> FACTORIES = List.of(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
/// Account factories offered when creating a new account from the UI.
|
||||||
|
///
|
||||||
|
/// Primary: Offline and Microsoft. Authlib-injector remains available for external Yggdrasil servers.
|
||||||
|
public static final List<AccountFactory<?>> FACTORIES = List.of(
|
||||||
|
FACTORY_OFFLINE,
|
||||||
|
FACTORY_MICROSOFT,
|
||||||
|
FACTORY_AUTHLIB_INJECTOR
|
||||||
|
);
|
||||||
|
|
||||||
// ==== login type / account factory mapping ====
|
// ==== login type / account factory mapping ====
|
||||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
@@ -41,9 +26,8 @@ import java.util.Objects;
|
|||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@JsonSerializable
|
@JsonSerializable
|
||||||
public final class AuthlibInjectorServerList extends ObservableSetting implements JsonSchemaSetting {
|
public final class AuthlibInjectorServerList extends ObservableSetting implements JsonSchemaSetting {
|
||||||
/// The LittleSkin Yggdrasil API endpoint bundled into newly created server lists.
|
/// The LittleSkin Yggdrasil API endpoint used only when recognizing an existing entry.
|
||||||
public static final String LITTLE_SKIN_URL = "https://littleskin.cn/api/yggdrasil/";
|
public static final String LITTLE_SKIN_URL = "https://littleskin.cn/api/yggdrasil/";
|
||||||
|
|
||||||
/// The JSON schema supported by this authlib-injector server list.
|
/// The JSON schema supported by this authlib-injector server list.
|
||||||
public static final JsonSchema CURRENT_SCHEMA =
|
public static final JsonSchema CURRENT_SCHEMA =
|
||||||
new JsonSchema("authlib-injector-servers", new JsonSchema.Version(1, 0, 0));
|
new JsonSchema("authlib-injector-servers", new JsonSchema.Version(1, 0, 0));
|
||||||
@@ -55,17 +39,10 @@ public final class AuthlibInjectorServerList extends ObservableSetting implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the default authlib-injector server list for a newly created file.
|
/// Creates the default authlib-injector server list for a newly created file.
|
||||||
|
///
|
||||||
|
/// The default list is empty; operators add their own authentication server when needed.
|
||||||
public static AuthlibInjectorServerList createDefault() {
|
public static AuthlibInjectorServerList createDefault() {
|
||||||
AuthlibInjectorServerList result = new AuthlibInjectorServerList();
|
return new AuthlibInjectorServerList();
|
||||||
result.addLittleSkinIfAbsent();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds the bundled LittleSkin server when it is not already present.
|
|
||||||
public void addLittleSkinIfAbsent() {
|
|
||||||
if (getServers().stream().noneMatch(server -> LITTLE_SKIN_URL.equals(server.getUrl()))) {
|
|
||||||
getServers().add(new AuthlibInjectorServer(LITTLE_SKIN_URL));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The schema used by this authlib-injector server list file.
|
/// The schema used by this authlib-injector server list file.
|
||||||
|
|||||||
+2
-17
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
/// @author Glavo
|
/// @author Glavo
|
||||||
|
|||||||
@@ -1,65 +1,29 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import org.glavo.url.WebURL;
|
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||||
import org.jackhuang.hmcl.download.*;
|
import org.jackhuang.hmcl.download.HMCLDownloadProvider;
|
||||||
import org.jackhuang.hmcl.task.FetchTask;
|
import org.jackhuang.hmcl.task.FetchTask;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||||
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
public final class DownloadProviders {
|
public final class DownloadProviders {
|
||||||
private DownloadProviders() {
|
private DownloadProviders() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String DEFAULT_BMCLAPI_ROOT = "https://bmclapi2.bangbang93.com";
|
private static final MirrorRoots MIRROR_ROOTS =
|
||||||
private static final String BMCLAPI_ROOT;
|
new MirrorRoots(System.getProperty("hmcl.bmclapi.override"));
|
||||||
|
|
||||||
static {
|
private static final HMCLDownloadProvider PROVIDER = new HMCLDownloadProvider(MIRROR_ROOTS::getRankedRoots);
|
||||||
@Nullable String bmclOverride = System.getProperty("hmcl.bmclapi.override");
|
|
||||||
if (StringUtils.isBlank(bmclOverride)) {
|
|
||||||
BMCLAPI_ROOT = DEFAULT_BMCLAPI_ROOT;
|
|
||||||
} else {
|
|
||||||
WebURL url;
|
|
||||||
try {
|
|
||||||
url = WebURL.parseBrowserInput(bmclOverride);
|
|
||||||
} catch (Exception e) {
|
|
||||||
url = null;
|
|
||||||
LOG.warning("Invalid BMCLAPI override URL: " + bmclOverride, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url == null) {
|
/// Initializes download provider settings, mirror probing, and download thread settings.
|
||||||
BMCLAPI_ROOT = DEFAULT_BMCLAPI_ROOT;
|
|
||||||
} else {
|
|
||||||
BMCLAPI_ROOT = StringUtils.removeSuffix(url.toString(), "/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final HMCLDownloadProvider PROVIDER = new HMCLDownloadProvider(BMCLAPI_ROOT);
|
|
||||||
|
|
||||||
/// Initializes download provider settings and synchronizes download thread settings.
|
|
||||||
public static void init() {
|
public static void init() {
|
||||||
InvalidationListener onChangeDownloadThreads = observable -> {
|
InvalidationListener onChangeDownloadThreads = observable -> {
|
||||||
FetchTask.setDownloadExecutorConcurrency(settings().autoDownloadThreadsProperty().get()
|
FetchTask.setDownloadExecutorConcurrency(settings().autoDownloadThreadsProperty().get()
|
||||||
@@ -72,13 +36,18 @@ public final class DownloadProviders {
|
|||||||
|
|
||||||
FXUtils.onChangeAndOperate(settings().versionListSourceProperty(), PROVIDER::setVersionListSource);
|
FXUtils.onChangeAndOperate(settings().versionListSourceProperty(), PROVIDER::setVersionListSource);
|
||||||
FXUtils.onChangeAndOperate(settings().fileDownloadSourceProperty(), PROVIDER::setFileSource);
|
FXUtils.onChangeAndOperate(settings().fileDownloadSourceProperty(), PROVIDER::setFileSource);
|
||||||
|
|
||||||
|
MIRROR_ROOTS.maybeProbeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Get current primary preferred download provider
|
||||||
* Get current primary preferred download provider
|
|
||||||
*/
|
|
||||||
public static DownloadProvider getDownloadProvider() {
|
public static DownloadProvider getDownloadProvider() {
|
||||||
|
MIRROR_ROOTS.maybeProbeAsync();
|
||||||
return PROVIDER;
|
return PROVIDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the current ranked BMCLAPI-compatible mirror roots (fastest first).
|
||||||
|
public static @Unmodifiable List<String> getRankedMirrorRoots() {
|
||||||
|
return MIRROR_ROOTS.getRankedRoots();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
public enum EnumCommonDirectory {
|
public enum EnumCommonDirectory {
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2025 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
@@ -97,7 +82,28 @@ public final class FontManager {
|
|||||||
|
|
||||||
private static final ObjectProperty<FontReference> font = new SimpleObjectProperty<>();
|
private static final ObjectProperty<FontReference> font = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
/// Bundled OFL fonts: Noto Sans SC for headings and controls, Noto Serif SC for body text.
|
||||||
|
private static final String[] BUNDLED_FONTS = {
|
||||||
|
"/assets/fonts/NotoSansSC-Regular.ttf",
|
||||||
|
"/assets/fonts/NotoSansSC-Bold.ttf",
|
||||||
|
"/assets/fonts/NotoSerifSC-Regular.ttf"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Registers the bundled fonts with JavaFX so style sheets can refer to them by family name.
|
||||||
|
private static void loadBundledFonts() {
|
||||||
|
for (String resource : BUNDLED_FONTS) {
|
||||||
|
try (var in = FontManager.class.getResourceAsStream(resource)) {
|
||||||
|
if (in == null || Font.loadFont(in, DEFAULT_FONT_SIZE) == null) {
|
||||||
|
LOG.warning("Failed to load bundled font " + resource);
|
||||||
|
}
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
LOG.warning("Failed to load bundled font " + resource, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
loadBundledFonts();
|
||||||
updateFont();
|
updateFont();
|
||||||
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
|
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
@@ -167,12 +152,6 @@ public final class GameDirectoryManager {
|
|||||||
gameDirectories.getGameDirectories().add(new GameDirectory(
|
gameDirectories.getGameDirectories().add(new GameDirectory(
|
||||||
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
|
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
|
||||||
}
|
}
|
||||||
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
|
|
||||||
needRebuildGameDirectories = true;
|
|
||||||
GameDirectories gameDirectories = userGameDirectories();
|
|
||||||
gameDirectories.getGameDirectories().add(new GameDirectory(
|
|
||||||
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
|
|
||||||
}
|
|
||||||
|
|
||||||
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
|
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
|
||||||
if (needRebuildGameDirectories) {
|
if (needRebuildGameDirectories) {
|
||||||
@@ -181,15 +160,13 @@ public final class GameDirectoryManager {
|
|||||||
|
|
||||||
assert !mergedGameDirectories.isEmpty();
|
assert !mergedGameDirectories.isEmpty();
|
||||||
|
|
||||||
@Nullable GameDirectoryID selectedId = settings().selectedGameDirectoryProperty().get();
|
// Dedicated server client: always prefer the fixed workspace `.minecraft` directory.
|
||||||
GameDirectory currentGameDirectory = null;
|
GameDirectory currentGameDirectory = null;
|
||||||
|
for (GameDirectory gameDirectory : mergedGameDirectories) {
|
||||||
if (selectedId != null) {
|
if (DEFAULT_GAME_DIRECTORY_ID.equals(gameDirectory.getId())
|
||||||
for (GameDirectory gameDirectory : mergedGameDirectories) {
|
|| isGameDirectoryPath(gameDirectory, CURRENT_GAME_DIRECTORY_PATH)) {
|
||||||
if (gameDirectory.getId().equals(selectedId)) {
|
currentGameDirectory = gameDirectory;
|
||||||
currentGameDirectory = gameDirectory;
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,16 +207,13 @@ public final class GameDirectoryManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the built-in game directories only when no game directory exists.
|
/// Creates the fixed workspace game directory when no game directory exists.
|
||||||
private static boolean createDefaultGameDirectoriesIfEmpty() {
|
private static boolean createDefaultGameDirectoriesIfEmpty() {
|
||||||
if (localGameDirectories().getGameDirectories().isEmpty()
|
if (localGameDirectories().getGameDirectories().isEmpty()
|
||||||
&& userGameDirectories().getGameDirectories().isEmpty()) {
|
&& userGameDirectories().getGameDirectories().isEmpty()) {
|
||||||
localGameDirectories().getGameDirectories()
|
localGameDirectories().getGameDirectories()
|
||||||
.add(new GameDirectory(
|
.add(new GameDirectory(
|
||||||
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
|
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
|
||||||
userGameDirectories().getGameDirectories()
|
|
||||||
.add(new GameDirectory(
|
|
||||||
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
@@ -37,7 +22,8 @@ public enum GameInstanceIconType {
|
|||||||
NEO_FORGE("/assets/img/neoforge.png"),
|
NEO_FORGE("/assets/img/neoforge.png"),
|
||||||
FURNACE("/assets/img/furnace.png"),
|
FURNACE("/assets/img/furnace.png"),
|
||||||
QUILT("/assets/img/quilt.png"),
|
QUILT("/assets/img/quilt.png"),
|
||||||
APRIL_FOOLS("/assets/img/april_fools.png"),
|
/// Retained for settings migration; UI no longer offers this icon.
|
||||||
|
APRIL_FOOLS("/assets/img/command.png"),
|
||||||
CLEANROOM("/assets/img/cleanroom.png"),
|
CLEANROOM("/assets/img/cleanroom.png"),
|
||||||
LEGACY_FABRIC("/assets/img/legacyfabric.png")
|
LEGACY_FABRIC("/assets/img/legacyfabric.png")
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonDeserializationContext;
|
import com.google.gson.JsonDeserializationContext;
|
||||||
@@ -82,7 +67,7 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
|||||||
public static final class Instance extends GameSettings implements JsonSchemaSetting {
|
public static final class Instance extends GameSettings implements JsonSchemaSetting {
|
||||||
/// The JSON schema supported by instance-specific game settings.
|
/// The JSON schema supported by instance-specific game settings.
|
||||||
public static final JsonSchema CURRENT_SCHEMA =
|
public static final JsonSchema CURRENT_SCHEMA =
|
||||||
new JsonSchema("instance-game-settings", new JsonSchema.Version(1, 0, 0));
|
new JsonSchema("instance-game-settings", new JsonSchema.Version(1, 0, 1));
|
||||||
|
|
||||||
/// Creates an empty instance setting.
|
/// Creates an empty instance setting.
|
||||||
public Instance() {
|
public Instance() {
|
||||||
@@ -388,6 +373,19 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
|||||||
return detectedJava;
|
return detectedJava;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property name for disabling automatic replacement with `javaw.exe` on Windows.
|
||||||
|
public static final String PROPERTY_NOT_USE_JAVAW = "notUseJavaw";
|
||||||
|
|
||||||
|
/// Whether to keep the selected Java executable instead of replacing it with `javaw.exe`; defaults to `false`.
|
||||||
|
@SerializedName(PROPERTY_NOT_USE_JAVAW)
|
||||||
|
private final InheritableProperty<Boolean> notUseJavaw = newInheritableProperty(PROPERTY_NOT_USE_JAVAW, false);
|
||||||
|
|
||||||
|
/// Returns the property disabling automatic replacement of a Windows Java executable with a sibling `javaw.exe` file.
|
||||||
|
/// If `true`, the selected executable is used unchanged. This property has no effect on other runtime platforms.
|
||||||
|
public InheritableProperty<Boolean> notUseJavawProperty() {
|
||||||
|
return notUseJavaw;
|
||||||
|
}
|
||||||
|
|
||||||
/// Property name for customized JVM options.
|
/// Property name for customized JVM options.
|
||||||
public static final String PROPERTY_JVM_OPTIONS = "jvmOptions";
|
public static final String PROPERTY_JVM_OPTIONS = "jvmOptions";
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
@@ -44,7 +29,7 @@ import java.util.Objects;
|
|||||||
public final class GameSettingsPresets extends ObservableSetting implements JsonSchemaSetting {
|
public final class GameSettingsPresets extends ObservableSetting implements JsonSchemaSetting {
|
||||||
/// The JSON schema supported by this game settings preset store.
|
/// The JSON schema supported by this game settings preset store.
|
||||||
public static final JsonSchema CURRENT_SCHEMA =
|
public static final JsonSchema CURRENT_SCHEMA =
|
||||||
new JsonSchema("game-settings", new JsonSchema.Version(1, 0, 0));
|
new JsonSchema("game-settings", new JsonSchema.Version(1, 0, 1));
|
||||||
|
|
||||||
/// Creates an empty game settings preset store.
|
/// Creates an empty game settings preset store.
|
||||||
public GameSettingsPresets() {
|
public GameSettingsPresets() {
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
/// The initial display mode of the game window.
|
/// The initial display mode of the game window.
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2024 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
@@ -40,7 +25,6 @@ import org.jackhuang.hmcl.theme.BackgroundLoadPolicy;
|
|||||||
import org.jackhuang.hmcl.theme.BuiltinBackground;
|
import org.jackhuang.hmcl.theme.BuiltinBackground;
|
||||||
import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
|
import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
|
||||||
import org.jackhuang.hmcl.theme.ThemeColor;
|
import org.jackhuang.hmcl.theme.ThemeColor;
|
||||||
import org.jackhuang.hmcl.theme.ThemeReference;
|
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.*;
|
import org.jackhuang.hmcl.util.gson.*;
|
||||||
@@ -74,7 +58,6 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
|||||||
static final String PROPERTY_SELECTED_INSTANCE = "selectedInstance";
|
static final String PROPERTY_SELECTED_INSTANCE = "selectedInstance";
|
||||||
|
|
||||||
/// Default launcher theme used when no stored theme reference is available.
|
/// Default launcher theme used when no stored theme reference is available.
|
||||||
public static final ThemeReference DEFAULT_THEME_REFERENCE = new ThemeReference("hmcl.default", null);
|
|
||||||
|
|
||||||
/// Theme appearance override key for theme brightness mode.
|
/// Theme appearance override key for theme brightness mode.
|
||||||
public static final String THEME_APPEARANCE_BRIGHTNESS_MODE = "themeBrightnessMode";
|
public static final String THEME_APPEARANCE_BRIGHTNESS_MODE = "themeBrightnessMode";
|
||||||
@@ -192,32 +175,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
|||||||
return language;
|
return language;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether preview builds are accepted by update checks.
|
|
||||||
@SerializedName("acceptPreviewUpdate")
|
|
||||||
private final BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
|
|
||||||
|
|
||||||
/// Returns the preview update opt-in property.
|
|
||||||
public BooleanProperty acceptPreviewUpdateProperty() {
|
|
||||||
return acceptPreviewUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether automatic update dialogs are disabled.
|
|
||||||
@SerializedName("disableAutoShowUpdateDialog")
|
|
||||||
private final BooleanProperty disableAutoShowUpdateDialog = new SimpleBooleanProperty(false);
|
|
||||||
|
|
||||||
/// Returns the automatic update dialog disable property.
|
|
||||||
public BooleanProperty disableAutoShowUpdateDialogProperty() {
|
|
||||||
return disableAutoShowUpdateDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether April Fools features are disabled.
|
|
||||||
@SerializedName("disableAprilFools")
|
|
||||||
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);
|
|
||||||
|
|
||||||
/// Returns the April Fools disable property.
|
|
||||||
public BooleanProperty disableAprilFoolsProperty() {
|
|
||||||
return disableAprilFools;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The common Minecraft directory selection mode.
|
/// The common Minecraft directory selection mode.
|
||||||
@SerializedName("commonDirectoryType")
|
@SerializedName("commonDirectoryType")
|
||||||
@@ -265,23 +223,6 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
|||||||
|
|
||||||
// UI
|
// UI
|
||||||
|
|
||||||
// Theme selection
|
|
||||||
|
|
||||||
/// The installed theme selected by the launcher, or `null` when older settings do not contain a theme reference.
|
|
||||||
@SerializedName("selectedTheme")
|
|
||||||
private final ObjectProperty<@Nullable ThemeReference> selectedTheme =
|
|
||||||
new SimpleObjectProperty<>(DEFAULT_THEME_REFERENCE);
|
|
||||||
|
|
||||||
/// Returns the selected installed theme property.
|
|
||||||
public ObjectProperty<@Nullable ThemeReference> selectedThemeProperty() {
|
|
||||||
return selectedTheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the selected installed theme, falling back to the built-in default theme.
|
|
||||||
public ThemeReference getSelectedThemeOrDefault() {
|
|
||||||
return Objects.requireNonNullElse(selectedTheme.get(), DEFAULT_THEME_REFERENCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme appearance overrides
|
// Theme appearance overrides
|
||||||
|
|
||||||
/// Theme appearance setting keys overridden by the launcher.
|
/// Theme appearance setting keys overridden by the launcher.
|
||||||
@@ -516,7 +457,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
|||||||
|
|
||||||
/// The selected game version list download source.
|
/// The selected game version list download source.
|
||||||
@SerializedName("versionListSource")
|
@SerializedName("versionListSource")
|
||||||
private final ObjectProperty<DownloadSource> versionListSource = new RawPreservingObjectProperty<>(DownloadSource.DEFAULT);
|
private final ObjectProperty<DownloadSource> versionListSource = new RawPreservingObjectProperty<>(DownloadSource.MIRROR);
|
||||||
|
|
||||||
/// Returns the selected game version list download source property.
|
/// Returns the selected game version list download source property.
|
||||||
public ObjectProperty<DownloadSource> versionListSourceProperty() {
|
public ObjectProperty<DownloadSource> versionListSourceProperty() {
|
||||||
@@ -525,7 +466,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
|||||||
|
|
||||||
/// The selected file download source.
|
/// The selected file download source.
|
||||||
@SerializedName("fileDownloadSource")
|
@SerializedName("fileDownloadSource")
|
||||||
private final ObjectProperty<DownloadSource> fileDownloadSource = new RawPreservingObjectProperty<>(DownloadSource.DEFAULT);
|
private final ObjectProperty<DownloadSource> fileDownloadSource = new RawPreservingObjectProperty<>(DownloadSource.MIRROR);
|
||||||
|
|
||||||
/// Returns the selected file download source property.
|
/// Returns the selected file download source property.
|
||||||
public ObjectProperty<DownloadSource> fileDownloadSourceProperty() {
|
public ObjectProperty<DownloadSource> fileDownloadSourceProperty() {
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
@@ -24,15 +9,12 @@ import javafx.beans.property.DoubleProperty;
|
|||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableMap;
|
import javafx.collections.ObservableMap;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
||||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||||
import org.jackhuang.hmcl.util.gson.ObservableSetting;
|
import org.jackhuang.hmcl.util.gson.ObservableSetting;
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -189,25 +171,6 @@ public final class LauncherState extends ObservableSetting implements JsonSchema
|
|||||||
this.height.set(height);
|
this.height.set(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The latest update version that has already prompted the user.
|
|
||||||
@SerializedName("promptedVersion")
|
|
||||||
private final StringProperty promptedVersion = new SimpleStringProperty();
|
|
||||||
|
|
||||||
/// Returns the latest prompted update version.
|
|
||||||
public @Nullable String getPromptedVersion() {
|
|
||||||
return promptedVersion.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the latest prompted update version property.
|
|
||||||
public StringProperty promptedVersionProperty() {
|
|
||||||
return promptedVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the latest prompted update version.
|
|
||||||
public void setPromptedVersion(@Nullable String promptedVersion) {
|
|
||||||
this.promptedVersion.set(promptedVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tip markers that prevent repeated prompts.
|
/// Tip markers that prevent repeated prompts.
|
||||||
@SerializedName("shownTips")
|
@SerializedName("shownTips")
|
||||||
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
@@ -352,7 +337,6 @@ public final class LegacyConfigMigrator {
|
|||||||
JsonObject current = new JsonObject();
|
JsonObject current = new JsonObject();
|
||||||
current.add(JsonSchema.PROPERTY_SCHEMA, JsonUtils.GSON.toJsonTree(UserState.CURRENT_SCHEMA, JsonSchema.class));
|
current.add(JsonSchema.PROPERTY_SCHEMA, JsonUtils.GSON.toJsonTree(UserState.CURRENT_SCHEMA, JsonSchema.class));
|
||||||
copyMember(legacy, current, "agreementVersion");
|
copyMember(legacy, current, "agreementVersion");
|
||||||
copyMember(legacy, current, "terracottaAgreementVersion");
|
|
||||||
copyMember(legacy, current, "platformPromptVersion");
|
copyMember(legacy, current, "platformPromptVersion");
|
||||||
return Objects.requireNonNull(JsonUtils.GSON.fromJson(current, UserState.class));
|
return Objects.requireNonNull(JsonUtils.GSON.fromJson(current, UserState.class));
|
||||||
}
|
}
|
||||||
@@ -411,7 +395,6 @@ public final class LegacyConfigMigrator {
|
|||||||
moveMember(json, state, "height");
|
moveMember(json, state, "height");
|
||||||
migrateLegacyWindowContentSize(state, "width");
|
migrateLegacyWindowContentSize(state, "width");
|
||||||
migrateLegacyWindowContentSize(state, "height");
|
migrateLegacyWindowContentSize(state, "height");
|
||||||
moveMember(json, state, "promptedVersion");
|
|
||||||
moveMember(json, state, "shownTips");
|
moveMember(json, state, "shownTips");
|
||||||
|
|
||||||
LauncherState result = JsonUtils.GSON.fromJson(state, LauncherState.class);
|
LauncherState result = JsonUtils.GSON.fromJson(state, LauncherState.class);
|
||||||
@@ -442,18 +425,12 @@ public final class LegacyConfigMigrator {
|
|||||||
if (authlibInjectorServers != null) {
|
if (authlibInjectorServers != null) {
|
||||||
servers.add("servers", authlibInjectorServers);
|
servers.add("servers", authlibInjectorServers);
|
||||||
}
|
}
|
||||||
JsonElement addedLittleSkin = json.remove("addedLittleSkin");
|
json.remove("addedLittleSkin");
|
||||||
boolean shouldAddLittleSkin = !(addedLittleSkin instanceof JsonPrimitive primitive
|
|
||||||
&& primitive.isBoolean()
|
|
||||||
&& primitive.getAsBoolean());
|
|
||||||
|
|
||||||
AuthlibInjectorServerList result = JsonUtils.GSON.fromJson(servers, AuthlibInjectorServerList.class);
|
AuthlibInjectorServerList result = JsonUtils.GSON.fromJson(servers, AuthlibInjectorServerList.class);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = new AuthlibInjectorServerList();
|
result = new AuthlibInjectorServerList();
|
||||||
}
|
}
|
||||||
if (shouldAddLittleSkin) {
|
|
||||||
result.addLittleSkinIfAbsent();
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
|
import org.glavo.url.WebURL;
|
||||||
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.Unmodifiable;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
|
/// Ranks BMCLAPI-compatible mirror roots by a lightweight latency probe.
|
||||||
|
///
|
||||||
|
/// Game downloads are many small/medium files. Racing the same file across mirrors wastes
|
||||||
|
/// volunteer OpenBMCLAPI bandwidth; ranking entry points and keeping file-level concurrency
|
||||||
|
/// (plus ordered fallback) is the better fit for that workload.
|
||||||
|
@NotNullByDefault
|
||||||
|
public final class MirrorRoots {
|
||||||
|
/// Public BMCLAPI entry points that speak the same path layout.
|
||||||
|
public static final @Unmodifiable List<String> DEFAULT_ROOTS = List.of(
|
||||||
|
"https://bmclapi2.bangbang93.com",
|
||||||
|
"https://bmclapi.bangbang93.com"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final String PROBE_PATH = "/mc/game/version_manifest.json";
|
||||||
|
private static final long REPROBE_INTERVAL_NANOS = 30L * 60L * 1_000_000_000L;
|
||||||
|
|
||||||
|
private final CopyOnWriteArrayList<String> rankedRoots = new CopyOnWriteArrayList<>(DEFAULT_ROOTS);
|
||||||
|
private final AtomicBoolean probing = new AtomicBoolean(false);
|
||||||
|
private volatile long lastProbeNanos;
|
||||||
|
|
||||||
|
/// Creates a ranker seeded with the built-in BMCLAPI roots, or a single override root.
|
||||||
|
public MirrorRoots(@Nullable String overrideRoot) {
|
||||||
|
if (StringUtils.isNotBlank(overrideRoot)) {
|
||||||
|
WebURL url;
|
||||||
|
try {
|
||||||
|
url = WebURL.parseBrowserInput(overrideRoot);
|
||||||
|
} catch (Exception e) {
|
||||||
|
url = null;
|
||||||
|
LOG.warning("Invalid BMCLAPI override URL: " + overrideRoot, e);
|
||||||
|
}
|
||||||
|
if (url != null) {
|
||||||
|
rankedRoots.clear();
|
||||||
|
rankedRoots.add(StringUtils.removeSuffix(url.toString(), "/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current ranked mirror roots (fastest first). Never empty.
|
||||||
|
public @Unmodifiable List<String> getRankedRoots() {
|
||||||
|
List<String> snapshot = List.copyOf(rankedRoots);
|
||||||
|
return snapshot.isEmpty() ? DEFAULT_ROOTS : snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current primary (fastest) mirror root.
|
||||||
|
public String getPrimaryRoot() {
|
||||||
|
return getRankedRoots().get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a background probe when none is running and the previous result is stale.
|
||||||
|
public void maybeProbeAsync() {
|
||||||
|
if (rankedRoots.size() <= 1)
|
||||||
|
return;
|
||||||
|
long now = System.nanoTime();
|
||||||
|
if (now - lastProbeNanos < REPROBE_INTERVAL_NANOS && lastProbeNanos != 0)
|
||||||
|
return;
|
||||||
|
if (!probing.compareAndSet(false, true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Schedulers.io().execute(() -> {
|
||||||
|
try {
|
||||||
|
probeAndRank();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
LOG.warning("Failed to probe download mirrors", t);
|
||||||
|
} finally {
|
||||||
|
lastProbeNanos = System.nanoTime();
|
||||||
|
probing.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void probeAndRank() {
|
||||||
|
List<String> candidates = List.copyOf(rankedRoots);
|
||||||
|
if (candidates.size() <= 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
record Score(String root, long nanos, boolean ok) {
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Score> scores = new ArrayList<>(candidates.size());
|
||||||
|
for (String root : candidates) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
boolean ok = false;
|
||||||
|
try {
|
||||||
|
HttpURLConnection connection = NetworkUtils.createHttpConnection(root + PROBE_PATH);
|
||||||
|
connection.setInstanceFollowRedirects(true);
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setRequestProperty("Range", "bytes=0-0");
|
||||||
|
connection.connect();
|
||||||
|
int code = connection.getResponseCode();
|
||||||
|
// Accept normal OK, partial content, or redirect-resolved success.
|
||||||
|
ok = code >= 200 && code < 400;
|
||||||
|
connection.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.info("Mirror probe failed for " + root + ": " + e);
|
||||||
|
}
|
||||||
|
scores.add(new Score(root, System.nanoTime() - started, ok));
|
||||||
|
}
|
||||||
|
|
||||||
|
scores.sort(Comparator
|
||||||
|
.comparing((Score score) -> !score.ok)
|
||||||
|
.thenComparingLong(Score::nanos));
|
||||||
|
|
||||||
|
List<String> next = scores.stream().map(Score::root).toList();
|
||||||
|
if (!next.equals(candidates)) {
|
||||||
|
rankedRoots.clear();
|
||||||
|
rankedRoots.addAll(next);
|
||||||
|
LOG.info("Ranked download mirrors: " + next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
public final class SambaException extends RuntimeException {
|
public final class SambaException extends RuntimeException {
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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.setting;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
|||||||
@@ -1,32 +1,11 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2025 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.paint.Color;
|
|
||||||
import org.glavo.monetfx.Brightness;
|
|
||||||
import org.glavo.monetfx.ColorRole;
|
|
||||||
import org.glavo.monetfx.ColorScheme;
|
|
||||||
import org.jackhuang.hmcl.theme.ResolvedTheme;
|
|
||||||
import org.jackhuang.hmcl.theme.ThemeColor;
|
|
||||||
import org.jackhuang.hmcl.theme.Themes;
|
import org.jackhuang.hmcl.theme.Themes;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -115,55 +94,14 @@ public final class StyleSheets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String getBrightnessStyleSheet() {
|
private static String getBrightnessStyleSheet() {
|
||||||
return Themes.getColorScheme().getBrightness() == Brightness.LIGHT
|
return "/assets/css/brightness-light.css";
|
||||||
? "/assets/css/brightness-light.css"
|
|
||||||
: "/assets/css/brightness-dark.css";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addColor(StringBuilder builder, String name, Color color) {
|
|
||||||
builder.append(" ").append(name)
|
|
||||||
.append(": ").append(ThemeColor.getColorDisplayName(color)).append(";\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addColor(StringBuilder builder, String name, Color color, double opacity) {
|
|
||||||
builder.append(" ").append(name)
|
|
||||||
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(color, opacity)).append(";\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addColor(StringBuilder builder, ColorScheme scheme, ColorRole role, double opacity) {
|
|
||||||
builder.append(" ").append(role.getVariableName()).append("-transparent-%02d".formatted((int) (100 * opacity)))
|
|
||||||
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(scheme.getColor(role), opacity))
|
|
||||||
.append(";\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the launcher color style sheet.
|
||||||
|
///
|
||||||
|
/// The launcher ships a single warm palette; user-selected themes are no longer applied.
|
||||||
private static String getThemeStyleSheet() {
|
private static String getThemeStyleSheet() {
|
||||||
final String blueCss = "/assets/css/blue.css";
|
return "/assets/css/warm.css";
|
||||||
|
|
||||||
if (ResolvedTheme.DEFAULT.equals(Themes.getTheme()))
|
|
||||||
return blueCss;
|
|
||||||
|
|
||||||
ColorScheme scheme = Themes.getColorScheme();
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.append("* {\n");
|
|
||||||
for (ColorRole colorRole : ColorRole.ALL) {
|
|
||||||
addColor(builder, colorRole.getVariableName(), scheme.getColor(colorRole));
|
|
||||||
}
|
|
||||||
|
|
||||||
addColor(builder, "-monet-primary-seed", scheme.getPrimaryColorSeed());
|
|
||||||
|
|
||||||
addColor(builder, scheme, ColorRole.PRIMARY, 0.5);
|
|
||||||
addColor(builder, scheme, ColorRole.PRIMARY, 0.8);
|
|
||||||
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.5);
|
|
||||||
addColor(builder, scheme, ColorRole.SURFACE, 0.5);
|
|
||||||
addColor(builder, scheme, ColorRole.SURFACE, 0.8);
|
|
||||||
addColor(builder, scheme, ColorRole.ON_SURFACE_VARIANT, 0.38);
|
|
||||||
addColor(builder, scheme, ColorRole.SURFACE_CONTAINER_LOW, 0.8);
|
|
||||||
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.8);
|
|
||||||
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
|
|
||||||
|
|
||||||
builder.append("}\n");
|
|
||||||
return toStyleSheetUri(builder.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init(Scene scene) {
|
public static void init(Scene scene) {
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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;
|
package org.jackhuang.hmcl.setting;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
@@ -121,15 +106,6 @@ public final class UserState extends ObservableSetting implements JsonSchemaSett
|
|||||||
return agreementVersion;
|
return agreementVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The accepted Terracotta agreement version.
|
|
||||||
@SerializedName("terracottaAgreementVersion")
|
|
||||||
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
|
|
||||||
|
|
||||||
/// Returns the accepted Terracotta agreement version property.
|
|
||||||
public IntegerProperty terracottaAgreementVersionProperty() {
|
|
||||||
return terracottaAgreementVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The platform prompt version shown to the user.
|
/// The platform prompt version shown to the user.
|
||||||
@SerializedName("platformPromptVersion")
|
@SerializedName("platformPromptVersion")
|
||||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.property;
|
package org.jackhuang.hmcl.setting.property;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNullByDefault;
|
import org.jetbrains.annotations.NotNullByDefault;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.property;
|
package org.jackhuang.hmcl.setting.property;
|
||||||
|
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
|
|||||||
+2
-17
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.property;
|
package org.jackhuang.hmcl.setting.property;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.setting.GameSettings;
|
import org.jackhuang.hmcl.setting.GameSettings;
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
/*
|
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||||
* Hello Minecraft! Launcher
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||||
* Copyright (C) 2026 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.property;
|
package org.jackhuang.hmcl.setting.property;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
/*
|
|
||||||
* Hello Minecraft! Launcher
|
|
||||||
* Copyright (C) 2025 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.terracotta;
|
|
||||||
|
|
||||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
|
||||||
import org.glavo.url.WebURL;
|
|
||||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
|
||||||
import org.jackhuang.hmcl.task.FetchTask;
|
|
||||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
|
||||||
import org.jackhuang.hmcl.task.Task;
|
|
||||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
|
||||||
import org.jackhuang.hmcl.util.DigestUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
|
||||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.UrlResponseInfo;
|
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
|
||||||
import org.jackhuang.hmcl.util.tree.TarFileTree;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.security.DigestInputStream;
|
|
||||||
import java.security.DigestOutputStream;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.HexFormat;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|
||||||
|
|
||||||
public final class TerracottaBundle {
|
|
||||||
private final Path root;
|
|
||||||
|
|
||||||
/// Candidate bundle URLs in attempt order.
|
|
||||||
private final List<WebURL> links;
|
|
||||||
|
|
||||||
private final FileDownloadTask.IntegrityCheck hash;
|
|
||||||
|
|
||||||
private final Map<String, FileDownloadTask.IntegrityCheck> files;
|
|
||||||
|
|
||||||
/// Creates a bundle using the supplied download candidates and archive/file integrity metadata.
|
|
||||||
/// The candidate list and file map are retained without copying.
|
|
||||||
public TerracottaBundle(Path root, List<WebURL> links, FileDownloadTask.IntegrityCheck hash, Map<String, FileDownloadTask.IntegrityCheck> files) {
|
|
||||||
this.root = root;
|
|
||||||
this.links = links;
|
|
||||||
this.hash = hash;
|
|
||||||
this.files = files;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Path> download(AbstractTerracottaProvider.DownloadContext context) {
|
|
||||||
return Task.supplyAsync(() -> Files.createTempFile("terracotta-", ".tar.gz"))
|
|
||||||
.thenComposeAsync(Schedulers.javafx(), pkg -> {
|
|
||||||
FileDownloadTask download = new FileDownloadTask(links, pkg, hash) {
|
|
||||||
@Override
|
|
||||||
protected Context getContext(@Nullable UrlResponseInfo response, boolean checkETag, @Nullable String bmclapiHash) throws IOException {
|
|
||||||
FetchTask.Context delegate = super.getContext(response, checkETag, bmclapiHash);
|
|
||||||
return new Context() {
|
|
||||||
@Override
|
|
||||||
public void withResult(boolean success) {
|
|
||||||
super.withResult(success);
|
|
||||||
delegate.withResult(success);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reset() throws IOException {
|
|
||||||
delegate.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte[] buffer, int offset, int len) throws IOException {
|
|
||||||
context.checkCancellation();
|
|
||||||
delegate.write(buffer, offset, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
if (isSuccess()) {
|
|
||||||
context.checkCancellation();
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
context.bindProgress(download.progressProperty());
|
|
||||||
return download.thenSupplyAsync(() -> pkg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<?> install(Path pkg) {
|
|
||||||
return Task.runAsync(() -> {
|
|
||||||
Files.createDirectories(root);
|
|
||||||
FileUtils.cleanDirectory(root);
|
|
||||||
|
|
||||||
try (TarFileTree tree = TarFileTree.open(pkg)) {
|
|
||||||
for (Map.Entry<String, FileDownloadTask.IntegrityCheck> entry : files.entrySet()) {
|
|
||||||
String file = entry.getKey();
|
|
||||||
FileDownloadTask.IntegrityCheck check = entry.getValue();
|
|
||||||
|
|
||||||
Path path = root.resolve(file);
|
|
||||||
TarArchiveEntry archive = tree.getEntry("/" + file);
|
|
||||||
if (archive == null) {
|
|
||||||
throw new ArtifactMalformedException(String.format("Expecting %s file in terracotta bundle.", file));
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDigest digest = DigestUtils.getDigest(check.algorithm());
|
|
||||||
try (
|
|
||||||
InputStream is = tree.getInputStream(archive);
|
|
||||||
OutputStream os = new DigestOutputStream(Files.newOutputStream(path), digest)
|
|
||||||
) {
|
|
||||||
is.transferTo(os);
|
|
||||||
}
|
|
||||||
|
|
||||||
String hash = HexFormat.of().formatHex(digest.digest());
|
|
||||||
if (!check.checksum().equalsIgnoreCase(hash)) {
|
|
||||||
throw new ChecksumMismatchException(check.algorithm(), check.checksum(), hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (OperatingSystem.CURRENT_OS) {
|
|
||||||
case LINUX, MACOS, FREEBSD -> Files.setPosixFilePermissions(path, FileUtils.parsePosixFilePermission(archive.getMode()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).whenComplete(exception -> {
|
|
||||||
if (exception != null) {
|
|
||||||
FileUtils.deleteDirectory(root);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path locate(String file) {
|
|
||||||
FileDownloadTask.IntegrityCheck check = files.get(file);
|
|
||||||
if (check == null) {
|
|
||||||
throw new AssertionError(String.format("Expecting %s file in terracotta bundle.", file));
|
|
||||||
}
|
|
||||||
return root.resolve(file).toAbsolutePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractTerracottaProvider.Status status() throws IOException {
|
|
||||||
if (Files.exists(root) && isLocalBundleValid()) {
|
|
||||||
return AbstractTerracottaProvider.Status.READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (TerracottaMetadata.hasLegacyVersionFiles()) {
|
|
||||||
return AbstractTerracottaProvider.Status.LEGACY_VERSION;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warning("Cannot determine whether legacy versions exist.", e);
|
|
||||||
}
|
|
||||||
return AbstractTerracottaProvider.Status.NOT_EXIST;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isLocalBundleValid() throws IOException { // FIXME: Make control flow clearer.
|
|
||||||
long total = 0;
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
|
|
||||||
for (Map.Entry<String, FileDownloadTask.IntegrityCheck> entry : files.entrySet()) {
|
|
||||||
Path path = root.resolve(entry.getKey());
|
|
||||||
FileDownloadTask.IntegrityCheck check = entry.getValue();
|
|
||||||
if (!Files.isReadable(path)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDigest digest = DigestUtils.getDigest(check.algorithm());
|
|
||||||
try (InputStream is = new DigestInputStream(Files.newInputStream(path), digest)) {
|
|
||||||
int n;
|
|
||||||
while ((n = is.read(buffer)) >= 0) {
|
|
||||||
total += n;
|
|
||||||
if (total >= 50 * 1024 * 1024) { // >=50MB
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!HexFormat.of().formatHex(digest.digest()).equalsIgnoreCase(check.checksum())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 1077 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user