Compare commits
145
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 |
No files matched your search
@@ -59,6 +59,9 @@ Jenkinsfile text eol=lf
|
||||
|
||||
# Minecraft schematic
|
||||
*.litematic binary
|
||||
*.nbt binary
|
||||
*.schem binary
|
||||
*.schematic binary
|
||||
|
||||
# Compiled / intermediate build artefacts
|
||||
*.bin binary
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.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:
|
||||
build:
|
||||
|
||||
@@ -23,8 +34,26 @@ jobs:
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Build with Gradle
|
||||
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel
|
||||
# Only main writes the dependency cache; pull requests read it.
|
||||
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:
|
||||
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -16,16 +14,7 @@ permissions:
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
if: false
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v7
|
||||
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 });
|
||||
- name: Disabled
|
||||
run: echo "pr-size-label is disabled on this fork"
|
||||
@@ -77,3 +77,6 @@ language-subtag-registry
|
||||
!/.gemini/config.yaml
|
||||
.codex
|
||||
/.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">
|
||||
<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="myName" value="HMCL" />
|
||||
</copyright>
|
||||
<copyright>
|
||||
<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" />
|
||||
</copyright>
|
||||
</component>
|
||||
Generated
+1
@@ -3,6 +3,7 @@
|
||||
<LanguageOptions name="JAVA">
|
||||
<option name="fileTypeOverride" value="3" />
|
||||
<option name="addBlankAfter" value="false" />
|
||||
<option name="block" value="false" />
|
||||
</LanguageOptions>
|
||||
</settings>
|
||||
</component>
|
||||
+29
-55
@@ -1,4 +1,3 @@
|
||||
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
|
||||
@@ -11,6 +10,7 @@ import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
||||
import org.jackhuang.hmcl.gradle.pack.ReleaseType
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||
import org.jackhuang.hmcl.gradle.utils.OfficialBuildValueSource
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
@@ -155,33 +155,28 @@ tasks.compileJava {
|
||||
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
|
||||
}
|
||||
|
||||
val hmclProperties = buildList {
|
||||
add("hmcl.version" to project.version.toString())
|
||||
add("hmcl.add-opens" to addOpens.joinToString(" "))
|
||||
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 createPropertiesFile = tasks.register<WriteProperties>("createPropertiesFile") {
|
||||
destinationFile.set(layout.buildDirectory.file("hmcl.properties"))
|
||||
encoding = "UTF-8"
|
||||
|
||||
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
|
||||
val createPropertiesFile = tasks.register("createPropertiesFile") {
|
||||
outputs.file(hmclPropertiesFile)
|
||||
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
|
||||
|
||||
doLast {
|
||||
val targetFile = hmclPropertiesFile.get().asFile
|
||||
targetFile.parentFile.mkdir()
|
||||
targetFile.bufferedWriter().use {
|
||||
for ((k, v) in hmclProperties) {
|
||||
it.write("$k=$v\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
property("hmcl.version", project.version.toString())
|
||||
property("hmcl.add-opens", addOpens.joinToString(" "))
|
||||
System.getenv("GITHUB_SHA")?.let { property("hmcl.version.hash", it) }
|
||||
property("hmcl.version.type", versionType)
|
||||
property("hmcl.microsoft.auth.id", microsoftAuthId)
|
||||
property("hmcl.curseforge.apikey", curseForgeApiKey)
|
||||
property("hmcl.authlib-injector.version", libs.authlib.injector.get().version!!)
|
||||
property("hmcl.lwjgl-unsafe-agent.version", libs.lwjgl.unsafe.agent.get().version!!)
|
||||
property("hmcl.official", providers.of(OfficialBuildValueSource::class) {
|
||||
parameters.publicKeyFile.set(layout.projectDirectory.file("src/main/resources/assets/hmcl_signature_publickey.der"))
|
||||
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 {
|
||||
@@ -215,7 +210,7 @@ tasks.shadowJar {
|
||||
}
|
||||
|
||||
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(),
|
||||
"Main-Class" to "org.jackhuang.hmcl.Main",
|
||||
"Multi-Release" to "true",
|
||||
@@ -237,13 +232,12 @@ tasks.shadowJar {
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn(createPropertiesFile)
|
||||
dependsOn(upsideDownTranslate)
|
||||
dependsOn(createLocaleNamesResourceBundle)
|
||||
dependsOn(createLanguageList)
|
||||
|
||||
into("assets/") {
|
||||
from(hmclPropertiesFile)
|
||||
from(createPropertiesFile.flatMap { it.destinationFile })
|
||||
from(embedResources)
|
||||
}
|
||||
|
||||
@@ -253,10 +247,6 @@ tasks.processResources {
|
||||
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)
|
||||
@@ -367,6 +357,11 @@ fun parseToolOptions(options: String?): MutableList<String> {
|
||||
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
|
||||
tasks.withType<JavaExec> {
|
||||
if (name != "run") {
|
||||
@@ -405,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
|
||||
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -133,7 +133,7 @@ public class JFXDatePickerContent extends VBox {
|
||||
setText(null);
|
||||
|
||||
if (Objects.equals(oldItem, item) && oldEmpty == empty) return;
|
||||
ripplerContainer.releaseRippleImmediately();
|
||||
ripplerContainer.resetFeedback();
|
||||
|
||||
if (!empty && item != null) {
|
||||
setGraphic(root);
|
||||
|
||||
@@ -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 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
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.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.TLSSecurity;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
@@ -42,6 +28,7 @@ public final class EntryPoint {
|
||||
private EntryPoint() {
|
||||
}
|
||||
|
||||
/// Initializes TLS protection before downloading dependencies and starting the launcher.
|
||||
public static void main(String[] args) {
|
||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||
@@ -49,6 +36,7 @@ public final class EntryPoint {
|
||||
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_LOCAL_HOME.resolve("logs"));
|
||||
TLSSecurity.install();
|
||||
|
||||
checkWine();
|
||||
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
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.FXUtils;
|
||||
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.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
@@ -142,7 +125,6 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
});
|
||||
@@ -280,11 +262,6 @@ public final class Launcher extends Application {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
LOG.shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
|
||||
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("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("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
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 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_SUPPORTED_JAVA_VERSION = 17;
|
||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||
|
||||
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 int RECOMMENDED_JAVA_VERSION = 25;
|
||||
|
||||
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 GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||
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() {
|
||||
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 {
|
||||
EnumSet<Architecture> supportedArchitectures;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
@@ -162,4 +141,18 @@ public final class Metadata {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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;
|
||||
@@ -31,11 +16,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -379,7 +360,7 @@ public abstract class AddonProjectInfoRegistry<I extends AddonProjectInfo> {
|
||||
|| ".+\\".indexOf(ch) >= 0;
|
||||
}
|
||||
|
||||
/// Removes supported separators and pictographs from a secondary name.
|
||||
/// 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))
|
||||
@@ -404,8 +385,7 @@ public abstract class AddonProjectInfoRegistry<I extends AddonProjectInfo> {
|
||||
if (isSupportedSubnameChar(ch)) {
|
||||
builder.appendCodePoint(ch);
|
||||
} else if (!(Character.isWhitespace(ch)
|
||||
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0
|
||||
|| ch >= 0x1F300 && ch <= 0x1FAFF)) {
|
||||
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0)) {
|
||||
return "";
|
||||
}
|
||||
offset += Character.charCount(ch);
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.addon.update;
|
||||
|
||||
import org.jackhuang.hmcl.addon.LocalAddonFile;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
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.setting.DownloadSource;
|
||||
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.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
private volatile DownloadSource versionListSource = DownloadSource.DEFAULT;
|
||||
private volatile DownloadSource fileSource = DownloadSource.DEFAULT;
|
||||
private volatile DownloadSource versionListSource = DownloadSource.MIRROR;
|
||||
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(
|
||||
DownloadSource source,
|
||||
WebURL defaultUrl, WebURL bmclapiUrl
|
||||
WebURL officialUrl,
|
||||
List<WebURL> mirrorUrls
|
||||
) {
|
||||
if (LocaleUtils.IS_CHINA_MAINLAND) {
|
||||
return switch (source) {
|
||||
case DEFAULT, OFFICIAL -> DownloadCandidates.of(defaultUrl, bmclapiUrl);
|
||||
case MIRROR -> DownloadCandidates.of(bmclapiUrl, defaultUrl);
|
||||
};
|
||||
if (preferMirror(source)) {
|
||||
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
|
||||
candidates.addAll(spreadPrimary(mirrorUrls.stream().map(DownloadCandidate::of).toList(), officialUrl.href()));
|
||||
candidates.add(DownloadCandidate.of(officialUrl));
|
||||
return DownloadCandidates.of(List.copyOf(candidates));
|
||||
} else {
|
||||
return switch (source) {
|
||||
case DEFAULT, OFFICIAL -> DownloadCandidates.of(defaultUrl);
|
||||
case MIRROR -> DownloadCandidates.of(bmclapiUrl, defaultUrl);
|
||||
};
|
||||
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
|
||||
candidates.add(DownloadCandidate.of(officialUrl));
|
||||
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) {
|
||||
if (urls.isEmpty()) {
|
||||
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
|
||||
@Nullable Boolean mirrorFirst;
|
||||
if (LocaleUtils.IS_CHINA_MAINLAND) {
|
||||
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
|
||||
mirrorFirst = switch (source) {
|
||||
case DEFAULT, MIRROR -> true;
|
||||
case OFFICIAL -> false;
|
||||
@@ -82,7 +100,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
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) {
|
||||
DownloadCandidate candidate = DownloadCandidate.of(url);
|
||||
if (candidate.url() == null) {
|
||||
@@ -91,79 +109,114 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
continue;
|
||||
}
|
||||
|
||||
@Nullable DownloadCandidate mirrorCandidate = null;
|
||||
boolean fallback = false;
|
||||
boolean added = false;
|
||||
for (MirrorRule rule : rules) {
|
||||
if (url.startsWith(rule.source)) {
|
||||
mirrorCandidate = DownloadCandidate.of(rule.target + url.substring(rule.source.length()));
|
||||
fallback = rule.fallback;
|
||||
@Nullable List<DownloadCandidate> list = rule.getCandidates(candidate, mirrorFirst);
|
||||
if (list != null) {
|
||||
candidates.addAll(list);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mirrorCandidate == null) {
|
||||
if (!added)
|
||||
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));
|
||||
}
|
||||
|
||||
private final String bmclRoot;
|
||||
private final Supplier<@Unmodifiable List<String>> mirrorRoots;
|
||||
private final List<MirrorRule> rules;
|
||||
|
||||
public HMCLDownloadProvider(String bmclRoot) {
|
||||
this.bmclRoot = bmclRoot;
|
||||
public HMCLDownloadProvider(Supplier<@Unmodifiable List<String>> mirrorRoots) {
|
||||
this.mirrorRoots = Objects.requireNonNull(mirrorRoots);
|
||||
this.rules = List.of(
|
||||
new MirrorRule("https://bmclapi2.bangbang93.com", bmclRoot),
|
||||
new MirrorRule("https://launchermeta.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://piston-meta.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://piston-data.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://launcher.mojang.com", bmclRoot),
|
||||
new MirrorRule("https://libraries.minecraft.net", bmclRoot + "/libraries"),
|
||||
new MirrorRule("http://files.minecraftforge.net/maven", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://files.minecraftforge.net/maven", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://maven.minecraftforge.net", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://maven.neoforged.net/releases/", bmclRoot + "/maven/"),
|
||||
new MirrorRule("http://dl.liteloader.com/versions", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://dl.liteloader.com/versions", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://meta.fabricmc.net", bmclRoot + "/fabric-meta"),
|
||||
new MirrorRule("https://maven.fabricmc.net", bmclRoot + "/maven"),
|
||||
new MirrorRule("https://authlib-injector.yushi.moe", bmclRoot + "/mirrors/authlib-injector"),
|
||||
new MirrorRule("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||
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"),
|
||||
new MirrorRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
||||
new MirrorRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
new MultiRootRule("https://bmclapi2.bangbang93.com", Function.identity()),
|
||||
new MultiRootRule("https://bmclapi.bangbang93.com", Function.identity()),
|
||||
new MultiRootRule("https://launchermeta.mojang.com", Function.identity()),
|
||||
new MultiRootRule("https://piston-meta.mojang.com", Function.identity()),
|
||||
new MultiRootRule("https://piston-data.mojang.com", Function.identity()),
|
||||
new MultiRootRule("https://launcher.mojang.com", Function.identity()),
|
||||
new MultiRootRule("https://libraries.minecraft.net", root -> root + "/libraries"),
|
||||
new MultiRootRule("http://files.minecraftforge.net/maven", root -> root + "/maven"),
|
||||
new MultiRootRule("https://files.minecraftforge.net/maven", root -> root + "/maven"),
|
||||
new MultiRootRule("https://maven.minecraftforge.net", root -> root + "/maven"),
|
||||
(source, mirrorFirst) -> {
|
||||
String neoforgedMaven = "https://maven.neoforged.net/";
|
||||
String neoforgedMavenRelease = "https://maven.neoforged.net/releases/";
|
||||
|
||||
if (source.url() == null || !source.url().href().startsWith(neoforgedMaven))
|
||||
return null;
|
||||
|
||||
var candidates = new ArrayList<DownloadCandidate>();
|
||||
List<String> roots = mirrorRoots.get();
|
||||
|
||||
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
|
||||
new MirrorRule("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth", true),
|
||||
new MirrorRule("https://cdn.modrinth.com", "https://mod.mcimirror.top", true),
|
||||
new MirrorRule("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge", true),
|
||||
new MirrorRule("https://edge.forgecdn.net", "https://mod.mcimirror.top", true)
|
||||
new SimpleRule("https://api.modrinth.com", null, "https://mod.mcimirror.top/modrinth"),
|
||||
new SimpleRule("https://cdn.modrinth.com", null, "https://mod.mcimirror.top"),
|
||||
new SimpleRule("https://api.curseforge.com", null, "https://mod.mcimirror.top/curseforge"),
|
||||
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
|
||||
protected Task<? extends ComponentRemoteVersionList<?>> fetchVersionsAsync(GameComponentType type, @Nullable GameVersionNumber gameVersion) {
|
||||
assert (gameVersion == null) == (type == GameComponentType.GAME);
|
||||
String bmclRoot = primaryMirrorRoot();
|
||||
switch (type) {
|
||||
case FORGE -> {
|
||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchOfficial = ForgeRemoteVersion.fetchAsync(this, gameVersion);
|
||||
|
||||
DownloadSource source = versionListSource;
|
||||
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
|
||||
if (source == DownloadSource.OFFICIAL) {
|
||||
return fetchOfficial;
|
||||
}
|
||||
|
||||
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchBMCL = ForgeRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
|
||||
if (source == DownloadSource.MIRROR) {
|
||||
if (preferMirror(source)) {
|
||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
||||
} else {
|
||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
||||
@@ -173,11 +226,11 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchOfficial = NeoForgeRemoteVersion.fetchAsync(this, gameVersion);
|
||||
|
||||
DownloadSource source = versionListSource;
|
||||
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
|
||||
if (source == DownloadSource.OFFICIAL) {
|
||||
return fetchOfficial;
|
||||
}
|
||||
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchBMCL = NeoForgeRemoteVersion.fetchBMCLAsync(bmclRoot, gameVersion);
|
||||
if (source == DownloadSource.MIRROR || source == DownloadSource.DEFAULT) {
|
||||
if (preferMirror(source)) {
|
||||
return new FallbackTask<>(fetchBMCL, fetchOfficial);
|
||||
} else {
|
||||
return new FallbackTask<>(fetchOfficial, fetchBMCL);
|
||||
@@ -209,7 +262,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
return getCandidates(
|
||||
versionListSource,
|
||||
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() {
|
||||
DownloadSource source = versionListSource;
|
||||
return new FabricLikeVersionListCandidates(
|
||||
getCandidates(source, FabricRemoteVersion.LOADER_META_URL, WebURL.parse(bmclRoot + "/fabric-meta/v2/versions/loader")),
|
||||
getCandidates(source, FabricRemoteVersion.GAME_META_URL, WebURL.parse(bmclRoot + "/fabric-meta/v2/versions/game"))
|
||||
getCandidates(source, FabricRemoteVersion.LOADER_META_URL, mirrorUrls("/fabric-meta/v2/versions/loader")),
|
||||
getCandidates(source, FabricRemoteVersion.GAME_META_URL, mirrorUrls("/fabric-meta/v2/versions/game"))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
@@ -241,10 +294,11 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
|
||||
@Override
|
||||
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
|
||||
String location = assetObject.getLocation();
|
||||
return getCandidates(
|
||||
fileSource,
|
||||
WebURL.parse("https://resources.download.minecraft.net/" + assetObject.getLocation()),
|
||||
WebURL.parse(bmclRoot + "/assets/" + assetObject.getLocation())
|
||||
WebURL.parse("https://resources.download.minecraft.net/" + location),
|
||||
mirrorUrls("/assets/" + location)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,10 +341,73 @@ public final class HMCLDownloadProvider extends DownloadProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private record MirrorRule(String source, String target, boolean fallback) {
|
||||
public MirrorRule(String source, String target) {
|
||||
this(source, target, false);
|
||||
@NotNullByDefault
|
||||
@FunctionalInterface
|
||||
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 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.download.DefaultCacheRepository;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.download.DefaultGameBuilder;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonObject;
|
||||
@@ -470,9 +455,9 @@ public class HMCLGameInstance extends DefaultGameInstance {
|
||||
return GameInstanceIconType.OPTIFINE.getIcon();
|
||||
|
||||
GameVersionNumber version = getVersion();
|
||||
if (version.isAprilFools())
|
||||
return GameInstanceIconType.APRIL_FOOLS.getIcon();
|
||||
else if (version instanceof GameVersionNumber.LegacySnapshot || version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
|
||||
if (version instanceof GameVersionNumber.LegacySnapshot
|
||||
|| version.isAprilFools()
|
||||
|| version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
|
||||
return GameInstanceIconType.COMMAND.getIcon();
|
||||
else if (version instanceof GameVersionNumber.Old)
|
||||
return GameInstanceIconType.CRAFT_TABLE.getIcon();
|
||||
@@ -582,7 +567,7 @@ public class HMCLGameInstance extends DefaultGameInstance {
|
||||
maxMemory = noJVMOptions
|
||||
? null
|
||||
: Math.toIntExact(HMCLGameRepository.getAutoAllocatedMemory(
|
||||
SystemInfo.getPhysicalMemoryStatus().available(),
|
||||
SystemInfo.getPhysicalMemoryStatus(),
|
||||
javaRuntime.getPlatform()) / 1024L / 1024L);
|
||||
} else {
|
||||
maxMemory = vs.getMaxMemory();
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -106,6 +91,12 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
if (gameVersion == GameVersionNumber.unknown() || gameVersion.compareTo("1.1") < 0)
|
||||
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);
|
||||
if (lang.isEmpty())
|
||||
return;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
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.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Bits;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -511,23 +498,43 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public static long getAutoAllocatedMemory(long available, Platform platform) {
|
||||
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
|
||||
if (usable <= 0) {
|
||||
return available;
|
||||
public static long getAutoAllocatedMemory(PhysicalMemoryStatus memoryStatus, Platform platform) {
|
||||
final long minHeapSize;
|
||||
final long maxHeapSize;
|
||||
|
||||
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
|
||||
final long suggested;
|
||||
if (usable <= threshold)
|
||||
// Reserve memory for off-heap memory and HMCL itself
|
||||
final long reserved = 256 * 1024 * 1024; // 256 MiB
|
||||
|
||||
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);
|
||||
else if (usable <= threshold2)
|
||||
suggested = (long) (threshold1 * 0.8 + (usable - threshold1) * 0.4);
|
||||
else
|
||||
suggested = Math.min(
|
||||
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
|
||||
16L * 1024 * 1024 * 1024);
|
||||
return platform.getBits() == Bits.BIT_32
|
||||
? Math.min(suggested, 768 * 1024 * 1024) // https://github.com/HMCL-dev/HMCL/issues/6638
|
||||
: suggested;
|
||||
suggested = (long) (threshold1 * 0.8 + (threshold2 - threshold1) * 0.4 + (usable - threshold2) * 0.2);
|
||||
|
||||
return MathUtils.clamp(suggested & ~0xFFFFFL, minHeapSize, maxHeapSize);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
@@ -443,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());
|
||||
GameVersionNumber gameVersion = gameInstance.getVersion();
|
||||
|
||||
@@ -748,13 +734,18 @@ public final class LauncherHelper {
|
||||
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<>();
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION)
|
||||
.yesOrNo(() -> {
|
||||
if (!launchingStepsPane.canStartChild()) {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
return;
|
||||
}
|
||||
DownloadProvider downloadProvider = repository.getDependency().getDownloadProvider();
|
||||
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
@@ -768,7 +759,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
future.completeExceptionally(new CancellationException());
|
||||
}
|
||||
}), i18n("download.java"), it -> future.completeExceptionally(new CancellationException()));
|
||||
}), i18n("download.java"), it -> future.completeExceptionally(new CancellationException()), launchingStepsPane);
|
||||
}, () -> future.completeExceptionally(new CancellationException())).build());
|
||||
|
||||
return future;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
@@ -76,7 +61,7 @@ public final class ModpackHelper {
|
||||
);
|
||||
|
||||
static {
|
||||
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
|
||||
MultiMCComponents.setImplementation(Metadata.FULL_NAME + " v" + Metadata.VERSION);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
@@ -146,7 +131,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
String html;
|
||||
try {
|
||||
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("%success%", i18n("message.success"))
|
||||
.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 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import kala.compress.archivers.ArchiveEntry;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
@@ -76,8 +61,12 @@ public final class JavaManager {
|
||||
"Semeru"
|
||||
};
|
||||
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_USER_HOME.resolve("java"));
|
||||
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java"));
|
||||
/// Managed Mojang/Disco Java installs for this workspace (beside `.minecraft`), not the user-global HMCL profile.
|
||||
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) {
|
||||
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonObject;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonArray;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
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 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 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 ====
|
||||
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.annotations.JsonAdapter;
|
||||
@@ -41,9 +26,8 @@ import java.util.Objects;
|
||||
@NotNullByDefault
|
||||
@JsonSerializable
|
||||
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/";
|
||||
|
||||
/// The JSON schema supported by this authlib-injector server list.
|
||||
public static final JsonSchema CURRENT_SCHEMA =
|
||||
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.
|
||||
///
|
||||
/// The default list is empty; operators add their own authentication server when needed.
|
||||
public static AuthlibInjectorServerList createDefault() {
|
||||
AuthlibInjectorServerList result = 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));
|
||||
}
|
||||
return new AuthlibInjectorServerList();
|
||||
}
|
||||
|
||||
/// The schema used by this authlib-injector server list file.
|
||||
|
||||
+2
-17
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonParseException;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
/// @author Glavo
|
||||
|
||||
@@ -1,65 +1,29 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.download.*;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.HMCLDownloadProvider;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class DownloadProviders {
|
||||
private DownloadProviders() {
|
||||
}
|
||||
|
||||
private static final String DEFAULT_BMCLAPI_ROOT = "https://bmclapi2.bangbang93.com";
|
||||
private static final String BMCLAPI_ROOT;
|
||||
private static final MirrorRoots MIRROR_ROOTS =
|
||||
new MirrorRoots(System.getProperty("hmcl.bmclapi.override"));
|
||||
|
||||
static {
|
||||
@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);
|
||||
}
|
||||
private static final HMCLDownloadProvider PROVIDER = new HMCLDownloadProvider(MIRROR_ROOTS::getRankedRoots);
|
||||
|
||||
if (url == null) {
|
||||
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.
|
||||
/// Initializes download provider settings, mirror probing, and download thread settings.
|
||||
public static void init() {
|
||||
InvalidationListener onChangeDownloadThreads = observable -> {
|
||||
FetchTask.setDownloadExecutorConcurrency(settings().autoDownloadThreadsProperty().get()
|
||||
@@ -72,13 +36,18 @@ public final class DownloadProviders {
|
||||
|
||||
FXUtils.onChangeAndOperate(settings().versionListSourceProperty(), PROVIDER::setVersionListSource);
|
||||
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() {
|
||||
MIRROR_ROOTS.maybeProbeAsync();
|
||||
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 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public enum EnumCommonDirectory {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -97,7 +82,28 @@ public final class FontManager {
|
||||
|
||||
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 {
|
||||
loadBundledFonts();
|
||||
updateFont();
|
||||
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
|
||||
}
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -167,12 +152,6 @@ public final class GameDirectoryManager {
|
||||
gameDirectories.getGameDirectories().add(new GameDirectory(
|
||||
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();
|
||||
if (needRebuildGameDirectories) {
|
||||
@@ -181,15 +160,13 @@ public final class GameDirectoryManager {
|
||||
|
||||
assert !mergedGameDirectories.isEmpty();
|
||||
|
||||
@Nullable GameDirectoryID selectedId = settings().selectedGameDirectoryProperty().get();
|
||||
// Dedicated server client: always prefer the fixed workspace `.minecraft` directory.
|
||||
GameDirectory currentGameDirectory = null;
|
||||
|
||||
if (selectedId != null) {
|
||||
for (GameDirectory gameDirectory : mergedGameDirectories) {
|
||||
if (gameDirectory.getId().equals(selectedId)) {
|
||||
currentGameDirectory = gameDirectory;
|
||||
break;
|
||||
}
|
||||
for (GameDirectory gameDirectory : mergedGameDirectories) {
|
||||
if (DEFAULT_GAME_DIRECTORY_ID.equals(gameDirectory.getId())
|
||||
|| isGameDirectoryPath(gameDirectory, CURRENT_GAME_DIRECTORY_PATH)) {
|
||||
currentGameDirectory = gameDirectory;
|
||||
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() {
|
||||
if (localGameDirectories().getGameDirectories().isEmpty()
|
||||
&& userGameDirectories().getGameDirectories().isEmpty()) {
|
||||
localGameDirectories().getGameDirectories()
|
||||
.add(new GameDirectory(
|
||||
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;
|
||||
} else {
|
||||
return false;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
@@ -37,7 +22,8 @@ public enum GameInstanceIconType {
|
||||
NEO_FORGE("/assets/img/neoforge.png"),
|
||||
FURNACE("/assets/img/furnace.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"),
|
||||
LEGACY_FABRIC("/assets/img/legacyfabric.png")
|
||||
;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonDeserializationContext;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
/// The initial display mode of the game window.
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jackhuang.hmcl.util.gson.JsonSchema;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonObject;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
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.NetworkBackgroundImageCachePolicy;
|
||||
import org.jackhuang.hmcl.theme.ThemeColor;
|
||||
import org.jackhuang.hmcl.theme.ThemeReference;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
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";
|
||||
|
||||
/// 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.
|
||||
public static final String THEME_APPEARANCE_BRIGHTNESS_MODE = "themeBrightnessMode";
|
||||
@@ -192,32 +175,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
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.
|
||||
@SerializedName("commonDirectoryType")
|
||||
@@ -265,23 +223,6 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
|
||||
// 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 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.
|
||||
@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.
|
||||
public ObjectProperty<DownloadSource> versionListSourceProperty() {
|
||||
@@ -525,7 +466,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
|
||||
|
||||
/// The selected file download source.
|
||||
@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.
|
||||
public ObjectProperty<DownloadSource> fileDownloadSourceProperty() {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.annotations.JsonAdapter;
|
||||
@@ -24,15 +9,12 @@ import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableMap;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.ObservableSetting;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -189,25 +171,6 @@ public final class LauncherState extends ObservableSetting implements JsonSchema
|
||||
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.
|
||||
@SerializedName("shownTips")
|
||||
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.*;
|
||||
@@ -352,7 +337,6 @@ public final class LegacyConfigMigrator {
|
||||
JsonObject current = new JsonObject();
|
||||
current.add(JsonSchema.PROPERTY_SCHEMA, JsonUtils.GSON.toJsonTree(UserState.CURRENT_SCHEMA, JsonSchema.class));
|
||||
copyMember(legacy, current, "agreementVersion");
|
||||
copyMember(legacy, current, "terracottaAgreementVersion");
|
||||
copyMember(legacy, current, "platformPromptVersion");
|
||||
return Objects.requireNonNull(JsonUtils.GSON.fromJson(current, UserState.class));
|
||||
}
|
||||
@@ -411,7 +395,6 @@ public final class LegacyConfigMigrator {
|
||||
moveMember(json, state, "height");
|
||||
migrateLegacyWindowContentSize(state, "width");
|
||||
migrateLegacyWindowContentSize(state, "height");
|
||||
moveMember(json, state, "promptedVersion");
|
||||
moveMember(json, state, "shownTips");
|
||||
|
||||
LauncherState result = JsonUtils.GSON.fromJson(state, LauncherState.class);
|
||||
@@ -442,18 +425,12 @@ public final class LegacyConfigMigrator {
|
||||
if (authlibInjectorServers != null) {
|
||||
servers.add("servers", authlibInjectorServers);
|
||||
}
|
||||
JsonElement addedLittleSkin = json.remove("addedLittleSkin");
|
||||
boolean shouldAddLittleSkin = !(addedLittleSkin instanceof JsonPrimitive primitive
|
||||
&& primitive.isBoolean()
|
||||
&& primitive.getAsBoolean());
|
||||
json.remove("addedLittleSkin");
|
||||
|
||||
AuthlibInjectorServerList result = JsonUtils.GSON.fromJson(servers, AuthlibInjectorServerList.class);
|
||||
if (result == null) {
|
||||
result = new AuthlibInjectorServerList();
|
||||
}
|
||||
if (shouldAddLittleSkin) {
|
||||
result.addLittleSkinIfAbsent();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonElement;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 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 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonArray;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public final class SambaException extends RuntimeException {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
@@ -1,32 +1,11 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
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 java.nio.charset.StandardCharsets;
|
||||
@@ -115,55 +94,14 @@ public final class StyleSheets {
|
||||
}
|
||||
|
||||
private static String getBrightnessStyleSheet() {
|
||||
return Themes.getColorScheme().getBrightness() == Brightness.LIGHT
|
||||
? "/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");
|
||||
return "/assets/css/brightness-light.css";
|
||||
}
|
||||
|
||||
/// Returns the launcher color style sheet.
|
||||
///
|
||||
/// The launcher ships a single warm palette; user-selected themes are no longer applied.
|
||||
private static String getThemeStyleSheet() {
|
||||
final String blueCss = "/assets/css/blue.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());
|
||||
return "/assets/css/warm.css";
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// 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 com.google.gson.JsonParseException;
|
||||
@@ -121,15 +106,6 @@ public final class UserState extends ObservableSetting implements JsonSchemaSett
|
||||
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.
|
||||
@SerializedName("platformPromptVersion")
|
||||
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting.property;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting.property;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
|
||||
+2
-17
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting.property;
|
||||
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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/>.
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.setting.property;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,376 +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 com.google.gson.JsonObject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class TerracottaManager {
|
||||
private TerracottaManager() {
|
||||
}
|
||||
|
||||
private static final AtomicReference<TerracottaState> STATE_V = new AtomicReference<>(TerracottaState.Bootstrap.INSTANCE);
|
||||
private static final ReadOnlyObjectWrapper<TerracottaState> STATE = new ReadOnlyObjectWrapper<>(STATE_V.getPlain());
|
||||
private static final InvocationDispatcher<TerracottaState> STATE_D = InvocationDispatcher.runOn(Platform::runLater, STATE::set);
|
||||
|
||||
static {
|
||||
Schedulers.io().execute(() -> {
|
||||
try {
|
||||
if (TerracottaMetadata.PROVIDER == null)
|
||||
throw new IOException("Unsupported platform: " + org.jackhuang.hmcl.util.platform.Platform.CURRENT_PLATFORM);
|
||||
|
||||
switch (TerracottaMetadata.PROVIDER.status()) {
|
||||
case NOT_EXIST -> setState(new TerracottaState.Uninitialized(false));
|
||||
case LEGACY_VERSION -> setState(new TerracottaState.Uninitialized(true));
|
||||
case READY -> launch(setState(new TerracottaState.Launching()), false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot initialize Terracotta.", e);
|
||||
compareAndSet(TerracottaState.Bootstrap.INSTANCE, new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static ReadOnlyObjectProperty<TerracottaState> stateProperty() {
|
||||
return STATE.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static final Thread DAEMON = Lang.thread(TerracottaManager::runBackground, "Terracotta Background Daemon", true);
|
||||
|
||||
@FXThread // Written in FXThread, read-only on background daemon
|
||||
private static volatile boolean daemonRunning = false;
|
||||
|
||||
private static void runBackground() {
|
||||
final long ACTIVE = TimeUnit.MILLISECONDS.toNanos(500);
|
||||
final long BACKGROUND = TimeUnit.SECONDS.toMillis(15);
|
||||
|
||||
while (true) {
|
||||
if (daemonRunning) {
|
||||
LockSupport.parkNanos(ACTIVE);
|
||||
} else {
|
||||
long deadline = System.currentTimeMillis() + BACKGROUND;
|
||||
do {
|
||||
LockSupport.parkUntil(deadline);
|
||||
} while (!daemonRunning && System.currentTimeMillis() < deadline - 100);
|
||||
}
|
||||
|
||||
if (!(STATE_V.get() instanceof TerracottaState.PortSpecific state)) {
|
||||
continue;
|
||||
}
|
||||
int port = state.port;
|
||||
int index = state instanceof TerracottaState.Ready ready ? ready.index : Integer.MIN_VALUE;
|
||||
|
||||
TerracottaState next;
|
||||
try {
|
||||
TerracottaState.Ready object = HttpRequest.GET(String.format("http://127.0.0.1:%d/state", port))
|
||||
.retry(5)
|
||||
.getJson(TerracottaState.Ready.class);
|
||||
if (object.index <= index) {
|
||||
continue;
|
||||
}
|
||||
object.port = port;
|
||||
next = object;
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Cannot fetch state from Terracotta.", e);
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
|
||||
compareAndSet(state, next);
|
||||
}
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static void switchDaemon(boolean active) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
boolean dr = daemonRunning;
|
||||
if (dr != active) {
|
||||
daemonRunning = active;
|
||||
if (active) {
|
||||
LockSupport.unpark(DAEMON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AbstractTerracottaProvider getProvider() {
|
||||
AbstractTerracottaProvider provider = TerracottaMetadata.PROVIDER;
|
||||
if (provider == null) {
|
||||
throw new AssertionError("Terracotta Provider must NOT be null.");
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static boolean isInvalidBundle(Path file) {
|
||||
return !FileUtils.getName(file).equalsIgnoreCase(TerracottaMetadata.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState.Preparing download() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState.Preparing preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1), true);
|
||||
|
||||
Task.composeAsync(() -> getProvider().download(preparing))
|
||||
.thenComposeAsync(pkg -> {
|
||||
if (!preparing.requestInstallFence()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getProvider().install(pkg).thenRunAsync(() -> {
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching, true);
|
||||
}
|
||||
});
|
||||
}).whenComplete(exception -> {
|
||||
if (exception instanceof CancellationException) {
|
||||
// no-op
|
||||
} else if (exception instanceof DownloadException) {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.NETWORK));
|
||||
} else {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
}
|
||||
}).start();
|
||||
|
||||
return compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState.Preparing install(Path bundle) {
|
||||
FXUtils.checkFxUserThread();
|
||||
if (isInvalidBundle(bundle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
TerracottaState.Preparing preparing;
|
||||
if (state instanceof TerracottaState.Preparing previousPreparing && previousPreparing.requestInstallFence()) {
|
||||
preparing = previousPreparing;
|
||||
} else if (state instanceof TerracottaState.Uninitialized || state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable()) {
|
||||
preparing = new TerracottaState.Preparing(new ReadOnlyDoubleWrapper(-1), false);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
Task.composeAsync(() -> getProvider().install(bundle))
|
||||
.thenRunAsync(() -> {
|
||||
TerracottaState.Launching launching = new TerracottaState.Launching();
|
||||
if (compareAndSet(preparing, launching)) {
|
||||
launch(launching, true);
|
||||
}
|
||||
})
|
||||
.whenComplete(exception -> {
|
||||
compareAndSet(preparing, new TerracottaState.Fatal(TerracottaState.Fatal.Type.INSTALL));
|
||||
}).start();
|
||||
|
||||
return state != preparing && compareAndSet(state, preparing) ? preparing : null;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
public static TerracottaState recover() {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (!(state instanceof TerracottaState.Fatal && ((TerracottaState.Fatal) state).isRecoverable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// FIXME: A temporary limit has been employed in TerracottaBundle#checkExisting, making
|
||||
// hash check accept 50MB at most. Calling it on JavaFX should be safe.
|
||||
return switch (getProvider().status()) {
|
||||
case NOT_EXIST, LEGACY_VERSION -> download();
|
||||
case READY -> {
|
||||
TerracottaState.Launching launching = setState(new TerracottaState.Launching());
|
||||
launch(launching, false);
|
||||
yield launching;
|
||||
}
|
||||
};
|
||||
} catch (RuntimeException | IOException e) {
|
||||
LOG.warning("Cannot determine Terracotta state.", e);
|
||||
return setState(new TerracottaState.Fatal(TerracottaState.Fatal.Type.UNKNOWN));
|
||||
}
|
||||
}
|
||||
|
||||
private static void launch(TerracottaState.Launching state, boolean removeLegacy) {
|
||||
Task.supplyAsync(() -> {
|
||||
Path path = Files.createTempDirectory(String.format("hmcl-terracotta-%d", ThreadLocalRandom.current().nextLong())).resolve("http").toAbsolutePath();
|
||||
ManagedProcess process = new ManagedProcess(new ProcessBuilder(getProvider().ofCommandLine(path)));
|
||||
process.pumpInputStream(SystemUtils::onLogLine);
|
||||
process.pumpErrorStream(SystemUtils::onLogLine);
|
||||
|
||||
long exitTime = -1;
|
||||
while (true) {
|
||||
if (Files.exists(path)) {
|
||||
JsonObject object = JsonUtils.fromNonNullJson(Files.readString(path), JsonObject.class);
|
||||
return object.get("port").getAsInt();
|
||||
}
|
||||
|
||||
if (!process.isRunning()) {
|
||||
if (exitTime == -1) {
|
||||
exitTime = System.currentTimeMillis();
|
||||
} else if (System.currentTimeMillis() - exitTime >= 10000) {
|
||||
throw new IllegalStateException(String.format("Process has exited for 10s, code = %s", process.getExitCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).whenComplete(Schedulers.javafx(), (port, exception) -> {
|
||||
TerracottaState next;
|
||||
if (exception == null) {
|
||||
next = new TerracottaState.Unknown(port);
|
||||
if (removeLegacy) {
|
||||
TerracottaMetadata.removeLegacyVersionFiles();
|
||||
}
|
||||
} else {
|
||||
next = new TerracottaState.Fatal(TerracottaState.Fatal.Type.TERRACOTTA);
|
||||
}
|
||||
compareAndSet(state, next);
|
||||
}).start();
|
||||
}
|
||||
|
||||
public static Task<String> exportLogs() {
|
||||
if (STATE_V.get() instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return new GetTask(WebURL.parse(String.format("http://127.0.0.1:%d/log?fetch=true", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
public static TerracottaState.Waiting setWaiting() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
new GetTask(WebURL.parse(String.format("http://127.0.0.1:%d/state/ide", portSpecific.port)))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.start();
|
||||
return new TerracottaState.Waiting(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String getPlayerName() {
|
||||
Account account = Accounts.getSelectedAccount();
|
||||
return account != null ? account.getProfileName() : i18n("terracotta.player_anonymous");
|
||||
}
|
||||
|
||||
public static TerracottaState.HostScanning setScanning() {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
List<Pair<String, String>> query = new ArrayList<>(nodes.size() + 1);
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (WebURL node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery(
|
||||
"http://127.0.0.1:%d/state/scanning".formatted(portSpecific.port), query
|
||||
)).setSignificance(Task.TaskSignificance.MINOR);
|
||||
}).start();
|
||||
|
||||
return new TerracottaState.HostScanning(-1, -1, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Task<TerracottaState.GuestConnecting> setGuesting(String room) {
|
||||
TerracottaState state = STATE_V.get();
|
||||
if (state instanceof TerracottaState.PortSpecific portSpecific) {
|
||||
return Task.supplyAsync(Schedulers.io(), TerracottaNodeList::fetch)
|
||||
.thenComposeAsync(nodes -> {
|
||||
ArrayList<Pair<String, String>> query = new ArrayList<>(nodes.size() + 2);
|
||||
query.add(pair("room", room));
|
||||
query.add(pair("player", getPlayerName()));
|
||||
for (WebURL node : nodes) {
|
||||
query.add(pair("public_nodes", node.toString()));
|
||||
}
|
||||
return new GetTask(NetworkUtils.withQuery("http://127.0.0.1:%d/state/guesting".formatted(portSpecific.port), query))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenSupplyAsync(() -> new TerracottaState.GuestConnecting(-1, -1, null))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends TerracottaState> T setState(T value) {
|
||||
if (value == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
STATE_V.set(value);
|
||||
STATE_D.accept(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static boolean compareAndSet(TerracottaState previous, TerracottaState next) {
|
||||
if (next == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (STATE_V.compareAndSet(previous, next)) {
|
||||
STATE_D.accept(next);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,205 +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 com.google.gson.annotations.SerializedName;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.GeneralProvider;
|
||||
import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionRange;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class TerracottaMetadata {
|
||||
private TerracottaMetadata() {
|
||||
}
|
||||
|
||||
private record Options(String version, String classifier) {
|
||||
public String replace(String value) {
|
||||
return value.replace("${version}", version).replace("${classifier}", classifier);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
public record Link(
|
||||
@SerializedName("desc") LocalizedText description,
|
||||
@SerializedName("link") String link
|
||||
) {
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record Package(
|
||||
@SerializedName("hash") String hash,
|
||||
@SerializedName("files") Map<String, String> files
|
||||
) {
|
||||
}
|
||||
|
||||
@JsonSerializable
|
||||
private record Config(
|
||||
@SerializedName("version_latest") String latest,
|
||||
|
||||
@SerializedName("packages") Map<String, Package> pkgs,
|
||||
@SerializedName("downloads") List<String> downloads,
|
||||
@SerializedName("downloads_CN") List<String> downloadsCN,
|
||||
@SerializedName("links") List<Link> links
|
||||
) {
|
||||
private @Nullable TerracottaBundle resolve(Options options) {
|
||||
Package pkg = pkgs.get(options.classifier);
|
||||
if (pkg == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Stream<String> stream = downloads.stream(), streamCN = downloadsCN.stream();
|
||||
@Unmodifiable List<WebURL> links = (LocaleUtils.IS_CHINA_MAINLAND ? Stream.concat(streamCN, stream) : Stream.concat(stream, streamCN))
|
||||
.map(link -> WebURL.parse(options.replace(link)))
|
||||
.toList();
|
||||
|
||||
Map<String, FileDownloadTask.IntegrityCheck> files = pkg.files.entrySet().stream().collect(Collectors.toUnmodifiableMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> new FileDownloadTask.IntegrityCheck("SHA-512", entry.getValue())
|
||||
));
|
||||
|
||||
return new TerracottaBundle(
|
||||
Metadata.DEPENDENCIES_DIRECTORY.resolve(options.replace("terracotta/${version}")).toAbsolutePath(),
|
||||
links, new FileDownloadTask.IntegrityCheck("SHA-512", pkg.hash),
|
||||
files
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static final AbstractTerracottaProvider PROVIDER;
|
||||
public static final String PACKAGE_NAME;
|
||||
public static final List<Link> PACKAGE_LINKS;
|
||||
public static final String FEEDBACK_LINK = NetworkUtils.withQuery("https://docs.hmcl.net/multiplayer/feedback.html", Map.of(
|
||||
"v", "v1",
|
||||
"launcher_version", Metadata.VERSION
|
||||
));
|
||||
|
||||
private static final String LATEST;
|
||||
|
||||
static {
|
||||
Config config;
|
||||
try (InputStream is = TerracottaMetadata.class.getResourceAsStream("/assets/terracotta.json")) {
|
||||
config = JsonUtils.fromNonNullJsonFully(is, Config.class);
|
||||
} catch (IOException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
|
||||
LATEST = config.latest;
|
||||
|
||||
Options options = new Options(config.latest, OperatingSystem.CURRENT_OS.getCheckedName() + "-" + Architecture.SYSTEM_ARCH.getCheckedName());
|
||||
TerracottaBundle bundle = config.resolve(options);
|
||||
AbstractTerracottaProvider provider;
|
||||
if (bundle == null || (provider = locateProvider(bundle, options)) == null) {
|
||||
PROVIDER = null;
|
||||
PACKAGE_NAME = null;
|
||||
PACKAGE_LINKS = null;
|
||||
} else {
|
||||
PROVIDER = provider;
|
||||
PACKAGE_NAME = options.replace("terracotta-${version}-${classifier}-pkg.tar.gz");
|
||||
|
||||
List<Link> packageLinks = config.links.stream()
|
||||
.map(link -> new Link(link.description, options.replace(link.link)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.shuffle(packageLinks);
|
||||
PACKAGE_LINKS = Collections.unmodifiableList(packageLinks);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static AbstractTerracottaProvider locateProvider(TerracottaBundle bundle, Options options) {
|
||||
String prefix = options.replace("terracotta-${version}-${classifier}");
|
||||
|
||||
// FIXME: As HMCL is a cross-platform application, developers may mistakenly locate
|
||||
// non-existent files in non-native platform logic without assertion errors during debugging.
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> {
|
||||
if (!OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_10))
|
||||
yield null;
|
||||
|
||||
yield new GeneralProvider(bundle, bundle.locate(prefix + ".exe"));
|
||||
}
|
||||
case LINUX, FREEBSD -> new GeneralProvider(bundle, bundle.locate(prefix));
|
||||
case MACOS -> new MacOSProvider(
|
||||
bundle, bundle.locate(prefix), bundle.locate(prefix + ".pkg")
|
||||
);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static void removeLegacyVersionFiles() {
|
||||
try (DirectoryStream<Path> terracotta = collectLegacyVersionFiles()) {
|
||||
if (terracotta == null)
|
||||
return;
|
||||
|
||||
for (Path path : terracotta) {
|
||||
try {
|
||||
FileUtils.deleteDirectory(path);
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Unable to remove legacy terracotta files: %s", path), e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Unable to remove legacy terracotta files.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasLegacyVersionFiles() throws IOException {
|
||||
try (DirectoryStream<Path> terracotta = collectLegacyVersionFiles()) {
|
||||
return terracotta != null && terracotta.iterator().hasNext();
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable DirectoryStream<Path> collectLegacyVersionFiles() throws IOException {
|
||||
Path terracottaDir = Metadata.DEPENDENCIES_DIRECTORY.resolve("terracotta");
|
||||
if (Files.notExists(terracottaDir))
|
||||
return null;
|
||||
|
||||
VersionRange<VersionNumber> range = VersionNumber.atMost(LATEST);
|
||||
return Files.newDirectoryStream(terracottaDir, path -> {
|
||||
String name = FileUtils.getName(path);
|
||||
return !LATEST.equals(name) && range.contains(VersionNumber.asVersion(name));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* 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.terracotta;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.glavo.url.WebURL;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class TerracottaNodeList {
|
||||
private static final String NODE_LIST_URL = "https://terracotta.glavo.site/nodes";
|
||||
|
||||
@JsonSerializable
|
||||
private record TerracottaNode(String url, @Nullable String region) implements Validation {
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
Validation.requireNonNull(url, "TerracottaNode.url cannot be null");
|
||||
try {
|
||||
WebURL.parse(url);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new JsonParseException("Invalid URL: " + url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cached node URLs, or `null` before the first fetch completes.
|
||||
private static volatile @Nullable @Unmodifiable List<WebURL> list;
|
||||
|
||||
/// Returns the cached unmodifiable node URLs, fetching once and caching an empty list on failure.
|
||||
public static @Unmodifiable List<WebURL> fetch() {
|
||||
@Nullable @Unmodifiable List<WebURL> list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
synchronized (TerracottaNodeList.class) {
|
||||
list = TerracottaNodeList.list;
|
||||
if (list != null)
|
||||
return list;
|
||||
|
||||
try {
|
||||
List<TerracottaNode> nodes = HttpRequest.GET(NODE_LIST_URL)
|
||||
.getJson(JsonUtils.listTypeOf(TerracottaNode.class));
|
||||
|
||||
if (nodes == null) {
|
||||
list = List.of();
|
||||
LOG.info("No available Terracotta nodes found");
|
||||
} else {
|
||||
list = nodes.stream()
|
||||
.filter(node -> {
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
try {
|
||||
node.validate();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Invalid terracotta node: " + node, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
|
||||
})
|
||||
.map(it -> WebURL.parse(it.url()))
|
||||
.toList();
|
||||
LOG.info("Terracotta node list: " + list);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to fetch terracotta node list", e);
|
||||
list = List.of();
|
||||
}
|
||||
|
||||
TerracottaNodeList.list = list;
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private TerracottaNodeList() {
|
||||
}
|
||||
}
|
||||
@@ -1,333 +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 com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.value.ObservableDoubleValue;
|
||||
import org.jackhuang.hmcl.terracotta.profile.TerracottaProfile;
|
||||
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSubtype;
|
||||
import org.jackhuang.hmcl.util.gson.JsonType;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract sealed class TerracottaState {
|
||||
protected TerracottaState() {
|
||||
}
|
||||
|
||||
public boolean isUIFakeState() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final class Bootstrap extends TerracottaState {
|
||||
static final Bootstrap INSTANCE = new Bootstrap();
|
||||
|
||||
private Bootstrap() {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Uninitialized extends TerracottaState {
|
||||
private final boolean hasLegacy;
|
||||
|
||||
Uninitialized(boolean hasLegacy) {
|
||||
this.hasLegacy = hasLegacy;
|
||||
}
|
||||
|
||||
public boolean hasLegacy() {
|
||||
return hasLegacy;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Preparing extends TerracottaState implements AbstractTerracottaProvider.DownloadContext {
|
||||
private final ReadOnlyDoubleWrapper progress;
|
||||
|
||||
private final AtomicBoolean hasInstallFence;
|
||||
|
||||
Preparing(ReadOnlyDoubleWrapper progress, boolean hasInstallFence) {
|
||||
this.progress = progress;
|
||||
this.hasInstallFence = new AtomicBoolean(hasInstallFence);
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty progressProperty() {
|
||||
return progress.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public boolean requestInstallFence() {
|
||||
return hasInstallFence.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
public boolean hasInstallFence() {
|
||||
return hasInstallFence.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindProgress(ObservableDoubleValue progress) {
|
||||
this.progress.bind(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkCancellation() {
|
||||
if (!hasInstallFence()) {
|
||||
throw new CancellationException("User has installed terracotta from local archives.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Launching extends TerracottaState {
|
||||
Launching() {
|
||||
}
|
||||
}
|
||||
|
||||
static abstract sealed class PortSpecific extends TerracottaState {
|
||||
transient int port;
|
||||
|
||||
protected PortSpecific(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonType(
|
||||
property = "state",
|
||||
subtypes = {
|
||||
@JsonSubtype(clazz = Waiting.class, name = "waiting"),
|
||||
@JsonSubtype(clazz = HostScanning.class, name = "host-scanning"),
|
||||
@JsonSubtype(clazz = HostStarting.class, name = "host-starting"),
|
||||
@JsonSubtype(clazz = HostOK.class, name = "host-ok"),
|
||||
@JsonSubtype(clazz = GuestConnecting.class, name = "guest-connecting"),
|
||||
@JsonSubtype(clazz = GuestStarting.class, name = "guest-starting"),
|
||||
@JsonSubtype(clazz = GuestOK.class, name = "guest-ok"),
|
||||
@JsonSubtype(clazz = Exception.class, name = "exception"),
|
||||
}
|
||||
)
|
||||
static abstract sealed class Ready extends PortSpecific {
|
||||
@SerializedName("index")
|
||||
final int index;
|
||||
|
||||
@SerializedName("state")
|
||||
private final String state;
|
||||
|
||||
Ready(int port, int index, String state) {
|
||||
super(port);
|
||||
this.index = index;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUIFakeState() {
|
||||
return this.index == -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Unknown extends PortSpecific {
|
||||
Unknown(int port) {
|
||||
super(port);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Waiting extends Ready {
|
||||
Waiting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostScanning extends Ready {
|
||||
HostScanning(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostStarting extends Ready {
|
||||
HostStarting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class HostOK extends Ready implements Validation {
|
||||
@SerializedName("room")
|
||||
private final String code;
|
||||
|
||||
@SerializedName("profile_index")
|
||||
private final int profileIndex;
|
||||
|
||||
@SerializedName("profiles")
|
||||
private final List<TerracottaProfile> profiles;
|
||||
|
||||
HostOK(int port, int index, String state, String code, int profileIndex, List<TerracottaProfile> profiles) {
|
||||
super(port, index, state);
|
||||
this.code = code;
|
||||
this.profileIndex = profileIndex;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (code == null) {
|
||||
throw new JsonParseException("code is null");
|
||||
}
|
||||
if (profiles == null) {
|
||||
throw new JsonParseException("profiles is null");
|
||||
}
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public List<TerracottaProfile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return state instanceof HostOK hostOK && this.index - hostOK.index <= profileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestConnecting extends Ready {
|
||||
GuestConnecting(int port, int index, String state) {
|
||||
super(port, index, state);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestStarting extends Ready {
|
||||
public enum Difficulty {
|
||||
UNKNOWN,
|
||||
EASIEST,
|
||||
SIMPLE,
|
||||
MEDIUM,
|
||||
TOUGH
|
||||
}
|
||||
|
||||
@SerializedName("difficulty")
|
||||
private final Difficulty difficulty;
|
||||
|
||||
GuestStarting(int port, int index, String state, Difficulty difficulty) {
|
||||
super(port, index, state);
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
|
||||
public Difficulty getDifficulty() {
|
||||
return difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GuestOK extends Ready implements Validation {
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
@SerializedName("profile_index")
|
||||
private final int profileIndex;
|
||||
|
||||
@SerializedName("profiles")
|
||||
private final List<TerracottaProfile> profiles;
|
||||
|
||||
GuestOK(int port, int index, String state, String url, int profileIndex, List<TerracottaProfile> profiles) {
|
||||
super(port, index, state);
|
||||
this.url = url;
|
||||
this.profileIndex = profileIndex;
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (profiles == null) {
|
||||
throw new JsonParseException("profiles is null");
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public List<TerracottaProfile> getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isForkOf(TerracottaState state) {
|
||||
return state instanceof GuestOK guestOK && this.index - guestOK.index <= profileIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Exception extends Ready implements Validation {
|
||||
public enum Type {
|
||||
PING_HOST_FAIL,
|
||||
PING_HOST_RST,
|
||||
GUEST_ET_CRASH,
|
||||
HOST_ET_CRASH,
|
||||
PING_SERVER_RST,
|
||||
SCAFFOLDING_INVALID_RESPONSE
|
||||
}
|
||||
|
||||
private static final TerracottaState.Exception.Type[] LOOKUP = Type.values();
|
||||
|
||||
@SerializedName("type")
|
||||
private final int type;
|
||||
|
||||
Exception(int port, int index, String state, int type) {
|
||||
super(port, index, state);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (type < 0 || type >= LOOKUP.length) {
|
||||
throw new JsonParseException(String.format("Type must between [0, %s)", LOOKUP.length));
|
||||
}
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return LOOKUP[type];
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Fatal extends TerracottaState {
|
||||
public enum Type {
|
||||
OS,
|
||||
NETWORK,
|
||||
INSTALL,
|
||||
TERRACOTTA,
|
||||
UNKNOWN;
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
|
||||
public Fatal(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isRecoverable() {
|
||||
return this.type != Type.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1066 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user