Compare commits

..
151 changed files with 1348 additions and 3851 deletions

No files matched your search

@@ -1,11 +1,13 @@
name: Check Translations
name: Check Codes
on:
push:
paths:
- '**.java'
- '**.properties'
pull_request:
paths:
- '**.java'
- '**.properties'
jobs:
@@ -19,5 +21,5 @@ jobs:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style test
run: ./gradlew checkTranslations --no-daemon
- name: Check Codes
run: ./gradlew checkstyleMain checkstyleTest checkTranslations --no-daemon --parallel
-25
View File
@@ -1,25 +0,0 @@
name: Check Style
on:
push:
paths:
- '**.java'
pull_request:
paths:
- '**.java'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style main
run: ./gradlew checkstyleMain --no-daemon
- name: Check style test
run: ./gradlew checkstyleTest --no-daemon
+1
View File
@@ -18,6 +18,7 @@ minecraft-exported-crash-info*
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLBoot/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/buildSrc/build/
+26 -42
View File
@@ -1,7 +1,4 @@
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -15,44 +12,39 @@ plugins {
alias(libs.plugins.shadow)
}
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
.isNullOrEmpty())
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
if (buildNumber != null) {
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
"$versionRoot.0.$buildNumber"
} else {
"$versionRoot.$buildNumber"
}
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
version = if (shortCommit.isNullOrBlank()) {
"$versionRoot.SNAPSHOT"
} else if (isOfficial) {
"$versionRoot.dev-$shortCommit"
} else {
"$versionRoot.unofficial-$shortCommit"
}
}
version = "$versionRoot.$buildNumber"
dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
if (launcherExe.isBlank()) {
if (launcherExe == null) {
implementation(libs.hmclauncher)
}
}
@@ -98,18 +90,15 @@ fun attachSignature(jar: File) {
}
}
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
}
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
tasks.withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.compileJava {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
}
tasks.jar {
enabled = false
dependsOn(tasks["shadowJar"])
@@ -135,6 +124,7 @@ tasks.shadowJar {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
exclude(project(":HMCLBoot"))
}
manifest {
@@ -147,6 +137,7 @@ tasks.shadowJar {
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
@@ -170,7 +161,7 @@ tasks.shadowJar {
}
}
if (launcherExe.isNotBlank()) {
if (launcherExe != null) {
into("assets") {
from(file(launcherExe))
}
@@ -182,13 +173,6 @@ tasks.shadowJar {
}
}
tasks.processResources {
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
}
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
@@ -17,41 +17,26 @@
*/
package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.ModuleHelper;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
public final class EntryPoint {
private Main() {
private EntryPoint() {
}
public static void main(String[] args) {
@@ -62,27 +47,8 @@ public final class Main {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
System.getProperties().putIfAbsent("prism.forceGPU", "true");
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
checkDirectoryPath();
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
initIcon();
@@ -112,6 +78,8 @@ public final class Main {
}
}
} catch (IOException e) {
// Logger has not been started yet, so print directly to System.err
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
}
@@ -127,16 +95,17 @@ public final class Main {
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
String currentDir = System.getProperty("user.dir", "");
if (currentDir.contains("!")) {
LOG.error("The current working path contains an exclamation mark: " + currentDir);
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDirectory);
+ "The path is " + currentDir);
}
}
@@ -144,10 +113,10 @@ public final class Main {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("unable to patch JVM", e);
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("unable to patch JVM", e);
LOG.error("Unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
@@ -164,7 +133,7 @@ public final class Main {
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
e.printStackTrace(System.err);
LOG.warning("JavaFX is incomplete or not found", e);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
@@ -172,9 +141,20 @@ public final class Main {
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
// javafx.graphics
Module module = Class.forName("javafx.stage.Stage").getModule();
if (module.isNamed()) {
try {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class,
"implAddEnableNativeAccess", MethodType.methodType(Module.class));
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace(System.err);
LOG.error("Failed to add enable native access for JavaFX", e);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
@@ -185,11 +165,11 @@ public final class Main {
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
try {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
trySetMemoryAccessWarned.setAccessible(true);
trySetMemoryAccessWarned.invoke(null);
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
.invokeExact();
} catch (Throwable e) {
e.printStackTrace(System.err);
LOG.warning("Failed to enable unsafe memory access", e);
}
}
}
@@ -197,70 +177,8 @@ public final class Main {
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
exit(1);
}
} catch (Throwable ignored) {
}
private static void showErrorAndExit(String message) {
SwingUtils.showErrorDialog(message);
exit(1);
}
/**
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
*/
static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.WARNING, message).showAndWait();
return;
}
} catch (Throwable ignored) {
}
SwingUtils.showWarningDialog(message);
}
private static void fixLetsEncrypt() {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
try (InputStream ksStream = Files.newInputStream(ksPath)) {
defaultKeyStore.load(ksStream, "changeit".toCharArray());
}
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
}
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
merged.load(null, new char[0]);
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
for (String alias : Collections.list(defaultKeyStore.aliases()))
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
instance.init(merged);
SSLContext tls = SSLContext.getInstance("TLS");
tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -81,13 +81,14 @@ public final class Launcher extends Application {
try {
try {
ConfigHolder.init();
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (SambaException e) {
showAlert(AlertType.WARNING, i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
EntryPoint.exit(1);
}
// https://lapcatsoftware.com/articles/app-translocation.html
@@ -105,12 +106,8 @@ public final class Launcher extends Application {
return;
}
if (ConfigHolder.isUnsupportedVersion()) {
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
}
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -173,7 +170,7 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
Main.exit(0);
EntryPoint.exit(0);
}
}
@@ -213,7 +210,7 @@ public final class Launcher extends Application {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
Main.exit(1);
EntryPoint.exit(1);
}
@Override
@@ -19,13 +19,10 @@ package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
* Stores metadata about this application.
@@ -38,9 +35,6 @@ public final class Metadata {
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 8;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
@@ -100,31 +94,4 @@ public final class Metadata {
return !isStable() && !isDev();
}
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";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
supportedArchitectures = EnumSet.of(
Architecture.X86_64, Architecture.X86,
Architecture.ARM64, Architecture.ARM32,
Architecture.RISCV64, Architecture.LOONGARCH64
);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
else
supportedArchitectures = EnumSet.noneOf(Architecture.class);
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
OperatingSystem.CURRENT_OS.getCheckedName(),
Architecture.SYSTEM_ARCH.getCheckedName()
);
else
return null;
}
}
}
@@ -170,7 +170,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
Files.copy(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
FileUtils.writeText(toJson, JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -194,7 +194,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File file = getLocalVersionSettingFile(id);
if (file.exists())
try {
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
VersionSetting versionSetting = GSON.fromJson(Files.readString(file.toPath()), VersionSetting.class);
initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
@@ -427,7 +427,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = FileUtils.readText(json);
String jsonText = Files.readString(json.toPath());
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
@@ -28,10 +28,10 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -66,7 +66,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
config = JsonUtils.GSON.fromJson(Files.readString(json.toPath()), ModpackConfiguration.typeOf(Modpack.class));
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -160,10 +160,8 @@ public final class LauncherHelper {
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& OperatingSystem.WINDOWS_VERSION != null) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(java);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -140,7 +140,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
return JsonUtils.GSON.fromJson(Files.readString(file.toPath()), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -104,7 +103,6 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
@@ -56,7 +56,6 @@ public final class JavaManager {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
@@ -255,15 +254,16 @@ public final class JavaManager {
}
}
private static JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime java2) {
if (java1 == null)
return java2;
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
if (suggestedJavaVersion != null) {
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
if (java1.getParsedVersion() != java2.getParsedVersion())
// Prefer the Java version that is closer to the game's recommended Java version
return java1.getParsedVersion() < java2.getParsedVersion() ? java1 : java2;
else
return java1.getVersionNumber().compareTo(java2.getVersionNumber()) >= 0 ? java1 : java2;
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
}
@Nullable
@@ -279,6 +279,9 @@ public final class JavaManager {
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
GameJavaVersion suggestedJavaVersion =
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
JavaRuntime mandatory = null;
JavaRuntime suggested = null;
for (JavaRuntime java : javaRuntimes) {
@@ -306,10 +309,15 @@ public final class JavaManager {
}
if (!violationMandatory) {
mandatory = chooseJava(mandatory, java);
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
if (!violationSuggested)
suggested = chooseJava(suggested, java);
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
}
}
@@ -553,10 +561,6 @@ public final class JavaManager {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import java.io.IOException;
@@ -73,7 +72,7 @@ public final class AuthlibInjectorServers implements Validation {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
String content = FileUtils.readText(configLocation);
String content = Files.readString(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.warning("Malformed authlib-injectors.json", e);
@@ -50,7 +50,6 @@ import java.util.TreeMap;
public final class Config implements Observable {
public static final int CURRENT_VERSION = 2;
public static final int CURRENT_UI_VERSION = 0;
public static final Gson CONFIG_GSON = new GsonBuilder()
@@ -181,9 +180,6 @@ public final class Config implements Observable {
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("acceptPreviewUpdate")
private BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -674,18 +670,6 @@ public final class Config implements Observable {
this.promptedVersion.set(promptedVersion);
}
public boolean isAcceptPreviewUpdate() {
return acceptPreviewUpdate.get();
}
public BooleanProperty acceptPreviewUpdateProperty() {
return acceptPreviewUpdate;
}
public void setAcceptPreviewUpdate(boolean acceptPreviewUpdate) {
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
}
public ObservableMap<String, Object> getShownTips() {
return shownTips;
}
@@ -45,7 +45,6 @@ public final class ConfigHolder {
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
private static boolean ownerChanged = false;
private static boolean unsupportedVersion = false;
public static Config config() {
if (configInstance == null) {
@@ -73,10 +72,6 @@ public final class ConfigHolder {
return ownerChanged;
}
public static boolean isUnsupportedVersion() {
return unsupportedVersion;
}
public static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -87,8 +82,7 @@ public final class ConfigHolder {
LOG.info("Config location: " + configLocation);
configInstance = loadConfig();
if (!unsupportedVersion)
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
@@ -107,6 +101,7 @@ public final class ConfigHolder {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& configLocation.getFileSystem() == FileSystems.getDefault()
&& configLocation.toFile().canWrite()) {
LOG.warning("Config at " + configLocation + " is not writable, but it seems to be a Samba share or OpenJDK bug");
// There are some serious problems with the implementation of Samba or OpenJDK
throw new SambaException();
} else {
@@ -163,19 +158,12 @@ public final class ConfigHolder {
LOG.warning("Failed to get owner");
}
try {
String content = FileUtils.readText(configLocation);
String content = Files.readString(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
int configVersion = deserialized.getConfigVersion();
if (configVersion < Config.CURRENT_VERSION) {
ConfigUpgrader.upgradeConfig(deserialized, content);
} else if (configVersion > Config.CURRENT_VERSION) {
unsupportedVersion = true;
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", Config.CURRENT_VERSION, configVersion));
}
ConfigUpgrader.upgradeConfig(deserialized, content);
return deserialized;
}
} catch (JsonParseException e) {
@@ -192,7 +180,7 @@ public final class ConfigHolder {
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
String content = Files.readString(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -31,6 +31,8 @@ final class ConfigUpgrader {
private ConfigUpgrader() {
}
private static final int CURRENT_VERSION = 2;
/**
* This method is for the compatibility with old HMCL versions.
*
@@ -40,10 +42,17 @@ final class ConfigUpgrader {
static void upgradeConfig(Config deserialized, String rawContent) {
int configVersion = deserialized.getConfigVersion();
if (configVersion >= Config.CURRENT_VERSION)
if (configVersion == CURRENT_VERSION) {
return;
}
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, Config.CURRENT_VERSION));
if (configVersion > CURRENT_VERSION) {
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
deserialized.setConfigVersion(CURRENT_VERSION);
return;
}
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
if (configVersion < 1) {
@@ -91,6 +100,6 @@ final class ConfigUpgrader {
}
}
deserialized.setConfigVersion(Config.CURRENT_VERSION);
deserialized.setConfigVersion(CURRENT_VERSION);
}
}
@@ -21,7 +21,6 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -29,9 +28,6 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -152,46 +148,33 @@ public final class FontManager {
return null;
}
if (JavaRuntime.CURRENT_VERSION >= 9) {
try {
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
MethodType.methodType(Font[].class, String.class, double.class));
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
Font[] fonts = (Font[]) methodHandle.invokeExact(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
return font;
}
}
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
} catch (NoSuchMethodException | IllegalAccessException ignored) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font == null)
LOG.warning("Failed to load font from " + path);
return font;
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
return font;
}
}
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
} catch (Throwable e) {
LOG.warning("Failed to get default font with fc-match", e);
return null;
@@ -21,10 +21,7 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
@@ -57,6 +54,8 @@ public final class GlobalConfig implements Observable {
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
@@ -131,6 +130,18 @@ public final class GlobalConfig implements Observable {
enableOfflineAccount.set(value);
}
public StringProperty fontAntiAliasingProperty() {
return fontAntiAliasing;
}
public String getFontAntiAliasing() {
return fontAntiAliasing.get();
}
public void setFontAntiAliasing(String value) {
this.fontAntiAliasing.set(value);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
@@ -146,7 +157,8 @@ public final class GlobalConfig implements Observable {
"logRetention",
"userJava",
"disabledJava",
"enableOfflineAccount"
"enableOfflineAccount",
"fontAntiAliasing"
));
@Override
@@ -159,6 +171,7 @@ public final class GlobalConfig implements Observable {
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
if (src.enableOfflineAccount.get())
jsonObject.addProperty("enableOfflineAccount", true);
@@ -186,6 +199,7 @@ public final class GlobalConfig implements Observable {
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
@@ -59,10 +59,7 @@ public final class ProxyManager {
String password = config().getProxyPass();
if (username != null || password != null)
return new SimpleAuthenticator(
Lang.requireNonNullElse(username, ""),
Lang.requireNonNullElse(password, "").toCharArray()
);
return new SimpleAuthenticator(username, password.toCharArray());
else
return null;
} else
@@ -23,7 +23,6 @@ import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -68,7 +67,7 @@ public final class StyleSheets {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
FileUtils.writeText(temp, styleSheet, Charset.defaultCharset());
Files.writeString(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -176,6 +175,18 @@ public final class Controllers {
public static void initialize(Stage stage) {
LOG.info("Start initializing application");
if (System.getProperty("prism.lcdtext") == null) {
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
LOG.info("Enable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "true");
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
LOG.info("Disable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "false");
}
}
Controllers.stage = stage;
stageSizeChangeListener = o -> {
@@ -310,26 +321,6 @@ public final class Controllers {
}
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
Number shownTipVersion = null;
try {
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
} catch (ClassCastException e) {
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
}
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
Controllers.dialog(builder
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
.build());
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
@@ -27,7 +27,6 @@ import javafx.beans.WeakInvalidationListener;
import javafx.beans.WeakListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.event.Event;
import javafx.event.EventDispatcher;
@@ -55,8 +54,9 @@ import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.glavo.png.PNGType;
import org.glavo.png.PNGWriter;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.util.*;
@@ -78,16 +78,11 @@ import javax.imageio.stream.ImageInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.ref.WeakReference;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -333,8 +328,15 @@ public final class FXUtils {
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
public static void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
tooltip.setShowDelay(showDelay);
tooltip.setShowDuration(showDuration);
tooltip.setHideDelay(hideDelay);
Tooltip.install(node, tooltip);
}
public static void installFastTooltip(Node node, Tooltip tooltip) {
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installFastTooltip(Node node, String tooltip) {
@@ -342,7 +344,7 @@ public final class FXUtils {
}
public static void installSlowTooltip(Node node, Tooltip tooltip) {
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installSlowTooltip(Node node, String tooltip) {
@@ -784,21 +786,6 @@ public final class FXUtils {
}
}
public static Image loadWebPImage(InputStream input,
int requestedWidth, int requestedHeight,
boolean preserveRatio, boolean smooth) throws IOException {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
BufferedImage bufferedImage;
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
reader.setInput(imageInput, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
}
public static Image loadImage(Path path) throws Exception {
try (InputStream input = Files.newInputStream(path)) {
if ("webp".equalsIgnoreCase(FileUtils.getExtension(path)))
@@ -812,42 +799,6 @@ public final class FXUtils {
}
}
public static Image loadImage(Path path,
int requestedWidth, int requestedHeight,
boolean preserveRatio, boolean smooth) throws Exception {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
String ext = FileUtils.getExtension(path).toLowerCase(Locale.ROOT);
if ("webp".equalsIgnoreCase(ext))
return loadWebPImage(Channels.newInputStream(channel),
requestedWidth, requestedHeight, preserveRatio, smooth);
if (!IMAGE_EXTENSIONS.contains(ext)) {
byte[] header = new byte[12];
ByteBuffer buffer = ByteBuffer.wrap(header);
//noinspection StatementWithEmptyBody
while (buffer.hasRemaining() && channel.read(buffer) > 0) {
}
channel.position(0L);
if (!buffer.hasRemaining()) {
// WebP File
if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' &&
header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') {
return loadWebPImage(Channels.newInputStream(channel),
requestedWidth, requestedHeight, preserveRatio, smooth);
}
}
}
Image image = new Image(Channels.newInputStream(channel),
requestedWidth, requestedHeight,
preserveRatio, smooth);
if (image.isError())
throw image.getException();
return image;
}
}
public static Image loadImage(URL url) throws Exception {
URLConnection connection = NetworkUtils.createConnection(url);
if (connection instanceof HttpURLConnection) {
@@ -907,37 +858,15 @@ public final class FXUtils {
}
}
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return Task.composeAsync(() -> {
Path currentPath = remoteImageCache.get(url);
if (currentPath != null) {
if (Files.isReadable(currentPath))
return Task.completed(currentPath);
// The file is unavailable or unreadable.
remoteImageCache.remove(url);
try {
Files.deleteIfExists(currentPath);
} catch (IOException e) {
LOG.warning("An exception encountered while deleting broken cached image file.", e);
}
}
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
return new FileDownloadTask(NetworkUtils.toURL(url), newPath.toFile())
.thenSupplyAsync(() -> {
Path otherPath = remoteImageCache.putIfAbsent(url, newPath);
if (otherPath == null)
return newPath;
else {
// The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
Files.delete(newPath);
return otherPath;
}
});
}).thenApplyAsync(path -> loadImage(path, requestedWidth, requestedHeight, preserveRatio, smooth));
/**
* Load image from the internet. It will cache the data of images for the further usage.
* The cached data will be deleted when HMCL is closed or hidden.
*
* @param url the url of image. The image resource should be a file on the internet.
* @return the image resource within the jar.
*/
public static Image newRemoteImage(String url) {
return newRemoteImage(url, 0, 0, false, false, false);
}
/**
@@ -955,17 +884,52 @@ public final class FXUtils {
* the specified bounding box
* @return the image resource within the jar.
*/
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
SimpleObjectProperty<Image> image = new SimpleObjectProperty<>();
getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
image.set(result);
} else {
LOG.warning("An exception encountered while loading remote image: " + url, exception);
public static Image newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth, boolean backgroundLoading) {
Path currentPath = remoteImageCache.get(url);
if (currentPath != null) {
if (Files.isReadable(currentPath)) {
try (InputStream inputStream = Files.newInputStream(currentPath)) {
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
} catch (IOException e) {
LOG.warning("An exception encountered while reading data from cached image file.", e);
}
}
// The file is unavailable or unreadable.
remoteImageCache.remove(url);
try {
Files.deleteIfExists(currentPath);
} catch (IOException e) {
LOG.warning("An exception encountered while deleting broken cached image file.", e);
}
}
Image image = new Image(url, requestedWidth, requestedHeight, preserveRatio, smooth, backgroundLoading);
image.progressProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.doubleValue() >= 1.0 && !image.isError() && image.getPixelReader() != null && image.getWidth() > 0.0 && image.getHeight() > 0.0) {
Task.runAsync(() -> {
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
try ( // Make sure the file is released from JVM before we put the path into remoteImageCache.
OutputStream outputStream = Files.newOutputStream(newPath);
PNGWriter writer = new PNGWriter(outputStream, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL)
) {
writer.write(PNGJavaFXUtils.asArgbImage(image));
} catch (IOException e) {
try {
Files.delete(newPath);
} catch (IOException e2) {
e2.addSuppressed(e);
throw e2;
}
throw e;
}
})
.start();
if (remoteImageCache.putIfAbsent(url, newPath) != null) {
Files.delete(newPath); // The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
}
}).start();
}
});
return image;
}
@@ -22,7 +22,6 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.util.StringUtils;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
@@ -160,19 +159,28 @@ public final class HTMLRenderer {
private void appendImage(Node node) {
String src = node.absUrl("src");
URI imageUri = null;
try {
if (!src.isEmpty())
imageUri = URI.create(src);
} catch (Exception ignored) {
}
String alt = node.attr("alt");
if (StringUtils.isNotBlank(src)) {
if (imageUri != null) {
URI uri = URI.create(src);
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
int width = 0;
int height = 0;
double width = 0;
double height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = (int) Double.parseDouble(widthAttr);
height = (int) Double.parseDouble(heightAttr);
width = Double.parseDouble(widthAttr);
height = Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
@@ -182,12 +190,10 @@ public final class HTMLRenderer {
}
}
try {
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
.run();
if (image == null)
throw new AssertionError("Image loading task returned null");
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
if (image.isError()) {
LOG.warning("Failed to load image: " + uri, image.getException());
} else {
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
@@ -198,8 +204,6 @@ public final class HTMLRenderer {
}
children.add(imageView);
return;
} catch (Throwable e) {
LOG.warning("Failed to load image: " + src, e);
}
}
@@ -22,7 +22,6 @@ import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -313,9 +312,7 @@ public class InstallerItem extends Control {
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Number> holder;
private static final WeakListenerHolder holder = new WeakListenerHolder();
InstallerItemSkin(InstallerItem control) {
super(control);
@@ -323,14 +320,9 @@ public class InstallerItem extends Control {
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
if (FXUtils.JAVAFX_MAJOR_VERSION == 8)
holder = FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
else
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
} else {
pane = new HBox();
holder = null;
}
pane.getStyleClass().add("installer-item");
RipplerContainer container = new RipplerContainer(pane);
@@ -224,7 +224,7 @@ public final class LogWindow extends Stage {
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
@@ -1,123 +0,0 @@
/*
* 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/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public class TooltipInstaller {
public static final TooltipInstaller INSTALLER;
static {
TooltipInstaller installer = null;
try {
installer = new NewInstaller();
} catch (Throwable e) {
try {
installer = new OldInstaller();
} catch (Throwable e2) {
e2.addSuppressed(e);
LOG.warning("Failed to initialize TooltipInstaller", e2);
}
}
INSTALLER = installer != null ? installer : new TooltipInstaller();
}
TooltipInstaller() {
}
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
Tooltip.install(node, tooltip);
}
// For Java 8
private static final class OldInstaller extends TooltipInstaller {
private static final Constructor<?> createTooltipBehavior;
private static final Method installTooltipBehavior;
static {
try {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
createTooltipBehavior.setAccessible(true);
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installTooltipBehavior.setAccessible(true);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
installTooltipBehavior.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
LOG.warning("Failed to set tooltip show delay", e);
Tooltip.install(node, tooltip);
}
}
}
// For Java 9+
private static final class NewInstaller extends TooltipInstaller {
private static final MethodHandle setTooltipShowDelay;
private static final MethodHandle setTooltipShowDuration;
private static final MethodHandle setTooltipHideDelay;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodType methodType = MethodType.methodType(void.class, Duration.class);
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
setTooltipShowDelay.invokeExact(tooltip, showDelay);
setTooltipShowDuration.invokeExact(tooltip, showDuration);
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
} catch (Throwable e) {
LOG.warning("Failed to set tooltip show delay", e);
}
Tooltip.install(node, tooltip);
}
}
}
@@ -48,22 +48,11 @@ public final class UpgradeDialog extends JFXDialogLayout {
setBody(new ProgressIndicator());
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
boolean isPreview = remoteVersion.isPreview();
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
String id = null;
Node node = null;
if (isPreview) {
id = "nowpreview";
node = document.selectFirst("#" + id);
}
if (node == null) {
id = "nowchange";
node = document.selectFirst("#" + id);
}
Node node = document.selectFirst("#nowchange");
if (node == null || !"h1".equals(node.nodeName()))
throw new IOException("Cannot find current changelog in document");
throw new IOException("Cannot find #nowchange in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
@@ -71,7 +60,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
});
do {
if ("h1".equals(node.nodeName()) && !id.equals(node.attr("id"))) {
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
break;
}
renderer.appendNode(node);
@@ -78,22 +78,15 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setActionButtonVisible(false);
setOnScroll(event -> {
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
Account current = account.get();
if (current == null) return;
ObservableList<Account> accounts = Accounts.getAccounts();
int currentIndex = accounts.indexOf(current);
if (currentIndex < 0) return;
if (deltaY > 0) // up
int currentIndex = accounts.indexOf(account.get());
if (event.getDeltaY() > 0) { // up
currentIndex--;
else // down
} else { // down
currentIndex++;
}
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
});
}
@@ -267,7 +267,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}).executor(true);
};
if (factory instanceof OfflineAccountFactory && username != null && (!USERNAME_CHECKER_PATTERN.matcher(username).matches() || username.length() > 16)) {
if (factory instanceof OfflineAccountFactory && username != null && !USERNAME_CHECKER_PATTERN.matcher(username).matches()) {
JFXButton btnYes = new JFXButton(i18n("button.ok"));
btnYes.getStyleClass().add("dialog-error");
btnYes.setOnAction(e -> doCreate.run());
@@ -30,7 +30,8 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private Duration duration;
private Node previousNode, currentNode;
public TransitionPane() {
{
currentNode = getChildren().stream().findFirst().orElse(null);
FXUtils.setOverflowHidden(this);
}
@@ -90,12 +91,8 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private void updateContent(Node newView) {
if (getWidth() > 0 && getHeight() > 0) {
previousNode = currentNode;
if (previousNode == null) {
if (getChildren().isEmpty())
previousNode = EMPTY_PANE;
else
previousNode = getChildren().get(0);
}
if (previousNode == null)
previousNode = EMPTY_PANE;
} else
previousNode = EMPTY_PANE;
@@ -98,7 +98,7 @@ public final class MessageDialogPane extends HBox {
{
StackPane titlePane = new StackPane();
titlePane.getStyleClass().addAll("jfx-layout-heading", "title");
titlePane.getChildren().setAll(new Label(title != null ? title : type.getDisplayName()));
titlePane.getChildren().setAll(new Label(title != null ? title : i18n("message.info")));
StackPane content = new StackPane();
content.getStyleClass().add("jfx-layout-body");
@@ -261,7 +261,6 @@ public final class TaskListPane extends StackPane {
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
// @formatter:on
@@ -198,8 +198,6 @@ public class DecoratorController {
private Background getBackground() {
EnumBackgroundImage imageType = config().getBackgroundImageType();
if (imageType == null)
imageType = EnumBackgroundImage.DEFAULT;
Image image = null;
switch (imageType) {
@@ -124,7 +124,7 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
libraryPane.setVgap(16);
libraryPane.setHgap(16);
if (FXUtils.JAVAFX_MAJOR_VERSION < 25 && libraries.length <= 8) {
if (libraries.length <= 8) {
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
root.setCenter(libraryPane);
} else {
@@ -133,9 +133,6 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
scrollPane.setFitToHeight(true);
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
root.setCenter(scrollPane);
if (libraries.length <= 8)
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
}
}
@@ -18,11 +18,11 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
@@ -102,7 +102,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(i18n("modpack.mismatched_type", e.getRequired(), e.getFound()), i18n("message.error"), MessageType.ERROR);
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
} catch (IOException e) {
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageType.ERROR);
}
@@ -124,7 +124,6 @@ public class DownloadSettingsPage extends StackPane {
{
HBox hbox = new HBox(8);
hbox.setStyle("-fx-view-order: -1;"); // prevent the indicator from being covered by the hint
hbox.setAlignment(Pos.CENTER);
hbox.setPadding(new Insets(0, 0, 0, 30));
hbox.disableProperty().bind(config().autoDownloadThreadsProperty());
@@ -62,12 +62,12 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
@@ -197,17 +197,15 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchPane.setMaxWidth(230);
launchPane.setMaxHeight(55);
launchPane.setOnScroll(event -> {
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
String currentId = getCurrentGame();
int index = Lang.indexWhere(versions, instance -> instance.getId().equals(currentId));
int index = IntStream.range(0, versions.size())
.filter(i -> versions.get(i).getId().equals(getCurrentGame()))
.findFirst().orElse(-1);
if (index < 0) return;
if (deltaY > 0) // up
if (event.getDeltaY() > 0) {
index--;
else // down
} else {
index++;
}
profile.setSelectedVersion(versions.get((index + versions.size()) % versions.size()).getId());
});
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
@@ -18,11 +18,13 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ColorPicker;
@@ -34,6 +36,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.FontManager;
import org.jackhuang.hmcl.setting.Theme;
@@ -44,8 +47,11 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class PersonalizationPage extends StackPane {
@@ -216,6 +222,46 @@ public class PersonalizationPage extends StackPane {
fontPane.getContent().add(vbox);
}
{
BorderPane fontAntiAliasingPane = new BorderPane();
{
Label left = new Label(i18n("settings.launcher.font.anti_aliasing"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
fontAntiAliasingPane.setLeft(left);
}
{
@SuppressWarnings("unchecked")
JFXComboBox<Optional<FontSmoothingType>> cboAntiAliasing = new JFXComboBox<>(FXCollections.observableArrayList(
Optional.empty(),
Optional.of(FontSmoothingType.LCD),
Optional.of(FontSmoothingType.GRAY)
));
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.LCD));
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.GRAY));
} else {
cboAntiAliasing.setValue(Optional.empty());
}
cboAntiAliasing.setConverter(FXUtils.stringConverter(value -> {
if (value.isPresent()) {
return i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT));
} else {
return i18n("settings.launcher.font.anti_aliasing.auto");
}
}));
FXUtils.onChange(cboAntiAliasing.valueProperty(), value ->
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
.orElse(null)));
fontAntiAliasingPane.setRight(cboAntiAliasing);
}
fontPane.getContent().add(fontAntiAliasingPane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.font")), fontPane);
}
}
@@ -106,12 +106,9 @@ public final class SettingsPage extends SettingsView {
chkUpdateStable.setUserData(UpdateChannel.STABLE);
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
updateChannel.addListener(checkUpdateListener);
previewPane.selectedProperty().addListener(checkUpdateListener);
updateChannel.addListener((a, b, newValue) -> {
UpdateChecker.requestCheckUpdate(newValue);
});
// ====
}
@@ -37,7 +37,6 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
@@ -57,7 +56,6 @@ public abstract class SettingsView extends StackPane {
protected final JFXRadioButton chkUpdateStable;
protected final JFXRadioButton chkUpdateDev;
protected final JFXButton btnUpdate;
protected final OptionToggleButton previewPane;
protected final ScrollPane scroll;
public SettingsView() {
@@ -147,15 +145,6 @@ public abstract class SettingsView extends StackPane {
settingsPane.getContent().add(updatePane);
}
{
previewPane = new OptionToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
settingsPane.getContent().add(previewPane);
}
{
fileCommonLocation = new MultiFileItem<>();
fileCommonLocationSublist = new ComponentSublist();
@@ -18,9 +18,8 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
@@ -53,7 +52,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final JFXComboBox<Renderer> cboRenderer;
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
public AdvancedVersionSettingPage(Profile profile, String versionId, VersionSetting versionSetting) {
this.profile = profile;
this.versionId = versionId;
this.versionSetting = versionSetting;
@@ -240,27 +239,8 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
useNativeOpenALPane.selectedProperty().bindBidirectional(versionSetting.useNativeOpenALProperty());
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> {
if (versionSetting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
String nativesDirName = "natives-" + Platform.SYSTEM_PLATFORM;
if (versionId == null) {
return String.format("%s/%s/%s",
profile.getRepository().getBaseDirectory().toPath().resolve("versions").toAbsolutePath().normalize(),
i18n("settings.advanced.natives_directory.default.version_id"),
nativesDirName
);
} else {
return profile.getRepository().getVersionRoot(versionId).toPath()
.toAbsolutePath().normalize()
.resolve(nativesDirName)
.toString();
}
} else if (versionSetting.getNativesDirType() == NativesDirectoryType.CUSTOM) {
return versionSetting.getNativesDir();
} else {
return null;
}
}, versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
}
void unbindProperties() {
@@ -229,7 +229,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
private final JFXListView<RemoteMod> listView = new JFXListView<>();
protected ModDownloadListPageSkin(DownloadListPage control) {
super(control);
@@ -450,8 +449,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
boolean disableNext = disableAll || pageOffset == pageCount - 1;
nextPageButton.setDisable(disableNext);
lastPageButton.setDisable(disableNext);
listView.scrollTo(0);
};
FXUtils.onChange(control.pageCount, pageCountN -> {
@@ -507,6 +504,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
});
JFXListView<RemoteMod> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), getSkinnable().items);
FXUtils.onClicked(listView, () -> {
@@ -542,7 +540,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
imageView.setImage(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true, true));
}
}
});
@@ -220,7 +220,7 @@ public class DownloadPage extends Control implements DecoratorPage {
{
ImageView imageView = new ImageView();
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true));
imageView.setImage(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true, true));
}
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
@@ -359,7 +359,7 @@ public class DownloadPage extends Control implements DecoratorPage {
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(addon.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
}
} else {
content.setTitle(i18n("mods.broken_dependency.title"));
@@ -142,7 +142,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
toolbarNormal.getChildren().setAll(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add),
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder),
createToolbarButton2(i18n("folder.mod"), SVG.FOLDER_OPEN, skinnable::openModFolder),
createToolbarButton2(i18n("mods.check_updates"), SVG.UPDATE, skinnable::checkUpdates),
createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download),
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
@@ -43,7 +43,6 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -110,10 +109,8 @@ public final class Versions {
public static void deleteVersion(Profile profile, String version) {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER;
boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported();
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
i18n("version.manage.remove.confirm", version);
i18n("version.manage.remove.confirm.trash", version, version + "_removed");
JFXButton deleteButton = new JFXButton(i18n("button.delete"));
deleteButton.getStyleClass().add("dialog-error");
@@ -361,27 +361,14 @@ public final class WorldInfoPage extends SpinnerPane {
playerGameTypePane.setRight(gameTypeBox);
Tag tag = player.get("playerGameType");
Tag hardcoreTag = dataTag.get("hardcore");
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
if (tag instanceof IntTag) {
IntTag intTag = (IntTag) tag;
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
GameType gameType = GameType.of(intTag.getValue());
if (gameType != null) {
gameTypeBox.setValue(gameType);
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
if (newValue == GameType.HARDCORE) {
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 1);
}
} else {
intTag.setValue(newValue.ordinal());
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 0);
}
}
intTag.setValue(newValue.ordinal());
saveLevelDat();
}
});
@@ -619,13 +606,12 @@ public final class WorldInfoPage extends SpinnerPane {
}
private enum GameType {
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR, HARDCORE;
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR;
static final ObservableList<GameType> items = FXCollections.observableList(Arrays.asList(values()));
static GameType of(int d, boolean hardcore) {
if (hardcore && d == 0) return HARDCORE; // hardcore + survival
return d >= 0 && d < 4 ? items.get(d) : null;
static GameType of(int d) {
return d >= 0 && d <= items.size() ? items.get(d) : null;
}
@Override
@@ -64,7 +64,7 @@ final class ExecutableHeaderHelper {
ZipEntry entry = zip.getEntry(location);
if (entry != null && !entry.isDirectory()) {
try (InputStream in = zip.getInputStream(entry)) {
return Optional.of(IOUtils.readFullyAsByteArray(in));
return Optional.of(IOUtils.readFully(in));
}
}
}
@@ -18,9 +18,15 @@
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
class HMCLDownloadTask extends FileDownloadTask {
@@ -41,6 +47,15 @@ class HMCLDownloadTask extends FileDownloadTask {
switch (archiveFormat) {
case JAR:
break;
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200Utils.unpack(in, out);
}
break;
default:
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
@@ -55,7 +55,7 @@ public final class IntegrityChecker {
if (in == null) {
throw new IOException("Public key not found");
}
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFullyAsByteArray(in)));
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFully(in)));
} catch (GeneralSecurityException e) {
throw new IOException("Failed to load public key", e);
}
@@ -76,7 +76,7 @@ public final class IntegrityChecker {
}
if (SIGNATURE_FILE.equals(filename)) {
signature = IOUtils.readFullyAsByteArray(in);
signature = IOUtils.readFully(in);
} else {
md.reset();
fileFingerprints.put(filename, DigestUtils.digest(md, in));
@@ -21,23 +21,28 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.Optional;
public final class RemoteVersion {
public class RemoteVersion {
public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException {
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
boolean force = Optional.ofNullable(response.get("force")).map(JsonElement::getAsBoolean).orElse(false);
if (jarUrl != null && jarHash != null) {
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), preview, force);
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
return new RemoteVersion(channel, version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash), force);
} else if (jarUrl != null && jarHash != null) {
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), force);
} else {
throw new IOException("No download url is available");
}
@@ -51,16 +56,14 @@ public final class RemoteVersion {
private final String url;
private final Type type;
private final IntegrityCheck integrityCheck;
private final boolean preview;
private final boolean force;
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) {
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean force) {
this.channel = channel;
this.version = version;
this.url = url;
this.type = type;
this.integrityCheck = integrityCheck;
this.preview = preview;
this.force = force;
}
@@ -84,10 +87,6 @@ public final class RemoteVersion {
return integrityCheck;
}
public boolean isPreview() {
return preview;
}
public boolean isForce() {
return force;
}
@@ -98,6 +97,7 @@ public final class RemoteVersion {
}
public enum Type {
PACK_XZ,
JAR
}
}
@@ -27,15 +27,14 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.IOException;
import java.util.LinkedHashMap;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.*;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class UpdateChecker {
private UpdateChecker() {
}
private UpdateChecker() {}
private static final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
private static final BooleanBinding outdated = Bindings.createBooleanBinding(
@@ -56,7 +55,7 @@ public final class UpdateChecker {
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
public static void init() {
requestCheckUpdate(UpdateChannel.getChannel(), config().isAcceptPreviewUpdate());
requestCheckUpdate(UpdateChannel.getChannel());
}
public static RemoteVersion getLatestVersion() {
@@ -83,17 +82,16 @@ public final class UpdateChecker {
return checkingUpdate.getReadOnlyProperty();
}
private static RemoteVersion checkUpdate(UpdateChannel channel, boolean preview) throws IOException {
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
throw new IOException("Self verification failed");
}
LinkedHashMap<String, String> query = new LinkedHashMap<>();
query.put("version", Metadata.VERSION);
query.put("channel", preview ? channel.channelName + "-preview" : channel.channelName);
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, mapOf(
pair("version", Metadata.VERSION),
pair("channel", channel.channelName)));
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, query);
return RemoteVersion.fetch(channel, preview, url);
return RemoteVersion.fetch(channel, url);
}
private static boolean isDevelopmentVersion(String version) {
@@ -101,7 +99,7 @@ public final class UpdateChecker {
version.contains("SNAPSHOT"); // eg. 3.5.SNAPSHOT
}
public static void requestCheckUpdate(UpdateChannel channel, boolean preview) {
public static void requestCheckUpdate(UpdateChannel channel) {
Platform.runLater(() -> {
if (isCheckingUpdate())
return;
@@ -110,8 +108,8 @@ public final class UpdateChecker {
thread(() -> {
RemoteVersion result = null;
try {
result = checkUpdate(channel, preview);
LOG.info("Latest version (" + channel + ", preview=" + preview + ") is " + result);
result = checkUpdate(channel);
LOG.info("Latest version (" + channel + ") is " + result);
} catch (IOException e) {
LOG.warning("Failed to check for update", e);
}
@@ -21,18 +21,17 @@ import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import javafx.application.Platform;
import org.jackhuang.hmcl.EntryPoint;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.UpgradeDialog;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -103,16 +102,6 @@ public final class UpdateHandler {
return;
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version.update"), i18n("message.error"), MessageType.ERROR);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
builder.ok(null);
Controllers.dialog(builder.build());
return;
}
Controllers.dialog(new UpgradeDialog(version, () -> {
Path downloaded;
try {
@@ -136,7 +125,7 @@ public final class UpdateHandler {
}
requestUpdate(downloaded, getCurrentLocation());
Main.exit(0);
EntryPoint.exit(0);
} catch (IOException e) {
LOG.warning("Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR));
@@ -275,7 +264,7 @@ public final class UpdateHandler {
Path hmclVersionJson = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("hmclver.json");
if (Files.isRegularFile(hmclVersionJson)) {
try {
Map<?, ?> content = new Gson().fromJson(FileUtils.readText(hmclVersionJson), Map.class);
Map<?, ?> content = new Gson().fromJson(Files.readString(hmclVersionJson), Map.class);
Object ver = content.get("ver");
if (ver instanceof String && ((String) ver).startsWith("3.")) {
Files.delete(hmclVersionJson);
@@ -30,9 +30,9 @@ public final class ChunkBaseApp {
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3",
"1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12",
"1.11", "1.10", "1.9", "1.8", "1.7"
"1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3", "1.19",
"1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12", "1.11",
"1.10", "1.9", "1.8", "1.7"
};
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
@@ -17,11 +17,16 @@
*/
package org.jackhuang.hmcl.util;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* Utility for Adding JavaFX to module path.
*
@@ -31,7 +36,43 @@ public final class JavaFXPatcher {
private JavaFXPatcher() {
}
public static void patch(Set<String> modules, Path[] jarPaths, String[] addOpens) {
LOG.info("No need to patch JavaFX with Java 8");
/**
* Add JavaFX to module path at runtime.
*
* @param modules All module names
* @param jarPaths All jar paths
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
*/
public static void patch(Set<String> modules, Path[] jarPaths, String[] addOpens) throws ReflectiveOperationException {
// Find all modules
ModuleFinder finder = ModuleFinder.of(jarPaths);
// Load all modules as unnamed module
for (ModuleReference mref : finder.findAll()) {
((jdk.internal.loader.BuiltinClassLoader) ClassLoader.getSystemClassLoader()).loadModule(mref);
}
// Define all modules
Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, modules);
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
// Add-Exports and Add-Opens
try {
// Some hacks
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle handle = lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class));
for (String target : addOpens) {
String[] name = target.split("/", 2); // <module>/<package>
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
} catch (Throwable t) {
throw new ReflectiveOperationException(t);
}
}
}
@@ -26,10 +26,7 @@ import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.windows.WindowsVersion;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -181,19 +178,7 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull WindowsVersion windowsVersion) {
if (renderer == Renderer.DEFAULT)
return null;
if (windowsVersion.compareTo(WindowsVersion.WINDOWS_10) >= 0) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
} else if (windowsVersion.compareTo(WindowsVersion.WINDOWS_7) >= 0) {
if (renderer == Renderer.LLVMPIPE)
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
else
return null;
} else {
return null;
}
public static Library getMesaLoader(JavaRuntime javaVersion) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
}
}
@@ -41,9 +41,8 @@
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.EntryPoint;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
@@ -181,12 +180,6 @@ public final class SelfDependencyPatcher {
// We are probably on 8 and its on 11
throw new IncompatibleVersionException();
}
// So the problem with Java 8 is that some distributions DO NOT BUNDLE JAVAFX
// Why is this a problem? OpenJFX does not come in public bundles prior to Java 11
// So you're out of luck unless you change your JDK or update Java.
if (JavaRuntime.CURRENT_VERSION < 11) {
throw new IncompatibleVersionException();
}
SelfDependencyPatcher patcher = new SelfDependencyPatcher();
@@ -246,7 +239,7 @@ public final class SelfDependencyPatcher {
}
} else {
LOG.info("User choose not to download JavaFX");
Main.exit(0);
EntryPoint.exit(0);
}
throw new AssertionError();
}
@@ -120,43 +120,5 @@ public final class SwingFXUtils {
pw.setPixels(0, 0, bw, bh, pf, data, offset, scan);
return wimg;
}
public static WritableImage toFXImage(BufferedImage bimg, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return toFXImage(bimg, null);
}
int width = (int) requestedWidth;
int height = (int) requestedHeight;
// Calculate actual dimensions if preserveRatio is true
if (preserveRatio) {
double originalWidth = bimg.getWidth();
double originalHeight = bimg.getHeight();
double scaleX = requestedWidth / originalWidth;
double scaleY = requestedHeight / originalHeight;
double scale = Math.min(scaleX, scaleY);
width = (int) (originalWidth * scale);
height = (int) (originalHeight * scale);
}
// Create scaled BufferedImage
BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
Graphics2D g2d = scaledImage.createGraphics();
try {
if (smooth) {
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g2d.drawImage(bimg, 0, 0, width, height, null);
} finally {
g2d.dispose();
}
return toFXImage(scaledImage, null);
}
}
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.util.i18n;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.util.Lang;
import java.io.IOException;
@@ -116,10 +115,7 @@ public final class Locales {
if (resourceBundle == null) {
if (this != DEFAULT && this.locale == DEFAULT.locale) {
bundle = DEFAULT.getResourceBundle();
} else if (JavaInfo.CURRENT_ENVIRONMENT.getParsedVersion() < 9) {
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, UTF8Control.INSTANCE);
} else {
// Java 9+ uses UTF-8 as the default encoding for resource bundles
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale);
}
@@ -1,78 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
/**
* Utility for Adding JavaFX to module path.
*
* @author ZekerZhayard
*/
public final class JavaFXPatcher {
private JavaFXPatcher() {
}
/**
* Add JavaFX to module path at runtime.
*
* @param modules All module names
* @param jarPaths All jar paths
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
*/
public static void patch(Set<String> modules, Path[] jarPaths, String[] addOpens) throws ReflectiveOperationException {
// Find all modules
ModuleFinder finder = ModuleFinder.of(jarPaths);
// Load all modules as unnamed module
for (ModuleReference mref : finder.findAll()) {
((jdk.internal.loader.BuiltinClassLoader) ClassLoader.getSystemClassLoader()).loadModule(mref);
}
// Define all modules
Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, modules);
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
// Add-Exports and Add-Opens
try {
// Some hacks
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle handle = lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class));
for (String target : addOpens) {
String[] name = target.split("/", 2); // <module>/<package>
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
} catch (Throwable t) {
throw new ReflectiveOperationException(t);
}
}
}
@@ -1,46 +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.util;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
/**
* @author Glavo
*/
public final class ModuleHelper {
public static void addEnableNativeAccess(Class<?> clazzInModule) {
Module module = clazzInModule.getModule();
if (module.isNamed()) {
try {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class, "implAddEnableNativeAccess", MethodType.methodType(Module.class));
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
} else {
System.err.println("TODO: Add enable native access for anonymous modules is not yet supported");
}
}
private ModuleHelper() {
}
}
@@ -1,23 +0,0 @@
package org.jackhuang.hmcl.util.logging;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* @author Glavo
*/
final class CallerFinder {
private static final String PACKAGE_PREFIX = CallerFinder.class.getPackageName() + ".";
private static final Predicate<StackWalker.StackFrame> PREDICATE = stackFrame -> !stackFrame.getClassName().startsWith(PACKAGE_PREFIX);
private static final Function<Stream<StackWalker.StackFrame>, Optional<StackWalker.StackFrame>> FUNCTION = stream -> stream.filter(PREDICATE).findFirst();
private static final Function<StackWalker.StackFrame, String> FRAME_MAPPING = frame -> frame.getClassName() + "." + frame.getMethodName();
static String getCaller() {
return StackWalker.getInstance().walk(FUNCTION).map(FRAME_MAPPING).orElse(null);
}
private CallerFinder() {
}
}
@@ -106,26 +106,14 @@
-fx-background-color: -fx-base-darker-color;
}
.memory-used:disabled {
-fx-opacity: 0.4;
}
.memory-allocate {
-fx-background-color: derive(-fx-base-color, 100%);
}
.memory-allocate:disabled {
-fx-opacity: 0.4;
}
.memory-total {
-fx-background-color: -fx-base-rippler-color;
}
.memory-total:disabled {
-fx-opacity: 0.4;
}
.update-label {
-fx-text-fill: red;
}
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -21,7 +21,7 @@
about=About
about.copyright=Copyright
about.copyright.statement=Copyright © 2013-2026 huangyuhui and contributors.
about.copyright.statement=Copyright © 2025 huangyuhui
about.author=Author
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -124,7 +124,7 @@ account.methods.microsoft.purchase=Buy Minecraft
account.methods.microsoft.snapshot=You are using an unofficial build of HMCL. Please download the official build to log in.
account.methods.microsoft.snapshot.website=Official Website
account.methods.offline=Offline
account.methods.offline.name.special_characters=Use only letters, numbers, and underscores (max 16 chars)
account.methods.offline.name.special_characters=Using only English letters, numbers, and underscores is recommended
account.methods.offline.name.invalid=It is recommended to use only English letters, numbers and underscores for the username, and the length should not exceed 16 characters.\n\
\n\
\ · Legitimate: HuangYu, huang_Yu, Huang_Yu_123;\n\
@@ -183,7 +183,6 @@ button.export=Export
button.no=No
button.ok=OK
button.ok.countdown=OK (%d)
button.reveal_dir=Reveal
button.refresh=Refresh
button.remove=Remove
button.remove.confirm=Are you sure you want to permanently remove it? This action cannot be undone!
@@ -388,9 +387,6 @@ fatal.config_loading_failure=Cannot load configuration files.\n\
For macOS, try putting HMCL somewhere with permissions other than "Desktop", "Downloads", and "Documents" and try again.
fatal.config_loading_failure.unix=Hello Minecraft! Launcher could not load the configuration file because it was created by user "%1$s".\n\
Please open HMCL as root user (not recommended), or execute the following command in the terminal to change the ownership of the configuration file to the current user:\n%2$s
fatal.config_unsupported_version=The current configuration file was created by a newer version of Hello Minecraft! Launcher, and this version of HMCL cannot load it properly.\n\
Please update and restart HMCL.\n\
Before updating the launcher, any settings you modify will not be saved.
fatal.mac_app_translocation=Hello Minecraft! Launcher is isolated to a temporary directory by the OS due to macOS security mechanisms.\n\
Please move HMCL to a different directory before attempting to open. Otherwise, your settings and game data may be lost after restarting.\n\
Do you still want to continue?
@@ -402,16 +398,6 @@ fatal.apply_update_failure=We are sorry, but Hello Minecraft! Launcher is unable
fatal.apply_update_need_win7=Hello Minecraft! Launcher cannot automatically update on Windows XP/Vista.\n\
\n\
You can update manually by downloading a newer launcher version from %s.
fatal.deprecated_java_version=HMCL will require Java 17 or later to run in the future, but will still support launching games with Java 6~16.\n\
\n\
It is recommended to install the latest version of Java to ensure that HMCL works properly.\n\
\n\
You can continue to keep the old version of Java. HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
fatal.deprecated_java_version.update=HMCL will require Java 17 or later to run in the future. Please install the latest version of Java to ensure that HMCL can complete the upgrade.\n\
\n\
You can continue to keep the old version of Java.\
HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
fatal.deprecated_java_version.download_link=Download Java %d
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
fatal.illegal_char=Your user path contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
@@ -456,7 +442,7 @@ game.crash.reason.analyzing=Analyzing...
game.crash.reason.multiple=Several reasons detected:\n\n
game.crash.reason.block=The game crashed because of a block in the world.\n\
\n\
You can try removing this block using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
You can try removing this block using MCEdit or deleting the mod that added it.\n\
\n\
Block Type: %1$s\n\
Location: %2$s
@@ -472,7 +458,7 @@ game.crash.reason.duplicated_mod=The game cannot continue to run because of dupl
Each mod can only be installed once. Please delete the duplicate mod and try again.
game.crash.reason.entity=The game crashed because of an entity in the world.\n\
\n\
You can try removing the entity using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
You can try removing the entity using MCEdit or deleting the mod that added it.\n\
\n\
Entity Type: %1$s\n\
Location: %2$s
@@ -865,7 +851,6 @@ message.info=Information
message.success=Operation successfully completed
message.unknown=Unknown
message.warning=Warning
message.question=Question
modpack=Modpacks
modpack.choose=Choose Modpack
@@ -874,7 +859,7 @@ modpack.choose.local.detail=You can drag the modpack file here.
modpack.choose.remote=Download from URL
modpack.choose.remote.detail=A direct download link to the remote modpack file is required.
modpack.choose.repository=Download Modpack from CurseForge or Modrinth
modpack.choose.repository.detail=You can choose the desired modpack on the next page.
modpack.choose.repository.detail=Remember to go back to this page and drop the modpack file here after the modpack is downloaded.
modpack.choose.remote.tooltip=Please enter your modpack URL
modpack.completion=Downloading dependencies
modpack.desc=Describe your modpack, including an introduction and probably some changelog. Markdown and images from URL are currently supported.
@@ -945,20 +930,17 @@ modpack.wizard.step.initialization.server=Click here for more information on how
modrinth.category.adventure=Adventure
modrinth.category.atmosphere=Atmosphere
modrinth.category.audio=Audio
modrinth.category.babric=Babric
modrinth.category.blocks=Blocks
modrinth.category.bloom=Bloom
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=Cartoon
modrinth.category.challenging=Challenging
modrinth.category.colored-lighting=Colored Lighting
modrinth.category.combat=Combat
modrinth.category.core-shaders=Core Shaders
modrinth.category.combat=Combat
modrinth.category.cursed=Cursed
modrinth.category.datapack=Datapack
modrinth.category.decoration=Decoration
modrinth.category.economy=Economy
modrinth.category.entities=Entities
@@ -966,7 +948,6 @@ modrinth.category.environment=Environment
modrinth.category.equipment=Equipment
modrinth.category.fabric=Fabric
modrinth.category.fantasy=Fantasy
modrinth.category.folia=Folia
modrinth.category.foliage=Foliage
modrinth.category.fonts=Fonts
modrinth.category.food=Food
@@ -976,9 +957,7 @@ modrinth.category.gui=GUI
modrinth.category.high=High
modrinth.category.iris=Iris
modrinth.category.items=Items
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=Kitchen-Sink
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=Library
modrinth.category.lightweight=Lightweight
modrinth.category.liteloader=LiteLoader
@@ -996,10 +975,8 @@ modrinth.category.models=Models
modrinth.category.modloader=Modloader
modrinth.category.multiplayer=Multiplayer
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=Optimization
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=Path Tracing
modrinth.category.pbr=PBR
@@ -1028,6 +1005,8 @@ modrinth.category.vanilla-like=Vanilla-like
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Worldgen
modrinth.category.datapack=Datapack
modrinth.category.folia=Folia
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
@@ -1047,7 +1026,7 @@ mods.category=Category
mods.channel.alpha=Alpha
mods.channel.beta=Beta
mods.channel.release=Release
mods.check_updates=Update
mods.check_updates=Check for Updates
mods.check_updates.current_version=Current Version
mods.check_updates.empty=All mods are up-to-date
mods.check_updates.failed_check=Failed to check for updates.
@@ -1147,7 +1126,6 @@ world.info.player.food_level=Hunger Level
world.info.player.game_type=Game Mode
world.info.player.game_type.adventure=Adventure
world.info.player.game_type.creative=Creative
world.info.player.game_type.hardcore=Hardcore
world.info.player.game_type.spectator=Spectator
world.info.player.game_type.survival=Survival
world.info.player.health=Health
@@ -1274,7 +1252,6 @@ settings.advanced.natives_directory=Native Library Path
settings.advanced.natives_directory.choose=Choose the location of the desired native library
settings.advanced.natives_directory.custom=Custom
settings.advanced.natives_directory.default=Default
settings.advanced.natives_directory.default.version_id=<Version ID>
settings.advanced.natives_directory.hint=This option is intended only for users of Apple silicon or other not officially supported platforms. Please do not edit this option unless you know what you are doing.\n\
\n\
Before proceeding, please make sure all libraries (e.g. lwjgl.dll, libopenal.so) are provided in your desired directory.\n\
@@ -1344,6 +1321,10 @@ settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
settings.launcher.enable_game_list=Show instance list in homepage
settings.launcher.font=Font
settings.launcher.font.anti_aliasing=Anti-aliasing
settings.launcher.font.anti_aliasing.auto=Auto
settings.launcher.font.anti_aliasing.gray=Grayscale
settings.launcher.font.anti_aliasing.lcd=Sub-pixel
settings.launcher.general=General
settings.launcher.language=Language (Applies After Restart)
settings.launcher.launcher_log.export=Export Launcher Logs
@@ -1425,8 +1406,6 @@ update.note=Beta and Nightly channels may have more features or fixes, but they
update.latest=This is the latest version
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
update.tooltip=Update
update.preview=Preview HMCL releases early
update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release.
version=Games
version.name=Instance Name
@@ -1458,7 +1437,6 @@ version.manage.manage=Edit Instance
version.manage.manage.title=Edit Instance - %1s
version.manage.redownload_assets_index=Update Game Assets
version.manage.remove=Delete Instance
version.manage.remove.confirm=Are you sure you want to permanently remove the instance "%s"? This operation cannot be undone!!!
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%s"? You can still find its files in your recycle bin by the name of "%s".
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete the instance "%s"?
version.manage.remove_assets=Delete All Assets
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -21,6 +21,7 @@
about=Acerca de
about.copyright=Copyright
about.copyright.statement=Copyright © 2025 huangyuhui.
about.author=Autor
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -183,7 +184,6 @@ button.export=Exportar
button.no=No
button.ok=Aceptar
button.ok.countdown=Aceptar (%d)
button.reveal_dir=Carpeta
button.refresh=Refrescar
button.remove=Eliminar
button.remove.confirm=¿Estás seguro de que deseas eliminarlo de forma permanente? ¡Esta acción no se puede deshacer!
@@ -400,17 +400,6 @@ fatal.apply_update_failure=Lo sentimos, pero Hello Minecraft! Launcher no puede
fatal.apply_update_need_win7=Hello Minecraft! Launcher no puede actualizarse automáticamente en Windows XP/Vista.\n\
\n\
Puedes actualizar manualmente descargando una versión más reciente del launcher desde %s.
fatal.deprecated_java_version=HMCL requerirá Java 17 o posterior para funcionar en el futuro, pero seguirá siendo compatible con el lanzamiento de juegos con Java 6~16.\n\
\n\
Se recomienda instalar la última versión de Java para garantizar el correcto funcionamiento de HMCL.\n\
\n\
Puedes conservar la versión antigua de Java. HMCL puede reconocer y gestionar varias versiones de Java y elegirá automáticamente la adecuada para usted en función de la versión del juego.
fatal.deprecated_java_version.update=HMCL requerirá Java 17 o posterior para funcionar en el futuro. Por favor, instalar la última versión de Java para asegurarse de que el lanzador puede completar la actualización.\n\
\n\
Puedes conservar la versión antigua de Java.\
HMCL puede reconocer y gestionar varias versiones de Java \
y elegirá automáticamente la adecuada para usted en función de la versión del juego.
fatal.deprecated_java_version.download_link=Descargar Java %d
fatal.samba=Si ha abierto Hello Minecraft! Launcher desde una unidad de red Samba, es posible que algunas funciones no funcionen. Intenta actualizar tu Java o mover el launcher a otro directorio.
fatal.illegal_char=Su ruta de usuario contiene un carácter ilegal «=», por lo que algunas características podrían no funcionar correctamente.\n\
Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta offline.
@@ -446,7 +435,6 @@ folder.shaderpacks=Paquetes de sombreados
folder.saves=Mundos
folder.schematics=Esquemas
folder.screenshots=Capturas de pantalla
folder.world=Directorio del mundo
game=Juegos
game.crash.feedback=<b>¡Por favor, no comparta capturas de pantalla de esta interfaz con otras personas!</b> Si pide ayuda a otras personas, haga clic en <b>Exportar registros de colgado</b> y envíe el archivo exportado a otras personas para que lo analicen.
@@ -454,6 +442,12 @@ game.crash.info=Información de colgado
game.crash.reason=Motivo del colgado
game.crash.reason.analyzing=Analizando...
game.crash.reason.multiple=Se han detectado varias razones:\n\n
game.crash.reason.block=El juego se ha colgado debido a un bloqueo en el mundo.\n\
\n\
Puedes intentar eliminar este bloque usando MCEdit o borrando el mod que lo añadió.\n\
\n\
Tipo de bloque: %1$s\n\
Ubicación: %2$s
game.crash.reason.bootstrap_failed=El juego se ha colgado debido al mod «%1$s».\n\
\n\
Puedes intentar borrarlo o actualizarlo.
@@ -467,6 +461,12 @@ game.crash.reason.duplicated_mod=El juego no puede iniciarse debido a la existen
%2$s\n\
\n\
Cada mod sólo puede instalarse una vez, por favor, elimine el mod duplicado e inténtelo de nuevo.
game.crash.reason.entity=El juego se ha colgado debido a una entidad en el mundo.\n\
\n\
Puedes intentar eliminar la entidad usando MCEdit o borrando el mod que la añadió.\n\
\n\
Tipo de entidad: %1$s\n\
Ubicación: %2$s
game.crash.reason.fabric_version_0_12=La versión de Fabric Loader 0.12 o posterior es incompatible con los mods instalados actualmente. Necesitas bajarlo a la versión 0.11.7.
game.crash.reason.fabric_warnings=El cargador de Fabric Loader ha advertido:\n\
\n\
@@ -487,7 +487,7 @@ game.crash.reason.graphics_driver=El juego se ha colgado debido a un problema co
Si tu ordenador tiene una tarjeta gráfica discreta, tienes que comprobar si el juego utiliza gráficos integrados/núcleo. Si es así, inicie el launcher utilizando su tarjeta gráfica discreta. Si el problema persiste, probablemente debería considerar la posibilidad de adquirir una nueva tarjeta gráfica o un nuevo ordenador.\n\
\n\
Si utiliza su tarjeta gráfica integrada, tenga en cuenta que Minecraft 1.16.5 o anterior requiere <a href="https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html">Java 1.8.0_51 o más antiguo</a> para la serie de procesadores Intel(R) Core(TM) 3000 o anteriores.
game.crash.reason.macos_failed_to_find_service_port_for_display=El juego no puede continuar debido a un fallo al inicializar la ventana OpenGL en la plataforma con chips de Apple.\n\
game.crash.reason.macos_failed_to_find_service_port_for_display=El juego no puede continuar debido a un fallo al inicializar la ventana OpenGL en la plataforma Apple silicon.\n\
\n\
Para este problema, HMCL no tiene soluciones directas por el momento. Intente abrir cualquier navegador y ponerlo en pantalla completa, luego vuelva a HMCL, inicie el juego y <b>vuelva rápidamente a la página del navegador</b> antes de que aparezca la ventana del juego, espere a que aparezca la ventana del juego y luego vuelva a la ventana del juego.
game.crash.reason.illegal_access_error=El juego se ha colgado por culpa de algún(os) mod(s).\n\
@@ -864,7 +864,7 @@ modpack.choose.remote=Descargar desde Internet
modpack.choose.remote.detail=Se requiere un enlace de descarga directa al archivo modpack remoto
modpack.choose.remote.tooltip=Por favor, introduzca la URL de su modpack
modpack.choose.repository=Descargar Modpack de CurseForge o Modrinth
modpack.choose.repository.detail=Puedes elegir el modpack que desees en la página siguiente.
modpack.choose.repository.detail=Recuerda volver a esta página y soltar aquí el archivo del modpack una vez descargado.
modpack.completion=Descargando dependencias
modpack.desc=Describa su modpack, incluyendo una introducción y probablemente algún registro de cambios. Actualmente se admiten Markdown e imágenes desde URL.
modpack.description=Descripción
@@ -932,50 +932,33 @@ modpack.wizard.step.initialization.warning=Antes de hacer un modpack, por favor
modpack.wizard.step.initialization.server=Haga clic aquí para ver más tutoriales para hacer un modpack de servidor que se pueda actualizar automáticamente.
modrinth.category.adventure=Aventura
modrinth.category.atmosphere=Atmósfera
modrinth.category.audio=Audio
modrinth.category.babric=Babric
modrinth.category.blocks=Bloqueos
modrinth.category.bloom=Resplandor
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=Caricatura
modrinth.category.challenging=Desafío
modrinth.category.colored-lighting=Iluminación de color
modrinth.category.combat=Combate
modrinth.category.core-shaders=Sombreadores de núcleo
modrinth.category.combat=Combate
modrinth.category.cursed=Maldición
modrinth.category.datapack=Paquete de datos
modrinth.category.decoration=Decoración
modrinth.category.economy=Economía
modrinth.category.entities=Entidades
modrinth.category.environment=Medio ambiente
modrinth.category.equipment=Equipo
modrinth.category.fabric=Fabric
modrinth.category.fantasy=Fantasía
modrinth.category.folia=Folia
modrinth.category.foliage=Vegetación
modrinth.category.fonts=Fonts
modrinth.category.food=Alimentos
modrinth.category.forge=Forge
modrinth.category.game-mechanics=Mecánica de juego
modrinth.category.gui=GUI
modrinth.category.high=Alto
modrinth.category.iris=Iris
modrinth.category.items=Objetos
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=Fregadero de cocina
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=Biblioteca
modrinth.category.lightweight=Peso ligero
modrinth.category.liteloader=LiteLoader
modrinth.category.locale=Locale
modrinth.category.low=Bajo
modrinth.category.magic=Magia
modrinth.category.management=Gestión
modrinth.category.medium=Medio
modrinth.category.minecraft=Minecraft
modrinth.category.minigame=Minijuego
modrinth.category.misc=Misceláneo
@@ -985,23 +968,13 @@ modrinth.category.models=Modelos
modrinth.category.modloader=Cargador de mods
modrinth.category.multiplayer=Multijugador
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=Optimización
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=Trazado de rutas
modrinth.category.pbr=PBR
modrinth.category.potato=Mínimo
modrinth.category.purpur=Purpur
modrinth.category.quests=Misiones
modrinth.category.quilt=Quilt
modrinth.category.realistic=Realista
modrinth.category.reflections=Reflexiones
modrinth.category.rift=Rift
modrinth.category.screenshot=Extremo
modrinth.category.semi-realistic=Semirrealista
modrinth.category.shadows=Sombras
modrinth.category.simplistic=Simplista
modrinth.category.social=Social
modrinth.category.spigot=Spigot
@@ -1012,11 +985,12 @@ modrinth.category.themed=Temática
modrinth.category.transportation=Transporte
modrinth.category.tweaks=Ajustes
modrinth.category.utility=Utilidad
modrinth.category.vanilla=Vainilla
modrinth.category.vanilla-like=Similar a la vainilla
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Generación de mundos
modrinth.category.datapack=Paquete de datos
modrinth.category.folia=Folia
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
@@ -1036,7 +1010,7 @@ mods.category=Categoría
mods.channel.alpha=Alpha
mods.channel.beta=Beta
mods.channel.release=Release
mods.check_updates=Actualizar
mods.check_updates=Comprobar actualizaciones
mods.check_updates.current_version=Versión actual
mods.check_updates.empty=Todos los mods están actualizados
mods.check_updates.failed_check=No se ha podido comprobar si hay actualizaciones.
@@ -1098,12 +1072,7 @@ world.backup.create.locked=El mundo está actualmente en uso. Por favor, cierra
world.backup.create.success=Creada con éxito una nueva copia de seguridad: %s
world.backup.delete=Eliminar esta copia de seguridad
world.backup.processing=Creando nueva copia de seguridad ...
world.chunkbase=Chunk Base
world.chunkbase.end_city=Ciudad del End
world.chunkbase.seed_map=Vista previa de la generación mundial
world.chunkbase.stronghold=Fortaleza
world.chunkbase.nether_fortress=Fortaleza del Nether
world.datapack=Paquetes de datos
world.datapack=Gestionar paquetes de datos
world.datetime=Jugado por última vez en %s
world.download=Descargar Mundo
world.download.title=Descargar mundo - %1s
@@ -1148,8 +1117,7 @@ world.info.time=Tiempo de juego
world.info.time.format=%s días
world.locked=En uso
world.manage=Mundos
world.manage.button=Administrar
world.manage.title=Mundo - %s
world.manage.title=Mundos - %s
world.name=Nombre del mundo
world.name.enter=Introducir el nombre del mundo
world.show_all=Mostrar todo
@@ -1262,7 +1230,7 @@ settings.advanced.natives_directory=Ruta de la biblioteca nativa
settings.advanced.natives_directory.choose=Elija la ubicación de la biblioteca nativa deseada
settings.advanced.natives_directory.custom=Personalizado
settings.advanced.natives_directory.default=Por defecto
settings.advanced.natives_directory.hint=Esta opción está pensada sólo para usuarios de chips de Apple u otras plataformas no soportadas oficialmente. Por favor, no modifique esta opción a menos que sepa lo que está haciendo.\n\
settings.advanced.natives_directory.hint=Esta opción está pensada sólo para usuarios de Apple M1 u otras plataformas no soportadas oficialmente. Por favor, no modifique esta opción a menos que sepa lo que está haciendo.\n\
\n\
Antes de proceder, por favor, asegúrese de que todas las bibliotecas (por ejemplo, lwjgl.dll, libopenal.so) se proporcionan en su directorio deseado.
settings.advanced.no_jvm_args=No añadir argumentos JVM por defecto
@@ -1407,8 +1375,6 @@ update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden ten
update.latest=Esta es la última versión.
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
update.tooltip=Actualización
update.preview=Vista previa de actualizaciones anticipadas
update.preview.tooltip=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
version=Juegos
version.name=Nombre de instancia
@@ -1440,7 +1406,6 @@ version.manage.manage=Editar instancia
version.manage.manage.title=Editar instancia - %1s
version.manage.redownload_assets_index=Actualizar activos del juego
version.manage.remove=Eliminar instancia
version.manage.remove.confirm=¿Está seguro de que quiere eliminar permanentemente la instancia «%s»? ¡Esta operación no se puede deshacer!
version.manage.remove.confirm.trash=¿Estás seguro de que quieres eliminar la instancia «%s»? Todavía puedes encontrar sus archivos en tu papelera de reciclaje con el nombre de «%s».
version.manage.remove.confirm.independent=Dado que esta instancia está almacenada en un directorio aislado, al eliminarla también se eliminarán sus guardados y otros datos. ¿Aún quieres borrar la instancia %s?
version.manage.remove_assets=Borrar todas las activos del juego
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,6 +20,7 @@
about=について
about.copyright=著作権
about.copyright.statement=Copyright © 2025 huangyuhui
about.author=著者
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -351,11 +352,13 @@ game.crash.info=ゲームステータス
game.crash.reason=クラッシュアナライザー
game.crash.reason.analyzing=分析中..
game.crash.reason.multiple=複数の理由が検出されました:\n\n
game.crash.reason.block=ブロックが原因でゲームを実行できません。\nMCEditでこのブロックを削除するか、関連するModを直接削除できます。\nブロックタイプ:%1$s\nブロックの場所:%2$s
game.crash.reason.bootstrap_failed=mod %1$s がクラッシュしたため、ゲームを実行できません。\n削除または更新を試みることができます。
game.crash.reason.config=modが構成ファイルを解析できないため、ゲームを実行できません。\nMod %1$s は構成ファイル %2$s を解析できません。
game.crash.reason.debug_crash=クラッシュが手動でトリガーされるため、ゲームを実行できません。\n実際、ゲームは無実であり、すべてあなたの責任です。
game.crash.reason.duplicated_mod=Mod %1$s が重複してインストールされているため、現在のゲームを継続して実行できません。\n%2$s\n各Modは1つしかインストールできませんので、余分なModを削除して再度お試しください。
game.crash.reason.mixin_apply_mod_failed=Mixin が %1$s モジュールを適用できないため、現在のゲームを続行できません。\n問題を解決するために、このモジュールを削除または更新してみてください。
game.crash.reason.entity=エンティティが原因でゲームを実行できません。\nMCEditでこのエンティティを削除するか、関連するModを直接削除できます。\nエンティティタイプ:%1$s\nエンティティの場所:%2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
game.crash.reason.modmixin_failure=mod インジェクションの失敗により、現在のゲームを続行できません。\nこれは通常、MOD にバグがあるか、現在の環境と互換性がないことを意味します。\n間違った mod のログを確認できます。
@@ -788,7 +791,7 @@ settings.advanced.natives_directory=ネイティブライブラリパス(例
settings.advanced.natives_directory.choose=ネイティブライブラリパスを選択してください
settings.advanced.natives_directory.custom=Custom(ゲームが実行時に必要とするネイティブライブラリを提供する必要があります)
settings.advanced.natives_directory.default=Standard(ネイティブライブラリは当社が提供します)
settings.advanced.natives_directory.hint=このオプションは、Appleシリコンまたはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。HMCLは、不足しているネイティブライブラリを提供しません。
settings.advanced.natives_directory.hint=このオプションは、AppleM1またはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。HMCLは、不足しているネイティブライブラリを提供しません。
settings.advanced.no_jvm_args=デフォルトのJVM引数なし
settings.advanced.precall_command=起動前コマンド
settings.advanced.precall_command.prompt=ゲーム開始前に実行されます
@@ -946,7 +949,6 @@ version.manage.manage=バージョンの管理
version.manage.manage.title=バージョンの管理 - %1s
version.manage.redownload_assets_index=ゲームアセットファイルの更新
version.manage.remove=このバージョンを削除します
version.manage.remove.confirm=このバージョン %s を削除してもよろしいですか?このバージョンを再度復元することはできません。
version.manage.remove.confirm.trash=このバージョン %s を削除してもよろしいですか?このバージョンは、システムのゴミ箱に %s という名前で復元できます。
version.manage.remove.confirm.independent=このバージョンは独立モードであるため、このバージョンを削除すると、このバージョンに属するすべての保存済みワールドも削除されます。このバージョン %s を削除しますか?
version.manage.remove_assets=すべてのゲームリソースファイルの削除
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,6 +20,7 @@
about=О лаунчере
about.copyright=Авторские права
about.copyright.statement=Авторские права © 2025 huangyuhui.
about.author=Автор
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -397,17 +398,6 @@ fatal.apply_update_failure=Мы сожалеем, лаунчер не смог
Вы можете обновить программу вручную, скачав более новую версию с %s.
fatal.apply_update_need_win7=Лаунчер не может автоматически обновляться на Windows XP/Vista.\n\
Вы можете обновить программу вручную, скачав более новую версию с %s.
fatal.deprecated_java_version=В будущем для работы HMCL потребуется Java 17 или более поздняя версия, однако запуск игр будет поддерживаться и на Java 6~16.\n\
\n\
Рекомендуется установить последнюю версию Java, чтобы обеспечить правильную работу HMCL.\n\
\n\
Вы можете продолжать использовать старую версию Java. HMCL может распознавать и управлять несколькими версиями Java и автоматически выбирать подходящую Java в зависимости от версии игры.
fatal.deprecated_java_version.update=В будущем для работы HMCL потребуется Java 17 или более поздняя версия. Пожалуйста, установить последнюю версию Java, чтобы убедиться, что приложение сможет завершить обновление.\n\
\n\
Вы можете продолжать использовать старую версию Java.\
HMCL может распознавать и управлять несколькими версиями Java \
и автоматически выбирать подходящую Java в зависимости от версии игры.
fatal.deprecated_java_version.download_link=Скачать Java %d
fatal.samba=Если вы пытаетесь открыть лаунчер в общей папке Samba, он может не работать, попробуйте обновить Java или запустить лаунчер из локальной папки.
fatal.illegal_char=Недопустимый символ «=» в пути к папке пользователя. Лаунчер может работать, но некоторые функции не будут работать.\n\
Вы не сможете использовать аккаунт authlib-injector или изменить скин для аккаунта в режиме офлайн.
@@ -443,7 +433,6 @@ folder.shaderpacks=Пакеты шейдеров
folder.saves=Миры
folder.schematics=Схемы
folder.screenshots=Снимки экрана
folder.world=Папка мира
game=Игры
game.crash.feedback=<b>Пожалуйста, не делитесь скриншотами этого интерфейса с другими!</b> Если вы просите помощи у других, нажмите <b>«Экспорт журналов с ошибками»</b> и отправьте экспортированный файл другим для анализа.
@@ -451,6 +440,12 @@ game.crash.info=Сведения об отказе
game.crash.reason=Анализатор сбоев
game.crash.reason.analyzing=Анализирование...
game.crash.reason.multiple=Обнаружено несколько причин:\n\n
game.crash.reason.block=Игра отказала из-за блока в мире.\n\
\n\
Удалите этот блок с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип блока: %1$s\n\
Расположение блока: %2$s
game.crash.reason.bootstrap_failed=Игра отказала из-за мода «%1$s».\n\
\n\
Попробуйте удалить или обновить его.
@@ -461,6 +456,12 @@ game.crash.reason.duplicated_mod=Не удалось продолжить зап
%2$s\n\
\n\
Каждый мод должен быть установлен только один, удалите дублирующий мод и попробуйте снова.
game.crash.reason.entity=Игра отказала из-за сущность в мире.\n\
\n\
Удалите эту сущность с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип сущность: %1$s\n\
Расположение сущность: %2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12 (и выше) несовместим с установленными модами. Следует понизить версию fabric до 0.11.7.
game.crash.reason.mixin_apply_mod_failed=Игра отказала из-за невозможности применить mixin к моду «%1$s».\n\
\n\
@@ -862,7 +863,7 @@ modpack.choose.remote=Скачать
modpack.choose.remote.detail=Необходима прямая ссылка на скачивание модпака
modpack.choose.remote.tooltip=Введите URL-адрес модпака
modpack.choose.repository=Скачать модпак с CurseForge или Modrinth
modpack.choose.repository.detail=Вы сможете выбрать нужный вам модпак на следующей странице.
modpack.choose.repository.detail=Не забудьте вернуться на эту страницу и перетащить файл модпака сюда после того, как модпак будет скачан.
modpack.completion=Скачивание зависимостей
modpack.desc=Опишите модпак, включая введение и журнал изменений. Поддерживаются Markdown и изображения по URL-адресу.
modpack.description=Описание
@@ -930,50 +931,33 @@ modpack.wizard.step.initialization.warning=Перед созданием мод
modpack.wizard.step.initialization.server=Нажмите здесь для получения дополнительных руководств по созданию модпака на сервере, который может автообновляться.
modrinth.category.adventure=Приключение
modrinth.category.atmosphere=Атмосфера
modrinth.category.audio=Аудио
modrinth.category.babric=Babric
modrinth.category.blocks=Блоки
modrinth.category.bloom=Свечение
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=Мультяшный
modrinth.category.challenging=Вызов
modrinth.category.colored-lighting=Цветное освещение
modrinth.category.combat=Бой
modrinth.category.core-shaders=Ядро шейдеров
modrinth.category.combat=Бой
modrinth.category.cursed=Cursed
modrinth.category.datapack=Набор данных
modrinth.category.decoration=Украшения
modrinth.category.economy=Экономика
modrinth.category.entities=Сущности
modrinth.category.environment=Окружающая среда
modrinth.category.equipment=Оборудование
modrinth.category.fabric=Fabric
modrinth.category.fantasy=Фэнтези
modrinth.category.folia=Folia
modrinth.category.foliage=Растительность
modrinth.category.fonts=Шрифты
modrinth.category.food=Еда
modrinth.category.forge=Forge
modrinth.category.game-mechanics=Игровая механика
modrinth.category.gui=ГИП
modrinth.category.high=Высокий
modrinth.category.iris=Iris
modrinth.category.items=Предметы
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=Kitchen-Sink
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=Библиотека
modrinth.category.lightweight=Легкий
modrinth.category.liteloader=LiteLoader
modrinth.category.locale=Язык
modrinth.category.low=Низкий
modrinth.category.magic=Магия
modrinth.category.management=Управление
modrinth.category.medium=Средний
modrinth.category.minecraft=Minecraft
modrinth.category.minigame=Мини-игра
modrinth.category.misc=Разное
@@ -983,23 +967,13 @@ modrinth.category.models=Модели
modrinth.category.modloader=Мод-загрузчик
modrinth.category.multiplayer=Сетевая игра
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=Оптимизация
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=Трассировка пути
modrinth.category.pbr=PBR
modrinth.category.potato=Минимум
modrinth.category.purpur=Purpur
modrinth.category.quests=Квесты
modrinth.category.quilt=Quilt
modrinth.category.realistic=Реалистичный
modrinth.category.reflections=Отражения
modrinth.category.rift=Rift
modrinth.category.screenshot=Экстрим
modrinth.category.semi-realistic=Полуреалистичный
modrinth.category.shadows=Тени
modrinth.category.simplistic=Упрощенный
modrinth.category.social=Социальная
modrinth.category.spigot=Spigot
@@ -1010,11 +984,12 @@ modrinth.category.themed=Тематический
modrinth.category.transportation=Транспорт
modrinth.category.tweaks=Усовершенствование
modrinth.category.utility=Утилиты
modrinth.category.vanilla=Ванильное
modrinth.category.vanilla-like=Ванильное
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=Генерация мира
modrinth.category.datapack=Набор данных
modrinth.category.folia=Folia
modrinth.category.8x-=8x-
modrinth.category.16x=16x
modrinth.category.32x=32x
@@ -1096,12 +1071,7 @@ world.backup.create.locked=В настоящее время мир находи
world.backup.create.success=Успешно создано новое резервное копирование: %s
world.backup.delete=Удалить эту резервную копию
world.backup.processing=Создание новой резервной копии ...
world.chunkbase=Chunk Base
world.chunkbase.end_city=Город Края
world.chunkbase.seed_map=Предпросмотр генерации мира
world.chunkbase.stronghold=Крепость
world.chunkbase.nether_fortress=Крепость Нижнего мира
world.datapack=Наборы данных
world.datapack=Управлять наборами данных
world.datetime=Последний запуск игры %s
world.download=Скачать мир
world.download.title=Скачать мир - %1s
@@ -1146,8 +1116,7 @@ world.info.time=Время игры
world.info.time.format=%s дн.
world.locked=В эксплуатации
world.manage=Миры
world.manage.button=Управлять
world.manage.title=Мир - %s
world.manage.title=Миры - %s
world.name=Название мира
world.name.enter=Введите название мира
world.show_all=Показать все
@@ -1437,7 +1406,6 @@ version.manage.manage=Изменить сборку
version.manage.manage.title=Изменить сборку - %1s
version.manage.redownload_assets_index=Обновить файлы игровых активов
version.manage.remove=Удалить
version.manage.remove.confirm=Удалить %s? Вы не сможете восстановить эту сборку снова!
version.manage.remove.confirm.trash=Удалить %s? Вы можете восстановить эту сборку с именем %s из корзины системы.
version.manage.remove.confirm.independent=Поскольку эта сборка находится в режиме разделения, удаление этой сборки приведет к удалению всех миров, принадлежащих этой сборке. Удалить %s?
version.manage.remove_assets=Удалить файлы игровых активов
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,7 @@
about=關於
about.copyright=著作權
about.copyright.statement=著作權所有 © 2013-2026 huangyuhui 與貢獻者
about.copyright.statement=著作權所有 © 2025 huangyuhui
about.author=作者
about.author.statement=bilibili @huanghongxun
about.claim=使用者協議
@@ -97,7 +97,7 @@ account.register=註冊
account.manage=帳戶清單
account.copy_uuid=複製該帳戶的 UUID
account.methods=登入方式
account.methods.authlib_injector=外部登入
account.methods.authlib_injector=外置登入
account.methods.microsoft=Microsoft 帳戶
account.methods.microsoft.birth=如何變更帳戶出生日期
account.methods.microsoft.deauthorize=移除應用存取權
@@ -131,7 +131,7 @@ account.methods.ban_query=查詢帳戶是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。請下載官方版本進行登入。
account.methods.microsoft.snapshot.website=官方網站
account.methods.offline=離線模式
account.methods.offline.name.special_characters=建議使用英文字母、數字以及底線命名,且長度不超過 16 個字元
account.methods.offline.name.special_characters=建議使用英文字母、數字以及底線命名
account.methods.offline.name.invalid=遊戲使用者名稱建議僅使用英文字母、數字及底線,且長度不超過 16 個字元。\n\
\n\
\ · 一些有效的使用者名稱:HuangYu、huang_Yu、Huang_Yu_123;\n\
@@ -186,7 +186,6 @@ button.export=匯出
button.no=否
button.ok=確定
button.ok.countdown=確定 (%d)
button.reveal_dir=開啟目錄
button.refresh=重新整理
button.remove=刪除
button.remove.confirm=你確認要刪除嗎?該操作無法復原!
@@ -377,14 +376,10 @@ fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft
fatal.config_in_temp_dir=你正在暫存目錄中開啟 Hello Minecraft! Launcher,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
fatal.config_unsupported_version=當前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被保存。
fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Launcher 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
fatal.apply_update_need_win7=Hello Minecraft! Launcher 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
fatal.deprecated_java_version=HMCL 未來需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。建議安裝最新版本的 Java 以確保 HMCL 正常執行。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能執行,請安裝最新版本的 Java 以確保 HMCL 能夠完成升級。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
fatal.deprecated_java_version.download_link=下載 Java %d
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
fatal.illegal_char=由於您的使用者目錄路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
fatal.unsupported_platform=Minecraft 尚未你您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「軟繪製器」,以獲得更好的相容性。
@@ -420,13 +415,13 @@ game.crash.info=遊戲訊息
game.crash.reason=崩潰原因
game.crash.reason.analyzing=分析中……
game.crash.reason.multiple=檢測到多個原因:\n\n
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯存檔刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯存檔刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s
game.crash.reason.bootstrap_failed=目前遊戲由於模組「%1$s」出現問題,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.config=目前遊戲由於無法解析模組配置檔案,無法繼續執行\n模組「%1$s」的配置檔案「%2$s」無法被解析。
game.crash.reason.debug_crash=目前遊戲由於手動觸發崩潰,無法繼續執行。\n事實上遊戲並沒有問題,問題都是你造成的!
game.crash.reason.duplicated_mod=目前遊戲由於模組重複安裝,無法繼續執行。\n%s\n每種模組只能安裝一個,請你刪除多餘的模組再試。
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯存檔刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯存檔刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
@@ -439,7 +434,7 @@ game.crash.reason.mod_name=目前遊戲由於模組檔案名稱問題,無法
game.crash.reason.incomplete_forge_installation=目前遊戲由於 Forge 安裝不完整,無法繼續執行。\n請在「實例管理 - 自動安裝」中移除 Forge 並重新安裝。
game.crash.reason.file_already_exists=目前遊戲由於檔案「%1$s」已經存在,無法繼續執行。\n如果你認為這個檔案可以刪除,你可以在備份這個檔案後嘗試刪除它,並重新啟動遊戲。
game.crash.reason.file_changed=目前遊戲由於檔案校驗失敗,無法繼續執行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。
game.crash.reason.gl_operation_failure=目前遊戲由於你使用的某些模組/光影包/資源包/紋理包出現問題,無法繼續執行。\n請先嘗試停用你所使用的模組/光影包/資源包/紋理包再試。
game.crash.reason.gl_operation_failure=目前遊戲由於你使用的某些模組/光影包/資源包/紋理包出現問題,無法繼續執行。\n請先嘗試禁用你所使用的模組/光影包/資源包/紋理包再試。
game.crash.reason.graphics_driver=目前遊戲由於你的顯示卡驅動存在問題崩潰了。請嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。\n\
如果你的電腦同時存在獨立顯示卡與整合式顯示卡,你需要檢查遊戲是否使用整合式顯示卡啟動。如果是,請嘗試使用獨立顯示卡開啟 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。\n\
如果你確實需要使用整合式顯示卡,請檢查你電腦的 CPU 是否為 Intel(R) Core(TM) 3000 系列或更舊的處理器。如果是,對於 Minecraft 1.16.5 及更低版本,請你將遊戲所使用的 Java 版本降級至 1.8.0_51 及更低版本;否則你需要更換獨立顯示卡或新電腦。
@@ -546,9 +541,9 @@ install.success=安裝成功
java.add=添加 Java
java.add.failed=Java 無效或與目前平臺不相容
java.disable=停用此 Java
java.disable.confirm=你確定要停用此 Java 嗎?
java.disabled.management=管理已停用的 Java
java.disable=禁用此 Java
java.disable.confirm=你確定要禁用此 Java 嗎?
java.disabled.management=管理已禁用的 Java
java.disabled.management.remove=從清單中移除此 Java
java.disabled.management.restore=重新啟用此 Java
java.download=下載 Java
@@ -672,7 +667,6 @@ message.info=提示
message.success=完成
message.unknown=未知
message.warning=警告
message.question=確認
modpack=模組包
modpack.choose=選取要安裝的遊戲模組包檔案
@@ -681,7 +675,7 @@ modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以
modpack.choose.remote=從網路下載模組包
modpack.choose.remote.detail=需要提供模組包的下載連結
modpack.choose.repository=從 CurseForge/Modrinth 下載模組包
modpack.choose.repository.detail=可直接在跳轉到的頁面安裝模組包
modpack.choose.repository.detail=下載後記得回到這個介面,把模組包拖進來哦
modpack.choose.remote.tooltip=要下載的模組包的連結
modpack.completion=下載模組包相關檔案
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新紀錄。支援 Markdown (圖片請上傳至網路)。
@@ -750,20 +744,17 @@ modpack.wizard.step.initialization.server=點選此處查看有關伺服器自
modrinth.category.adventure=冒險
modrinth.category.atmosphere=氛圍
modrinth.category.audio=聲音
modrinth.category.babric=Babric
modrinth.category.blocks=方塊
modrinth.category.bloom=泛光
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=卡通
modrinth.category.challenging=高難度
modrinth.category.colored-lighting=彩色光照
modrinth.category.combat=戰鬥
modrinth.category.core-shaders=核心著色器
modrinth.category.combat=戰鬥
modrinth.category.cursed=Cursed
modrinth.category.datapack=資料包
modrinth.category.decoration=裝飾
modrinth.category.economy=經濟
modrinth.category.entities=實體
@@ -771,7 +762,6 @@ modrinth.category.environment=環境
modrinth.category.equipment=裝備
modrinth.category.fabric=Fabric
modrinth.category.fantasy=幻想
modrinth.category.folia=Folia
modrinth.category.foliage=植被
modrinth.category.fonts=字體
modrinth.category.food=食物
@@ -781,9 +771,7 @@ modrinth.category.gui=GUI
modrinth.category.high=高
modrinth.category.iris=Iris
modrinth.category.items=物品
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=大雜燴
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=支援庫
modrinth.category.lightweight=輕量
modrinth.category.liteloader=LiteLoader
@@ -801,12 +789,10 @@ modrinth.category.models=模型
modrinth.category.modloader=ModLoader
modrinth.category.multiplayer=多人
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=最佳化
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=路徑追蹤
modrinth.category.path-tracing=路径追踪
modrinth.category.pbr=PBR
modrinth.category.potato=極低
modrinth.category.purpur=Purpur
@@ -833,6 +819,8 @@ modrinth.category.vanilla-like=類原生
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=世界生成
modrinth.category.datapack=資料包
modrinth.category.folia=Folia
mods=模組
mods.add=新增模組
@@ -893,7 +881,7 @@ datapack.choose_datapack=選取要匯入的資料包壓縮檔
datapack.extension=資料包
datapack.title=世界 [%s] - 資料包
web.failed=載入頁面失敗
web.failed=加載頁面失敗
web.open_in_browser=是否要在瀏覽器中開啟此連結:\n%s
web.view_in_browser=在瀏覽器中查看完整日誌
@@ -901,10 +889,10 @@ world=世界
world.add=新增世界
world.backup=世界備份
world.backup.create.new_one=建立新備份
world.backup.create.failed=建立備份失敗。\n%s
world.backup.create.failed=創建備份失敗。\n%s
world.backup.create.locked=該世界正在使用中,請關閉遊戲後重試。
world.backup.create.success=成功建立新備份:%s
world.backup.delete=刪除此備份
world.backup.create.success=成功創建新備份:%s
world.backup.delete=删除此備份
world.backup.processing=正在備份中……
world.chunkbase=世界地圖
world.chunkbase.end_city=終界城地圖
@@ -943,7 +931,6 @@ world.info.player.food_level=饑餓值
world.info.player.game_type=遊戲模式
world.info.player.game_type.adventure=冒險
world.info.player.game_type.creative=創造
world.info.player.game_type.hardcore=極限
world.info.player.game_type.spectator=旁觀者
world.info.player.game_type.survival=生存
world.info.player.health=生命值
@@ -996,7 +983,7 @@ schematics.create_directory.prompt=請輸入新目錄名稱
schematics.create_directory.failed=建立目錄失敗
schematics.create_directory.failed.already_exists=目錄已存在
schematics.create_directory.failed.empty_name=名稱不能為空
schematics.create_directory.failed.invalid_name=名稱中包含無效字元
schematics.create_directory.failed.invalid_name=名稱中包含非法字元
schematics.info.description=描述
schematics.info.enclosing_size=框大小
schematics.info.name=名稱
@@ -1044,7 +1031,7 @@ settings.advanced.custom_commands.hint=自訂指令被呼叫時將包含如下
\ · $INST_FABRIC: 若安裝了 Fabric,將會存在本環境變數;\n\
\ · $INST_QUILT: 若安裝了 Quilt,將會存在本環境變數。
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
settings.advanced.dont_check_jvm_validity=不檢查 Java 虛擬機與遊戲的相容性
settings.advanced.dont_check_jvm_validity=不檢查 JVM 與遊戲的相容性
settings.advanced.dont_patch_natives=不嘗試自動取代本機庫
settings.advanced.environment_variables=環境變數
settings.advanced.game_dir.default=預設 (".minecraft/")
@@ -1054,7 +1041,7 @@ settings.advanced.java_permanent_generation_space.prompt=單位 MiB
settings.advanced.jvm=Java 虛擬機設定
settings.advanced.jvm_args=Java 虛擬機參數
settings.advanced.jvm_args.prompt=\ · 若在「Java 虛擬機參數」中輸入的參數與預設參數相同,則不會新增;\n\
\ · 若在「Java 虛擬機參數」輸入任何 GC 參數,預設參數的 G1 參數會被停用;\n\
\ · 若在「Java 虛擬機參數」輸入任何 GC 參數,預設參數的 G1 參數會被禁用;\n\
\ · 啟用下方「不新增預設的 Java 虛擬機參數」可在啟動遊戲時不新增預設參數。
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
@@ -1067,9 +1054,8 @@ settings.advanced.natives_directory=本機庫路徑 (LWJGL)
settings.advanced.natives_directory.choose=選取本機庫路徑
settings.advanced.natives_directory.custom=自訂 (由你提供遊戲需要的本機庫)
settings.advanced.natives_directory.default=預設 (由啟動器提供遊戲本機庫)
settings.advanced.natives_directory.default.version_id=<版本 ID>
settings.advanced.natives_directory.hint=本選項提供給 Apple Silicon 等未受遊戲官方支援的平台來自訂遊戲本機庫。如果你不知道本選項的含義,請你不要修改本選項,否則會導致遊戲無法啟動。\n\n如果你要修改本選項,你需要保證自訂目錄下有遊戲所需的本機庫檔案,如 lwjgl.dll (liblwjgl.so), openal.dll (libopenal.so) 等檔案。啟動器不會幫你補全缺少的本機庫檔案。\n\n注意:建議指定的本機庫檔案路徑使用全英文字元,否則可能導致遊戲啟動失敗。
settings.advanced.no_jvm_args=不新增預設的 Java 虛擬機參數
settings.advanced.natives_directory.hint=本選項提供給 Apple M1 等未受遊戲官方支援的平台來自訂遊戲本機庫。如果你不知道本選項的含義,請你不要修改本選項,否則會導致遊戲無法啟動。\n\n如果你要修改本選項,你需要保證自訂目錄下有遊戲所需的本機庫檔案,如 lwjgl.dll (liblwjgl.so), openal.dll (libopenal.so) 等檔案。啟動器不會幫你補全缺少的本機庫檔案。\n\n注意:建議指定的本機庫檔案路徑使用全英文字元,否則可能導致遊戲啟動失敗。
settings.advanced.no_jvm_args=不新增預設的 JVM 參數
settings.advanced.precall_command=遊戲啟動前執行指令
settings.advanced.precall_command.prompt=將在遊戲啟動前呼叫使用
settings.advanced.process_priority=處理程式優先度
@@ -1116,7 +1102,7 @@ settings.game.java_directory.template=%s (%s)
settings.game.management=管理
settings.game.working_directory=執行路徑 (建議使用模組時選取「各實例獨立」。修改後請自行移動相關遊戲檔案,如存檔、模組設定等)
settings.game.working_directory.choose=選取執行目錄
settings.game.working_directory.hint=在「執行路徑」選項中選取「各實例獨立」使目前實例獨立存放設定、存檔、模組等資料。使用模組時建議開啟此選項以避免不同實例模組衝突。修改此選項後需自行移動存檔等檔案。
settings.game.working_directory.hint=在「執行路徑」選項中選取「各實例獨立」使目前實例獨立存放設定、存檔、模組等資料。使用模組時建議開啟此選項以避免不同版本模組衝突。修改此選項後需自行移動存檔等檔案。
settings.icon=遊戲圖示
@@ -1132,6 +1118,10 @@ settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
settings.launcher.font=字體
settings.launcher.font.anti_aliasing=抗鋸齒 (重啟後生效)
settings.launcher.font.anti_aliasing.auto=自動
settings.launcher.font.anti_aliasing.gray=灰度
settings.launcher.font.anti_aliasing.lcd=子像素
settings.launcher.general=一般
settings.launcher.language=語言 (重啟後生效)
settings.launcher.launcher_log.export=匯出啟動器日誌
@@ -1209,8 +1199,6 @@ update.note=開發版與預覽版包含更多的功能以及錯誤修復,但
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
update.preview=提前測試 HMCL 預覽版本
update.preview.tooltip=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
version=遊戲
version.name=遊戲實例名稱
@@ -1242,7 +1230,6 @@ version.manage.manage=實例管理
version.manage.manage.title=實例管理 - %1s
version.manage.redownload_assets_index=更新遊戲資源檔案
version.manage.remove=刪除該實例
version.manage.remove.confirm=真的要刪除實例「%s」嗎? 你將無法找回被刪除的檔案!!!
version.manage.remove.confirm.trash=真的要刪除實例「%s」嗎? 你可以在系統的資源回收筒 (或垃圾桶) 中還原目錄「%s」來找回該實例。
version.manage.remove.confirm.independent=由於該實例啟用了「(全域/實例特定) 遊戲設定 → 執行路徑 → 各實例獨立」設定,刪除該實例將導致該遊戲的存檔等資料一同被刪除!真的要刪除實例「%s」嗎?
version.manage.remove_assets=刪除所有遊戲資源檔案
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -20,7 +20,7 @@
about=关于
about.copyright=版权
about.copyright.statement=版权所有 © 2013-2026 huangyuhui 及贡献者
about.copyright.statement=版权所有 © 2025 huangyuhui
about.author=作者
about.author.statement=bilibili @huanghongxun
about.claim=用户协议
@@ -139,7 +139,7 @@ account.methods.ban_query=检测账户是否被封禁
account.methods.microsoft.snapshot=你正在使用第三方提供的 HMCL。请下载官方版本来登录微软账户。
account.methods.microsoft.snapshot.website=官方网站
account.methods.offline=离线模式
account.methods.offline.name.special_characters=建议使用英文字符、数字以及下划线命名,且长度不超过 16 个字符
account.methods.offline.name.special_characters=建议使用英文字符、数字以及下划线命名
account.methods.offline.name.invalid=游戏用户名建议仅使用英文字母、数字及下划线,且长度不超过 16 个字符。\n\
\n\
\ · 一些合法用户名:HuangYu、huang_Yu、Huang_Yu_123;\n\
@@ -181,7 +181,7 @@ archive.version=版本
assets.download=下载资源
assets.download_all=检查资源文件完整性
assets.index.malformed=资源文件的索引文件损坏。你可以在相应实例的“实例管理”页面中,点击左下角“管理 → 更新游戏资源文件”以修复该问题。\n你可以点击右上角帮助按钮进行求助。
assets.index.malformed=资源文件的索引文件损坏。你可以在相应版本的“版本管理”页面中,点击左下角“管理 → 更新游戏资源文件”以修复该问题。\n你可以点击右上角帮助按钮进行求助。
button.cancel=取消
button.change_source=切换下载源
@@ -194,7 +194,6 @@ button.export=导出
button.no=否
button.ok=确定
button.ok.countdown=确定 (%d)
button.reveal_dir=打开文件夹
button.refresh=刷新
button.remove=删除
button.remove.confirm=你确定要删除吗?此操作无法撤销!
@@ -386,17 +385,13 @@ fatal.config_change_owner_root=你正在使用 root 账户启动 Hello Minecraft
fatal.config_in_temp_dir=你正在临时文件夹中启动 Hello Minecraft! Launcher,你的设置和游戏数据可能会丢失。建议将 HMCL 移动至其他位置再启动。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 HMCL 对“%s”文件夹及该文件夹下的文件拥有读写权限。\n对于 macOS,尝试将 HMCL 放在除“桌面”“下载”“文稿”之外的有权限的地方再试。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 无法加载配置文件,因为配置文件是由用户“%1$s”创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$s\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.config_unsupported_version=当前配置文件是由更高版本的 Hello Minecraft! Launcher 创建的,当前版本的 HMCL 无法正常加载。请更新并重启 HMCL。\n在更新启动器之前,你修改的所有设置都不会被保存。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.mac_app_translocation=由于 macOS 的安全机制,Hello Minecraft! Launcher 被系统隔离至临时文件夹中。\n请将 HMCL 移动到其他文件夹后再尝试启动,否则你的设置和游戏数据可能会在重启后丢失。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.apply_update_need_win7=Hello Minecraft! Launcher 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
fatal.deprecated_java_version=HMCL 未来需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。建议安装最新版本的 Java 以确保 HMCL 能正常工作。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能运行,请安装最新版本的 Java 以确保 HMCL 能够完成升级。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。
fatal.deprecated_java_version.download_link=下载 Java %d
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.illegal_char=由于你的用户文件夹路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“软渲染器”,以获得更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/版本特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“软渲染器”,以获得更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
@@ -413,7 +408,7 @@ feedback.qq_group.statement=欢迎加入 HMCL 用户群,加入后请遵守群
file=文件
folder.config=配置文件夹
folder.game=实例运行文件夹
folder.game=游戏文件夹
folder.logs=日志文件夹
folder.mod=模组文件夹
folder.resourcepacks=资源包文件夹
@@ -428,14 +423,14 @@ game.crash.feedback=<b>请不要将本界面截图给他人!</b>如果你要
game.crash.info=游戏信息
game.crash.reason=崩溃原因
game.crash.reason.analyzing=分析中……
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑存档删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑存档删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s
game.crash.reason.bootstrap_failed=当前游戏由于模组“%1$s”出现问题,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.config=当前游戏由于无法解析模组配置文件,无法继续运行。\n无法解析模组“%1$s”的配置文件“%2$s”。
game.crash.reason.multiple=检测到多个原因:\n\n
game.crash.reason.debug_crash=当前游戏由于手动触发崩溃,无法继续运行。\n事实上游戏并没有问题,问题都是你造成的!
game.crash.reason.duplicated_mod=当前游戏由于模组“%1$s”重复安装,无法继续运行。\n%2$s\n每种模组只能安装一个,请你删除多余的模组再试。
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑存档删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑存档删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本与当前已经安装的模组可能不兼容,你需要将 Fabric Loader 降级至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
game.crash.reason.file_already_exists=当前游戏由于文件“%1$s”已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
@@ -449,12 +444,12 @@ game.crash.reason.graphics_driver=当前游戏由于显卡驱动问题而崩溃
game.crash.reason.macos_failed_to_find_service_port_for_display=当前游戏由于 Apple Silicon 平台下初始化 OpenGL 窗口失败,无法继续运行。\n对于该问题,HMCL 暂无直接性的解决方案。请你尝试任意打开一个浏览器并全屏,然后再回到 HMCL 启动游戏。在弹出游戏窗口前<b>迅速切回浏览器页面</b>,等待游戏窗口出现后再切回游戏窗口。
game.crash.reason.illegal_access_error=当前游戏由于某些模组的问题,无法继续运行。\n如果你认识“%1$s”,你可以更新或删除对应模组再试。
game.crash.reason.install_mixinbootstrap=当前游戏由于缺失 MixinBootstrap,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/mixinbootstrap">MixinBootstrap</a> 解决该问题。若安装后崩溃,可以尝试在该模组的文件名开头添加半角感叹号 (!) 解决。
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
game.crash.reason.jdk_9=当前游戏由于 Java 版本过高,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 8</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 1.8.x 的版本。
game.crash.reason.jvm_32bit=当前游戏由于内存分配过大,超过了 32 位 Java 虚拟机内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。<a href="https://bell-sw.com/pages/downloads/#downloads">下载 Java</a>\n如果你的电脑是 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以在“(全局/实例特定) 游戏设置 → 游戏内存”中关闭“自动分配内存”,并把内存分配值调整为 1024 MiB 或以下。
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/版本特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
game.crash.reason.jdk_9=当前游戏由于 Java 版本过高,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 8</a>,并在“(全局/版本特定) 游戏设置 → 游戏 Java”中将 Java 设置为 1.8.x 的版本。
game.crash.reason.jvm_32bit=当前游戏由于内存分配过大,超过了 32 位 Java 虚拟机内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。<a href="https://bell-sw.com/pages/downloads/#downloads">下载 Java</a>\n如果你的电脑是 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以在“(全局/版本特定) 游戏设置 → 游戏内存”中关闭“自动分配内存”,并把内存分配值调整为 1024 MiB 或以下。
game.crash.reason.loading_crashed_forge=当前游戏由于模组“%1$s (%2$s)”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.loading_crashed_fabric=当前游戏由于模组“%1$s”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/实例特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/版本特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
game.crash.reason.mac_jdk_8u261=当前游戏由于你所使用的 Forge/OptiFine 与 Java 冲突而崩溃。\n请尝试更新 Forge/OptiFine,或使用 Java 8u251 及更早版本启动。
game.crash.reason.mod=当前游戏由于“%1$s”的问题,无法继续运行。\n你可以更新或删除已经安装的“%1$s”再试。
game.crash.reason.mod_resolution=当前游戏由于模组依赖问题,无法继续运行。Fabric 提供了如下信息:\n%1$s
@@ -464,37 +459,37 @@ game.crash.reason.mod_resolution_missing=当前游戏由于缺少前置模组,
game.crash.reason.mod_resolution_missing_minecraft=当前游戏由于模组和 Minecraft 游戏版本不匹配,无法继续运行。\n“%1$s”需要 Minecraft %2$s 才能运行。\n如果你要继续使用你已经安装的模组,你可以选择安装对应的 Minecraft 版本。如果你要继续使用当前 Minecraft 版本,你需要安装对应版本的模组。
game.crash.reason.mod_resolution_mod_version=%1$s (版本号 %2$s)
game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本)
game.crash.reason.forge_repeat_installation=当前游戏由于 Forge 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“实例管理 → 自动安装”卸载安装的 OptiFine。
game.crash.reason.forge_repeat_installation=当前游戏由于 Forge 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“版本管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“版本管理 → 自动安装”卸载安装的 OptiFine。
game.crash.reason.forgemod_resolution=当前游戏由于模组依赖问题,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
game.crash.reason.forge_found_duplicate_mods=当前游戏由于模组重复安装,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
game.crash.reason.modmixin_failure=当前游戏由于某些模组注入失败,无法继续运行。\n这一般代表着该模组存在问题,或与当前环境不兼容。\n你可以查看日志寻找出错模组。
game.crash.reason.night_config_fixes=当前游戏由于 Night Config 库的一些问题,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模组,这或许能帮助你解决这个问题。\n更多详情,可访问该模组的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 仓库</a>。
game.crash.reason.forge_error=Forge/NeoForge 可能已经提供了错误信息。\n你可以查看日志,并根据错误报告中的日志信息进行对应处理。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\n%1$s
game.crash.reason.mod_resolution0=当前游戏由于一些模组出现问题,无法继续运行。\n你可以查看日志寻找出错模组。
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/实例特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/版本特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
game.crash.reason.mod_name=当前游戏由于模组文件名称问题,无法继续运行。\n模组文件名称应只使用半角的大小写字母 (Aa~Zz)、数字 (0~9)、横线 (-)、下划线 (_)和点 (.)。\n请到模组文件夹中将所有不合规的模组文件名修改为上述合规字符。
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“实例管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“版本管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“版本管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
game.crash.reason.mod_files_are_decompressed=当前游戏由于模组文件被解压了,无法继续运行。\n请直接把整个模组文件放进模组文件夹中即可。\n解压模组文件会导致游戏出错。请删除模组文件夹中已被解压的模组,然后再启动游戏。
game.crash.reason.shaders_mod=当前游戏由于同时安装了 OptiFine 和 Shaders 模组,无法继续运行。\n由于 OptiFine 已集成 Shaders 模组的功能,所以只需删除 Shaders 模组即可。
game.crash.reason.rtss_forest_sodium=当前游戏由于 RivaTuner Statistics Server (RTSS) 与 Sodium 不兼容,导致游戏崩溃。\n点击 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此处</a> 查看详情。
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=当前游戏由于你所安装的模组过多,超出了游戏的 ID 限制,无法继续运行。\n请尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a> 等修复模组,或删除部分大型模组。
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“实例管理 → 自动安装”中更换 OptiFine 的版本。
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“版本管理 → 自动安装”中更换 OptiFine 的版本。
game.crash.reason.modlauncher_8=当前游戏由于你所使用的 Forge 版本与当前使用的 Java 冲突而崩溃。请尝试更新 Forge 到 36.2.26 或更高版本或换用版本低于 1.8.0.320 的 Java。<a href="https://bell-sw.com/pages/downloads/?version=java-8&package=jre-full">Liberica JDK 8u312+7</a>
game.crash.reason.no_class_def_found_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。\n缺失:\n%1$s
game.crash.reason.no_such_method_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。
game.crash.reason.opengl_not_supported=当前游戏由于你的显卡驱动存在问题,无法继续运行。\n原因是 OpenGL 不受支持,你现在是否在远程桌面或者串流模式下?如果是,请直接使用原电脑启动游戏。\n或者尝试升级你的显卡驱动到最新版本后再尝试启动游戏。如果你的电脑同时存在独立显卡和集成显卡,你需要检查游戏是否使用集成显卡启动。如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑。
game.crash.reason.openj9=当前游戏无法在 OpenJ9 虚拟机上运行。请你在“(全局/实例特定) 游戏设置 → 游戏 Java”中更换为使用 Hotspot 虚拟机的 Java,并重新启动游戏。如果没有下载安装,你可以在网上自行下载。
game.crash.reason.out_of_memory=当前游戏由于内存不足,无法继续运行。\n这可能是内存分配过小,或者模组数量过多导致的。\n你可以在“(全局/实例特定) 游戏设置 → 游戏内存”中调整内存分配值以允许游戏在更大内存下运行。\n如果仍然出现该错误,你可能需要换一台更好的电脑。
game.crash.reason.openj9=当前游戏无法在 OpenJ9 虚拟机上运行。请你在“(全局/版本特定) 游戏设置 → 游戏 Java”中更换为使用 Hotspot 虚拟机的 Java,并重新启动游戏。如果没有下载安装,你可以在网上自行下载。
game.crash.reason.out_of_memory=当前游戏由于内存不足,无法继续运行。\n这可能是内存分配过小,或者模组数量过多导致的。\n你可以在“(全局/版本特定) 游戏设置 → 游戏内存”中调整内存分配值以允许游戏在更大内存下运行。\n如果仍然出现该错误,你可能需要换一台更好的电脑。
game.crash.reason.resolution_too_high=当前游戏由于资源包/纹理包分辨率过高,无法继续运行。\n你可以更换一个分辨率更低的资源包/纹理包,或者更换一个显存更大的显卡。
game.crash.reason.stacktrace=原因未知。请点击日志按钮查看详细信息。\n下面是一些关键词,其中可能包含模组名称,你可以通过搜索的方式查找有关信息。\n%s
game.crash.reason.too_old_java=当前游戏由于 Java 版本过低,无法继续运行。\n你需要在“(全局/实例特定) 游戏设置 → 游戏 Java”中更换 Java %1$s 或更新版本的 Java,并重新启动游戏。如果没有下载安装,你可以点击 <a href="https://learn.microsoft.com/java/openjdk/download">此处</a> 下载 Microsoft JDK。
game.crash.reason.too_old_java=当前游戏由于 Java 版本过低,无法继续运行。\n你需要在“(全局/版本特定) 游戏设置 → 游戏 Java”中更换 Java %1$s 或更新版本的 Java,并重新启动游戏。如果没有下载安装,你可以点击 <a href="https://learn.microsoft.com/java/openjdk/download">此处</a> 下载 Microsoft JDK。
game.crash.reason.unknown=原因未知。请点击日志按钮查看详细信息。
game.crash.reason.unsatisfied_link_error=当前游戏由于缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在“(全局/实例特定) 游戏设置 → 高级设置”中修改了本地库路径选项,请你修改回默认模式。\n如果你正在使用默认模式,请检查游戏文件夹路径是否只包含英文字母、数字和下划线。\n如果是,那么请检查是否为模组或 HMCL 导致了本地库缺失的问题。如果你确定是 HMCL 引起的,建议你向我们反馈。\n<b>对于 Windows 用户,你还可以尝试在“控制面板 → 时钟和区域 → 区域 → 管理 → 更改系统区域设置”中将“当前系统区域设置”修改为“中文(简体,中国大陆)”,并关闭“Beta 版:使用 Unicode UTF-8 提供全球语言支持”选项;</b>\n<b>或将游戏文件夹路径中的所有非英文字符的名称 (例如中文、空格等) 修改为英文字符。</b>\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库!
game.crash.reason.unsatisfied_link_error=当前游戏由于缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在“(全局/版本特定) 游戏设置 → 高级设置”中修改了本地库路径选项,请你修改回默认模式。\n如果你正在使用默认模式,请检查游戏文件夹路径是否只包含英文字母、数字和下划线。\n如果是,那么请检查是否为模组或 HMCL 导致了本地库缺失的问题。如果你确定是 HMCL 引起的,建议你向我们反馈。\n<b>对于 Windows 用户,你还可以尝试在“控制面板 → 时钟和区域 → 区域 → 管理 → 更改系统区域设置”中将“当前系统区域设置”修改为“中文(简体,中国大陆)”,并关闭“Beta 版:使用 Unicode UTF-8 提供全球语言支持”选项;</b>\n<b>或将游戏文件夹路径中的所有非英文字符的名称 (例如中文、空格等) 修改为英文字符。</b>\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库!
game.crash.title=游戏意外退出
game.directory=游戏文件夹路径
game.version=游戏实例
game.version=游戏版本
help=帮助
help.doc=Hello Minecraft! Launcher 帮助文档
@@ -505,7 +500,7 @@ input.number=必须是数字
input.not_empty=必填项
input.url=必须是合法的链接
install=添加实例
install=添加游戏
install.change_version=更换版本
install.change_version.confirm=你确定要将 %s 从 %s 更换成 %s 吗?
install.change_version.process=更换版本
@@ -547,8 +542,8 @@ install.modpack=安装整合包
install.modpack.installation=安装整合包
install.name.invalid=名称中包含非 ASCII 字符(如 Emoji 表情或中文字符)。\n建议修改名称。名称建议仅包含英文字母、数字和下划线,以防启动游戏时出现问题。是否继续安装?
install.new_game=安装新游戏
install.new_game.already_exists=此实例已经存在,请换一个名字
install.new_game.current_game_version=当前游戏实例
install.new_game.already_exists=此版本已经存在,请换一个名字
install.new_game.current_game_version=当前游戏版本
install.new_game.installation=安装新游戏
install.new_game.malformed=名字不合法
install.select=请选择安装方式
@@ -590,10 +585,10 @@ lang.default=跟随系统语言
launch.advice=%s 是否继续启动?
launch.advice.multi=检测到以下问题:\n\n%s\n\n这些问题可能导致游戏无法正常启动或影响游戏体验,是否继续启动?\n你可以点击右上角帮助按钮进行求助。
launch.advice.java.auto=当前选择的 Java 版本不满足游戏要求。\n点击“是”即可由 HMCL 来自动选取合适的 Java 版本。\n或者你可以在“(全局/实例特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。
launch.advice.java.auto=当前选择的 Java 版本不满足游戏要求。\n点击“是”即可由 HMCL 来自动选取合适的 Java 版本。\n或者你可以在“(全局/版本特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。
launch.advice.java.modded_java_7=Minecraft 1.7.2 及更低版本需要 Java 7 及更低版本。
launch.advice.corrected=我们已经修复了 Java 版本问题。如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.uncorrected=如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.corrected=我们已经修复了 Java 版本问题。如果你确实希望使用你自定义的 Java,你可以在“(全局/版本特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.uncorrected=如果你确实希望使用你自定义的 Java,你可以在“(全局/版本特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.different_platform=你正在使用 32 位 Java 启动游戏。建议更换至 64 位 Java。
launch.advice.forge2760_liteloader=Forge 14.23.5.2760 与 LiteLoader 不兼容。请更新 Forge 至 14.23.5.2773 或更高版本。
launch.advice.forge28_2_2_optifine=Forge 28.2.2 及更高版本与 OptiFine 不兼容。请降级 Forge 至 28.2.1 或更低版本。
@@ -611,7 +606,7 @@ launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2
launch.advice.vanilla_x86.translation=Minecraft 尚未为你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n你可以在 <a href="https://learn.microsoft.com/java/openjdk/download">这里</a> 下载 <b>x86-64</b> 架构的 Java 以获得更完整的体验。
launch.advice.unknown=由于以下原因,无法继续启动游戏:
launch.failed=启动失败
launch.failed.cannot_create_jvm=无法创建 Java 虚拟机。可能是虚拟机参数有问题。你可以在“(全局/实例特定) 游戏设置 → 高级设置 → Java 虚拟机设置”中移除所有虚拟机参数后,尝试再次启动游戏。
launch.failed.cannot_create_jvm=无法创建 Java 虚拟机。可能是虚拟机参数有问题。你可以在“(全局/版本特定) 游戏设置 → 高级设置 → Java 虚拟机设置”中移除所有虚拟机参数后,尝试再次启动游戏。
launch.failed.creating_process=启动失败。在创建新进程时发生错误,可能是 Java 路径错误。
launch.failed.command_too_long=命令长度超过限制,无法创建批处理脚本。请导出为 PowerShell 脚本。
launch.failed.decompressing_natives=未能解压游戏本地库。
@@ -622,7 +617,7 @@ launch.failed.execution_policy.failed_to_set=设置执行策略失败
launch.failed.execution_policy.hint=当前执行策略阻止你执行 PowerShell 脚本。\n点击“确定”允许当前用户执行本地 PowerShell 脚本,或点击“取消”保持现状。
launch.failed.exited_abnormally=游戏非正常退出。请查看日志文件,或联系他人寻求帮助。
launch.failed.java_version_too_low=你所指定的 Java 版本过低。请重新设置 Java 版本。
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。是否使用默认 Java 启动游戏?点击“是”使用默认 Java 继续启动游戏,\n或者在“(全局/实例特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。\n你可以点击右上角帮助按钮进行求助。
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。是否使用默认 Java 启动游戏?点击“是”使用默认 Java 继续启动游戏,\n或者在“(全局/版本特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。\n你可以点击右上角帮助按钮进行求助。
launch.failed.sigkill=游戏被用户或系统强制终止。
launch.state.dependencies=处理游戏依赖
launch.state.done=启动完成
@@ -682,7 +677,6 @@ message.info=提示
message.success=完成
message.unknown=未知
message.warning=警告
message.question=确认
modpack=整合包
modpack.choose=选择要安装的游戏整合包文件
@@ -691,7 +685,7 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
modpack.choose.remote=从互联网下载整合包
modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.repository=从 CurseForge/Modrinth 下载整合包
modpack.choose.repository.detail=可直接在跳转到的页面安装整合包
modpack.choose.repository.detail=下载后记得回到这个界面,把整合包拖进来哦
modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.completion=下载整合包相关文件
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录。支持 Markdown (图片请用网络链接)。
@@ -750,30 +744,27 @@ modpack.wizard.step.2=文件选择
modpack.wizard.step.2.title=选中你想添加到整合包中的文件或文件夹
modpack.wizard.step.3=整合包类型
modpack.wizard.step.3.title=选择整合包导出类型
modpack.wizard.step.initialization.exported_version=要导出的游戏实例
modpack.wizard.step.initialization.exported_version=要导出的游戏版本
modpack.wizard.step.initialization.force_update=强制升级整合包至最新版本 (需要自建服务器)
modpack.wizard.step.initialization.include_launcher=包含启动器
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
modpack.wizard.step.initialization.warning=在制作整合包前,请你确认你选择的实例可以正常启动,\n并保证你的 Minecraft 是正式版而非快照,\n而且不应当将不允许非官方途径传播的模组、资源 (纹理) 包等纳入整合包。\n整合包会保存你目前的下载源设置。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
modpack.wizard.step.initialization.warning=在制作整合包前,请你确认你选择的版本可以正常启动,\n并保证你的 Minecraft 是正式版而非快照,\n而且不应当将不允许非官方途径传播的模组、资源 (纹理) 包等纳入整合包。\n整合包会保存你目前的下载源设置。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
modpack.wizard.step.initialization.server=点击此处查看有关服务器自动更新整合包的制作教程
modrinth.category.adventure=冒险
modrinth.category.atmosphere=氛围
modrinth.category.audio=声音
modrinth.category.babric=Babric
modrinth.category.blocks=方块
modrinth.category.bloom=泛光
modrinth.category.bta-babric=BTA (Babric)
modrinth.category.bukkit=Bukkit
modrinth.category.bungeecord=BungeeCord
modrinth.category.canvas=Canvas
modrinth.category.cartoon=卡通
modrinth.category.challenging=高难度
modrinth.category.colored-lighting=彩色光照
modrinth.category.combat=战斗
modrinth.category.core-shaders=核心着色器
modrinth.category.combat=战斗
modrinth.category.cursed=Cursed
modrinth.category.datapack=数据包
modrinth.category.decoration=装饰
modrinth.category.economy=经济
modrinth.category.entities=实体
@@ -781,7 +772,6 @@ modrinth.category.environment=环境
modrinth.category.equipment=装备
modrinth.category.fabric=Fabric
modrinth.category.fantasy=幻想
modrinth.category.folia=Folia
modrinth.category.foliage=植被
modrinth.category.fonts=字体
modrinth.category.food=食物
@@ -791,9 +781,7 @@ modrinth.category.gui=GUI
modrinth.category.high=高
modrinth.category.iris=Iris
modrinth.category.items=物品
modrinth.category.java-agent=Java Agent
modrinth.category.kitchen-sink=大杂烩
modrinth.category.legacy-fabric=Legacy Fabric
modrinth.category.library=支持库
modrinth.category.lightweight=轻量
modrinth.category.liteloader=LiteLoader
@@ -811,10 +799,8 @@ modrinth.category.models=模型
modrinth.category.modloader=Modloader
modrinth.category.multiplayer=多人
modrinth.category.neoforge=NeoForge
modrinth.category.nilloader=NilLoader
modrinth.category.optifine=OptiFine
modrinth.category.optimization=优化
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=路径追踪
modrinth.category.pbr=PBR
@@ -843,6 +829,8 @@ modrinth.category.vanilla-like=类原生
modrinth.category.velocity=Velocity
modrinth.category.waterfall=Waterfall
modrinth.category.worldgen=世界生成
modrinth.category.datapack=数据包
modrinth.category.folia=Folia
mods=模组
mods.add=添加模组
@@ -889,7 +877,7 @@ mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge
mods.restore=回退
mods.url=官方页面
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
mods.install=安装到当前实例
mods.install=安装到当前版本
mods.save_as=下载到本地文件夹
nbt.entries=%s 个条目
@@ -954,7 +942,6 @@ world.info.player.food_level=饥饿值
world.info.player.game_type=游戏模式
world.info.player.game_type.adventure=冒险
world.info.player.game_type.creative=创造
world.info.player.game_type.hardcore=极限
world.info.player.game_type.spectator=旁观者
world.info.player.game_type.survival=生存
world.info.player.health=生命值
@@ -1042,9 +1029,9 @@ settings.advanced.modify=编辑高级设置
settings.advanced.title=高级设置 - %s
settings.advanced.custom_commands=自定义命令
settings.advanced.custom_commands.hint=自定义命令被调用时将包含如下的环境变量:\n\
\ · $INST_NAME: 实例名称;\n\
\ · $INST_ID: 实例名称;\n\
\ · $INST_DIR: 当前实例运行路径;\n\
\ · $INST_NAME: 版本名称;\n\
\ · $INST_ID: 版本名称;\n\
\ · $INST_DIR: 当前版本运行路径;\n\
\ · $INST_MC_DIR: 当前游戏文件夹路径;\n\
\ · $INST_JAVA: 游戏运行使用的 Java 路径;\n\
\ · $INST_FORGE: 若安装了 Forge,将会存在本环境变量;\n\
@@ -1054,11 +1041,11 @@ settings.advanced.custom_commands.hint=自定义命令被调用时将包含如
\ · $INST_FABRIC: 若安装了 Fabric,将会存在本环境变量;\n\
\ · $INST_QUILT: 若安装了 Quilt,将会存在本环境变量。
settings.advanced.dont_check_game_completeness=不检查游戏完整性
settings.advanced.dont_check_jvm_validity=不检查 Java 虚拟机与游戏的兼容性
settings.advanced.dont_check_jvm_validity=不检查 JVM 与游戏的兼容性
settings.advanced.dont_patch_natives=不尝试自动替换本地库
settings.advanced.environment_variables=环境变量
settings.advanced.game_dir.default=默认 (".minecraft/")
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
settings.advanced.game_dir.independent=各版本独立 (存放在 ".minecraft/versions/<版本名>/",除 assets、libraries 外)
settings.advanced.java_permanent_generation_space=内存永久保存区域
settings.advanced.java_permanent_generation_space.prompt=单位 MiB
settings.advanced.jvm=Java 虚拟机设置
@@ -1077,9 +1064,8 @@ settings.advanced.natives_directory=本地库路径 (LWJGL)
settings.advanced.natives_directory.choose=选择本地库路径
settings.advanced.natives_directory.custom=自定义 (由你提供游戏需要的本地库)
settings.advanced.natives_directory.default=默认 (由启动器提供游戏本地库)
settings.advanced.natives_directory.default.version_id=<实例名称>
settings.advanced.natives_directory.hint=本选项提供给 Apple Silicon 等未受游戏官方支持的平台来自定义游戏本地库。如果你不知道本选项的含义,请不要修改本选项,否则会导致游戏无法启动!\n\n如果你要修改本选项,你需要保证自定义文件夹下有游戏所需的本地库文件,如 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 等文件。启动器不会帮你补全缺少的本地库文件!\n\n注意:指定的本地库文件路径建议只包含英文大小写字母、数字和下划线,否则可能会导致启动游戏失败。
settings.advanced.no_jvm_args=不添加默认的 Java 虚拟机参数
settings.advanced.natives_directory.hint=本选项提供给 Apple M1 等未受游戏官方支持的平台来自定义游戏本地库。如果你不知道本选项的含义,请不要修改本选项,否则会导致游戏无法启动!\n\n如果你要修改本选项,你需要保证自定义文件夹下有游戏所需的本地库文件,如 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 等文件。启动器不会帮你补全缺少的本地库文件!\n\n注意:指定的本地库文件路径建议只包含英文大小写字母、数字和下划线,否则可能会导致启动游戏失败。
settings.advanced.no_jvm_args=不添加默认的 JVM 参数
settings.advanced.precall_command=游戏启动前执行命令
settings.advanced.precall_command.prompt=将在游戏启动前调用
settings.advanced.process_priority=进程优先级
@@ -1110,7 +1096,7 @@ settings.custom=自定义
settings.game=游戏设置
settings.game.copy_global=复制全局游戏设置
settings.game.copy_global.copy_all=复制全部
settings.game.copy_global.copy_all.confirm=你确定要覆盖当前实例特定游戏设置吗?此操作无法撤销!
settings.game.copy_global.copy_all.confirm=你确定要覆盖当前版本特定游戏设置吗?此操作无法撤销!
settings.game.current=游戏
settings.game.dimension=游戏窗口分辨率
settings.game.exploration=浏览
@@ -1124,9 +1110,9 @@ settings.game.java_directory.invalid=Java 路径不正确
settings.game.java_directory.version=指定 Java 版本
settings.game.java_directory.template=%s (%s)
settings.game.management=管理
settings.game.working_directory=版本隔离 (建议使用模组时选择“各实例独立”。改后需移动存档、模组等相关游戏文件)
settings.game.working_directory=版本隔离 (建议使用模组时选择“各版本独立”。改后需移动存档、模组等相关游戏文件)
settings.game.working_directory.choose=选择运行文件夹
settings.game.working_directory.hint=在“版本隔离”中选择“各实例独立”使当前实例独立存放设置、存档、模组等数据。使用模组时建议启用此选项以避免不同实例模组冲突。修改此选项后需自行移动存档等文件。
settings.game.working_directory.hint=在“版本隔离”中选择“各版本独立”使当前版本独立存放设置、存档、模组等数据。使用模组时建议启用此选项以避免不同版本模组冲突。修改此选项后需自行移动存档等文件。
settings.icon=游戏图标
@@ -1142,6 +1128,10 @@ settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.font=字体
settings.launcher.font.anti_aliasing=抗锯齿 (重启后生效)
settings.launcher.font.anti_aliasing.auto=自动
settings.launcher.font.anti_aliasing.gray=灰度
settings.launcher.font.anti_aliasing.lcd=子像素
settings.launcher.general=通用
settings.launcher.language=语言 (重启后生效)
settings.launcher.launcher_log.export=导出启动器日志
@@ -1178,13 +1168,13 @@ settings.physical_memory=物理内存大小
settings.show_log=查看日志
settings.tabs.installers=自动安装
settings.take_effect_after_restart=重启后生效
settings.type=实例游戏设置类型
settings.type.global=全局游戏设置 (未启用“实例特定游戏设置”的实例共用此设置)
settings.type=版本游戏设置类型
settings.type.global=全局游戏设置 (未启用“版本特定游戏设置”的版本共用此设置)
settings.type.global.manage=全局游戏设置
settings.type.global.edit=编辑全局游戏设置
settings.type.special.enable=启用实例特定游戏设置 (不影响其他游戏实例)
settings.type.special.edit=编辑实例特定游戏设置
settings.type.special.edit.hint=当前游戏实例“%s”启用了“实例特定游戏设置”,因此本页面选项不对该实例生效。点击链接前往该实例的“游戏设置”页。
settings.type.special.enable=启用版本特定游戏设置 (不影响其他游戏版本)
settings.type.special.edit=编辑版本特定游戏设置
settings.type.special.edit.hint=当前游戏版本“%s”启用了“版本特定游戏设置”,因此本页面选项不对该版本生效。点击链接前往该版本的“游戏设置”页。
sponsor=赞助
sponsor.bmclapi=国内下载源由 BMCLAPI 提供高速下载服务。BMCLAPI 为公益服务,赞助 BMCLAPI 可以帮助作者更好地提供稳定高速的下载服务。[点击此处查阅详细信息]
@@ -1219,15 +1209,13 @@ update.note=开发版与预览版包含更多的功能以及错误修复,但
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
update.preview=提前预览 HMCL 版本
update.preview.tooltip=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
version=游戏
version.name=游戏实例名称
version.cannot_read=读取游戏实例失败,无法进行自动安装
version.empty=没有游戏实例
version.name=游戏版本名称
version.cannot_read=读取游戏版本失败,无法进行自动安装
version.empty=没有游戏版本
version.empty.add=进入下载页安装游戏
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“实例列表”切换游戏文件夹。
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。
version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。
version.game.old=远古版
version.game.release=正式版
@@ -1236,30 +1224,29 @@ version.game.snapshot=快照
version.game.snapshots=快照
version.launch=启动游戏
version.launch.test=测试游戏
version.switch=切换实例
version.switch=切换版本
version.launch_script=生成启动脚本
version.launch_script.failed=生成启动脚本失败
version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕:%s
version.manage=实例列表
version.manage=版本列表
version.manage.clean=清理游戏文件夹
version.manage.clean.tooltip=清理“logs”和“crash-reports”文件夹
version.manage.duplicate=复制游戏实例
version.manage.duplicate=复制游戏版本
version.manage.duplicate.duplicate_save=复制存档
version.manage.duplicate.prompt=请输入新游戏名称
version.manage.duplicate.confirm=新的游戏实例将复制该实例文件夹 (".minecraft/versions/<实例名>") 下的文件,并带有独立的运行文件夹和设置。
version.manage.manage=实例管理
version.manage.manage.title=实例管理 - %1s
version.manage.duplicate.confirm=新的游戏版本将复制该版本文件夹 (".minecraft/versions/<版本名>") 下的文件,并带有独立的运行文件夹和设置。
version.manage.manage=版本管理
version.manage.manage.title=版本管理 - %1s
version.manage.redownload_assets_index=更新游戏资源文件
version.manage.remove=删除该实例
version.manage.remove.confirm=真的要删除实例“%s”吗?你将无法找回被删除的文件!!!
version.manage.remove.confirm.trash=真的要删除实例“%s”吗?你可以在系统的回收站中还原“%s”文件夹来找回该实例。
version.manage.remove.confirm.independent=由于该游戏启用了“(全局/实例特定) 游戏设置 → 版本隔离 → 各实例独立”选项,删除该实例将导致该游戏的存档等数据一同被删除!真的要删除实例“%s”吗?
version.manage.remove=删除该版本
version.manage.remove.confirm.trash=真的要删除版本“%s”吗?你可以在系统的回收站中还原“%s”文件夹来找回该版本。
version.manage.remove.confirm.independent=由于该游戏启用了“(全局/版本特定) 游戏设置 → 版本隔离 → 各版本独立”选项,删除该版本将导致该游戏的存档等数据一同被删除!真的要删除版本“%s”吗?
version.manage.remove_assets=删除所有游戏资源文件
version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该实例
version.manage.rename=重命名该版本
version.manage.rename.message=请输入要修改的名称
version.manage.rename.fail=重命名实例失败,可能文件被占用或者名字有特殊字符。
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。
version.settings=游戏设置
version.update=更新整合包
Binary file not shown.
@@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="%lang%">
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
File diff suppressed because it is too large. Load diff
+41 -185
View File
@@ -170,7 +170,7 @@ all-the-mods-6;164;;All the Mods 6;;ATM6
garden-of-glass-questbook-edition;165;;Garden of Glass (Questbook Edition);;
;166;;圣剑传奇;Sword Legendary;
;167;;FTB Presents Direwolf20 1.16;;
team-rustic;168;;Rusty Sink 2;;
team-rustic;168;;Rusty Sink 1.20;;
enigmatica2expertskyblock;169;;Enigmatica 2: Expert Skyblock;;E2ES
;170;;休闲-生存-2021版;Leisure Survival-2021;ls21
hexxit-updated;171;;Hexxit Updated;;
@@ -195,7 +195,7 @@ mineshafts-monsters;189;;矿井与怪物;Mineshafts & Monsters;
crazy-craft-remastered;190;;Crazy Craft Remastered;;
engineers-life-2;191;;Engineer's Life 2;;EL2
hr-new-beginnings-2;192;;HR: New Beginnings 2;;
minecolonies-official;193;;模拟殖民地;MineColonies Official;
minecolonies-testpack;193;;模拟殖民地;MineColonies Official;
tnp-limitless;194;;TNP Limitless;;
tnp-limitless-2;195;;TNP Limitless 2;;
tnp-limitless-3;196;;TNP Limitless 3;;
@@ -251,9 +251,9 @@ fabulously-optimized;248;;难以置信的优化;Fabulously Optimized;FO
better-minecraft-otv-edition;249;;Better Minecraft (OfflineTV Edition);;
evolution-reset;250;;进化:重启;Evolution: Reset;ER
the-nuclear-wasteland;251;;核荒原;The Nuclear Wasteland;
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BoC
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BOC
;253;;边界生存;Border Survival;BSV
medieval-mc-forge-mmc1;255;;Medieval Minecraft;;MMC
medieval-mc-fabric;255;;Medieval Minecraft;;MMC
heavens-of-sorcery;256;;巫术天堂;Heavens of Sorcery;
herodotus;257;;希罗多德;Herodotus-Authentic Edition:Official;HDS
;258;;重生世界;Rebirth world;RW
@@ -282,7 +282,7 @@ world-of-dragons-ii;282;;World of Dragons II;;
tensura-unleashed;283;;Tensura Unleashed;;
skyopolis-4;284;;Skyopolis 4;;
ftb-oceanblock;286;;FTB OceanBlock;;
tolkiencraft-iii-return-to-middle-earth;287;;托尔金工艺3 - 重返中洲;TolkienCraft III - Return to Middle-earth;TC3
tolkiencraft-iii-return-to-middle-earth;287;;TolkienCraft III - Return to Middle-earth;;
delivery-inc;288;;Delivery Inc.;;
poetica;289;;Poetica;;
divine-journey-2;290;;神圣之旅2;Divine Journey 2;DJ2
@@ -354,7 +354,7 @@ all-the-mods-expert-remastered;355;;All The Mods Expert: Remastered;;
all-the-mods-0;356;;All the Mods 0;;ATM0
all-the-mods-fabric;357;;All the Mods Fabric;;ATMF
the-pixelmon-modpack;358;;The Pixelmon Modpack;;
better-mc-skyblock;359;;更好的我的世界空岛;Better MC Skyblock;BMCS
better-minecraft-skyblock;359;;更好的我的世界:空岛;Better Minecraft Skyblock;
age-of-fate;360;;命运时代;Age of Fate;AOF
minecraft-kingdoms;361;;MC Kingdoms;;
;362;;幻想旅程;Imaginary Journey;
@@ -413,6 +413,7 @@ epoch-runner-formerly-nuclear-beyond-modpack;417;;时代领跑人;Epoch Runner;
;418;;星海征途;The Journey of Star Sea;TJoSS
roguelike-adventures-and-dungeons-2;419;;冒险与地牢2;Roguelike Adventures and Dungeons 2;RAD2
multiskyblock;420;;模块化空岛;MultiSkyBlock;MSB
;421;;人性化;Humanized;H
create-live-3;422;;Create Live 3;;
magnificia;423;;Magnificia;;
;424;;魔法金属-新手向导;ManaMetalMod - WithNewbHelper;
@@ -483,7 +484,7 @@ dimensionhopper;490;;Dimension Hopper:The Fall;;
technology-empty-island;491;;科技空岛;RxanGrben;
greed-the-resource-4-infinity;492;;Greed the Resource 4 - Infinity;;
caveopolis;493;;Caveopolis;;
;494;;坚毅征途;HardCraft;HC
;494;;硬核生存;HardCraft;HC
;495;;怀末韵云;Institutions mz;MZ1
feed-the-factory;496;;Feed the Factory;;FTF
fantasy-realm;497;;幻想领域;Fantasy Realm;
@@ -566,6 +567,7 @@ expert-of-magic-art-world;573;;魔艺世界;Expert of Magic Art World;EOMAW
nuclear-radiation-2;576;;核污染二;nuclear radiation 2;NR2
;577;;白狐铃的奇思妙想;Arctic Fox Ling Fantasy;AFLF
;578;;MineSpace;;MS
;579;;史诗战斗:兵团战争;EPIC Fight Corps War;EFC
;580;;月见清兰的个人优化;TskimiSeiran's Idea;TkSrI
;581;;兔头:真视世界;;
;582;;灾厄战争;;
@@ -582,7 +584,7 @@ the-winter-frontier;591;;冬境边域;The Winter Frontier;TWF
;593;;太阳机械厂;Create Sun;CS
dawn-craft;594;;破晓之界;DawnCraft: Echoes of Legends;DC
aof-presents-skylore;595;;AOF: Skylore;;
keletus-gregpack;596;;可乐兔的格雷整合包;Keletu's GregPack;GtMagic
keletus-gregpack;596;;可乐兔的格雷整合包;Keletu's GregPack;
ftb-skies;597;;FTB Skies;;
tnp-limitless-5;598;;TNP Limitless 5;;
create-back-on-track;599;;Create: Back on Track;;
@@ -612,8 +614,8 @@ eternal-night-modpacks;622;;永夜;Eternal Night;ETN
all-the-mods-9;623;;All the Mods 9;;ATM9
spoornpack;624;;Spoornpack;;
;625;;起源RPG;;ORPG
;626;;织彩钓月;Oriun Tagayasu;OTSU
;627;;冰河物语;;
;626;;钓月织梦;Tsuri No Gatsu;TSNG
;627;;冰河物语;Ice Age Fairy;IAF
hommage;628;;Hommage;;
days-to-craft;629;;Seven Days To Craft;;
emcworld;630;;质能世界;EMCWorld;
@@ -666,7 +668,7 @@ journey-watch-ytz;676;;征途 守望;Journey Watch;JW
;677;;Arcomua Lite 原版整合;Arcomua Lite;AL
incremental-industries;678;;Incremental Industries;;
;679;;海王星计划:重构;NeptunePlan:Reboot;NPR
;680;;求生日记:荒野;Survival Diary: Wilderness;SWD
;680;;中世纪生存日记;Medieval Survival Diary;MSD
a-new-technological-era;681;;A New Technological Era;;ANTE
;682;;超能力之旅;Meet Your Superpower;MYS
musketeer;683;;Musketeer - One for All;;
@@ -690,7 +692,7 @@ enigmatica9;699;;Enigmatica 9;;E9
create-arcane-engineering;701;;机械动力:奥术工程;Create: Arcane Engineering;CAE
chroma-endless-2;702;;Chroma Endless 2;;
techrevolution;703;;TechRevolution;;
cabin;704;;Create Above & Beyond In Newer;;CABIN
cabin;704;;CABIN: Create Above & Beyond In Newer;;CABIN
aoc;705;;All of Create;;
;706;;交错维度3;Among Dimensions 3;AD
gregfactory-sky;707;;GregFactory Sky;;GFS
@@ -733,8 +735,8 @@ life-in-the-village-3;740;;Life in the village 3;;LitV3
;745;;FTB Genesis;;
cook-in-skyblock;746;;天空厨房;Cook in skyland;CiS
;747;;被恐惧支配 1;Dominated by Fear 1;DoF
new-vanilla-newhome;748;;新的原版;New Vanilla;NVA
;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
new-vanilla-modpack;748;;新的原版;New Vanilla;NVA
;749;;群峦救援非官方版;Terrafirma Rescue Unofficial;TFRU
stoneopolis;750;;Stoneopolis;;
ftb-skies-expert;751;;FTB Skies Expert;;
wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
@@ -742,7 +744,7 @@ wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
;754;;随心之旅;spontaneous journey;
oxmodpack;755;;OxMODPACK 8 +Online;;
gregtech-community-pack-modern;756;;GregTech Community Pack Modern;;GCPM
;757;;锻造大师4;Master Blacksmith4;MB4
;757;;锻造大师3;Master Blacksmith3;MB3
;758;;科技用于探索世界;Technology Exploration World;
;759;;幻梦闲笔;Aetherial Voyage;
fear-the-mist;760;;雾中的恐惧;Fear The Mist;
@@ -762,7 +764,7 @@ infernal-rpg;771;;Infernal Origins;;
;774;;AIR for JAVA;;AFJ
;775;;Create Extra;;
frs-optimisation-for-survival;776;;Fr的生存优化;Fr's Optimisation for Survival;FROS
exile-magic-constructor;777;;Exile Magic Constructor;;EMC
;777;;Exile Magic Constructor;;EMC
;778;;Landscapes Reimagined Genesis;;
al-nl;779;;冒险生活 - 新生活;Adventure Life - New Life;AL-NL
harvista;780;;拾穗;Harvista;
@@ -817,19 +819,19 @@ all-the-mods-9-to-the-sky;828;;All The Mod 9 - To The Sky;;ATM9S
fallenpetals;830;;落英;FallenPetals;FP
dungeon-heroes;831;;Dungeon Heroes (RPG Series);;
play-as-dragon-gothic-edition;832;;Play as Dragon: Gothic Edition;;
skyblock-burgeria;833;;空岛汉堡店;Skyblock Burgeria;
skyblock-burgeria;833;;空岛汉堡店;;
crafting-craft-2;834;;合成工艺2;Crafting Craft 2;
;835;;僵尸入侵100天;Zombie Invade 100 Days;ZI100D
compact-world-expert;836;;Compact World Expert;;CWE
;837;;宁然一隅;A Fragment of Peace;AFoP
a-fragment-of-peace;837;;宁然一隅;A Fragment of Peace;AFoP
easy-create-modpack;838;;Easy Create;;
;839;;流线鱼概念;Fish Spi Rally;FSRMP
meatballcraft;840;;肉丸工艺,次元飞升;MeatballCraft, Dimensional Ascension;MBC
meatballcraft;840;;肉丸工艺,次元提升;MeatballCraft, Dimensional Ascension;
seaopolis-submerged;841;;海之城: 潜没水中;Seaopolis: Submerged;
;842;;空中厕所;PoopSky;
compact-sky;843;;收缩空岛;Compact Sky;
back-to-back;844;;背靠背;Back to Back;BTB
ane2;845;;冒险与探索;Adventure & Exploration;AnE
adventure-exploration-part2;845;;冒险与探索;Adventure & Exploration;AnE
;846;;复兴;Renaissance;
live-like-a-castaway;847;;木筏求生2;Raftcraft2;RC2
mermaid-legend;848;;人鱼传说;Mermaid legend;
@@ -845,6 +847,7 @@ create-prepare-to-dye;856;;Create Prepare to Dye;;
create-leisurely-delight;858;;机械动力:悠然乐事;Create Delight;CD
;859;;蔚蓝悠悠;Azure Leisurely;AL
infinityevolved-reloaded;860;;Infinity Evolved: Reloaded;;
;861;;空中厕所:生产线;Poopsky:Assembly Line;PAL
;862;;佛罗伦萨迷思;Fantasy in Florence;FIF
craftoria;863;;Craftoria;;
mechanic-craft;864;;MechanicCraft;;
@@ -875,7 +878,7 @@ create-stellar;888;;Create Stellar;;
monifactory;889;;Monifactory;;
create-factory-builder;890;;Create: Factory Builder;;
;891;;群峦传说:宝宝快乐版;TerraFirma: Baby Happy Edition;TFBH
;892;;阶段: 残骸;Phase: the Debris;PTD
;892;;阶段: 残骸;Phase: the Debris;
;893;;AE管道;AE Pipes;APE
skyt-2;894;;SkyT 2;;
stagecraft;895;;StageCraft;;
@@ -906,7 +909,7 @@ strictly-medieval-3-forge;917;;Strictly Medival 3;;
easy-to-ascension;920;;易如飞升;Easy to Ascension;
;921;;幽匿末日;The Last Day of Sculk;TLDS
;922;;交错空岛;Crossisland;CrI
;923;;机动工程:刀剑与魔法;Mechanical Engineering: SlashBlade and Magic;CRMD
;923;;机械动力:魔法乐事;Create Magic's Delight;CRMD
minetech-evolution;924;;科技进化;Minetech Evolution;MTE
gregified-sky-edition;925;;Gregified: Sky Edition;;
;926;;原版扩展;Additonal Vanilla;AV
@@ -914,14 +917,14 @@ create-on-a-potato;927;;Create on a Potato PC: Ultimate;;CPU
;928;;现实主义;realism;RLM
;929;;晋升之路;path of advance;POA
;930;;永无止境:超越常新;Above and Beyond : Refresh;CABR
cosmic-frontiers;931;;星河边疆;Cosmic Frontiers;CF
cosmic-frontiers;931;;Cosmic Frontiers;;
integrated-minecraft;932;;Integrated MC;;
;933;;Rising Legends;;
;934;;城乡之旅;Harvest's Journey;HJ
;934;;城乡之旅;Travel Township;TT
;935;;龙鸣牢大;FarmerKobeBryant;FKB
setgamedifficulty-reika;936;;SetGameDifficulty Reika;;
epic-journey-gtceu;937;;Epic Journey GTCEu;;
terrafirmacraft-leap-the-top;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
gregtech-skybound;939;;Gregtech: Skybound;;
simple-tfc;940;;简单群峦;SimpleTFC;
ftb-presents-direwolf20-s14;941;;FTB Presents Direwolf20 1.21;;
@@ -945,10 +948,10 @@ gregtania6-skyblock;959;;Gregtania6 Skyblock;;
harmonious-engineering;960;;Harmonious Engineering;;
valley-of-farming-and-ranching;961;;农牧物语;Valley of Farming and Ranching;
;962;;机械动力:火车;Create Train;
the-old-era-of-the-republic-fot-yifei;963;;理想国冒险之旅;;AOIC
;963;;理想国冒险之旅;;AOIC
;964;;生欲:求生2;Vivere The Zombie Apocalypse-II;VZA2
;965;;起源-世界;OriginCraft Pulses;
legacies-mc;966;;Legacies;;
;966;;Legacies;;
the-legend-of-2900;967;;海上机械师;The Legend Of 2900;TLOU
mirasites-100-days;968;;畸变:蛊之奇境;Mirasites-BrokenMemories;MBM
;969;;末日余晖:独自一人;;MRYH
@@ -967,7 +970,7 @@ venoms-craft;981;;Venom's Craft;;
;982;;慢生活冒险;;
;983;;多元工艺:绘镇曲;Painted Town Melody;PTM
;984;;遗忘之海;Leisurely Life;
;985;;幻想国度;Fancy_Land;
;985;;幻想国度;F_Land;
;986;;群峦传说:重制;TerraFirmaCraft Remake;TFCRe
;987;;未来;;
;988;;起源生物—崛起;;
@@ -985,12 +988,12 @@ industrial-machines;1000;;Industrial Machines;;
auto-terrafirmacraft-tfc-create;1001;;Auto-TerraFirmaCraft (TFC + Create);;ATFC
;1002;;时间之书:星的日记;The Book of Time: A Star's Journal;TASL
;1003;;特摄世界;Tokusatsu World;TW
journey-of-reincarnation;1004;;转世之旅;Tensura: Journey of Reincarnation;
journey-of-reincarnation;1004;;转世之旅;Journey of Reincarnation;
;1005;;冒险与休闲;Adventure and Leisure;AL
enigmatica10;1006;;Enigmatica 10;;E10
mc-chocolate-edition;1007;;Chocolate Edition Minecraft;;
mc-chocolate-edition;1007;;The Chocolate Edition;;
civilization-l;1008;;Civilization-L;;
terrafirma-advent;1009;;群峦:降临;TerraFirma:Advent;TFA
;1009;;群峦:降临;TerraFirma:Advent;TFA
valhelsia-7;1010;;Valhelsia 7;;
mechanical-mastery-plus;1011;;Mechanical Mastery Plus;;
ashes-to-ashes;1012;;尘归尘;Ashes To Ashes;ATA
@@ -998,23 +1001,23 @@ ashes-to-ashes;1012;;尘归尘;Ashes To Ashes;ATA
create-silence-and-prosperity;1014;;机械动力:寂静与繁荣;Create Silence and Prosperity;CSP
mif;1015;;MI:Foundation;;MIF
c2mse2;1016;;Create 2 Mekanism: Sky Edition 2;;C2MSE2
;1017;;耕云钓月;Kumo O Tagayasu;KOTS
;1017;;Tsuri No Gatsu 5 - FDCraft Version;;TSNG5-FDC
;1018;;无限战争4;Infinity War4;INF4
;1019;;群峦传说:工业长路;;
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
hmbo;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
;1020;;自然冒险 初生;Nature Adventure Newborn;NAB
;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
create-star-valley;1022;;Create: Gears of History;;
;1023;;阿尔法计划;AlphaPlan;AP
spell-dimension;1024;;咒次元;Spell Dimension;SD
skyfactory-5;1025;;天空工厂 5;SkyFactory 5;SF5
;1026;;破劫之刃;Break Disaster;
reminiscent-create;1027;;Reminiscent Create;;
techsert;1028;;匿踪之城;Techsert;TH
techsert;1028;;Techsert;;TH
prehistoric-world-dinosaurs-adventure;1029;;Prehistoric World - Dinosaurs & Adventure;;
terminal;1030;;终末;Terminal;
isolated-crystal-3;1031;;异次元水晶 3;Isolated Crystal 3;
;1031;;异次元水晶 3;Isolated Crystal 3;
;1032;;机械动力:无限总是小于零;Create:Infinity Always Smaller Than Zero;CIASZ
;1033;;踪灭I-起源;;
;1033;;踪灭I-开端;;
;1034;;踪灭II;;
rabbits-dawn-craft;1035;;某兔子的破晓之界;A Certain Rabbit's DawnCraft - Dawn of the Deep Soul;RDC
;1036;;铁砧天空;;ASKY
@@ -1024,7 +1027,7 @@ the-adventure-of-valkyriens;1038;;瓦尔基里大冒险;The Adventure of Valkyri
;1040;;群峦传说:天才版;;
compression;1041;;Compression;;
compact-sky-easy;1042;;Compact Sky: Easy;;CSE
;1043;;丰收与探索;Harvest and Exploration;
;1043;;丰收与探索;;
;1044;;脆骨症:黯光;No Flesh Within Chest-DIM;NFWC-DIM
;1045;;小雷的优化冒险;;
cobblemon-eternal-edition;1046;;Cobblemon Eternal;;
@@ -1032,150 +1035,3 @@ cobblemon-eternal-edition;1046;;Cobblemon Eternal;;
gregtech-expert;1048;;GregTech Expert;;
;1049;;Beta Horizons;;
minecraft-legendary-edition;1050;;Legendary Edition;;
;1051;;Origenceal LPackage;;OLP
;1052;;我的世界:未闻的故事;;mtus
;1053;;Versatile;;
;1054;;勇者之章 Ⅲ;Chapter of Yuusha Ⅲ;CY3
forge-frontier;1055;;Create - Forge Frontier;;
create-workshop-uwu;1056;;Create: Workshop UwU Edition;;
;1057;;古钠重铸;Oldium Optimized;OO
;1058;;越寰;Leaping across the world;LTW
ftb-oceanblock-2;1059;;FTB OceanBlock 2;;
;1060;;伊甸空天2;Eden Aerospace 2;EAS2
create-infinity-mechanism;1061;;机械动力:无限构件;Create:Infinity Mechanism;CIM
beyond-depth;1062;;Beyond Depth;;
;1063;;枪械交界地;TACZ Amid Crucible Zone;
;1064;;真幻世界;Realfantacy World;R.F.
minenufactory;1065;;创世工厂;Minenufactory;MF
craft-to-exile-2;1066;;放逐之路2;Craft to Exile 2 (VR Support);CTE2
;1067;;龙境III:菌土重来;Dragon Odyssey:Return of the Fungal Earth;DO3
the-culinary-journey;1068;;美食的旅途;The Culinary Journey;TCJ
;1069;;缥缈方舟;;PMFZ
;1070;;东方匠魂传;Touhou Tinker Fantasy;TTF
;1071;;亵渎;;MYBL
techopolis-3;1072;;Techopolis 3;;
sky1234;1074;;sky1234;;
star-technology;1075;;Star Technology;;
;1076;;Hyper World;;HW
;1077;;妖月的魔法金属整合包;yaoyaoDream's ManaMetalMod Modpack;YMM
techcraft-severwinter;1078;;科技工艺:严冬;TechCraft: SevereWinter;TCSW
randblock2-winterize;1079;;随机空岛 2:银装素裹;RandBlock2:Winterize;
create-the-brass-concerto;1080;;机械动力:黄铜协奏曲;Create: The Brass Concerto;CBC
explorers-eve;1081;;Explorer's Eve;;
all-the-magic-arcana;1082;;All the Magic :Arcana;;ATMAr
;1083;;创想新世界;Inspired Creations: New World;ICNW
farming-crossing-5-made-to-order;1084;;农业森友会 5;Farming Crossing 5: Made to Order!;
;1085;;Ben10 Craft;;BEN10
;1086;;渺小的世界;Small world;
mierno;1087;;迷厄路;Mierno;
;1088;;最后那些天;Those last day;
;1089;;泠月狐基础整合;LunarFox;
roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures and Dungeons 3;RAD3
;1091;;羽舲界;Hanekmio Instance;HMI
;1092;;玄理2:洗剪版;EnigTech 2 - Dishwasher's Cut;ET2DC
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
egged-up;1094;;Egged Up;;
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;
;1096;;愚者;The Fool;TF
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
;1099;;血月低语:暗夜惊魂;Bloodmoon's Whisper;BW
;1100;;MC饭;Delicious in Minecraft;DIM
;1101;;美味的甜点总是有毒;;
delight-desert-chef;1102;;乐事:沙漠厨师;Delight: Desert Chef;DDC
;1103;;异界随行诗;Otherworld Traveling Poems;
;1104;;无边荒漠;;
;1105;;空中厕所2;PoopSky2;
fear-nightfall;1106;;Fear Nightfall;;
homestead-cozy;1107;;Homestead - A Cozy Survival Experience;;HD
madness-of-the-skies;1108;;Madness of the skies;;
;1109;;魔之逆鳞;Nemesis of Demons;NOD
emc-and-fe-plus2;1110;;EMC & FE ++;;E&F
;1111;;云之谷;CloudValley;
bm-exosphere;1112;;BM: Exosphere;;BME
;1113;;锻造大师:侠之器;Master Blacksmith:Chivalrous blade;MB:CB
;1114;;自然;Nature;NA
;1115;;农场物语;FarmingTales;FT
optimaxcraft;1116;;OptiMaxCraft;;OMC
;1117;;决战黎明;Decisive Battle at Dawn;DBAD
floaty-creatoria;1118;;天工浮生;Floaty Creatoria;
;1119;;简约生电优化;Simplicity Survival Optimization;SSO
;1120;;星辰之下;Under the Stars;UtS
madness-of-the-skies-2;1121;;Madness of the Skies 2;;
raspberry-flavoured;1122;;Raspberry Flavoured;;RF
skillet-man;1123;;平底锅侠;Skillet Man;
;1124;;格雷科技:新征程;GregTech Odyssey;GTO
;1125;;无中生无尽;Ex Nihilo Ad Avaritia;ENAA
;1126;;Undertale;;
create-creative-and-beyond;1127;;机械动力:创造与超越;Create Creative and Beyond;CCB
create-delight-remake;1128;;机械动力:齿轮盛宴;Create:Delight Remake;CDR
ftb-skies-2;1129;;FTB Skies 2;;
;1130;;星辰时代:科技魔法;Stellar Era: Technomagic;SETM
aircrashed-engineer;1131;;坠机工程师;Aircrashed Engineer;ACE
project-tfcreate;1132;;Project: TFCreate;;
;1133;;落幕曲;;
;1134;;超大陆:勇气;Supercontinent:Pluck;SUPL
create-chronicles-the-endventure;1135;;Create Chronicles: The Endventure;;
life-in-the-village-4;1136;;Life in the village 4;;LITV4
real-island-2-modular-craft;1137;;真实空岛2:模块化之旅;Real Island 2 - Modular Craft;RI2
;1138;;奥术咖啡馆;;
unyielding;1139;;Unyielding;;
;1140;;剑与王国;;SNK
the-fungal-infection-coop;1141;;The Fungal Infection | Co-Op Base Defense;;
mc-eternal-2;1142;;永恒的MC 2;MC Eternal 2;MCE2
solo-leveling-reawakening;1143;;Solo Leveling - Reawakening;;
create-ultimate-selection;1144;;Create: Ultimate Selection - QUESTS NOW!;;
;1145;;元素觉醒;Elemental Awakening;
endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE Horror Experience;;
;1147;;锻造之旅;;
picky;1148;;Picky;;
resource-factory;1149;;资源工厂;Resource Factory;
stacia-2-expert;1150;;Stacia 2 Expert;;S2E
;1151;;田园乐事;;
;1152;;世界之旅;Journey of the world;JOTW
;1153;;清新优化;Fresh optimization;FRO
;1154;;遗世之匠;Touhou:The Forgotten Swordsmith;
;1155;;千禧:蝉鸣之时;;
skyrealm-descent;1157;;Skyrealm: Descent;;
;1158;;远征:以太利亚;Expedition: Ætheria;
;1159;;快速优化;Rapid Optimization;RO
liminal-industries;1160;;阈限工业;Liminal Industries;
;1161;;史诗冒险;;EA
;1162;;根自日常;ROOTINE;
blue-archive-x-mineraft;1163;;我的世界x蔚蓝档案;BlueArchive x Minecraft;BACraft
;1164;;Fresh & Smooth;;
;1165;;机动重启工程:风之旅;The Caravan of Winds;TCW
;1166;;Furry Fandom Club Boss Rush Official Offline;;FFCBR
immersive-ultra;1167;;极致沉浸;Immersive Ultra;
;1168;;Polymania;;
survivearchive;1169;;SurviveArchive - A Blue Archive modpack;;
beloong;1170;;化龍;BeLoong;
;1171;;畅游自然;;SiF
;1172;;退休工艺;Retirement Craft;RmC
;1173;;随机枪械乱斗;NimShade's Random Gunfight;NRG
;1174;;雪狐的基础整合;SF Integrated Modpack;SFI
;1175;;硬核惊变;Hardcore Surprise;HS
society-sunlit-valley;1176;;Society: Sunlit Valley;;
ciscos-fantasy-medieval-rpg-dragonfyre;1177;;Cisco's Fantasy Medieval RPG [Dragonfyre];;
all-the-mods-10-sky;1178;;All the Mods 10: To the Sky;;ATM10S
;1179;;谐伴匠途;;
the-cursed-wanderer-reborn;1180;;咒游诸界:重生;The Cursed Wanderer: Reborn;TCWR
;1181;;Turbo Vulkan;;TuVk
uls-zsurvival-craft;1182;;Z-Liminex;;
;1183;;MITE 重生 3;MITE Reborn 3;MR
reclamation-reclaim-the-world;1184;;Reclamation - Reclaim the World!;;
;1185;;寰宇地球;Earthworld;EW
pokehaan-craft-2;1186;;Pokehaan Craft 2;;
project-nope-zone;1187;;Project Nope Zone;;PNZ
all-the-simple-zero;1188;;一切简易;All the Simple;ATS
;1189;;匠战妖域;Tinkers' Warfare Demon Realm;twdr
;1190;;空想生存;;
;1191;;东方起源鉴;Touhou Origins Annals;TOA
realm-of-destiny;1192;;生命之境;Realm of Destiny;ROD
;1193;;奇妙的世界;Special World;
;1194;;毁灭;DESTRUCTION;DES
pvzw;1195;;植物大战僵尸:世界;Plants VS Zombies : World;PVZW
;1196;;农场物语Forge版;FarmingTales Forge;FT-WT
new-century-adventure;1197;;新世纪之险;New Century Adventure;NCA
repalmon;1198;;Re帕鲁梦;;RPLM
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
@@ -4007,17 +4007,6 @@
"size": 27971214
}
}
},
"software-renderer-loader": {
"name": "org.glavo:llvmpipe-loader:1.0",
"downloads": {
"artifact": {
"path": "org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
"sha1": "ff255415e5c4b2a18970da0a8e552b557ca013ae",
"size": 12964773
}
}
}
},
"windows-x86": {
@@ -17,10 +17,6 @@
*/
package org.jackhuang.hmcl;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public final class JavaFXLauncher {
private JavaFXLauncher() {
@@ -31,21 +27,9 @@ public final class JavaFXLauncher {
static {
// init JavaFX Toolkit
try {
// Java 9 or Latter
final MethodHandle startup =
MethodHandles.publicLookup().findStatic(
javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class));
startup.invokeExact((Runnable) () -> {
javafx.application.Platform.startup(() -> {
});
started = true;
} catch (NoSuchMethodException e) {
// Java 8
try {
Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance();
started = true;
} catch (Throwable e0) {
e0.printStackTrace();
}
} catch (IllegalStateException e) {
started = true;
} catch (Throwable e) {
@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.Platform;
@@ -31,6 +30,7 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
@@ -45,7 +45,7 @@ public class GameCrashWindowTest {
ManagedProcess process = new ManagedProcess(null, Arrays.asList("commands", "2"));
String logs = FileUtils.readText(new File("../HMCLCore/src/test/resources/logs/too_old_java.txt"));
String logs = Files.readString(new File("../HMCLCore/src/test/resources/logs/too_old_java.txt").toPath());
CountDownLatch latch = new CountDownLatch(1);
FXUtils.runInFX(() -> {
+7
View File
@@ -0,0 +1,7 @@
plugins {
id("java-library")
}
tasks.withType<JavaCompile> {
options.release.set(8)
}
@@ -15,13 +15,24 @@
* 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.gradle.ci;
package org.jackhuang.hmcl;
/// @author Glavo
public final class JenkinsUtils {
import org.jackhuang.hmcl.util.UTF8Control;
public static final boolean IS_ON_CI = "1".equals(System.getenv("HMCL_CI"));
import java.util.ResourceBundle;
private JenkinsUtils() {
final class BootProperties {
private static ResourceBundle resourceBundle;
static ResourceBundle getResourceBundle() {
if (resourceBundle == null) {
resourceBundle = ResourceBundle.getBundle("assets.lang.boot", new UTF8Control());
}
return resourceBundle;
}
private BootProperties() {
}
}
@@ -15,17 +15,16 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
package org.jackhuang.hmcl;
/**
* @author Glavo
* This is a dummy class and will be overwritten in the shadow jar.
*/
public final class ModuleHelper {
public static void addEnableNativeAccess(Class<?> clazzInModule) {
// do nothing
public final class EntryPoint {
private EntryPoint() {
}
private ModuleHelper() {
public static void main(String[] args) {
throw new AssertionError("This method should not be called, please verify that the HMCL is complete.");
}
}
@@ -0,0 +1,73 @@
/*
* 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;
import org.jackhuang.hmcl.util.SwingUtils;
/**
* @author Glavo
*/
public final class Main {
private static final int MINIMUM_JAVA_VERSION = 11;
private Main() {
}
/**
* Check if the current Java version is compatible with HMCL.
*/
static boolean checkJavaVersion(String javaVersion) {
if (javaVersion == null) {
return false;
}
try {
int major;
int dot = javaVersion.indexOf('.');
if (dot >= 0) {
major = Integer.parseInt(javaVersion.substring(0, dot));
if (major == 1 && dot < javaVersion.length() - 1) {
int begin = dot + 1;
dot = javaVersion.indexOf('.', begin);
major = dot > begin
? Integer.parseInt(javaVersion.substring(begin, dot))
: Integer.parseInt(javaVersion.substring(begin));
}
} else {
major = Integer.parseInt(javaVersion);
}
return major >= MINIMUM_JAVA_VERSION;
} catch (NumberFormatException e) {
return false;
}
}
public static void main(String[] args) throws Throwable {
if (checkJavaVersion(System.getProperty("java.version"))) {
EntryPoint.main(args);
} else {
String errorMessage = BootProperties.getResourceBundle().getString("boot.unsupported_java_version");
System.err.println(errorMessage);
SwingUtils.showErrorDialog(errorMessage);
System.exit(1);
}
}
}
@@ -1,7 +1,27 @@
package org.jackhuang.hmcl.ui;
/*
* 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.util;
import javax.swing.*;
/**
* @author Glavo
*/
public final class SwingUtils {
static {
if (System.getProperty("swing.defaultlaf") == null) {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
package org.jackhuang.hmcl.util;
import java.io.IOException;
import java.io.InputStream;
@@ -28,11 +28,9 @@ import java.util.ResourceBundle;
import static java.nio.charset.StandardCharsets.UTF_8;
final class UTF8Control extends ResourceBundle.Control {
public final class UTF8Control extends ResourceBundle.Control {
public static final UTF8Control INSTANCE = new UTF8Control();
private UTF8Control() {}
public UTF8Control() {}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IOException {
@@ -0,0 +1,19 @@
#
# 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/>.
#
boot.unsupported_java_version=HMCL requires Java 11 or later to run, but still supports launching games with Java 8. Please install the latest version of Java and try opening HMCL again.\nYou can keep your old version of Java. HMCL can detect and manage multiple Java installations, and will automatically select the appropriate Java version for your game.
@@ -15,4 +15,5 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
versionRoot=3.6
boot.unsupported_java_version=HMCL 需要 Java 11 或更高版本才能執行,但依然支援使用 Java 8 啟動遊戲。請安裝最新版本的 Java 再嘗試開啟 HMCL。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
@@ -0,0 +1,19 @@
#
# 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/>.
#
boot.unsupported_java_version=HMCL 需要 Java 11 或更高版本才能运行,但依然支持使用 Java 8 启动游戏。请安装最新版本的 Java 再尝试启动 HMCL。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
@@ -15,25 +15,25 @@
* 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.gradle.utils;
package org.jackhuang.hmcl;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// @author Glavo
public final class PropertiesUtils {
public static @NotNull Properties load(Path path) throws IOException {
Properties properties = new Properties();
try (var reader = Files.newBufferedReader(path)) {
properties.load(reader);
}
return properties;
}
public final class MainTest {
@Test
public void testCheckJavaVersion() {
assertFalse(Main.checkJavaVersion("1.6.0"));
assertFalse(Main.checkJavaVersion("1.6.0_45"));
assertFalse(Main.checkJavaVersion("1.7.0"));
assertFalse(Main.checkJavaVersion("1.7.0_80"));
assertFalse(Main.checkJavaVersion("1.8"));
assertFalse(Main.checkJavaVersion("1.8.0_321"));
private PropertiesUtils() {
assertTrue(Main.checkJavaVersion("11"));
assertTrue(Main.checkJavaVersion("11.0.26"));
assertTrue(Main.checkJavaVersion("21"));
}
}
+9
View File
@@ -2,6 +2,15 @@ plugins {
`java-library`
}
tasks.withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.compileJava {
options.compilerArgs.add("--add-exports=jdk.attach/sun.tools.attach=ALL-UNNAMED")
}
dependencies {
api(libs.kala.compress.zip)
api(libs.kala.compress.tar)
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyMap;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -32,7 +33,6 @@ import java.util.Optional;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.Nullable;
@@ -77,7 +77,7 @@ public class AuthlibInjectorServer implements Observable {
try {
AuthlibInjectorServer server = new AuthlibInjectorServer(url);
server.refreshMetadata(IOUtils.readFullyAsStringWithClosing(conn.getInputStream()));
server.refreshMetadata(new String(conn.getInputStream().readAllBytes(), UTF_8));
return server;
} finally {
conn.disconnect();
@@ -71,7 +71,8 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
pair("http://files.minecraftforge.net/maven", apiRoot + "/maven"),
pair("https://files.minecraftforge.net/maven", apiRoot + "/maven"),
pair("https://maven.minecraftforge.net", apiRoot + "/maven"),
pair("https://maven.neoforged.net/releases/", apiRoot + "/maven/"),
pair("https://maven.neoforged.net/releases/net/neoforged/forge", apiRoot + "/maven/net/neoforged/forge"),
pair("https://maven.neoforged.net/releases/net/neoforged/neoforge", apiRoot + "/maven/net/neoforged/neoforge"),
pair("http://dl.liteloader.com/versions/versions.json", apiRoot + "/maven/com/mumfrey/liteloader/versions.json"),
pair("http://dl.liteloader.com/versions", apiRoot + "/maven"),
pair("https://meta.fabricmc.net", apiRoot + "/fabric-meta"),
@@ -63,8 +63,9 @@ public class DefaultCacheRepository extends CacheRepository {
lock.writeLock().lock();
try {
if (Files.isRegularFile(indexFile))
index = JsonUtils.fromNonNullJson(FileUtils.readText(indexFile), Index.class);
if (Files.isRegularFile(indexFile)) {
index = JsonUtils.fromNonNullJson(Files.readString(indexFile), Index.class);
}
else
index = new Index();
} catch (IOException | JsonParseException e) {
@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
@@ -126,7 +125,7 @@ public final class ForgeInstallTask extends Task<Version> {
Optional<String> gameVersion = dependencyManager.getGameRepository().getGameVersion(version);
if (!gameVersion.isPresent()) throw new IOException();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
String installProfileText = FileUtils.readText(fs.getPath("install_profile.json"));
String installProfileText = Files.readString(fs.getPath("install_profile.json"));
Map<?, ?> installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class);
if (installProfile.containsKey("spec")) {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
@@ -159,7 +158,7 @@ public final class ForgeInstallTask extends Task<Version> {
Optional<String> gameVersion = dependencyManager.getGameRepository().getGameVersion(version);
if (!gameVersion.isPresent()) throw new IOException();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
String installProfileText = FileUtils.readText(fs.getPath("install_profile.json"));
String installProfileText = Files.readString(fs.getPath("install_profile.json"));
Map<?, ?> installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class);
if (installProfile.containsKey("spec")) {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
@@ -278,9 +278,9 @@ public class ForgeNewInstallTask extends Task<Version> {
@Override
public void preExecute() throws Exception {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
profile = JsonUtils.fromNonNullJson(Files.readString(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
processors = profile.getProcessors();
forgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
forgeVersion = JsonUtils.fromNonNullJson(Files.readString(fs.getPath(profile.getJson())), Version.class);
for (Library library : profile.getLibraries()) {
Path file = fs.getPath("maven").resolve(library.getPath());
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.*;
import java.nio.file.Path;
@@ -78,7 +77,7 @@ public class ForgeOldInstallTask extends Task<Version> {
ZipEntry forgeEntry = zipFile.getEntry(installProfile.getInstall().getFilePath());
try (InputStream is = zipFile.getInputStream(forgeEntry); OutputStream os = new FileOutputStream(forgeFile)) {
IOUtils.copyTo(is, os);
is.transferTo(os);
}
setResult(installProfile.getVersionInfo()
@@ -27,7 +27,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.net.URL;
@@ -84,7 +83,7 @@ public final class GameAssetDownloadTask extends Task<Void> {
public void execute() throws Exception {
AssetIndex index;
try {
index = JsonUtils.fromNonNullJson(FileUtils.readText(assetIndexFile), AssetIndex.class);
index = JsonUtils.fromNonNullJson(Files.readString(assetIndexFile), AssetIndex.class);
} catch (IOException | JsonParseException e) {
throw new GameAssetIndexDownloadTask.GameAssetIndexMalformedException();
}
@@ -27,7 +27,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
@@ -86,7 +85,7 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
}
} else {
try {
JsonUtils.fromNonNullJson(FileUtils.readText(assetIndexFile), AssetIndex.class);
JsonUtils.fromNonNullJson(Files.readString(assetIndexFile), AssetIndex.class);
return;
} catch (IOException | JsonParseException ignore) {
}
@@ -54,16 +54,15 @@ public final class GameRemoteVersion extends RemoteVersion {
@Override
public int compareTo(RemoteVersion o) {
if (!(o instanceof GameRemoteVersion)) {
if (!(o instanceof GameRemoteVersion))
return 0;
}
int dateCompare = o.getReleaseDate().compareTo(getReleaseDate());
if (dateCompare != 0) {
return dateCompare;
}
return GameVersionNumber.compare(o.getSelfVersion(), getSelfVersion());
return GameVersionNumber.compare(getSelfVersion(), o.getSelfVersion());
}
private static Type getReleaseType(ReleaseType type) {
@@ -26,7 +26,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -164,7 +163,7 @@ public class LibraryDownloadTask extends Task<Void> {
JarInputStream jar = new JarInputStream(new ByteArrayInputStream(data));
JarEntry entry = jar.getNextJarEntry();
while (entry != null) {
byte[] eData = IOUtils.readFullyWithoutClosing(jar);
byte[] eData = jar.readAllBytes();
if (entry.getName().equals("checksums.sha1")) {
hashes = new String(eData, StandardCharsets.UTF_8).split("\n");
}
@@ -9,7 +9,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.FileSystem;
@@ -87,7 +86,7 @@ public final class NeoForgeInstallTask extends Task<Version> {
Optional<String> gameVersion = dependencyManager.getGameRepository().getGameVersion(version);
if (!gameVersion.isPresent()) throw new IOException();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
String installProfileText = FileUtils.readText(fs.getPath("install_profile.json"));
String installProfileText = Files.readString(fs.getPath("install_profile.json"));
Map<?, ?> installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class);
if (LibraryAnalyzer.LibraryType.FORGE.getPatchId().equals(installProfile.get("profile")) && (Files.exists(fs.getPath("META-INF/NEOFORGE.RSA")) || installProfileText.contains("neoforge"))) {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
@@ -274,9 +274,9 @@ public class NeoForgeOldInstallTask extends Task<Version> {
@Override
public void preExecute() throws Exception {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
profile = JsonUtils.fromNonNullJson(Files.readString(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
processors = profile.getProcessors();
neoForgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
neoForgeVersion = JsonUtils.fromNonNullJson(Files.readString(fs.getPath(profile.getJson())), Version.class);
for (Library library : profile.getLibraries()) {
Path file = fs.getPath("maven").resolve(library.getPath());
@@ -175,7 +175,7 @@ public final class OptiFineInstallTask extends Task<Version> {
Path launchWrapperVersionText = fs.getPath("launchwrapper-of.txt");
if (Files.exists(launchWrapperVersionText)) {
String launchWrapperVersion = FileUtils.readText(launchWrapperVersionText).trim();
String launchWrapperVersion = Files.readString(launchWrapperVersionText).trim();
Path launchWrapperJar = fs.getPath("launchwrapper-of-" + launchWrapperVersion + ".jar");
Library launchWrapper = new Library(new Artifact("optifine", "launchwrapper-of", launchWrapperVersion));
@@ -192,7 +192,7 @@ public final class OptiFineInstallTask extends Task<Version> {
Path buildofText = fs.getPath("buildof.txt");
if (Files.exists(buildofText)) {
String buildof = FileUtils.readText(buildofText).trim();
String buildof = Files.readString(buildofText).trim();
VersionNumber buildofVer = VersionNumber.asVersion(buildof);
if (LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN.equals(originalMainClass)) {
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff. Show more