Compare commits

...
27 Commits
Author SHA1 Message Date
AlexYang ac578c044f chore: update avatar for yushijinhun and update a URL (#7078) 2026-10-25 21:34:53 +08:00
快乐凤爪王 fc68029adc feat: 帮助页面添加异常提示及刷新功能 (#7047) 2026-10-25 21:32:51 +08:00
Glavo f6a92c1806 过滤模组中文名中的 Emoji (#7077) 2026-10-25 20:55:54 +08:00
Glavo 58eb452e90 在播放 UI 动画前应用 UI 布局 (#7076) 2026-10-25 19:17:39 +08:00
Wulian233 fb8a96a170 优化账户页面「上传/编辑皮肤」按钮文案 (#7050)
Close #3633
2026-10-25 19:02:55 +08:00
快乐凤爪王 ced441a830 refactor: 提取 DownloadListCell (#7075) 2026-10-25 17:30:14 +08:00
Glavo c5693c3450 清理未使用的代码与资源 (#7074) 2026-10-25 16:59:13 +08:00
鱼前三百 bc27eda220 chore(event): 移除过时且远古的进程退出事件,并将下载速度事件与全局 EventBus 解耦 (#7063) 2026-10-25 16:40:47 +08:00
Matt Van Horn 0c40dcdeed fix: honor declared modern Java requirements on legacy Linux instances (#7073) 2026-10-25 16:39:11 +08:00
Glavo 4282147042 优化设备信息的检测与显示逻辑 (#7071) 2026-10-25 16:36:49 +08:00
Glavo 0874c98d21 Fix TLS security tests on JDKs that block compromised certificates (#7065) 2026-10-25 11:42:41 +08:00
ToobLac 0c3c7dd62a chore: Bump commonmark to 0.31.0 & Update deps.json (#7056) 2026-10-25 03:04:20 +08:00
Glavo f663e5568c 在关于页面显示用户环境信息 (#7040) 2026-10-24 21:32:25 +08:00
ToobLac b8c1738f96 perf(ui): MicrosoftAccountLoginPane 在缺失 client id 时直接禁用登录按钮并显示错误提示 (#7054) 2026-10-24 19:38:51 +08:00
Glavo 41c61645e7 Detect instance manifests among multiple JSON files (#7048) 2026-10-24 18:31:52 +08:00
ToobLac 9fc697751c chore(git): 补充 schematic git attr (#7053) 2026-10-24 18:21:30 +08:00
Glavo a4db0532b9 在 default_script.csv 和 sublanguages.csv 中记录数据来源 (#7049) 2026-10-24 14:14:24 +08:00
Minecraft269andMinecraft269 8954139b5e docs: update contributor count from 130 to 140 (#7043)
Co-authored-by: Minecraft269 <Minecraft269@users.noreply.github.com>
2026-10-24 13:52:30 +08:00
Glavo b2f7e724e4 使用 IANA 时区地区映射表推断设备地区 (#7046) 2026-10-24 12:38:08 +08:00
Glavo 0d20910ea3 Block compromised eMudhra CA keys in application TLS validation (#7033) 2026-10-22 21:27:52 +08:00
Glavo 5c90299073 移除下载页面提示 (#7038) 2026-10-22 21:04:39 +08:00
Glavo fc66cbcf96 更新模组数据 (2026-10-22) (#7032) 2026-10-22 02:15:14 +08:00
Glavo e7b9a95df0 为非官方构建添加醒目标识 (#7028) 2026-10-21 21:24:41 +08:00
ToobLac acc9740fa9 perf(addon): 完善 AddonUpdatesPage (#7003) 2026-10-20 21:27:59 +08:00
鱼前三百 5789f82617 fix: 在重新检查更新的时候提示正在检查更新 (#7015)
fix: #7014
2026-10-20 21:25:24 +08:00
Glavo 8827fd06d8 优化地区检测逻辑 (#7020) 2026-10-20 18:06:54 +08:00
Glavo 583e004d71 在中国大陆地区禁用游戏下载功能 (#7017) 2026-10-20 16:37:22 +08:00
123 changed files with 5402 additions and 1412 deletions

No files matched your search

+3
View File
@@ -59,6 +59,9 @@ Jenkinsfile text eol=lf
# Minecraft schematic
*.litematic binary
*.nbt binary
*.schem binary
*.schematic binary
# Compiled / intermediate build artefacts
*.bin binary
+23 -28
View File
@@ -11,6 +11,7 @@ import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
import org.jackhuang.hmcl.gradle.pack.CreateDeb
import org.jackhuang.hmcl.gradle.pack.ReleaseType
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import org.jackhuang.hmcl.gradle.utils.OfficialBuildValueSource
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -155,33 +156,28 @@ tasks.compileJava {
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
}
val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
add("hmcl.add-opens" to addOpens.joinToString(" "))
System.getenv("GITHUB_SHA")?.let {
add("hmcl.version.hash" to it)
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
}
val createPropertiesFile = tasks.register<WriteProperties>("createPropertiesFile") {
destinationFile.set(layout.buildDirectory.file("hmcl.properties"))
encoding = "UTF-8"
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile = tasks.register("createPropertiesFile") {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
doLast {
val targetFile = hmclPropertiesFile.get().asFile
targetFile.parentFile.mkdir()
targetFile.bufferedWriter().use {
for ((k, v) in hmclProperties) {
it.write("$k=$v\n")
}
}
}
property("hmcl.version", project.version.toString())
property("hmcl.add-opens", addOpens.joinToString(" "))
System.getenv("GITHUB_SHA")?.let { property("hmcl.version.hash", it) }
property("hmcl.version.type", versionType)
property("hmcl.microsoft.auth.id", microsoftAuthId)
property("hmcl.curseforge.apikey", curseForgeApiKey)
property("hmcl.authlib-injector.version", libs.authlib.injector.get().version!!)
property("hmcl.lwjgl-unsafe-agent.version", libs.lwjgl.unsafe.agent.get().version!!)
property("hmcl.official", providers.of(OfficialBuildValueSource::class) {
parameters.publicKeyFile.set(layout.projectDirectory.file("src/main/resources/assets/hmcl_signature_publickey.der"))
parameters.privateKeyLocation.set(providers.environmentVariable("HMCL_SIGNATURE_KEY"))
// Pin the official public key independently of the replaceable resource file.
// Under HMCL’s license agreement, you **MUST** change the software name or the version number in
// a reasonable way to distinguish it from the original version.
// You may modify this hash if and only if you have changed the software name or version number
// in accordance with the license requirements.
parameters.expectedSha256.set("36aadcfde8626b9d4c7a74facb3fdfda47e703c205a673fbe6a56b1d419f8b8e")
})
}
tasks.jar {
@@ -237,13 +233,12 @@ tasks.shadowJar {
}
tasks.processResources {
dependsOn(createPropertiesFile)
dependsOn(upsideDownTranslate)
dependsOn(createLocaleNamesResourceBundle)
dependsOn(createLanguageList)
into("assets/") {
from(hmclPropertiesFile)
from(createPropertiesFile.flatMap { it.destinationFile })
from(embedResources)
}
@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.TLSSecurity;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.JOptionPane;
@@ -42,6 +43,7 @@ public final class EntryPoint {
private EntryPoint() {
}
/// Initializes TLS protection before downloading dependencies and starting the launcher.
public static void main(String[] args) {
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
@@ -49,6 +51,7 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_LOCAL_HOME.resolve("logs"));
TLSSecurity.install();
checkWine();
@@ -42,14 +42,16 @@ public final class Metadata {
public static final String WINDOWS_APP_USER_MODEL_ID = "org.jackhuang.hmcl";
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
/// Whether the build metadata identifies this launcher as an official build.
public static final boolean OFFICIAL_BUILD = Boolean.parseBoolean(JarUtils.getAttribute("hmcl.official", "false"));
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final int RECOMMENDED_JAVA_VERSION = 21;
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String REPOSITORY_URL = "https://github.com/HMCL-dev/HMCL";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.game.GameComponentType;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.setting.DownloadSource;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
@@ -46,7 +46,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
DownloadSource source,
WebURL defaultUrl, WebURL bmclapiUrl
) {
if (LocaleUtils.IS_CHINA_MAINLAND) {
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
return switch (source) {
case DEFAULT, OFFICIAL -> DownloadCandidates.of(defaultUrl, bmclapiUrl);
case MIRROR -> DownloadCandidates.of(bmclapiUrl, defaultUrl);
@@ -66,7 +66,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
// true means prefer mirror, false means prefer official, null means do not use mirror
@Nullable Boolean mirrorFirst;
if (LocaleUtils.IS_CHINA_MAINLAND) {
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
mirrorFirst = switch (source) {
case DEFAULT, MIRROR -> true;
case OFFICIAL -> false;
@@ -158,7 +158,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchOfficial = ForgeRemoteVersion.fetchAsync(this, gameVersion);
DownloadSource source = versionListSource;
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
if (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL) {
return fetchOfficial;
}
@@ -173,7 +173,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchOfficial = NeoForgeRemoteVersion.fetchAsync(this, gameVersion);
DownloadSource source = versionListSource;
if (!LocaleUtils.IS_CHINA_MAINLAND && (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL)) {
if (source == DownloadSource.DEFAULT || source == DownloadSource.OFFICIAL) {
return fetchOfficial;
}
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchBMCL = NeoForgeRemoteVersion.fetchBMCLAsync(bmclRoot, gameVersion);
@@ -224,7 +224,7 @@ public final class HMCLDownloadProvider extends DownloadProvider {
@Override
public DownloadCandidates getCleanroomVersionListCandidates() {
return LocaleUtils.IS_CHINA_MAINLAND
return "CN".equals(RegionUtils.MACHINE_REGION)
? DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL, WebURL.parse("https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom/index.json"))
: DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL);
}
@@ -76,7 +76,7 @@ public final class ModpackHelper {
);
static {
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
MultiMCComponents.setImplementation(Metadata.FULL_NAME + " v" + Metadata.VERSION);
}
@Nullable
@@ -24,9 +24,9 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.terracotta.provider.AbstractTerracottaProvider;
import org.jackhuang.hmcl.terracotta.provider.GeneralProvider;
import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -91,7 +91,7 @@ public final class TerracottaMetadata {
}
Stream<String> stream = downloads.stream(), streamCN = downloadsCN.stream();
@Unmodifiable List<WebURL> links = (LocaleUtils.IS_CHINA_MAINLAND ? Stream.concat(streamCN, stream) : Stream.concat(stream, streamCN))
@Unmodifiable List<WebURL> links = ("CN".equals(RegionUtils.MACHINE_REGION) ? Stream.concat(streamCN, stream) : Stream.concat(stream, streamCN))
.map(link -> WebURL.parse(options.replace(link)))
.toList();
@@ -19,12 +19,12 @@ package org.jackhuang.hmcl.terracotta;
import com.google.gson.JsonParseException;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
@@ -84,7 +84,7 @@ public final class TerracottaNodeList {
return false;
}
return StringUtils.isBlank(node.region) || LocaleUtils.IS_CHINA_MAINLAND == "CN".equalsIgnoreCase(node.region);
return StringUtils.isBlank(node.region) || node.region.equalsIgnoreCase(RegionUtils.MACHINE_REGION);
})
.map(it -> WebURL.parse(it.url()))
.toList();
@@ -24,6 +24,9 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBase;
@@ -57,6 +60,7 @@ import org.jackhuang.hmcl.ui.terracotta.TerracottaPage;
import org.jackhuang.hmcl.ui.instances.GameListPage;
import org.jackhuang.hmcl.ui.instances.GameInstancePage;
import org.jackhuang.hmcl.ui.instances.Instances;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -118,6 +122,36 @@ public final class Controllers {
void run() throws Exception;
}
/// The shared launcher title, populated during UI initialization.
private static @Nullable StringProperty launcherTitle;
public static ReadOnlyStringProperty launcherTitleProperty() {
if (launcherTitle == null) {
launcherTitle = new SimpleStringProperty(Controllers.class, "launcherTitle", Metadata.OFFICIAL_BUILD
? i18n("launcher.title", Metadata.VERSION)
: i18n("launcher.title.unofficial", Metadata.VERSION));
if (Metadata.OFFICIAL_BUILD) {
@Nullable Boolean selfVerified = IntegrityChecker.getSelfVerified();
if (selfVerified != null) {
if (!selfVerified) {
launcherTitle.set(i18n("launcher.title.modified", Metadata.VERSION));
}
} else {
Schedulers.io().execute(() -> {
if (!IntegrityChecker.isSelfVerified()) {
FXUtils.runInFX(() -> {
//noinspection DataFlowIssue
launcherTitle.set(i18n("launcher.title.modified", Metadata.VERSION));
});
}
});
}
}
}
return launcherTitle;
}
/// Returns the stage currently attached to the main-window decorator.
///
/// @return the current stage, or `null` before initialization or after detachment
@@ -134,14 +168,6 @@ public final class Controllers {
return gameInstancePage;
}
@FXThread
public static void prepareGameInstancePage() {
if (gameInstancePage == null) {
LOG.info("Prepare the game instance page");
gameInstancePage = FXUtils.prepareNode(new GameInstancePage());
}
}
@FXThread
public static GameListPage getGameListPage() {
return gameListPage.get();
@@ -160,14 +186,6 @@ public final class Controllers {
return settingsPage;
}
@FXThread
public static void prepareSettingsPage() {
if (settingsPage == null) {
LOG.info("Prepare the settings page");
settingsPage = FXUtils.prepareNode(new LauncherSettingsPage());
}
}
@FXThread
public static AccountListPage getAccountListPage() {
return accountListPage.get();
@@ -181,14 +199,6 @@ public final class Controllers {
return downloadPage;
}
@FXThread
public static void prepareDownloadPage() {
if (downloadPage == null) {
LOG.info("Prepare the download page");
downloadPage = FXUtils.prepareNode(new DownloadPage());
}
}
@FXThread
public static Node getTerracottaPage() {
return terracottaPage.get();
@@ -215,7 +225,7 @@ public final class Controllers {
private static void configureMainStage(Stage stage) {
stage.setOnCloseRequest(event -> Launcher.stopApplication());
FXUtils.setIcon(stage);
stage.setTitle(Metadata.FULL_TITLE);
stage.titleProperty().bind(launcherTitleProperty());
WindowsNativeUtils.installWindowsAppUserModelRelaunchProperties(stage);
}
@@ -36,7 +36,6 @@ import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Bounds;
import javafx.geometry.Rectangle2D;
@@ -1226,16 +1225,6 @@ public final class FXUtils {
return node;
}
public static void prepareOnMouseEnter(Node node, Runnable action) {
node.addEventFilter(MouseEvent.MOUSE_ENTERED, new EventHandler<>() {
@Override
public void handle(MouseEvent e) {
node.removeEventFilter(MouseEvent.MOUSE_ENTERED, this);
action.run();
}
});
}
public static <T> void onScroll(Node node, List<T> list,
ToIntFunction<List<T>> finder,
Consumer<T> updater
@@ -91,6 +91,7 @@ public enum SVG {
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
LOCAL_CAFE_FILL("M4 21V19H20v2H4Zm4-4q-1.65 0-2.825-1.175T4 13V3H20q.825 0 1.4125.5875T22 5V8q0 .825-.5875 1.4125T20 10H18v3q0 1.65-1.175 2.825T14 17H8ZM18 8h2V5H18V8Z"),
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
MEMORY("M9 15V9h6v6H9Zm2-2h2v-2h-2v2Zm-2 8v-2H7q-.825 0-1.4125-.5875T5 17v-2H3v-2h2v-2H3V9h2V7q0-.825.5875-1.4125T7 5h2V3h2v2h2V3h2v2h2q.825 0 1.4125.5875T19 7v2h2v2h-2v2h2v2h-2v2q0 .825-.5875 1.4125T17 19h-2v2h-2v-2h-2v2H9Zm8-4V7H7v10h10Zm-5-5Z"),
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
MINIMIZE_CENTER("M6 13v-2h12v2H6Z"), // Not Material
@@ -132,6 +133,13 @@ public enum SVG {
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
WB_SUNNY_FILL("M11 4V1h2V4H11Zm0 19V20h2v3H11Zm9-10V11h3v2H20ZM1 13V11H4v2H1ZM18.7 6.7 17.3 5.3l1.75-1.8L20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3l1.4 1.4-1.75 1.8Zm14.1 0-1.75-1.8 1.4-1.4 1.8 1.75-1.45 1.45ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18q-2.5 0-4.25-1.75T6 12 7.75 7.75 12 6t4.25 1.75T18 12t-1.75 4.25T12 18Z"),
// https://lucide.dev/
LUCIDE_APP_WINDOW("M4 3C2.3553 3 1 4.3553 1 6V18c0 1.6447 1.3553 3 3 3H20c1.6447 0 3-1.3553 3-3V6c0-1.6447-1.3553-3-3-3H6 4zM4 5H5V7H3V6c0-.5713.4287-1 1-1zM7 5H9V7H7V5zm4 0h9c.5713 0 1 .4287 1 1V7H11V5zM3 9H6 21v9c0 .5713-.4287 1-1 1H4c-.5713 0-1-.4287-1-1V9z"),
LUCIDE_CPU("M7 1A1 1 0 006 2V3C4.3553 3 3 4.3553 3 6H2A1 1 0 001 7 1 1 0 002 8H3v3H2A1 1 0 001 12a1 1 0 001 1H3v3H2A1 1 0 001 17a1 1 0 001 1H3c0 1.6447 1.3553 3 3 3v1a1 1 0 001 1 1 1 0 001-1V21h3v1a1 1 0 001 1 1 1 0 001-1V21h3v1a1 1 0 001 1 1 1 0 001-1V21c1.6447 0 3-1.3553 3-3h1a1 1 0 001-1 1 1 0 00-1-1H21V13h1a1 1 0 001-1 1 1 0 00-1-1H21V8h1a1 1 0 001-1A1 1 0 0022 6H21c0-1.6447-1.3553-3-3-3V2A1 1 0 0017 1 1 1 0 0016 2V3H13V2A1 1 0 0012 1 1 1 0 0011 2V3H8V2A1 1 0 007 1zM6 5H18c.5713 0 1 .4287 1 1V18c0 .5713-.4287 1-1 1H6c-.5713 0-1-.4287-1-1V6c0-.5713.4287-1 1-1zM9 7C7.9093 7 7 7.9093 7 9v6c0 1.0907.9093 2 2 2h6c1.0907 0 2-.9093 2-2V9c0-1.0907-.9093-2-2-2H9zM9 9h6v6H9V9z"),
LUCIDE_GPU("M8 18v2h5V18H8Zm8-8c-.5641 0-1 .4359-1 1s.4359 1 1 1 1-.4359 1-1-.4359-1-1-1ZM8 10c-.5641 0-1 .4359-1 1s.4359 1 1 1c.5641 0 1-.4359 1-1s-.4359-1-1-1Zm8-2c1.645 0 3 1.355 3 3 0 1.645-1.355 3-3 3s-3-1.355-3-3c0-1.645 1.355-3 3-3ZM8 8c1.645 0 3 1.355 3 3 0 1.645-1.355 3-3 3s-3-1.355-3-3C5 9.355 6.355 8 8 8ZM3 6V16H20c.5641 0 1-.4359 1-1V7c0-.5641-.4359-1-1-1H3ZM2 2A1 1 0 013 3V4H20c1.645 0 3 1.355 3 3v8c0 1.645-1.355 3-3 3H15v2c0 1.0927-.9073 2-2 2H8c-1.0927 0-2-.9073-2-2V18H3v3A1 1 0 012 22 1 1 0 011 21V3A1 1 0 012 2Z"),
LUCIDE_MEMORY_STICK("M4 5C2.3553 5 1 6.3553 1 8v3 3c0 1.2949.84 2.4102 2 2.8262V18a1 1 0 001 1 1 1 0 001-1V17H7v1a1 1 0 001 1 1 1 0 001-1V17h2v1a1 1 0 001 1 1 1 0 001-1V17h2v1a1 1 0 001 1 1 1 0 001-1V17h2v1a1 1 0 001 1 1 1 0 001-1V16.8262c1.16-.416 2-1.5313 2-2.8262V11 8c0-1.6447-1.3553-3-3-3H4zM4 7H20c.5713 0 1 .4287 1 1v2h-.5a1 1 0 00-1 1 1 1 0 001 1H21v2c0 .5713-.4287 1-1 1H16 12 8 4c-.5713 0-1-.4287-1-1V12h.5a1 1 0 001-1 1 1 0 00-1-1H3V8c0-.5713.4287-1 1-1zM8 9A1 1 0 007 10v2a1 1 0 001 1 1 1 0 001-1V10A1 1 0 008 9zm4 0a1 1 0 00-1 1v2a1 1 0 001 1 1 1 0 001-1V10A1 1 0 0012 9zm4 0a1 1 0 00-1 1v2a1 1 0 001 1 1 1 0 001-1V10A1 1 0 0016 9z"),
;
public static final double DEFAULT_SIZE = 24;
@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
@@ -153,7 +154,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
.start();
}
});
FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload"));
FXUtils.installFastTooltip(btnUpload, i18n("account.skin." + (skinnable.getAccount() instanceof OfflineAccount ? "edit" : "upload")));
btnUpload.disableProperty().bind(Bindings.not(skinnable.canUploadSkin()));
spinnerUpload.setContent(btnUpload);
spinnerUpload.getStyleClass().add("small-spinner-pane");
@@ -40,7 +40,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -57,7 +57,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
String property = System.getProperty("hmcl.offline.auth.restricted", "auto");
if ("false".equals(property)
|| "auto".equals(property) && LocaleUtils.IS_CHINA_MAINLAND
|| "auto".equals(property) && "CN".equals(RegionUtils.MACHINE_REGION)
|| SettingsManager.userSettings().enableOfflineAccountProperty().get())
RESTRICTED.set(false);
else
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -140,23 +140,21 @@ public class MicrosoftAccountLoginPane extends JFXDialogLayout implements Dialog
rootContainer.setAlignment(Pos.TOP_CENTER);
StackPane warningPane = new StackPane();
rootContainer.getChildren().add(warningPane);
if (!IntegrityChecker.isOfficial()) {
var unofficialHintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
unofficialHintPane.setSegment(i18n("unofficial.hint"));
warningPane.getChildren().add(unofficialHintPane);
} else if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
HintPane snapshotHint = new HintPane(MessageDialogPane.MessageType.WARNING);
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
warningPane.getChildren().add(snapshotHint);
btnLogin.setDisable(true);
loginButtonSpinner.setLoading(false);
return;
warningPane.getChildren().setAll(unofficialHintPane);
}
rootContainer.getChildren().add(warningPane);
if (currentStep instanceof Step.Init) {
if (Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
HintPane snapshotHint = new HintPane(MessageDialogPane.MessageType.ERROR);
snapshotHint.setSegment(i18n("account.methods.microsoft.snapshot"));
warningPane.getChildren().setAll(snapshotHint);
btnLogin.setDisable(true);
loginButtonSpinner.setLoading(false);
} else if (currentStep instanceof Step.Init) {
btnLogin.setOnAction(e -> this.step.set(new Step.StartAuthorizationCodeLogin()));
loginButtonSpinner.setLoading(false);
@@ -26,31 +26,46 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
/// Displays one page, retaining the preceding page while a transition runs.
/// Content changes must be made on the JavaFX Application Thread.
@NotNullByDefault
public class TransitionPane extends StackPane {
private Node currentNode;
/// The requested page, or `null` before content is assigned.
private @Nullable Node currentNode;
/// Creates an empty container that clips content to its bounds.
public TransitionPane() {
FXUtils.setOverflowHidden(this);
}
public Node getCurrentNode() {
/// Returns the requested page, or `null` if none has been assigned.
public @Nullable Node getCurrentNode() {
return currentNode;
}
/// Replaces the content using [Motion#SHORT4] and [Motion#EASE].
public final void setContent(Node newView, AnimationProducer transition) {
setContent(newView, transition, Motion.SHORT4);
}
/// Replaces the content using the given duration and [Motion#EASE].
public final void setContent(Node newView, AnimationProducer transition, Duration duration) {
setContent(newView, transition, duration, Motion.EASE);
}
/// Replaces the content using the given animation and timing.
/// The first page, repeated assignments of the current page, and replacements
/// without animations are displayed immediately. Otherwise, CSS and layout
/// are processed in this container before the animation starts in a later
/// event-loop task. Mouse input is disabled while the transition is pending
/// or running.
public void setContent(Node newView, AnimationProducer transition,
Duration duration, Interpolator interpolator) {
Node previousNode = currentNode != newView && getWidth() > 0 && getHeight() > 0 ? currentNode : null;
@Nullable Node previousNode = currentNode != newView && getWidth() > 0 && getHeight() > 0 ? currentNode : null;
currentNode = newView;
if (!AnimationUtils.isAnimationEnabled() || previousNode == null || transition == ContainerAnimations.NONE) {
@@ -64,7 +79,7 @@ public class TransitionPane extends StackPane {
setMouseTransparent(true);
transition.init(this, previousNode, newView);
CacheHint cacheHint = newView instanceof Cacheable cacheable
@Nullable CacheHint cacheHint = newView instanceof Cacheable cacheable
? cacheable.getCacheHint(transition)
: null;
@@ -73,8 +88,12 @@ public class TransitionPane extends StackPane {
newView.setCacheHint(cacheHint);
}
// runLater or "init" will not work
// Let navigation handlers finish updating the page before preparing it.
Platform.runLater(() -> {
// runLater alone does not guarantee a CSS or layout pass. Keep that
// first-use work outside the animation's elapsed time.
applyCss();
layout();
Animation newAnimation = transition.animate(
this,
previousNode,
@@ -95,15 +114,19 @@ public class TransitionPane extends StackPane {
}
/// Prepares page properties and creates animations for content replacements.
public interface AnimationProducer {
/// Resets page transforms, making the old page opaque and the new page transparent.
default void init(TransitionPane container, Node previousNode, Node nextNode) {
AnimationUtils.reset(previousNode, true);
AnimationUtils.reset(nextNode, false);
}
/// Creates the transition between the prepared pages using the given timing.
Animation animate(Pane container, Node previousNode, Node nextNode,
Duration duration, Interpolator interpolator);
/// Returns the reverse transition, or `null` if none is specified.
default @Nullable TransitionPane.AnimationProducer opposite() {
return null;
}
@@ -749,6 +749,7 @@ public final class Decorator {
/// Replaces the attached stage when transparency or theme brightness requires a different decoration style.
///
/// Preserves the scene, navigation, dialogs, normal bounds, supported window states, and focus owner.
/// Copies the previous title before the replacement-stage initializer may override or bind it.
/// Hidden windows remain hidden. Does nothing while detached or when the current style is still suitable.
/// Must run on the JavaFX application thread after theme bindings have updated.
private void updateWindowStyle() {
@@ -765,8 +766,8 @@ public final class Decorator {
@Nullable Node focusOwner = previousStage.getScene().getFocusOwner();
Stage replacement = new Stage();
stageInitializer.accept(replacement);
replacement.setTitle(previousStage.getTitle());
stageInitializer.accept(replacement);
replacement.getIcons().setAll(previousStage.getIcons());
replacement.setOnCloseRequest(previousStage.getOnCloseRequest());
replacement.setResizable(previousStage.isResizable());
@@ -52,10 +52,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
@@ -23,7 +23,6 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
@@ -71,7 +70,6 @@ import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -170,10 +168,6 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
}
}
private <T> void setupCellValueFactory(TableColumn<AddonUpdateObject, T> column, Function<AddonUpdateObject, ObservableValue<T>> mapper) {
column.setCellValueFactory(param -> mapper.apply(param.getValue()));
}
private void updateFiles() {
AddonUpdateTask task = new AddonUpdateTask(
downloadProvider,
@@ -213,10 +207,10 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
csvTable.set(3, 0, "Update Source");
for (int i = 0; i < objects.size(); i++) {
csvTable.set(0, i + 1, objects.get(i).fileName.get());
csvTable.set(1, i + 1, objects.get(i).currentVersion.get());
csvTable.set(2, i + 1, objects.get(i).targetVersion.get());
csvTable.set(3, i + 1, objects.get(i).source.get());
csvTable.set(0, i + 1, objects.get(i).getFileName());
csvTable.set(1, i + 1, objects.get(i).getCurrentVersion());
csvTable.set(2, i + 1, objects.get(i).getTargetVersion());
csvTable.set(3, i + 1, objects.get(i).getSource());
}
csvTable.write(path);
@@ -238,34 +232,30 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
private static final class AddonUpdateObject {
final AddonUpdate data;
final BooleanProperty enabled = new SimpleBooleanProperty();
final StringProperty fileName = new SimpleStringProperty();
final StringProperty currentVersion = new SimpleStringProperty();
final StringProperty targetVersion = new SimpleStringProperty();
final StringProperty targetVersionType = new SimpleStringProperty();
final StringProperty source = new SimpleStringProperty();
final String targetVersionType;
final String source;
String changelog = null;
final BooleanProperty enabled = new SimpleBooleanProperty();
final ItemPropertyAsyncCache<Image, AddonUpdateObject> iconCache;
public AddonUpdateObject(AddonUpdate data) {
this.data = data;
enabled.set(!data.localAddonFile().isDisabled());
fileName.set(data.localAddonFile().getFileName());
currentVersion.set(data.currentVersion().version());
targetVersion.set(data.targetVersion().version());
targetVersionType.set(data.targetVersion().versionType().name());
source.set(switch (data.currentVersion().source()) {
this.targetVersionType = switch (data.targetVersion().versionType()) {
case Release -> i18n("addon.channel.release");
case Beta -> i18n("addon.channel.beta");
case Alpha -> i18n("addon.channel.alpha");
};
this.source = switch (data.currentVersion().source()) {
case CURSEFORGE -> i18n("addon.curseforge");
case MODRINTH -> i18n("addon.modrinth");
});
};
iconCache = new ItemPropertyAsyncCache.Soft<>(
this,
this::loadIcon,
this::getDefaultIcon
);
this.enabled.set(!data.localAddonFile().isDisabled());
this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon);
}
public AddonUpdate getData() {
@@ -280,42 +270,30 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
return enabled;
}
public StringProperty fileNameProperty() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName.set(fileName);
}
public LocalAddonFile getLocalAddonFile() {
public LocalAddonFile<?> getLocalAddonFile() {
return data.localAddonFile();
}
public String getFileName() {
return data.localAddonFile().getFileName();
}
public String getCurrentVersion() {
return currentVersion.get();
return data.currentVersion().version();
}
public StringProperty currentVersionProperty() {
return currentVersion;
public String getTargetVersion() {
return data.targetVersion().version();
}
public StringProperty targetVersionProperty() {
return targetVersion;
}
public StringProperty targetVersionTypeProperty() {
public String getTargetVersionType() {
return targetVersionType;
}
public StringProperty sourceProperty() {
public String getSource() {
return source;
}
public void setSource(String source) {
this.source.set(source);
}
private Image getDefaultIcon() {
if (data.localAddonFile() instanceof LocalModFile localModFile) {
return GameInstanceIconType
@@ -424,13 +402,20 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
@Override
public void updateItem(AddonUpdateObject item, boolean empty) {
AddonUpdateObject oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
if (oldItem == item && oldEmpty == empty) {
return;
}
ripplerContainer.releaseRippleImmediately();
if (oldItem != null) {
enabledBox.selectedProperty().unbindBidirectional(oldItem.enabledProperty());
}
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
return;
@@ -440,18 +425,13 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
item.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty()));
LocalAddonFile addonFile = item.getLocalAddonFile();
LocalAddonFile<?> addonFile = item.getLocalAddonFile();
if (oldItem != item) {
content.getTags().clear();
if (addonFile instanceof LocalModFile modFile) {
content.addTag(modFile.getId());
content.addTag(item.source.get());
} else if (addonFile instanceof ResourcePackFile) {
content.addTag(item.source.get());
}
}
content.getTags().clear();
if (addonFile instanceof LocalModFile modFile)
content.addTag(modFile.getId());
content.addTag(item.getSource());
content.addTag(item.getTargetVersionType());
if (addonFile instanceof LocalModFile modFile) {
content.setTitle(modFile.getName());
@@ -459,13 +439,7 @@ public class AddonUpdatesPage extends BorderPane implements DecoratorPage {
content.setTitle(resourcePackFile.getFileName());
}
content.setSubtitle(
item.getCurrentVersion()
+ " → "
+ item.targetVersion.get()
+ " | "
+ item.targetVersionType.get()
);
content.setSubtitle(item.getCurrentVersion() + " → " + item.getTargetVersion());
setGraphic(pane);
}
@@ -549,86 +549,10 @@ public class DownloadListPage extends Control implements DecoratorPage {
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
listView.setCellFactory(x -> new ListCell<>() {
private static final Insets PADDING = new Insets(9, 9, 0, 9);
private static final Insets LAST_PADDING = new Insets(9, 9, 9, 9);
private final RipplerContainer graphic;
private final StackPane wrapper = new StackPane();
private final TwoLineListItem content = new TwoLineListItem();
private final ImageContainer imageContainer = new ImageContainer(40);
{
setPadding(PADDING);
HBox container = new HBox(8);
container.setPadding(new Insets(8));
container.setCursor(Cursor.HAND);
container.setAlignment(Pos.CENTER_LEFT);
imageContainer.setMouseTransparent(true);
container.getChildren().setAll(imageContainer, content);
HBox.setHgrow(content, Priority.ALWAYS);
this.graphic = new RipplerContainer(container);
wrapper.getChildren().setAll(this.graphic);
wrapper.getStyleClass().add("card-no-padding");
FXUtils.onClicked(wrapper, () -> {
RemoteAddon item = getItem();
if (item != null)
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getInstanceOptional(), getSkinnable().callback));
});
setPrefWidth(0);
FXUtils.limitCellWidth(listView, this);
}
@Override
protected void updateItem(RemoteAddon item, boolean empty) {
RemoteAddon oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
if (oldItem == item && oldEmpty == empty) return;
this.graphic.releaseRippleImmediately();
if (empty || item == null) {
setGraphic(null);
} else {
setPadding(
getIndex() == getListView().getItems().size() - 1
? LAST_PADDING
: PADDING
);
@Nullable AddonProjectInfoRegistry<?> registry = AddonProjectInfoRegistry.getByAddonType(getSkinnable().type);
@Nullable AddonProjectInfo mod = registry != null ? registry.getInfoByCurseForgeId(item.slug()) : null;
content.setTitle(mod != null && I18n.isUseChinese() ? mod.displayName() : item.title());
String description = item.description();
if (description != null) {
description = description.replaceAll("\\R", " ");
}
content.setSubtitle(description);
content.getTags().clear();
for (String category : item.categories()) {
if (getSkinnable().shouldDisplayCategory(category))
content.addTag(getSkinnable().getLocalizedCategory(category, null));
}
iconLoader.load(imageContainer.imageProperty(), item.iconUrl());
setGraphic(wrapper);
}
}
});
listView.setCellFactory(x -> new DownloadListCell(getSkinnable(), listView, iconLoader));
}
getChildren().setAll(pane);
this.getChildren().setAll(pane);
}
private record CategoryIndented(int indent, RemoteAddonRepository.Category category) {
@@ -642,4 +566,91 @@ public class DownloadListPage extends Control implements DecoratorPage {
}
}
}
private static final class DownloadListCell extends ListCell<RemoteAddon> {
private final DownloadListPage page;
private final JFXListView<RemoteAddon> listView;
private final RemoteImageLoader iconLoader;
private static final Insets PADDING = new Insets(9, 9, 0, 9);
private static final Insets LAST_PADDING = new Insets(9, 9, 9, 9);
private final RipplerContainer graphic;
private final StackPane wrapper = new StackPane();
private final TwoLineListItem content = new TwoLineListItem();
private final ImageContainer imageContainer = new ImageContainer(40);
private DownloadListCell(DownloadListPage page, JFXListView<RemoteAddon> listView, RemoteImageLoader iconLoader) {
this.page = page;
this.listView = listView;
this.iconLoader = iconLoader;
setPadding(PADDING);
HBox container = new HBox(8);
container.setPadding(new Insets(8));
container.setCursor(Cursor.HAND);
container.setAlignment(Pos.CENTER_LEFT);
imageContainer.setMouseTransparent(true);
container.getChildren().setAll(imageContainer, content);
HBox.setHgrow(content, Priority.ALWAYS);
this.graphic = new RipplerContainer(container);
wrapper.getChildren().setAll(this.graphic);
wrapper.getStyleClass().add("card-no-padding");
FXUtils.onClicked(wrapper, () -> {
RemoteAddon item = getItem();
if (item != null) {
Controllers.navigate(new DownloadPage(page, item, page.getInstanceOptional(), page.callback));
}
});
setPrefWidth(0);
FXUtils.limitCellWidth(listView, this);
}
@Override
protected void updateItem(RemoteAddon item, boolean empty) {
RemoteAddon oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
if (oldItem == item && oldEmpty == empty) return;
this.graphic.releaseRippleImmediately();
if (empty || item == null) {
setGraphic(null);
} else {
setPadding(
getIndex() == listView.getItems().size() - 1
? LAST_PADDING
: PADDING
);
@Nullable AddonProjectInfoRegistry<?> registry = AddonProjectInfoRegistry.getByAddonType(page.type);
@Nullable AddonProjectInfo mod = registry != null ? registry.getInfoByCurseForgeId(item.slug()) : null;
content.setTitle(mod != null && I18n.isUseChinese() ? mod.displayName() : item.title());
String description = item.description();
if (description != null) {
description = description.replaceAll("\\R", " ");
}
content.setSubtitle(description);
content.getTags().clear();
for (String category : item.categories()) {
if (page.shouldDisplayCategory(category)) {
content.addTag(page.getLocalizedCategory(category, null));
}
}
iconLoader.load(imageContainer.imageProperty(), item.iconUrl());
setGraphic(wrapper);
}
}
}
}
@@ -26,18 +26,26 @@ import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.DataSizeUnit;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.CentralProcessor;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@NotNullByDefault
public final class AboutPage extends SpinnerPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
@@ -52,22 +60,121 @@ public final class AboutPage extends SpinnerPane {
ComponentList about = new ComponentList();
{
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
var launcher = LineButton.createExternalLinkButton(Metadata.REPOSITORY_URL);
launcher.setLargeTitle(true);
launcher.setLeading(FXUtils.newBuiltinImage("/assets/img/icon.png"));
launcher.setTitle("Hello Minecraft! Launcher");
launcher.setSubtitle(Metadata.VERSION);
var author = LineButton.createExternalLinkButton("https://space.bilibili.com/1445341");
author.setLargeTitle(true);
author.setLeading(FXUtils.newBuiltinImage("/assets/img/yellow_fish.png"));
author.setTitle("huanghongxun");
author.setSubtitle(i18n("about.author.statement"));
about.getContent().setAll(launcher, author);
about.getContent().setAll(launcher);
}
ComponentList thanks = loadIconedTwoLineList("/assets/about/thanks.json");
ComponentList environment = new ComponentList();
{
var system = new LinePane();
system.setLargeTitle(true);
system.setLeading(SVG.LUCIDE_APP_WINDOW.createIcon(32));
system.setTitle(i18n("about.environment.system"));
system.setSubtitle((OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? "%s %s (%s)".formatted(OperatingSystem.SYSTEM_NAME, OperatingSystem.SYSTEM_VERSION.getVersion(), Architecture.SYSTEM_ARCH.getDisplayName())
: "%s (%s %s, %s)".formatted(OperatingSystem.OS_RELEASE_PRETTY_NAME, OperatingSystem.SYSTEM_NAME, OperatingSystem.SYSTEM_VERSION.getVersion(), Architecture.SYSTEM_ARCH.getDisplayName())));
environment.getContent().add(system);
if (SystemInfo.getTotalMemorySize() > 0) {
var memory = new LinePane();
memory.setLargeTitle(true);
memory.setLeading(SVG.LUCIDE_MEMORY_STICK.createIcon(32));
memory.setTitle(i18n("about.environment.memory"));
memory.setSubtitle("%.1f GiB".formatted(DataSizeUnit.GIGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize())));
environment.getContent().add(memory);
}
@Nullable CentralProcessor cpu = SystemInfo.getCentralProcessor();
if (cpu != null) {
int cpuCount;
int logicalPerPackage;
int physicalPerPackage;
CentralProcessor.Cores cores = cpu.getCores();
if (cores != null && cores.packages() > 1) {
cpuCount = cores.packages();
// TODO: Usually, all CPUs on a multi-socket platform have the same core count, but environments
// like Linux allow some cores to be offline, in which case cores and threads cannot be evenly
// distributed. In the future, we may consider detecting the CPU topology in more detail to
// handle this case correctly.
logicalPerPackage = cores.logical() / cores.packages();
physicalPerPackage = cores.physical() / cores.packages();
} else {
cpuCount = 1;
if (cores != null) {
logicalPerPackage = cores.logical();
physicalPerPackage = cores.physical();
} else {
logicalPerPackage = 0;
physicalPerPackage = 0;
}
}
String cpuName;
if (logicalPerPackage == 0) {
cpuName = i18n("about.environment.cpu.name", cpu.getName());
} else if (logicalPerPackage > 0 && logicalPerPackage != physicalPerPackage)
cpuName = i18n("about.environment.cpu.name.cores.hyper_threading", cpu.getName(), physicalPerPackage, logicalPerPackage);
else
cpuName = i18n("about.environment.cpu.name.cores", cpu.getName(), physicalPerPackage);
for (int i = 1; i <= cpuCount; i++) {
var button = new LinePane();
button.setLargeTitle(true);
button.setTitle(i == 1 && cpuCount == 1
? i18n("about.environment.cpu")
: i18n("about.environment.cpu.n", i));
button.setSubtitle(cpuName);
button.setLeading(SVG.LUCIDE_CPU.createIcon(32));
environment.getContent().add(button);
}
}
@Nullable List<GraphicsCard> graphicsCards = SystemInfo.getGraphicsCards();
if (graphicsCards != null) {
@Unmodifiable List<GraphicsCard> visibleGraphicsCards = graphicsCards.stream()
.filter(card -> !card.isVirtualDisplayAdapter())
.toList();
int index = 1;
for (GraphicsCard graphicsCard : visibleGraphicsCards) {
String name = index == 1 && visibleGraphicsCards.size() == 1
? i18n("about.environment.gpu")
: i18n("about.environment.gpu.n", index);
index++;
var button = new LinePane();
button.setLargeTitle(true);
button.setTitle(name);
button.setSubtitle(graphicsCard.getName());
button.setLeading(SVG.LUCIDE_GPU.createIcon(32));
environment.getContent().add(button);
}
}
var java = new LinePane();
java.setLargeTitle(true);
java.setLeading(SVG.LOCAL_CAFE.createIcon(32));
java.setTitle("Java");
java.setSubtitle("%s, %s, %s".formatted(
Runtime.version().toString(),
System.getProperty("java.vm.name"),
System.getProperty("java.vendor")));
environment.getContent().add(java);
var javaFX = new LinePane();
javaFX.setLargeTitle(true);
javaFX.setLeading(SVG.LOCAL_CAFE.createIcon(32));
javaFX.setTitle("JavaFX");
javaFX.setSubtitle(System.getProperty("javafx.runtime.version"));
environment.getContent().add(javaFX);
}
ComponentList deps = loadIconedTwoLineList("/assets/about/deps.json");
@@ -94,8 +201,8 @@ public final class AboutPage extends SpinnerPane {
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("about")),
about,
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.environment")),
environment,
ComponentList.createComponentListTitle(i18n("about.dependency")),
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
@@ -141,7 +248,6 @@ public final class AboutPage extends SpinnerPane {
imageView.setImage(loadImage(image.getAsString()));
imageView.setMouseTransparent(true);
button.setLeading(imageView);
} else if (image.isJsonObject()) {
holder.add(FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> button.setLeading(darkMode
@@ -169,4 +275,5 @@ public final class AboutPage extends SpinnerPane {
return componentList;
}
}
@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
@@ -30,10 +32,12 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.util.ArrayList;
import java.util.List;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class HelpPage extends SpinnerPane {
@@ -62,7 +66,16 @@ public class HelpPage extends SpinnerPane {
private void loadHelp() {
showSpinner();
Task.supplyAsync(() -> HttpRequest.GET(Metadata.DOCS_URL + "/index.json").getJson(listTypeOf(HelpCategory.class)))
.thenAcceptAsync(Schedulers.javafx(), helpCategories -> {
.whenComplete(Schedulers.javafx(), (helpCategories, exception) -> {
if (exception != null) {
LOG.warning("Failed to load help page: ", exception);
showLoadFailure();
return;
}
try {
List<Node> nodes = new ArrayList<>();
for (HelpCategory category : helpCategories) {
ComponentList categoryPane = new ComponentList();
@@ -75,11 +88,24 @@ public class HelpPage extends SpinnerPane {
categoryPane.getContent().add(item);
}
content.getChildren().add(ComponentList.createComponentListTitle(category.title()));
content.getChildren().add(categoryPane);
nodes.add(ComponentList.createComponentListTitle(category.title()));
nodes.add(categoryPane);
}
content.getChildren().addAll(nodes);
hideSpinner();
}).start();
} catch (Exception e) {
LOG.warning("Failed to display help page: ", e);
showLoadFailure();
}
}).start();
}
private void showLoadFailure() {
setFailedReason(i18n("download.failed.refresh"));
setOnFailedAction(e -> loadHelp());
setLoading(false);
}
@JsonSerializable
@@ -118,7 +118,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
titleNode.setAlignment(Pos.CENTER_LEFT);
ImageView titleIcon = new ImageView(FXUtils.newBuiltinImage("/assets/img/icon-title.png"));
Label titleLabel = new Label(Metadata.FULL_TITLE);
Label titleLabel = new Label();
titleLabel.textProperty().bind(Controllers.launcherTitleProperty());
if (I18n.isUpsideDown()) {
titleIcon.setRotate(180);
titleLabel.setRotate(180);
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -131,9 +130,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
@Nullable GameInstanceID currentId = currentGame != null ? currentGame.getId() : null;
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, instance -> instance.getRepository().setSelectedInstance(instance));
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareGameInstancePage);
}
FXUtils.onSecondaryButtonClicked(gameListItem, () -> showGameListPopupMenu(gameListItem));
// third item in left sidebar
@@ -148,9 +144,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
downloadItem.setLeftIcon(SVG.DOWNLOAD);
downloadItem.setTitle(i18n("download"));
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(downloadItem, Controllers::prepareDownloadPage);
}
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
@@ -160,9 +153,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Controllers.getSettingsPage().showGameSettings(GameDirectoryManager.getSelectedRepository());
Controllers.navigate(Controllers.getSettingsPage());
});
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(launcherSettingsItem, Controllers::prepareSettingsPage);
}
// sixth item in left sidebar
AdvancedListItem terracottaItem = new AdvancedListItem();
@@ -118,10 +118,10 @@ public final class SettingsPage extends ScrollPane {
updateButton.setManaged(outdated);
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
if (outdated) {
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().version()));
} else if (UpdateChecker.isCheckingUpdate()) {
if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSubProperty.set(i18n("update.checking"));
} else if (outdated) {
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().version()));
} else if (UpdateChecker.errorProperty().get() != null) {
Throwable t = UpdateChecker.errorProperty().get();
if (t instanceof SelfVerificationException) {
@@ -53,8 +53,8 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.instances.Instances;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -540,7 +540,7 @@ public class TerracottaControllerPage extends StackPane {
VBox content = new VBox(10);
content.getChildren().add(ComponentList.createComponentListTitle(i18n("terracotta.status")));
if (!LocaleUtils.IS_CHINA_MAINLAND) {
if (!"CN".equals(RegionUtils.MACHINE_REGION)) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setText(i18n("terracotta.unsupported.region"));
content.getChildren().add(hintPane);
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
@@ -105,18 +106,25 @@ public final class IntegrityChecker {
}
}
private static volatile Boolean selfVerified = null;
private static volatile @Nullable Boolean selfVerified = null;
public static @Nullable Boolean getSelfVerified() {
return selfVerified;
}
/**
* Checks whether the current application is verified.
* This method is blocking.
*/
public static boolean isSelfVerified() {
@Nullable Boolean selfVerified = IntegrityChecker.selfVerified;
if (selfVerified != null) {
return selfVerified;
}
synchronized (IntegrityChecker.class) {
selfVerified = IntegrityChecker.selfVerified;
if (selfVerified != null) {
return selfVerified;
}
@@ -134,7 +142,7 @@ public final class IntegrityChecker {
LOG.warning("Failed to verify myself, is the JAR corrupt?", e);
selfVerified = false;
}
IntegrityChecker.selfVerified = selfVerified;
return selfVerified;
}
}
@@ -0,0 +1,376 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jetbrains.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// Detects the machine's region.
@NotNullByDefault
public final class RegionUtils {
/// The two-letter region code detected at class initialization, or `null` if none can be determined.
/// Shared time zones require a matching locale region or Windows user GeoID.
/// Later changes to system settings do not update this value.
public static final @Nullable String MACHINE_REGION = detectMachineRegion();
/// Detects the region from the system time zone, using [LocaleUtils#SYSTEM_DEFAULT]
/// and the Windows user GeoID to resolve ambiguity or supply a missing mapping.
///
/// @return the detected region code, or `null` if no region can be determined unambiguously
private static @Nullable String detectMachineRegion() {
@Unmodifiable List<String> candidates;
try {
candidates = findRegionsByTimeZone(ZoneId.systemDefault().getId());
} catch (Exception e) {
LOG.warning("Failed to load time zone region mapping", e);
candidates = List.of();
}
if (candidates.size() == 1)
return candidates.get(0);
String localeRegion = LocaleUtils.SYSTEM_DEFAULT.getCountry();
if (localeRegion.matches("[A-Z]{2}")) {
if ((candidates.isEmpty() || candidates.contains(localeRegion)))
return localeRegion;
} else {
localeRegion = null;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && NativeUtils.USE_JNA) {
@Nullable Kernel32 kernel32 = Kernel32.INSTANCE;
if (kernel32 != null) {
@Nullable String geoidRegion = mapGeoIdToRegion(kernel32.GetUserGeoID(WinConstants.GEOCLASS_NATION));
if (geoidRegion != null && (candidates.isEmpty() || candidates.contains(geoidRegion)))
return geoidRegion;
}
}
if (!candidates.isEmpty())
return candidates.get(0);
return localeRegion;
}
/// Reads the bundled mapping and returns the requested time zone's candidate regions in source order.
///
/// @param zoneId the time zone identifier to match against the first CSV field
/// @return an unmodifiable candidate list, or an empty list if the time zone is not mapped
/// @throws IOException if the resource is missing or cannot be read
/// @see <a href="https://data.iana.org/time-zones/tzdb/zone1970.tab">tzdb timezone descriptions</a>
@VisibleForTesting
static @Unmodifiable List<String> findRegionsByTimeZone(String zoneId) throws IOException {
try (@Nullable InputStream input = RegionUtils.class.getResourceAsStream("/assets/timezone_regions.csv")) {
if (input == null) {
throw new IOException("Missing time zone region mapping");
}
String prefix = zoneId + ",";
for (String line : Lang.toIterable(new String(input.readAllBytes(), StandardCharsets.ISO_8859_1).lines())) {
if (line.startsWith(prefix))
return List.of(line.substring(prefix.length()).split(","));
}
return List.of();
}
}
/// Returns the Windows two-letter region code for a known geographical location identifier.
/// Historical codes and the Windows code for Kosovo are retained. Locations for which
/// Windows reports the placeholder `XX`, including geographical groupings, are not mapped.
///
/// @param geoId the Windows geographical location identifier
/// @return the uppercase region code, or `null` if the identifier has no mapping
/// @see <a href="https://learn.microsoft.com/windows/win32/intl/table-of-geographical-locations">Table of Geographical Locations</a>
private static @Nullable String mapGeoIdToRegion(int geoId) {
return switch (geoId) {
case 2 -> "AG";
case 3 -> "AF";
case 4 -> "DZ";
case 5 -> "AZ";
case 6 -> "AL";
case 7 -> "AM";
case 8 -> "AD";
case 9 -> "AO";
case 10 -> "AS";
case 11 -> "AR";
case 12 -> "AU";
case 14 -> "AT";
case 17 -> "BH";
case 18 -> "BB";
case 19 -> "BW";
case 20 -> "BM";
case 21 -> "BE";
case 22 -> "BS";
case 23 -> "BD";
case 24 -> "BZ";
case 25 -> "BA";
case 26 -> "BO";
case 27 -> "MM";
case 28 -> "BJ";
case 29 -> "BY";
case 30 -> "SB";
case 32 -> "BR";
case 34 -> "BT";
case 35 -> "BG";
case 37 -> "BN";
case 38 -> "BI";
case 39 -> "CA";
case 40 -> "KH";
case 41 -> "TD";
case 42 -> "LK";
case 43 -> "CG";
case 44 -> "CD";
case 45 -> "CN";
case 46 -> "CL";
case 49 -> "CM";
case 50 -> "KM";
case 51 -> "CO";
case 54 -> "CR";
case 55 -> "CF";
case 56 -> "CU";
case 57 -> "CV";
case 59 -> "CY";
case 61 -> "DK";
case 62 -> "DJ";
case 63 -> "DM";
case 65 -> "DO";
case 66 -> "EC";
case 67 -> "EG";
case 68 -> "IE";
case 69 -> "GQ";
case 70 -> "EE";
case 71 -> "ER";
case 72 -> "SV";
case 73 -> "ET";
case 75 -> "CZ";
case 77 -> "FI";
case 78 -> "FJ";
case 80 -> "FM";
case 81 -> "FO";
case 84 -> "FR";
case 86 -> "GM";
case 87 -> "GA";
case 88 -> "GE";
case 89 -> "GH";
case 90 -> "GI";
case 91 -> "GD";
case 93 -> "GL";
case 94 -> "DE";
case 98 -> "GR";
case 99 -> "GT";
case 100 -> "GN";
case 101 -> "GY";
case 103 -> "HT";
case 104 -> "HK";
case 106 -> "HN";
case 108 -> "HR";
case 109 -> "HU";
case 110 -> "IS";
case 111 -> "ID";
case 113 -> "IN";
case 114 -> "IO";
case 116 -> "IR";
case 117 -> "IL";
case 118 -> "IT";
case 119 -> "CI";
case 121 -> "IQ";
case 122 -> "JP";
case 124 -> "JM";
case 125 -> "SJ";
case 126 -> "JO";
case 129 -> "KE";
case 130 -> "KG";
case 131 -> "KP";
case 133 -> "KI";
case 134 -> "KR";
case 136 -> "KW";
case 137 -> "KZ";
case 138 -> "LA";
case 139 -> "LB";
case 140 -> "LV";
case 141 -> "LT";
case 142 -> "LR";
case 143 -> "SK";
case 145 -> "LI";
case 146 -> "LS";
case 147 -> "LU";
case 148 -> "LY";
case 149 -> "MG";
case 151 -> "MO";
case 152 -> "MD";
case 154 -> "MN";
case 156 -> "MW";
case 157 -> "ML";
case 158 -> "MC";
case 159 -> "MA";
case 160 -> "MU";
case 162 -> "MR";
case 163 -> "MT";
case 164 -> "OM";
case 165 -> "MV";
case 166 -> "MX";
case 167 -> "MY";
case 168 -> "MZ";
case 173 -> "NE";
case 174 -> "VU";
case 175 -> "NG";
case 176 -> "NL";
case 177 -> "NO";
case 178 -> "NP";
case 180 -> "NR";
case 181 -> "SR";
case 182 -> "NI";
case 183 -> "NZ";
case 184 -> "PS";
case 185 -> "PY";
case 187 -> "PE";
case 190 -> "PK";
case 191 -> "PL";
case 192 -> "PA";
case 193 -> "PT";
case 194 -> "PG";
case 195 -> "PW";
case 196 -> "GW";
case 197 -> "QA";
case 198 -> "RE";
case 199 -> "MH";
case 200 -> "RO";
case 201 -> "PH";
case 202 -> "PR";
case 203 -> "RU";
case 204 -> "RW";
case 205 -> "SA";
case 206 -> "PM";
case 207 -> "KN";
case 208 -> "SC";
case 209 -> "ZA";
case 210 -> "SN";
case 212 -> "SI";
case 213 -> "SL";
case 214 -> "SM";
case 215 -> "SG";
case 216 -> "SO";
case 217 -> "ES";
case 218 -> "LC";
case 219 -> "SD";
case 220 -> "SJ";
case 221 -> "SE";
case 222 -> "SY";
case 223 -> "CH";
case 224 -> "AE";
case 225 -> "TT";
case 227 -> "TH";
case 228 -> "TJ";
case 231 -> "TO";
case 232 -> "TG";
case 233 -> "ST";
case 234 -> "TN";
case 235 -> "TR";
case 236 -> "TV";
case 237 -> "TW";
case 238 -> "TM";
case 239 -> "TZ";
case 240 -> "UG";
case 241 -> "UA";
case 242 -> "GB";
case 244 -> "US";
case 245 -> "BF";
case 246 -> "UY";
case 247 -> "UZ";
case 248 -> "VC";
case 249 -> "VE";
case 251 -> "VN";
case 252 -> "VI";
case 253 -> "VA";
case 254 -> "NA";
case 259 -> "WS";
case 260 -> "SZ";
case 261 -> "YE";
case 263 -> "ZM";
case 264 -> "ZW";
case 269 -> "CS";
case 270 -> "ME";
case 271 -> "RS";
case 273 -> "CW";
case 276 -> "SS";
case 300 -> "AI";
case 301 -> "AQ";
case 302 -> "AW";
case 306 -> "BV";
case 307 -> "KY";
case 309 -> "CX";
case 311 -> "CC";
case 312 -> "CK";
case 315 -> "FK";
case 317 -> "GF";
case 318 -> "PF";
case 319 -> "TF";
case 321 -> "GP";
case 322 -> "GU";
case 324 -> "GG";
case 325 -> "HM";
case 328 -> "JE";
case 330 -> "MQ";
case 331 -> "YT";
case 332 -> "MS";
case 333 -> "AN";
case 334 -> "NC";
case 335 -> "NU";
case 336 -> "NF";
case 337 -> "MP";
case 339 -> "PN";
case 342 -> "GS";
case 343 -> "SH";
case 347 -> "TK";
case 349 -> "TC";
case 351 -> "VG";
case 352 -> "WF";
case 15126 -> "IM";
case 19618 -> "MK";
case 30967 -> "SX";
case 31706 -> "MF";
case 7299303 -> "TL";
case 9914689 -> "XK";
case 10028789 -> "AX";
case 161832015 -> "BL";
case 161832256 -> "UM";
case 161832258 -> "BQ";
default -> null;
};
}
/// Prevents instantiation of this utility class.
private RegionUtils() {
}
}
@@ -116,7 +116,7 @@
},
{
"title": "CommonMark",
"subtitle": "Copyright (c) 2015 Robin Stocker.\nAll rights reserved.",
"subtitle": "Copyright © 2015 Robin Stocker.\nLicensed under the BSD 2-clause License.",
"externalLink": "https://github.com/commonmark/commonmark-java"
}
]
@@ -1,94 +0,0 @@
[
{
"image" : "/assets/img/yushijinhun.png",
"title" : "yushijinhun",
"subtitleLocalized" : "about.thanks_to.yushijinhun.statement",
"externalLink" : "https://github.com/yushijinhun"
},
{
"image" : "/assets/img/bangbang93.png",
"title" : "bangbang93",
"subtitleLocalized" : "about.thanks_to.bangbang93.statement",
"externalLink" : "https://www.bangbang93.com"
},
{
"image" : "/assets/img/glavo.png",
"title" : "Glavo",
"subtitleLocalized" : "about.thanks_to.glavo.statement",
"externalLink" : "https://github.com/Glavo"
},
{
"image" : "/assets/img/zekerzhayard.png",
"title" : "ZekerZhayard",
"subtitleLocalized" : "about.thanks_to.zekerzhayard.statement",
"externalLink" : "https://github.com/ZekerZhayard"
},
{
"image" : "/assets/img/zkitefly.png",
"title" : "Zkitefly",
"subtitleLocalized" : "about.thanks_to.zkitefly.statement",
"externalLink" : "https://github.com/zkitefly"
},
{
"image" : "/assets/img/burningtnt.png",
"title" : "Burning_TNT",
"subtitleLocalized" : "about.thanks_to.burningtnt.statement",
"externalLink" : "https://github.com/burningtnt"
},
{
"image" : "/assets/img/ShulkerSakura.png",
"title" : "ShulkerSakura",
"subtitleLocalized" : "about.thanks_to.shulkersakura.statement",
"externalLink" : "https://github.com/ShulkerSakura"
},
{
"image" : "/assets/img/gamerteam.png",
"title" : "gamerteam",
"subtitleLocalized" : "about.thanks_to.gamerteam.statement",
"externalLink" : "https://github.com/ZhaiSoul"
},
{
"image" : "/assets/img/red_lnn.png",
"title" : "Red_lnn",
"subtitleLocalized" : "about.thanks_to.red_lnn.statement",
"externalLink" : "https://redlnn.top"
},
{
"image" : "/assets/img/mcmod.png",
"titleLocalized" : "about.thanks_to.mcmod",
"subtitleLocalized" : "about.thanks_to.mcmod.statement",
"externalLink" : "https://www.mcmod.cn"
},
{
"image" : "/assets/img/chest.png",
"titleLocalized" : "about.thanks_to.mcbbs",
"subtitleLocalized" : "about.thanks_to.mcbbs.statement"
},
{
"image" : "/assets/img/mcim.png",
"titleLocalized" : "about.thanks_to.mcim",
"subtitleLocalized" : "about.thanks_to.mcim.statement",
"externalLink" : "https://github.com/mcmod-info-mirror"
},
{
"image" : "/assets/img/8mi-tech.png",
"titleLocalized" : "about.thanks_to.8mi-tech",
"subtitleLocalized" : "about.thanks_to.8mi-tech.statement",
"externalLink" : "https://8mi.tech"
},
{
"image" : {
"light" : "/assets/img/github.png",
"dark" : "/assets/img/github-white.png"
},
"titleLocalized" : "about.thanks_to.contributors",
"subtitleLocalized" : "about.thanks_to.contributors.statement",
"externalLink" : "https://github.com/HMCL-dev/HMCL/graphs/contributors"
},
{
"image" : "/assets/img/icon.png",
"titleLocalized" : "about.thanks_to.users",
"subtitleLocalized" : "about.thanks_to.users.statement",
"externalLink" : "https://docs.hmcl.net/groups.html"
}
]
File diff suppressed because it is too large. Load diff
@@ -498,7 +498,7 @@
506,,,莱姆星云,Lime Nebula,LN
507,void-genesis,,Void Genesis,,
508,create-flavored,,Create Flavored,,
509,terrafirmagreg-vintage,,群峦格雷,TerraFirmaGreg,TFG
509,terrafirmagreg-modern,,群峦格雷,TerraFirmaGreg,TFG
510,create-astral,,机械动力:星辰,Create: Astral,
511,,create-origin,起源创造,CreateOrigin,CO
512,random-item-skyblock-recondite-i,,Random Item Island - Recondite I,,
@@ -726,7 +726,7 @@
738,all-the-mods-9-no-frills,,All the Mods 9 - No Frills,,ATM9NF
739,toms-cobblemon,toms-cobblemon,Tom's Cobblemon,,
740,life-in-the-village-3,,Life in the village 3,,LitV3
741,,,联缘:NEXUS,,
741,,,NEXUSrpg,,
742,,create-cinematic,Create: Cinematic,,
743,,,神秘纪元,Thaumic Epoch,ThE
745,,,FTB Genesis,,
@@ -998,7 +998,7 @@
1017,kumo-o-tagayasu,,耕云钓月,Kumo O Tagayasu,KOTS
1018,,,无限战争4,Infinity War4,INF4
1019,,,群峦传说:工业长路,,
1020,,,自然冒险 新生,Nature Adventure Newborn,NAB
1020,,,自然冒险 新生,Nature Adventure Newborn,NAB
1021,asig,asig,浅忆基础整合,Asai Integrated Modpack,
1022,create-star-valley,,Create: Gears of History,,
1023,,,阿尔法计划,AlphaPlan,AP
@@ -1155,7 +1155,7 @@
1177,ciscos-fantasy-medieval-rpg-dragonfyre,,Cisco's Fantasy Medieval RPG [Dragonfyre],,
1178,all-the-mods-10-sky,,All the Mods 10: To the Sky,,ATM10S
1179,,,谐伴匠途,,
1181,,turbo_vulkan,转变思路优化,New-Path Optimization,NO
1181,,turbo_vulkan,转变思路优化,\u200cNew-Path Optimization,NO
1182,uls-zsurvival-craft,,Z-Liminex,,
1183,,,MITE 重生 3,MITE Reborn 3,MR
1184,reclamation-reclaim-the-world,,Reclamation - Reclaim the World!,,
@@ -1164,7 +1164,7 @@
1187,project-nope-zone,,Project Nope Zone,,PNZ
1188,all-the-simple-zero,all-the-simple-zero,一切简易,All the Simple,ATS
1189,,,匠战妖域,Tinkers' Warfare Demon Realm,twdr
1190,,,空想生存,,
1190,reverie-survival,,空想生存,,
1191,,,东方起源鉴,Touhou Origins Annals,TOA
1192,realm-of-destiny,,生命之境,Realm of Destiny,ROD
1193,,,奇妙的世界,Special World,
@@ -1241,7 +1241,7 @@
1264,arcadia-rpg,,Arcadia[RPG],,
1265,ozone-skyblock-reborn-2,,Ozone Skyblock Reborn 2,,
1266,witarcland,,维塔克兰,Witarcland,WTL
1267,big-globe-optimized-forge,big-globe-optimized-forge,Big Globe Optimized,,
1267,big-globe-optimized-forge,big-globe-optimized,Big Globe Optimized,,
1268,,656youhua,GD656轻量优化打底整合包,GD656 Ultimate Optimized,GDO
1269,terra-firma-far-horizons,,群峦传说:野望,Terra Firma Far Horizons,TFFH
1270,,,烬蓝物语 2,BlueAsh Things II,BAT2
@@ -1498,3 +1498,20 @@
1524,create-village-to-sky,,机械动力:下村上天,Create Village to Sky,
1525,essence-odyssey,,本源远征,Essence Odyssey,
1526,ftb-presents-architects-exodus,,FTB Presents Architect's Exodus,,
1527,,,机械动力 x 格雷科技,Create x GregTech,CTGT
1528,infinity-legacy-2,,无限遗产2,Infinity Legacy 2,IL2
1529,,,千界万刃,Myriad Dimension Blades,
1530,,bmtr,更好的我的世界铁路,Better MTR,BMTR
1531,space-tech-integrated,,航天科技:集成,Space Tech: Integrated,
1532,,,AE-无限工厂,AE2 Infinite Factory,AIF
1533,verdant-sky,,Verdant Sky,,
1534,,,空天工程师,Aerogineer,
1535,generations-modpack-forge,,Generations,,
1536,,,农夫但是劲大,Farmers But Powerful,
1537,,,神道传说,,GR
1538,createintegrated,,Create Integrated,,
1539,vcmc1-16-5,,唯我生存,VeselfCraft,VCMC
1540,,,侏罗纪重生之旅,Jurassic Reborn,
1541,,,暗涌:深岩恐惧,Undercurrent: Fear In the Deep Rock,FIDR
1542,cube-warper,,Cube Warper,,
1543,ftb-evolution-2,,FTB Evolution 2,,
1 #
498 506,,,莱姆星云,Lime Nebula,LN
499 507,void-genesis,,Void Genesis,,
500 508,create-flavored,,Create Flavored,,
501 509,terrafirmagreg-vintage,,群峦格雷,TerraFirmaGreg,TFG 509,terrafirmagreg-modern,,群峦格雷,TerraFirmaGreg,TFG
502 510,create-astral,,机械动力:星辰,Create: Astral,
503 511,,create-origin,起源创造,CreateOrigin,CO
504 512,random-item-skyblock-recondite-i,,Random Item Island - Recondite I,,
726 738,all-the-mods-9-no-frills,,All the Mods 9 - No Frills,,ATM9NF
727 739,toms-cobblemon,toms-cobblemon,Tom's Cobblemon,,
728 740,life-in-the-village-3,,Life in the village 3,,LitV3
729 741,,,联缘:NEXUS,, 741,,,NEXUSrpg,,
730 742,,create-cinematic,Create: Cinematic,,
731 743,,,神秘纪元,Thaumic Epoch,ThE
732 745,,,FTB Genesis,,
998 1017,kumo-o-tagayasu,,耕云钓月,Kumo O Tagayasu,KOTS
999 1018,,,无限战争4,Infinity War4,INF4
1000 1019,,,群峦传说:工业长路,,
1001 1020,,,自然冒险 新生,Nature Adventure Newborn,NAB 1020,,,自然冒险 新生,Nature Adventure Newborn,NAB
1002 1021,asig,asig,浅忆基础整合,Asai Integrated Modpack,
1003 1022,create-star-valley,,Create: Gears of History,,
1004 1023,,,阿尔法计划,AlphaPlan,AP
1155 1177,ciscos-fantasy-medieval-rpg-dragonfyre,,Cisco's Fantasy Medieval RPG [Dragonfyre],,
1156 1178,all-the-mods-10-sky,,All the Mods 10: To the Sky,,ATM10S
1157 1179,,,谐伴匠途,,
1158 1181,,turbo_vulkan,转变思路优化,New-Path Optimization,NO 1181,,turbo_vulkan,转变思路优化,\u200cNew-Path Optimization,NO
1159 1182,uls-zsurvival-craft,,Z-Liminex,,
1160 1183,,,MITE 重生 3,MITE Reborn 3,MR
1161 1184,reclamation-reclaim-the-world,,Reclamation - Reclaim the World!,,
1164 1187,project-nope-zone,,Project Nope Zone,,PNZ
1165 1188,all-the-simple-zero,all-the-simple-zero,一切简易,All the Simple,ATS
1166 1189,,,匠战妖域,Tinkers' Warfare Demon Realm,twdr
1167 1190,,,空想生存,, 1190,reverie-survival,,空想生存,,
1168 1191,,,东方起源鉴,Touhou Origins Annals,TOA
1169 1192,realm-of-destiny,,生命之境,Realm of Destiny,ROD
1170 1193,,,奇妙的世界,Special World,
1241 1264,arcadia-rpg,,Arcadia[RPG],,
1242 1265,ozone-skyblock-reborn-2,,Ozone Skyblock Reborn 2,,
1243 1266,witarcland,,维塔克兰,Witarcland,WTL
1244 1267,big-globe-optimized-forge,big-globe-optimized-forge,Big Globe Optimized,, 1267,big-globe-optimized-forge,big-globe-optimized,Big Globe Optimized,,
1245 1268,,656youhua,GD656轻量优化打底整合包,GD656 Ultimate Optimized,GDO
1246 1269,terra-firma-far-horizons,,群峦传说:野望,Terra Firma Far Horizons,TFFH
1247 1270,,,烬蓝物语 2,BlueAsh Things II,BAT2
1498 1524,create-village-to-sky,,机械动力:下村上天,Create Village to Sky,
1499 1525,essence-odyssey,,本源远征,Essence Odyssey,
1500 1526,ftb-presents-architects-exodus,,FTB Presents Architect's Exodus,,
1501 1527,,,机械动力 x 格雷科技,Create x GregTech,CTGT
1502 1528,infinity-legacy-2,,无限遗产2,Infinity Legacy 2,IL2
1503 1529,,,千界万刃,Myriad Dimension Blades,
1504 1530,,bmtr,更好的我的世界铁路,Better MTR,BMTR
1505 1531,space-tech-integrated,,航天科技:集成,Space Tech: Integrated,
1506 1532,,,AE-无限工厂,AE2 Infinite Factory,AIF
1507 1533,verdant-sky,,Verdant Sky,,
1508 1534,,,空天工程师,Aerogineer,
1509 1535,generations-modpack-forge,,Generations,,
1510 1536,,,农夫但是劲大,Farmers But Powerful,
1511 1537,,,神道传说,,GR
1512 1538,createintegrated,,Create Integrated,,
1513 1539,vcmc1-16-5,,唯我生存,VeselfCraft,VCMC
1514 1540,,,侏罗纪重生之旅,Jurassic Reborn,
1515 1541,,,暗涌:深岩恐惧,Undercurrent: Fear In the Deep Rock,FIDR
1516 1542,cube-warper,,Cube Warper,,
1517 1543,ftb-evolution-2,,FTB Evolution 2,,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

@@ -22,33 +22,20 @@
about=About
about.copyright=Copyright
about.copyright.statement=Copyright © 2013-2026 huangyuhui and contributors.
about.author.statement=bilibili @huanghongxun
about.claim=EULA
about.claim.statement=Click this link for full text.
about.environment=Environment
about.environment.cpu=CPU
about.environment.cpu.n=CPU %d
about.environment.cpu.name=%s
about.environment.cpu.name.cores=%s (%d cores)
about.environment.cpu.name.cores.hyper_threading=%s (%d cores / %d threads)
about.environment.gpu=GPU
about.environment.gpu.n=GPU %d
about.environment.memory=Memory
about.environment.system=Operating System
about.dependency=Third-party Libraries
about.legal=Legal Acknowledgement
about.thanks_to=Thanks to
about.thanks_to.bangbang93.statement=For providing the BMCLAPI download mirror. Please consider donating!
about.thanks_to.burningtnt.statement=Contribute a lot of technical support to HMCL.
about.thanks_to.contributors=All contributors on GitHub
about.thanks_to.contributors.statement=Without the awesome open-source community, HMCL would not have made it so far.
about.thanks_to.gamerteam.statement=For providing the default background image.
about.thanks_to.glavo.statement=Responsible for maintaining HMCL.
about.thanks_to.zekerzhayard.statement=Contribute a lot of technical support to HMCL.
about.thanks_to.zkitefly.statement=Responsible for maintaining the documentation of HMCL.
about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum)
about.thanks_to.mcbbs.statement=For providing the mcbbs.net download mirror for Chinese Mainland users. (No longer available)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=For providing the mod info cache acceleration service for Chinese Mainland users.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=For providing the Simplified Chinese translations and wiki for various mods.
about.thanks_to.red_lnn.statement=For providing the default background image.
about.thanks_to.shulkersakura.statement=For providing the logo for HMCL.
about.thanks_to.users=HMCL User Group Members
about.thanks_to.users.statement=Thanks for donations, bug reports, and so on.
about.thanks_to.yushijinhun.statement=For providing the authlib-injector related support.
about.thanks_to.8mi-tech=8Mi-Tech (Quanzhou) Co., Ltd.
about.thanks_to.8mi-tech.statement=Provides an accelerated download service for some of the game's static resources.
about.open_source=Open Source
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -144,6 +131,7 @@ account.move_to_portable=Convert to Portable Account\nThe account information wi
account.password=Password
account.portable=Portable
account.skin=Skin
account.skin.edit=Edit Skin
account.skin.file=Skin File
account.skin.model=Model
account.skin.model.default=Classic
@@ -156,7 +144,7 @@ account.skin.type.little_skin=LittleSkin
account.skin.type.little_skin.hint=You need to create a player with the same player name as your offline account on your skin provider site. Your skin will now be set to the skin assigned to your player on the skin provider site.
account.skin.type.local_file=Local Skin File
account.skin.type.steve=Steve
account.skin.upload=Upload/Edit Skin
account.skin.upload=Upload Skin
account.skin.upload.failed=Failed to upload skin.
account.skin.invalid_skin=Invalid skin file.
account.username=Username
@@ -382,6 +370,7 @@ curse.category.6555=Vanilla
datetime.format=MMM d, yyyy, h\:mm\:ss a
download=Download
download.china=Due to relevant laws and regulations, HMCL does not currently support downloading game-related content in Chinese Mainland. Please visit the <a href="https://mc.163.com/">official Minecraft website</a> to download Minecraft China Edition.
download.code.404=File "%s" not found on the remote server.
download.content=Addons
download.shader=Shaders
@@ -959,6 +948,9 @@ launcher.contact=Contact Us
launcher.crash=HMCL has encountered a fatal error! Please copy the following log and ask for help on our Discord, QQ group, GitHub, or other Minecraft forum.
launcher.crash.java_internal_error=HMCL has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java <a href="https://bell-sw.com/pages/downloads/#downloads">here</a>.
launcher.crash.hmcl_out_dated=HMCL has encountered a fatal error! Your launcher is outdated. Please update your launcher!
launcher.title=Hello Minecraft! Launcher v%s
launcher.title.modified=Hello Minecraft! Launcher v%s (Modified)
launcher.title.unofficial=Hello Minecraft! Launcher v%s (Unofficial Build)
launcher.update_java=Please update your Java version.
libraries.download=Downloading Libraries
@@ -22,33 +22,20 @@
about=حول
about.copyright=حقوق النشر
about.copyright.statement=حقوق النشر © 2013-2026 huangyuhui والمساهمون.
about.author.statement=bilibili @huanghongxun
about.claim=اتفاقية الترخيص
about.claim.statement=انقر على هذا الرابط للاطلاع على النص الكامل.
# about.environment=
# about.environment.cpu=
# about.environment.cpu.n=
# about.environment.cpu.name=
# about.environment.cpu.name.cores=
# about.environment.cpu.name.cores.hyper_threading=
# about.environment.gpu=
# about.environment.gpu.n=
# about.environment.memory=
# about.environment.system=
about.dependency=مكتبات الطرف الثالث
about.legal=الإقرارات القانونية
about.thanks_to=شكر وتقدير
about.thanks_to.bangbang93.statement=لتوفيره مرآة التنزيل BMCLAPI. يُرجى التفكير في التبرع!
about.thanks_to.burningtnt.statement=قدّم دعماً تقنياً كبيراً لـ HMCL.
about.thanks_to.contributors=جميع المساهمين على GitHub
about.thanks_to.contributors.statement=بدون مجتمع المصدر المفتوح الرائع، لم يكن HMCL ليصل إلى ما هو عليه اليوم.
about.thanks_to.gamerteam.statement=لتوفير صورة الخلفية الافتراضية.
about.thanks_to.glavo.statement=مسؤول عن صيانة HMCL.
about.thanks_to.zekerzhayard.statement=قدّم دعماً تقنياً كبيراً لـ HMCL.
about.thanks_to.zkitefly.statement=مسؤول عن صيانة توثيق HMCL.
about.thanks_to.mcbbs=MCBBS (منتدى Minecraft الصيني)
about.thanks_to.mcbbs.statement=لتوفير مرآة تنزيل mcbbs.net لمستخدمي البر الرئيسي الصيني. (لم تعد متاحة)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=لتوفير خدمة تسريع ذاكرة التخزين المؤقت لمعلومات الموديفكيشن لمستخدمي البر الرئيسي الصيني.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=لتوفير ترجمات الصينية المبسطة وويكي لمختلف الموديفكيشنز.
about.thanks_to.red_lnn.statement=لتوفير صورة الخلفية الافتراضية.
about.thanks_to.shulkersakura.statement=لتوفير الشعار الخاص بـ HMCL.
about.thanks_to.users=أعضاء مجموعة مستخدمي HMCL
about.thanks_to.users.statement=شكراً على التبرعات وتقارير الأخطاء وغير ذلك.
about.thanks_to.yushijinhun.statement=لتوفير الدعم المتعلق بـ authlib-injector.
# about.thanks_to.8mi-tech=
# about.thanks_to.8mi-tech.statement=
about.open_source=المصدر المفتوح
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -145,6 +132,7 @@ account.move_to_portable=تحويل إلى حساب محمول\nسيتم حفظ
account.password=كلمة المرور
account.portable=محمول
account.skin=المظهر
account.skin.edit=تعديل المظهر
account.skin.file=ملف المظهر
account.skin.model=النموذج
account.skin.model.default=كلاسيكي
@@ -157,7 +145,7 @@ account.skin.type.little_skin=LittleSkin
account.skin.type.little_skin.hint=تحتاج إلى إنشاء لاعب بنفس اسم حسابك غير المتصل على موقع مزود المظهر. سيتم تعيين مظهرك على المظهر المعيّن للاعبك على موقع مزود المظهر.
account.skin.type.local_file=ملف مظهر محلي
account.skin.type.steve=Steve
account.skin.upload=رفع/تعديل المظهر
account.skin.upload=رفع المظهر
account.skin.upload.failed=فشل رفع المظهر.
account.skin.invalid_skin=ملف المظهر غير صالح.
account.username=اسم المستخدم
@@ -384,6 +372,7 @@ curse.category.4552=سكريبتات
datetime.format=MMM d, yyyy, h\:mm\:ss a
download=تنزيل
# download.china=
download.code.404=الملف "%s" غير موجود على الخادم البعيد.
download.content=الإضافات
download.shader=الظلال
@@ -960,6 +949,9 @@ launcher.contact=تواصل معنا
launcher.crash=واجه HMCL خطأً فادحاً! يُرجى نسخ السجل التالي وطلب المساعدة على Discord أو مجموعة QQ أو GitHub أو أي منتدى Minecraft آخر.
launcher.crash.java_internal_error=واجه HMCL خطأً فادحاً بسبب تلف Java. يُرجى إلغاء تثبيت Java وتنزيل إصدار مناسب من <a href="https://bell-sw.com/pages/downloads/#downloads">هنا</a>.
launcher.crash.hmcl_out_dated=واجه HMCL خطأً فادحاً! المشغّل قديم. يُرجى تحديثه!
# launcher.title=
# launcher.title.modified=
# launcher.title.unofficial=
launcher.update_java=يُرجى تحديث إصدار Java الخاص بك.
libraries.download=جارٍ تنزيل المكتبات
@@ -21,33 +21,20 @@
about=Über
about.copyright=Urheberrecht
about.copyright.statement=Copyright © 2013-2026 huangyuhui und Mitwirkende.
about.author.statement=bilibili @huanghongxun
about.claim=EULA
about.claim.statement=Klicken Sie auf diesen Link für den vollständigen Text.
# about.environment=
# about.environment.cpu=
# about.environment.cpu.n=
# about.environment.cpu.name=
# about.environment.cpu.name.cores=
# about.environment.cpu.name.cores.hyper_threading=
# about.environment.gpu=
# about.environment.gpu.n=
# about.environment.memory=
# about.environment.system=
about.dependency=Drittanbieter-Bibliotheken
about.legal=Rechtliche Hinweise
about.thanks_to=Danksagung an
about.thanks_to.bangbang93.statement=Für die Bereitstellung des BMCLAPI-Download-Spiegels. Bitte ziehen Sie eine Spende in Betracht!
about.thanks_to.burningtnt.statement=Hat viel technische Unterstützung für HMCL beigesteuert.
about.thanks_to.contributors=Alle Mitwirkenden auf GitHub
about.thanks_to.contributors.statement=Ohne die großartige Open-Source-Community wäre HMCL nicht so weit gekommen.
about.thanks_to.gamerteam.statement=Für die Bereitstellung des Standard-Hintergrundbilds.
about.thanks_to.glavo.statement=Verantwortlich für die Pflege von HMCL.
about.thanks_to.zekerzhayard.statement=Hat viel technische Unterstützung für HMCL beigesteuert.
about.thanks_to.zkitefly.statement=Verantwortlich für die Pflege der HMCL-Dokumentation.
about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum)
about.thanks_to.mcbbs.statement=Für die Bereitstellung des mcbbs.net-Download-Spiegels für Benutzer auf dem chinesischen Festland. (Nicht mehr verfügbar)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=Für die Bereitstellung des Cache-Beschleunigungsdienstes für Mod-Informationen für Benutzer auf dem chinesischen Festland.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=Für die Bereitstellung der vereinfachten chinesischen Übersetzungen und des Wikis für verschiedene Mods.
about.thanks_to.red_lnn.statement=Für die Bereitstellung des Standard-Hintergrundbilds.
about.thanks_to.shulkersakura.statement=Für die Bereitstellung des Logos für HMCL.
about.thanks_to.users=Mitglieder der HMCL-Benutzergruppe
about.thanks_to.users.statement=Danke für Spenden, Fehlerberichte und mehr.
about.thanks_to.yushijinhun.statement=Für die Bereitstellung des authlib-injector-Supports.
about.thanks_to.8mi-tech=8Mi-Tech (Quanzhou) Co., Ltd.
about.thanks_to.8mi-tech.statement=Bietet einen beschleunigten Download-Dienst für einige der statischen Ressourcen des Spiels.
about.open_source=Open Source
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -143,6 +130,7 @@ account.move_to_portable=In portables Konto umwandeln\nDie Kontoinformationen we
account.password=Passwort
account.portable=Portabel
account.skin=Skin
account.skin.edit=Skin bearbeiten
account.skin.file=Skin-Datei
account.skin.model=Modell
account.skin.model.default=Klassisch
@@ -155,7 +143,7 @@ account.skin.type.little_skin=LittleSkin
account.skin.type.little_skin.hint=Sie müssen einen Spieler mit demselben Spielernamen wie Ihr Offline-Konto auf der Seite Ihres Skin-Anbieters erstellen. Ihr Skin wird nun auf den Skin festgelegt, der Ihrem Spieler auf der Skin-Anbieter-Seite zugewiesen ist.
account.skin.type.local_file=Lokale Skin-Datei
account.skin.type.steve=Steve
account.skin.upload=Skin hochladen/bearbeiten
account.skin.upload=Skin hochladen
account.skin.upload.failed=Fehler beim Hochladen des Skins.
account.skin.invalid_skin=Ungültige Skin-Datei.
account.username=Benutzername
@@ -381,6 +369,7 @@ curse.category.6555=Vanilla
datetime.format=dd.MM.yyyy, HH\:mm\:ss
download=Herunterladen
# download.china=
download.code.404=Datei "%s" wurde nicht auf dem Remote-Server gefunden.
download.content=Addons
download.shader=Shaderpakete
@@ -959,6 +948,9 @@ launcher.contact=Kontaktiere uns
launcher.crash=HMCL ist auf einen fatalen Fehler gestoßen! Bitte kopieren Sie das folgende Protokoll und bitten Sie in unserem Discord, der QQ-Gruppe, auf GitHub oder in einem anderen Minecraft-Forum um Hilfe.
launcher.crash.java_internal_error=HMCL ist auf einen fatalen Fehler gestoßen, da Ihre Java-Installation beschädigt ist. Bitte deinstallieren Sie Java und laden Sie eine geeignete Java-Version <a href="https://bell-sw.com/pages/downloads/#downloads">hier</a> herunter.
launcher.crash.hmcl_out_dated=HMCL ist auf einen fatalen Fehler gestoßen! Ihr Launcher ist veraltet. Bitte aktualisieren Sie Ihren Launcher!
# launcher.title=
# launcher.title.modified=
# launcher.title.unofficial=
launcher.update_java=Bitte aktualisieren Sie Ihre Java-Version.
libraries.download=Bibliotheken werden heruntergeladen
@@ -22,33 +22,20 @@
about=Acerca de
about.copyright=Copyright
# about.copyright.statement=
about.author.statement=bilibili @huanghongxun
about.claim=EULA
about.claim.statement=Haga clic en este enlace para ver el texto completo.
# about.environment=
# about.environment.cpu=
# about.environment.cpu.n=
# about.environment.cpu.name=
# about.environment.cpu.name.cores=
# about.environment.cpu.name.cores.hyper_threading=
# about.environment.gpu=
# about.environment.gpu.n=
# about.environment.memory=
# about.environment.system=
about.dependency=Bibliotecas de terceros
about.legal=Reconocimiento legal
about.thanks_to=Gracias a
about.thanks_to.bangbang93.statement=Por proporcionar el espejo de descarga de BMCLAPI, ¡por favor, considere hacer una donación!
about.thanks_to.burningtnt.statement=Aportar mucho apoyo técnico a HMCL.
about.thanks_to.contributors=Todos los colaboradores en GitHub
about.thanks_to.contributors.statement=Sin la impresionante comunidad de código abierto, HMCL no habría llegado tan lejos.
about.thanks_to.gamerteam.statement=Por proporcionar la imagen de fondo por defecto.
about.thanks_to.glavo.statement=Responsable del mantenimiento de HMCL.
about.thanks_to.zekerzhayard.statement=Aportar mucho apoyo técnico a HMCL.
about.thanks_to.zkitefly.statement=Responsable del mantenimiento de la documentación de HMCL.
about.thanks_to.mcbbs=MCBBS (Foro Chino Minecraft)
about.thanks_to.mcbbs.statement=Por proporcionar el espejo de descarga de mcbbs.net para los usuarios de China continental. (Ya no disponible)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=Por proporcionar el servicio de aceleración de caché de información de mods para los usuarios de China continental.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=Por proporcionar las traducciones al chino simplificado y la wiki de varios mods.
about.thanks_to.red_lnn.statement=Por proporcionar la imagen de fondo por defecto.
about.thanks_to.shulkersakura.statement=Por proporcionar el logotipo de HMCL.
about.thanks_to.users=Miembros del grupo de usuarios de HMCL
about.thanks_to.users.statement=Gracias por las donaciones, los informes de errores, etc.
about.thanks_to.yushijinhun.statement=Por proporcionar el soporte relacionado con authlib-injector.
# about.thanks_to.8mi-tech=
# about.thanks_to.8mi-tech.statement=
about.open_source=Código abierto
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -143,6 +130,7 @@ account.move_to_portable=Convertir en cuenta portátil\nLa información de la cu
account.password=Contraseña
account.portable=Cuenta portátil
account.skin=Aspecto
account.skin.edit=Editar aspecto
account.skin.file=Archivo de aspecto
account.skin.model=Modelo
account.skin.model.default=Clásico
@@ -155,7 +143,7 @@ account.skin.type.little_skin=LittleSkin
account.skin.type.little_skin.hint=Tienes que crear un jugador con el mismo nombre de jugador que tu cuenta sin conexión en el sitio web del proveedor de aspectos. Tu aspecto se ajustará ahora al aspecto asignado a tu jugador en el sitio web del proveedor de aspectos.
account.skin.type.local_file=Archivo local de aspecto
account.skin.type.steve=Steve
account.skin.upload=Subir/editar aspecto
account.skin.upload=Subir aspecto
account.skin.upload.failed=No se pudo subir el aspecto.
account.skin.invalid_skin=Archivo de aspecto inválido.
account.username=Nombre de usuario
@@ -382,6 +370,7 @@ curse.category.4552=Scripts
datetime.format=d MMM yyyy, H\:mm\:ss
download=Descargar
# download.china=
download.code.404=Archivo no encontrado en el servidor remoto: %s
download.content=Complementos
download.shader=Sombreadores
@@ -952,6 +941,9 @@ launcher.contact=Contacta con nosotros
launcher.crash=HMCL ha encontrado un error fatal. Por favor, copie el siguiente registro y pida ayuda en nuestra comunidad en Discord, GitHub o Minecraft Forums.
launcher.crash.java_internal_error=HMCL ha encontrado un error fatal porque su Java está dañado. Por favor, desinstala tu Java y descarga un Java adecuado <a href="https://bell-sw.com/pages/downloads/#downloads">aquí</a>.
launcher.crash.hmcl_out_dated=HMCL ha encontrado un error fatal. Su launcher está desactualizado. Por favor, ¡actualícelo!
# launcher.title=
# launcher.title.modified=
# launcher.title.unofficial=
launcher.update_java=Por favor, actualice su versión de Java.
libraries.download=Descargando bibliotecas
@@ -21,33 +21,20 @@
about=について
about.copyright=著作権
# about.copyright.statement=
about.author.statement=bilibili @huanghongxun
about.claim=EULA
about.claim.statement=全文はこのリンクをクリック
# about.environment=
# about.environment.cpu=
# about.environment.cpu.n=
# about.environment.cpu.name=
# about.environment.cpu.name.cores=
# about.environment.cpu.name.cores.hyper_threading=
# about.environment.gpu=
# about.environment.gpu.n=
# about.environment.memory=
# about.environment.system=
about.dependency=依存関係
about.legal=法的承認
about.thanks_to=感謝します…
about.thanks_to.bangbang93.statement=BMCLAPIダウンロードミラープロバイダー。寄付をご検討ください!
about.thanks_to.burningtnt.statement=HMCLに多くのテクニカルサポートを提供
about.thanks_to.contributors=GitHubのすべての貢献者
about.thanks_to.contributors.statement=HMCLをサポートしてくれたオープンソースコミュニティに感謝する
about.thanks_to.gamerteam.statement=デフォルトの背景画像プロバイダー
about.thanks_to.glavo.statement=HMCLのメンテナ
about.thanks_to.zekerzhayard.statement=HMCLに多くのテクニカルサポートを提供
about.thanks_to.zkitefly.statement=HMCLドキュメントメンテナ
about.thanks_to.mcbbs=MCBBS (Minecraft中国語フォーラム)
about.thanks_to.mcbbs.statement=中国本土ユーザー向けのmcbbs.netダウンロードミラープロバイダー(もう利用できません)
# about.thanks_to.mcim=
# about.thanks_to.mcim.statement=
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=様々なMODの簡体字中国語名前翻訳とウィキを提供する
about.thanks_to.red_lnn.statement=デフォルトの背景画像プロバイダー
about.thanks_to.shulkersakura.statement=HMCLロゴプロバイダー
about.thanks_to.users=HMCLユーザーグループのメンバー
about.thanks_to.users.statement=寄付、バグレポートなどに感謝します
about.thanks_to.yushijinhun.statement=authlib-injector関連のサポートを提供する
# about.thanks_to.8mi-tech=
# about.thanks_to.8mi-tech.statement=
about.open_source=オープンソース
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -132,6 +119,7 @@ account.missing.add=追加するにはここをクリック
account.password=パスワード
# account.portable=
account.skin=スキン
account.skin.edit=スキンを編集する
account.skin.file=スキンイメージファイル
account.skin.model=モデル
account.skin.model.default=Classic
@@ -370,6 +358,7 @@ curse.category.4552=スクリプト
datetime.format=yyyy/MM/dd H:mm:ss
download=ダウンロード
# download.china=
download.code.404=リモートサーバーにファイルが見つかりません:%s
download.content=ゲームコンテンツ
download.shader=シェーダー
@@ -762,6 +751,9 @@ launcher.contact=お問い合わせ
launcher.crash=Hello Minecraft!ランチャーがクラッシュしました!次のコンテンツをコピーして、MCBBS、Baidu Tieba、GitHub、またはMinecraftForumを介してフィードバックを送信してください。
# launcher.crash.java_internal_error=
launcher.crash.hmcl_out_dated=Hello Minecraft!ランチャーがクラッシュしました!ランチャーが古くなっています。ランチャーを更新してください!
# launcher.title=
# launcher.title.modified=
# launcher.title.unofficial=
launcher.update_java=Javaを更新してください。
# libraries.download=
@@ -21,33 +21,20 @@
about=有涉
about.copyright=持權
# about.copyright.statement=
about.author.statement=嗶站 @huanghongxun
about.claim=客契
about.claim.statement=擊鏈以詳閱
# about.environment=
# about.environment.cpu=
# about.environment.cpu.n=
# about.environment.cpu.name=
# about.environment.cpu.name.cores=
# about.environment.cpu.name.cores.hyper_threading=
# about.environment.gpu=
# about.environment.gpu.n=
# about.environment.memory=
# about.environment.system=
about.dependency=所依
about.legal=告以律
about.thanks_to=致謝
about.thanks_to.bangbang93.statement=鑄 BMCLAPI 焉。望君資勉旃!
about.thanks_to.burningtnt.statement=多助於技
about.thanks_to.contributors=建議、撰案之諸君
about.thanks_to.contributors.statement=苟无開源之社,無以至今日
about.thanks_to.gamerteam.statement=供本繪圖
about.thanks_to.glavo.statement=常理之
about.thanks_to.zekerzhayard.statement=多助於技
about.thanks_to.zkitefly.statement=理案文焉
about.thanks_to.mcbbs=礦藝館肆
about.thanks_to.mcbbs.statement=鑄礦藝館肆引源 (今廢)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=另遞改囊之訊於本朝
about.thanks_to.mcmod=礦藝改囊典
about.thanks_to.mcmod.statement=載改囊之漢名,並書其案文焉
about.thanks_to.red_lnn.statement=供本繪圖
about.thanks_to.shulkersakura.statement=鑄其徽
about.thanks_to.users=HMCL 群組諸員
about.thanks_to.users.statement=敬謝諸君之貲勉、報謬、謀案
about.thanks_to.yushijinhun.statement=有助於 authlib-injector
about.thanks_to.8mi-tech=八藍米資訊科技 (泉州) 有限公所
about.thanks_to.8mi-tech.statement=供遊戲部分靜態資源之加速下載役
about.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -143,6 +130,7 @@ account.move_to_portable=更爲私戶簿\n戶簿諸文並是啟者而錄
account.password=符節
account.portable=私戶簿
account.skin=外觀
account.skin.edit=纂外觀
account.skin.file=外觀圖案
account.skin.model=形
account.skin.model.default=寬
@@ -155,7 +143,7 @@ account.skin.type.little_skin=LittleSkin 外觀站
account.skin.type.little_skin.hint=君須立角之同名者於外觀站,爰傳外觀。離綫戶簿之外觀,其用外觀站之角也。\n君可求助於右上之鈕。
account.skin.type.local_file=案頭之外觀圖案
account.skin.type.steve=史迪武
account.skin.upload=匯/纂外觀
account.skin.upload=匯外觀
account.skin.upload.failed=匯之外觀而未成
account.skin.invalid_skin=外觀之案有謬
account.username=戶名
@@ -381,6 +369,7 @@ curse.category.4552=角本
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
download=引
# download.china=
download.code.404=伺服器無案曰:%s\n君可求助於右上之鈕。
download.content=戲案
download.shader=光影
@@ -774,6 +763,9 @@ launcher.contact=伏惟候告
launcher.crash=HMCL 有謬而弗能正。宜鈔下文而報謬于右下之鈕。
launcher.crash.java_internal_error=HMCL 不能行,以爪哇壞也。宜去是爪哇,而擊<a href="https://bell-sw.com/pages/downloads/#downloads">此</a>以置爪哇之適者。
launcher.crash.hmcl_out_dated=HMCL 有謬而弗能正之。余識君之啟者舊矣,宜新之。
launcher.title=Hello Minecraft! Launcher v%s
launcher.title.modified=Hello Minecraft! Launcher %s (已改)
launcher.title.unofficial=Hello Minecraft! Launcher %s (非官版)
launcher.update_java=宜迭更爪哇。\n君可求助於 https://docs.hmcl.net/help.html。
libraries.download=引之所依
@@ -21,33 +21,20 @@
about=О лаунчере
about.copyright=Авторские права
about.copyright.statement=Copyright © 2013-2026 huangyuhui и контрибьюторы.
about.author.statement=bilibili @huanghongxun
about.claim=EULA
about.claim.statement=Кликните на ссылку, чтобы увидеть весь текст.
# about.environment=
# about.environment.cpu=
# about.environment.cpu.n=
# about.environment.cpu.name=
# about.environment.cpu.name.cores=
# about.environment.cpu.name.cores.hyper_threading=
# about.environment.gpu=
# about.environment.gpu.n=
# about.environment.memory=
# about.environment.system=
about.dependency=Сторонние библиотеки
about.legal=Правовая информация
about.thanks_to=Отдельная благодарность
about.thanks_to.bangbang93.statement=За предоставление зеркала загрузки BMCLAPI. Пожалуйста, подумайте о пожертвовании!
about.thanks_to.burningtnt.statement=Оказал большую техническую поддержку HMCL.
about.thanks_to.contributors=Всем участникам на GitHub
about.thanks_to.contributors.statement=Без потрясающего сообщества с открытым исходным кодом HMCL не добился бы таких успехов.
about.thanks_to.gamerteam.statement=За предоставление фонового изображения.
about.thanks_to.glavo.statement=Отвечает за поддержание HMCL.
about.thanks_to.zekerzhayard.statement=Оказал большую техническую поддержку HMCL.
about.thanks_to.zkitefly.statement=Отвечает за поддержание документации HMCL.
about.thanks_to.mcbbs=MCBBS (Китайский форум Minecraft)
about.thanks_to.mcbbs.statement=За предоставление зеркала загрузки mcbbs.net для пользователей материкового Китая. (Больше недоступно)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=За предоставление услуги ускорения кэширования информации о модах для пользователей из материкового Китая.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=За предоставление упрощенного китайского перевода и вики для различных модов.
about.thanks_to.red_lnn.statement=За предоставление фонового изображения.
about.thanks_to.shulkersakura.statement=За предоставление логотипа для HMCL.
about.thanks_to.users=Членам группы пользователей HMCL
about.thanks_to.users.statement=Спасибо за пожертвования, отчёты об ошибках и так далее.
about.thanks_to.yushijinhun.statement=За предоставление поддержки, связанной с authlib-injector.
about.thanks_to.8mi-tech=8Mi-Tech (Quanzhou) Co., Ltd.
about.thanks_to.8mi-tech.statement=Обеспечивает ускоренную загрузку некоторых статических ресурсов игры.
about.open_source=Открытый исходный код
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -139,6 +126,7 @@ account.move_to_portable=Конвертировать в портативный
account.password=Пароль
account.portable=Портативный
account.skin=Скин
account.skin.edit=Изменить скин
account.skin.file=Файл скина
account.skin.model=Модель
account.skin.model.default=Классическая
@@ -151,7 +139,7 @@ account.skin.type.little_skin=LittleSkin
account.skin.type.little_skin.hint=Вам нужно создать игрока с тем же именем, что и у вашего оффлайн-аккаунта на сайте поставщика скинов. Теперь ваш скин будет установлен на скин, назначенный вашему игроку на сайте поставщика скинов.
account.skin.type.local_file=Локальный файл скина
account.skin.type.steve=Стив
account.skin.upload=Загрузить/изменить скин
account.skin.upload=Загрузить скин
account.skin.upload.failed=Не удалось загрузить скин.
account.skin.invalid_skin=Недопустимый файл скина.
account.username=Имя пользователя
@@ -378,6 +366,7 @@ curse.category.6555=Ванильный
datetime.format=d MMM yyyy г., HH\:mm\:ss
download=Скачать
# download.china=
download.code.404=Файл «%s» не найден на удаленном сервере.
download.content=Аддоны
download.shader=Шейдеры
@@ -891,6 +880,9 @@ launcher.contact=Связаться с нами
launcher.crash=Лаунчер столкнулся с фатальной ошибкой! Скопируйте следующий лог и попросите помощи в нашем Discord, группе QQ, GitHub или на другом форуме Minecraft.
launcher.crash.java_internal_error=HMCL столкнулся с критической ошибкой, потому что ваша Java повреждена. Удалите Java и скачайте подходящую <a href="https://bell-sw.com/pages/downloads/#downloads">здесь</a>.
launcher.crash.hmcl_out_dated=Лаунчер столкнулся с фатальной ошибкой! Ваш лаунчер устарел. Обновите его!
# launcher.title=
# launcher.title.modified=
# launcher.title.unofficial=
launcher.update_java=Пожалуйста, обновите версию Java.
libraries.download=Скачивание библиотек
@@ -22,33 +22,20 @@
about=Про програму
about.copyright=Авторське право
about.copyright.statement=Copyright © 2013-2026 huangyuhui та контриб'ютори.
about.author.statement=bilibili @huanghongxun
about.claim=Ліцензійна угода з кінцевим користувачем (EULA)
about.claim.statement=Натисніть це посилання для перегляду повного тексту.
# about.environment=
# about.environment.cpu=
# about.environment.cpu.n=
# about.environment.cpu.name=
# about.environment.cpu.name.cores=
# about.environment.cpu.name.cores.hyper_threading=
# about.environment.gpu=
# about.environment.gpu.n=
# about.environment.memory=
# about.environment.system=
about.dependency=Сторонні бібліотеки
about.legal=Правове визнання
about.thanks_to=Подяки
about.thanks_to.bangbang93.statement=За надання дзеркала завантаження BMCLAPI. Будь ласка, подумайте про пожертвування!
about.thanks_to.burningtnt.statement=Надання великої технічної підтримки HMCL.
about.thanks_to.contributors=Усі учасники на GitHub
about.thanks_to.contributors.statement=Без чудового спільнотного руху з відкритим кодом, HMCL не дійшла б так далеко.
about.thanks_to.gamerteam.statement=За надання типового фонового зображення.
about.thanks_to.glavo.statement=Відповідальний за підтримку HMCL.
about.thanks_to.zekerzhayard.statement=Надання великої технічної підтримки HMCL.
about.thanks_to.zkitefly.statement=Відповідальний за підтримку документації HMCL.
about.thanks_to.mcbbs=MCBBS (Китайський форум Minecraft)
about.thanks_to.mcbbs.statement=За надання дзеркала завантаження mcbbs.net для користувачів материкового Китаю. (Більше не доступне)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=За надання служби прискорення кешу інформації модів для користувачів материкового Китаю.
about.thanks_to.mcmod=MCMod (mcmod.cn)
about.thanks_to.mcmod.statement=За надання спрощених китайських перекладів та вікі для різних модів.
about.thanks_to.red_lnn.statement=За надання типового фонового зображення.
about.thanks_to.shulkersakura.statement=За надання логотипу для HMCL.
about.thanks_to.users=Учасники групи користувачів HMCL
about.thanks_to.users.statement=Дякуємо за пожертви, повідомлення про помилки тощо.
about.thanks_to.yushijinhun.statement=За надання підтримки, пов'язаної з authlib-injector.
about.thanks_to.8mi-tech=8Mi-Tech (Quanzhou) Co., Ltd.
about.thanks_to.8mi-tech.statement=Надає сервіс прискореного завантаження для деяких статичних ресурсів гри.
about.open_source=Відкрите джерело
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -145,6 +132,7 @@ account.move_to_portable=Перетворити на портативний об
account.password=Пароль
account.portable=Портативний
account.skin=Скін
account.skin.edit=Редагувати скін
account.skin.file=Файл скіна
account.skin.model=Модель
account.skin.model.default=Класична
@@ -157,7 +145,7 @@ account.skin.type.little_skin=LittleSkin
account.skin.type.little_skin.hint=Вам потрібно створити гравця з тим же ім'ям гравця, що й у вашого автономного облікового запису, на сайті постачальника скінів. Ваш скін тепер буде встановлено на скін, призначений вашому гравцю на сайті постачальника скінів.
account.skin.type.local_file=Локальний файл скіна
account.skin.type.steve=Стів
account.skin.upload=Завантажити/Редагувати скін
account.skin.upload=Завантажити скін
account.skin.upload.failed=Не вдалося завантажити скін.
account.skin.invalid_skin=Недійсний файл скіна.
account.username=Ім'я користувача
@@ -378,6 +366,7 @@ curse.category.6555=Ваніла
datetime.format=dd.MM.yyyy, HH:mm:ss
download=Завантажити
# download.china=
download.code.404=Файл "%s" не знайдено на віддаленому сервері.
download.content=Додатки
download.shader=Шейдери
@@ -914,6 +903,9 @@ launcher.contact=Зв'яжіться з нами
launcher.crash=Hello Minecraft! Лаунчер зіткнувся з фатальною помилкою! Скопіюйте наступний журнал та попросіть допомоги на нашому Discord, групі QQ, GitHub або іншому форумі Minecraft.
launcher.crash.java_internal_error=Hello Minecraft! Лаунчер зіткнувся з фатальною помилкою, оскільки ваша Java пошкоджена. Видаліть вашу Java та завантажте відповідну Java <a href="https://bell-sw.com/pages/downloads/#downloads">тут</a>.
launcher.crash.hmcl_out_dated=Hello Minecraft! Лаунчер зіткнувся з фатальною помилкою! Ваш лаунчер застарів. Оновіть ваш лаунчер!
# launcher.title=
# launcher.title.modified=
# launcher.title.unofficial=
launcher.update_java=Оновіть вашу версію Java.
libraries.download=Завантаження бібліотек
@@ -21,33 +21,20 @@
about=关于
about.copyright=版权
about.copyright.statement=版权所有 © 2013-2026 huangyuhui 及贡献者
about.author.statement=bilibili @huanghongxun
about.claim=用户协议
about.claim.statement=点击链接以查看全文
about.environment=环境
about.environment.cpu=处理器
about.environment.cpu.n=处理器 %d
about.environment.cpu.name=%s
about.environment.cpu.name.cores=%s (%d 核心)
about.environment.cpu.name.cores.hyper_threading=%s (%d 核心 / %d 线程)
about.environment.gpu=显卡
about.environment.gpu.n=显卡 %d
about.environment.memory=内存
about.environment.system=操作系统
about.dependency=依赖
about.legal=法律声明
about.thanks_to=鸣谢
about.thanks_to.bangbang93.statement=提供 BMCLAPI 下载源。请赞助支持 BMCLAPI!
about.thanks_to.burningtnt.statement=为 HMCL 贡献许多技术支持
about.thanks_to.contributors=所有通过 Issue、Pull Request 等方式参与本项目的贡献者
about.thanks_to.contributors.statement=没有开源社区的支持,HMCL 就无法走到今天
about.thanks_to.gamerteam.statement=提供默认背景图
about.thanks_to.glavo.statement=负责 HMCL 的日常维护
about.thanks_to.zekerzhayard.statement=为 HMCL 贡献许多技术支持
about.thanks_to.zkitefly.statement=负责维护 HMCL 的文档
about.thanks_to.mcbbs=MCBBS (我的世界中文论坛)
about.thanks_to.mcbbs.statement=提供 MCBBS 下载源 (现已停止服务)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=为中国大陆用户提供模组信息缓存加速服务
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
about.thanks_to.mcmod.statement=提供模组简体中文名映射表与模组百科
about.thanks_to.red_lnn.statement=提供默认背景图
about.thanks_to.shulkersakura.statement=提供 HMCL 的徽标
about.thanks_to.users=HMCL 用户群成员
about.thanks_to.users.statement=感谢用户群成员赞助充电、积极催更、反馈问题、出谋划策
about.thanks_to.yushijinhun.statement=提供 authlib-injector 相关支持
about.thanks_to.8mi-tech=八蓝米信息科技 (泉州) 有限公司
about.thanks_to.8mi-tech.statement=提供了游戏部分静态资源的加速下载服务
about.open_source=开源
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
@@ -144,6 +131,7 @@ account.move_to_portable=转换为便携账户\n该账户的信息会保存至
account.password=密码
account.portable=便携账户
account.skin=皮肤
account.skin.edit=编辑皮肤
account.skin.file=皮肤图片文件
account.skin.model=模型
account.skin.model.default=宽型
@@ -156,7 +144,7 @@ account.skin.type.little_skin=LittleSkin 皮肤站
account.skin.type.little_skin.hint=你需要在皮肤站中创建并使用和该离线账户同名的角色。此时离线账户皮肤将显示为皮肤站上对应角色所设置的皮肤。\n你可以点击右上角帮助按钮进行求助。
account.skin.type.local_file=本地皮肤图片文件
account.skin.type.steve=Steve
account.skin.upload=上传/编辑皮肤
account.skin.upload=上传皮肤
account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件
account.username=用户名
@@ -382,6 +370,7 @@ curse.category.6555=原生
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
download=下载
download.china=根据相关法律法规,HMCL 在中国大陆地区暂不支持下载游戏相关内容。请前往<a href="https://mc.163.com/">我的世界官方网站</a>下载我的世界中国版。
download.code.404=远程服务器不包含需要下载的文件: %s\n你可以点击右上角帮助按钮进行求助。
download.content=游戏内容
download.shader=光影
@@ -777,6 +766,9 @@ launcher.contact=联系我们
launcher.crash=HMCL 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。
launcher.crash.java_internal_error=HMCL 由于当前 Java 损坏而无法继续运行。请卸载当前 Java,点击 <a href="https://bell-sw.com/pages/downloads/#downloads">此处</a> 安装合适的 Java 版本。
launcher.crash.hmcl_out_dated=HMCL 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
launcher.title=Hello Minecraft! Launcher %s
launcher.title.modified=Hello Minecraft! Launcher %s (已修改)
launcher.title.unofficial=Hello Minecraft! Launcher %s (非官方构建)
launcher.update_java=请更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
libraries.download=下载依赖库
@@ -21,33 +21,20 @@
about=關於
about.copyright=著作權
about.copyright.statement=著作權所有 © 2013-2026 huangyuhui 與貢獻者
about.author.statement=bilibili @huanghongxun
about.claim=使用者協議
about.claim.statement=點擊連結以查看全文
about.environment=環境
about.environment.cpu=處理器
about.environment.cpu.n=處理器 %d
about.environment.cpu.name=%s
about.environment.cpu.name.cores=%s (%d 核心)
about.environment.cpu.name.cores.hyper_threading=%s (%d 核心 / %d 執行緒)
about.environment.gpu=顯示卡
about.environment.gpu.n=顯示卡 %d
about.environment.memory=記憶體
about.environment.system=作業系統
about.dependency=相依元件
about.legal=法律宣告
about.thanks_to=鳴謝
about.thanks_to.bangbang93.statement=提供 BMCLAPI 下載來源。請贊助支援 BMCLAPI!
about.thanks_to.burningtnt.statement=為 HMCL 貢獻許多技術支援
about.thanks_to.contributors=所有透過 Issue、Pull Request 等管道參與本專案的貢獻者
about.thanks_to.contributors.statement=沒有開源社群的支援,HMCL 無法走到今天
about.thanks_to.gamerteam.statement=提供預設背景圖
about.thanks_to.glavo.statement=負責 HMCL 的日常維護
about.thanks_to.zekerzhayard.statement=為 HMCL 貢獻許多技術支援
about.thanks_to.zkitefly.statement=負責維護 HMCL 的文件
about.thanks_to.mcbbs=MCBBS (我的世界中文論壇)
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源 (現已停止服務)
about.thanks_to.mcim=mcmod-info-mirror
about.thanks_to.mcim.statement=為中國大陸使用者提供模組資訊快取加速服務
about.thanks_to.mcmod=MC 百科 (mcmod.cn)
about.thanks_to.mcmod.statement=提供模組簡體中文名映射表與模組百科
about.thanks_to.red_lnn.statement=提供預設背景圖
about.thanks_to.shulkersakura.statement=提供 HMCL 的標誌
about.thanks_to.users=HMCL 使用者群組成員
about.thanks_to.users.statement=感謝使用者群組成員贊助充電、積極催更、回報問題、出謀劃策
about.thanks_to.yushijinhun.statement=提供 authlib-injector 相關支援
about.thanks_to.8mi-tech=八藍米信息科技 (泉州) 有限公司
about.thanks_to.8mi-tech.statement=提供了遊戲部分靜態資源的加速下載服務
about.open_source=開放原始碼
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL/)
@@ -142,6 +129,7 @@ account.move_to_portable=轉換為可攜式帳戶\n該帳戶的資訊會儲存
account.password=密碼
account.portable=可攜式帳戶
account.skin=外觀
account.skin.edit=編輯外觀
account.skin.file=外觀圖片檔案
account.skin.model=模型
account.skin.model.default=寬型
@@ -154,7 +142,7 @@ account.skin.type.little_skin=LittleSkin 皮膚站
account.skin.type.little_skin.hint=你需要在皮膚站中新增並使用和該離線帳戶同名角色。此時離線帳戶外觀將為皮膚站上對應角色所設定的外觀。
account.skin.type.local_file=本機外觀圖片檔案
account.skin.type.steve=Steve
account.skin.upload=上傳/編輯外觀
account.skin.upload=上傳外觀
account.skin.upload.failed=外觀上傳失敗
account.skin.invalid_skin=無法識別的外觀檔案
account.username=使用者名稱
@@ -380,6 +368,7 @@ curse.category.6555=原生
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
download=下載
download.china=根據相關法律法規,HMCL 在中國大陸地區暫不支援下載遊戲相關內容。請前往<a href="https://mc.163.com/">我的世界官方網站</a>下載我的世界中國版。
download.code.404=遠端伺服器沒有需要下載的檔案:%s
download.content=遊戲內容
download.shader=光影
@@ -771,6 +760,9 @@ launcher.contact=聯絡我們
launcher.crash=HMCL 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub、Discord 或 HMCL QQ 群回報問題。
launcher.crash.java_internal_error=HMCL 由於目前 Java 損壞而無法繼續執行。請移除目前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
launcher.crash.hmcl_out_dated=HMCL 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
launcher.title=Hello Minecraft! Launcher %s
launcher.title.modified=Hello Minecraft! Launcher %s (已修改)
launcher.title.unofficial=Hello Minecraft! Launcher %s (非官方建置)
launcher.update_java=請更新你的 Java
libraries.download=下載依賴庫
@@ -0,0 +1,549 @@
# IANA tzdb version: 2026e
# Generated from https://data.iana.org/time-zones/tzdb-2026e/zone1970.tab (public domain).
# Compatibility links from https://data.iana.org/time-zones/tzdb-2026e/backward (public domain).
# Update with ./gradlew updateTimeZoneRegions --tzdb-version 2026e.
Africa/Abidjan,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Accra,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Addis_Ababa,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Algiers,DZ
Africa/Asmara,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Asmera,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Bamako,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Bangui,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Banjul,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Bissau,GW
Africa/Blantyre,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Brazzaville,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Bujumbura,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Cairo,EG
Africa/Casablanca,MA
Africa/Ceuta,ES
Africa/Conakry,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Dakar,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Dar_es_Salaam,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Djibouti,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Douala,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/El_Aaiun,EH
Africa/Freetown,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Gaborone,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Harare,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Johannesburg,ZA,LS,SZ
Africa/Juba,SS
Africa/Kampala,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Khartoum,SD
Africa/Kigali,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Kinshasa,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Lagos,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Libreville,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Lome,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Luanda,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Lubumbashi,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Lusaka,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Malabo,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Maputo,MZ,BI,BW,CD,MW,RW,ZM,ZW
Africa/Maseru,ZA,LS,SZ
Africa/Mbabane,ZA,LS,SZ
Africa/Mogadishu,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Monrovia,LR
Africa/Nairobi,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Africa/Ndjamena,TD
Africa/Niamey,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Nouakchott,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Ouagadougou,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Porto-Novo,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
Africa/Sao_Tome,ST
Africa/Timbuktu,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Africa/Tripoli,LY
Africa/Tunis,TN
Africa/Windhoek,NA
America/Adak,US
America/Anchorage,US
America/Anguilla,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Antigua,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Araguaina,BR
America/Argentina/Buenos_Aires,AR
America/Argentina/Catamarca,AR
America/Argentina/ComodRivadavia,AR
America/Argentina/Cordoba,AR
America/Argentina/Jujuy,AR
America/Argentina/La_Rioja,AR
America/Argentina/Mendoza,AR
America/Argentina/Rio_Gallegos,AR
America/Argentina/Salta,AR
America/Argentina/San_Juan,AR
America/Argentina/San_Luis,AR
America/Argentina/Tucuman,AR
America/Argentina/Ushuaia,AR
America/Aruba,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Asuncion,PY
America/Atikokan,PA,CA,KY
America/Atka,US
America/Bahia,BR
America/Bahia_Banderas,MX
America/Barbados,BB
America/Belem,BR
America/Belize,BZ
America/Blanc-Sablon,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Boa_Vista,BR
America/Bogota,CO
America/Boise,US
America/Buenos_Aires,AR
America/Cambridge_Bay,CA
America/Campo_Grande,BR
America/Cancun,MX
America/Caracas,VE
America/Catamarca,AR
America/Cayenne,GF
America/Cayman,PA,CA,KY
America/Chicago,US
America/Chihuahua,MX
America/Ciudad_Juarez,MX
America/Coral_Harbour,PA,CA,KY
America/Cordoba,AR
America/Costa_Rica,CR
America/Coyhaique,CL
America/Creston,US,CA
America/Cuiaba,BR
America/Curacao,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Danmarkshavn,GL
America/Dawson,CA
America/Dawson_Creek,CA
America/Denver,US
America/Detroit,US
America/Dominica,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Edmonton,CA
America/Eirunepe,BR
America/El_Salvador,SV
America/Ensenada,MX
America/Fort_Nelson,CA
America/Fort_Wayne,US
America/Fortaleza,BR
America/Glace_Bay,CA
America/Godthab,GL
America/Goose_Bay,CA
America/Grand_Turk,TC
America/Grenada,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Guadeloupe,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Guatemala,GT
America/Guayaquil,EC
America/Guyana,GY
America/Halifax,CA
America/Havana,CU
America/Hermosillo,MX
America/Indiana/Indianapolis,US
America/Indiana/Knox,US
America/Indiana/Marengo,US
America/Indiana/Petersburg,US
America/Indiana/Tell_City,US
America/Indiana/Vevay,US
America/Indiana/Vincennes,US
America/Indiana/Winamac,US
America/Indianapolis,US
America/Inuvik,CA
America/Iqaluit,CA
America/Jamaica,JM
America/Jujuy,AR
America/Juneau,US
America/Kentucky/Louisville,US
America/Kentucky/Monticello,US
America/Knox_IN,US
America/Kralendijk,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/La_Paz,BO
America/Lima,PE
America/Los_Angeles,US
America/Louisville,US
America/Lower_Princes,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Maceio,BR
America/Managua,NI
America/Manaus,BR
America/Marigot,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Martinique,MQ
America/Matamoros,MX
America/Mazatlan,MX
America/Mendoza,AR
America/Menominee,US
America/Merida,MX
America/Metlakatla,US
America/Mexico_City,MX
America/Miquelon,PM
America/Moncton,CA
America/Monterrey,MX
America/Montevideo,UY
America/Montreal,CA,BS
America/Montserrat,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Nassau,CA,BS
America/New_York,US
America/Nipigon,CA,BS
America/Nome,US
America/Noronha,BR
America/North_Dakota/Beulah,US
America/North_Dakota/Center,US
America/North_Dakota/New_Salem,US
America/Nuuk,GL
America/Ojinaga,MX
America/Panama,PA,CA,KY
America/Pangnirtung,CA
America/Paramaribo,SR
America/Phoenix,US,CA
America/Port-au-Prince,HT
America/Port_of_Spain,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Porto_Acre,BR
America/Porto_Velho,BR
America/Puerto_Rico,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Punta_Arenas,CL
America/Rainy_River,CA
America/Rankin_Inlet,CA
America/Recife,BR
America/Regina,CA
America/Resolute,CA
America/Rio_Branco,BR
America/Rosario,AR
America/Santa_Isabel,MX
America/Santarem,BR
America/Santiago,CL
America/Santo_Domingo,DO
America/Sao_Paulo,BR
America/Scoresbysund,GL
America/Shiprock,US
America/Sitka,US
America/St_Barthelemy,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/St_Johns,CA
America/St_Kitts,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/St_Lucia,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/St_Thomas,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/St_Vincent,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Swift_Current,CA
America/Tegucigalpa,HN
America/Thule,GL
America/Thunder_Bay,CA,BS
America/Tijuana,MX
America/Toronto,CA,BS
America/Tortola,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Vancouver,CA
America/Virgin,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
America/Whitehorse,CA
America/Winnipeg,CA
America/Yakutat,US
America/Yellowknife,CA
Antarctica/Casey,AQ
Antarctica/Davis,AQ
Antarctica/DumontDUrville,PG,AQ,FM
Antarctica/Macquarie,AU
Antarctica/Mawson,AQ
Antarctica/McMurdo,NZ,AQ
Antarctica/Palmer,AQ
Antarctica/Rothera,AQ
Antarctica/South_Pole,NZ,AQ
Antarctica/Syowa,SA,AQ,KW,YE
Antarctica/Troll,AQ
Antarctica/Vostok,AQ
Arctic/Longyearbyen,DE,DK,NO,SE,SJ
Asia/Aden,SA,AQ,KW,YE
Asia/Almaty,KZ
Asia/Amman,JO
Asia/Anadyr,RU
Asia/Aqtau,KZ
Asia/Aqtobe,KZ
Asia/Ashgabat,TM
Asia/Ashkhabad,TM
Asia/Atyrau,KZ
Asia/Baghdad,IQ
Asia/Bahrain,QA,BH
Asia/Baku,AZ
Asia/Bangkok,TH,CX,KH,LA,VN
Asia/Barnaul,RU
Asia/Beirut,LB
Asia/Bishkek,KG
Asia/Brunei,MY,BN
Asia/Calcutta,IN
Asia/Chita,RU
Asia/Choibalsan,MN
Asia/Chongqing,CN
Asia/Chungking,CN
Asia/Colombo,LK
Asia/Dacca,BD
Asia/Damascus,SY
Asia/Dhaka,BD
Asia/Dili,TL
Asia/Dubai,AE,OM,RE,SC,TF
Asia/Dushanbe,TJ
Asia/Famagusta,CY
Asia/Gaza,PS
Asia/Harbin,CN
Asia/Hebron,PS
Asia/Ho_Chi_Minh,VN
Asia/Hong_Kong,HK
Asia/Hovd,MN
Asia/Irkutsk,RU
Asia/Istanbul,TR
Asia/Jakarta,ID
Asia/Jayapura,ID
Asia/Jerusalem,IL
Asia/Kabul,AF
Asia/Kamchatka,RU
Asia/Karachi,PK
Asia/Kashgar,CN
Asia/Kathmandu,NP
Asia/Katmandu,NP
Asia/Khandyga,RU
Asia/Kolkata,IN
Asia/Krasnoyarsk,RU
Asia/Kuala_Lumpur,SG,AQ,MY
Asia/Kuching,MY,BN
Asia/Kuwait,SA,AQ,KW,YE
Asia/Macao,MO
Asia/Macau,MO
Asia/Magadan,RU
Asia/Makassar,ID
Asia/Manila,PH
Asia/Muscat,AE,OM,RE,SC,TF
Asia/Nicosia,CY
Asia/Novokuznetsk,RU
Asia/Novosibirsk,RU
Asia/Omsk,RU
Asia/Oral,KZ
Asia/Phnom_Penh,TH,CX,KH,LA,VN
Asia/Pontianak,ID
Asia/Pyongyang,KP
Asia/Qatar,QA,BH
Asia/Qostanay,KZ
Asia/Qyzylorda,KZ
Asia/Rangoon,MM,CC
Asia/Riyadh,SA,AQ,KW,YE
Asia/Saigon,VN
Asia/Sakhalin,RU
Asia/Samarkand,UZ
Asia/Seoul,KR
Asia/Shanghai,CN
Asia/Singapore,SG,AQ,MY
Asia/Srednekolymsk,RU
Asia/Taipei,TW
Asia/Tashkent,UZ
Asia/Tbilisi,GE
Asia/Tehran,IR
Asia/Tel_Aviv,IL
Asia/Thimbu,BT
Asia/Thimphu,BT
Asia/Tokyo,JP,AU
Asia/Tomsk,RU
Asia/Ujung_Pandang,ID
Asia/Ulaanbaatar,MN
Asia/Ulan_Bator,MN
Asia/Urumqi,CN
Asia/Ust-Nera,RU
Asia/Vientiane,TH,CX,KH,LA,VN
Asia/Vladivostok,RU
Asia/Yakutsk,RU
Asia/Yangon,MM,CC
Asia/Yekaterinburg,RU
Asia/Yerevan,AM
Atlantic/Azores,PT
Atlantic/Bermuda,BM
Atlantic/Canary,ES
Atlantic/Cape_Verde,CV
Atlantic/Faeroe,FO
Atlantic/Faroe,FO
Atlantic/Jan_Mayen,DE,DK,NO,SE,SJ
Atlantic/Madeira,PT
Atlantic/Reykjavik,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Atlantic/South_Georgia,GS
Atlantic/St_Helena,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Atlantic/Stanley,FK
Australia/ACT,AU
Australia/Adelaide,AU
Australia/Brisbane,AU
Australia/Broken_Hill,AU
Australia/Canberra,AU
Australia/Currie,AU
Australia/Darwin,AU
Australia/Eucla,AU
Australia/Hobart,AU
Australia/LHI,AU
Australia/Lindeman,AU
Australia/Lord_Howe,AU
Australia/Melbourne,AU
Australia/NSW,AU
Australia/North,AU
Australia/Perth,AU
Australia/Queensland,AU
Australia/South,AU
Australia/Sydney,AU
Australia/Tasmania,AU
Australia/Victoria,AU
Australia/West,AU
Australia/Yancowinna,AU
Brazil/Acre,BR
Brazil/DeNoronha,BR
Brazil/East,BR
Brazil/West,BR
CET,BE,LU,NL
Canada/Atlantic,CA
Canada/Central,CA
Canada/Eastern,CA,BS
Canada/Mountain,CA
Canada/Newfoundland,CA
Canada/Pacific,CA
Canada/Saskatchewan,CA
Canada/Yukon,CA
Chile/Continental,CL
Chile/EasterIsland,CL
Cuba,CU
EET,GR
Egypt,EG
Eire,IE
Europe/Amsterdam,BE,LU,NL
Europe/Andorra,AD
Europe/Astrakhan,RU
Europe/Athens,GR
Europe/Belfast,GB,GG,IM,JE
Europe/Belgrade,RS,BA,HR,ME,MK,SI
Europe/Berlin,DE,DK,NO,SE,SJ
Europe/Bratislava,CZ,SK
Europe/Brussels,BE,LU,NL
Europe/Bucharest,RO
Europe/Budapest,HU
Europe/Busingen,CH,DE,LI
Europe/Chisinau,MD
Europe/Copenhagen,DE,DK,NO,SE,SJ
Europe/Dublin,IE
Europe/Gibraltar,GI
Europe/Guernsey,GB,GG,IM,JE
Europe/Helsinki,FI,AX
Europe/Isle_of_Man,GB,GG,IM,JE
Europe/Istanbul,TR
Europe/Jersey,GB,GG,IM,JE
Europe/Kaliningrad,RU
Europe/Kiev,UA
Europe/Kirov,RU
Europe/Kyiv,UA
Europe/Lisbon,PT
Europe/Ljubljana,RS,BA,HR,ME,MK,SI
Europe/London,GB,GG,IM,JE
Europe/Luxembourg,BE,LU,NL
Europe/Madrid,ES
Europe/Malta,MT
Europe/Mariehamn,FI,AX
Europe/Minsk,BY
Europe/Monaco,FR,MC
Europe/Moscow,RU
Europe/Nicosia,CY
Europe/Oslo,DE,DK,NO,SE,SJ
Europe/Paris,FR,MC
Europe/Podgorica,RS,BA,HR,ME,MK,SI
Europe/Prague,CZ,SK
Europe/Riga,LV
Europe/Rome,IT,SM,VA
Europe/Samara,RU
Europe/San_Marino,IT,SM,VA
Europe/Sarajevo,RS,BA,HR,ME,MK,SI
Europe/Saratov,RU
Europe/Simferopol,RU,UA
Europe/Skopje,RS,BA,HR,ME,MK,SI
Europe/Sofia,BG
Europe/Stockholm,DE,DK,NO,SE,SJ
Europe/Tallinn,EE
Europe/Tirane,AL
Europe/Tiraspol,MD
Europe/Ulyanovsk,RU
Europe/Uzhgorod,UA
Europe/Vaduz,CH,DE,LI
Europe/Vatican,IT,SM,VA
Europe/Vienna,AT
Europe/Vilnius,LT
Europe/Volgograd,RU
Europe/Warsaw,PL
Europe/Zagreb,RS,BA,HR,ME,MK,SI
Europe/Zaporozhye,UA
Europe/Zurich,CH,DE,LI
GB,GB,GG,IM,JE
GB-Eire,GB,GG,IM,JE
Hongkong,HK
Iceland,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
Indian/Antananarivo,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Indian/Chagos,IO
Indian/Christmas,TH,CX,KH,LA,VN
Indian/Cocos,MM,CC
Indian/Comoro,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Indian/Kerguelen,MV,TF
Indian/Mahe,AE,OM,RE,SC,TF
Indian/Maldives,MV,TF
Indian/Mauritius,MU
Indian/Mayotte,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
Indian/Reunion,AE,OM,RE,SC,TF
Iran,IR
Israel,IL
Jamaica,JM
Japan,JP,AU
Kwajalein,MH
Libya,LY
MET,BE,LU,NL
Mexico/BajaNorte,MX
Mexico/BajaSur,MX
Mexico/General,MX
NZ,NZ,AQ
NZ-CHAT,NZ
Navajo,US
PRC,CN
Pacific/Apia,WS
Pacific/Auckland,NZ,AQ
Pacific/Bougainville,PG
Pacific/Chatham,NZ
Pacific/Chuuk,PG,AQ,FM
Pacific/Easter,CL
Pacific/Efate,VU
Pacific/Enderbury,KI
Pacific/Fakaofo,TK
Pacific/Fiji,FJ
Pacific/Funafuti,KI,MH,TV,UM,WF
Pacific/Galapagos,EC
Pacific/Gambier,PF
Pacific/Guadalcanal,SB,FM
Pacific/Guam,GU,MP
Pacific/Honolulu,US
Pacific/Johnston,US
Pacific/Kanton,KI
Pacific/Kiritimati,KI
Pacific/Kosrae,FM
Pacific/Kwajalein,MH
Pacific/Majuro,KI,MH,TV,UM,WF
Pacific/Marquesas,PF
Pacific/Midway,AS,UM
Pacific/Nauru,NR
Pacific/Niue,NU
Pacific/Norfolk,NF
Pacific/Noumea,NC
Pacific/Pago_Pago,AS,UM
Pacific/Palau,PW
Pacific/Pitcairn,PN
Pacific/Pohnpei,SB,FM
Pacific/Ponape,SB,FM
Pacific/Port_Moresby,PG,AQ,FM
Pacific/Rarotonga,CK
Pacific/Saipan,GU,MP
Pacific/Samoa,AS,UM
Pacific/Tahiti,PF
Pacific/Tarawa,KI,MH,TV,UM,WF
Pacific/Tongatapu,TO
Pacific/Truk,PG,AQ,FM
Pacific/Wake,KI,MH,TV,UM,WF
Pacific/Wallis,KI,MH,TV,UM,WF
Pacific/Yap,PG,AQ,FM
Poland,PL
Portugal,PT
ROK,KR
Singapore,SG,AQ,MY
Turkey,TR
US/Alaska,US
US/Aleutian,US
US/Arizona,US,CA
US/Central,US
US/East-Indiana,US
US/Eastern,US
US/Hawaii,US
US/Indiana-Starke,US
US/Michigan,US
US/Mountain,US
US/Pacific,US
US/Samoa,AS,UM
W-SU,RU
WET,PT
1 # IANA tzdb version: 2026e
2 # Generated from https://data.iana.org/time-zones/tzdb-2026e/zone1970.tab (public domain).
3 # Compatibility links from https://data.iana.org/time-zones/tzdb-2026e/backward (public domain).
4 # Update with ./gradlew updateTimeZoneRegions --tzdb-version 2026e.
5 Africa/Abidjan,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
6 Africa/Accra,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
7 Africa/Addis_Ababa,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
8 Africa/Algiers,DZ
9 Africa/Asmara,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
10 Africa/Asmera,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
11 Africa/Bamako,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
12 Africa/Bangui,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
13 Africa/Banjul,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
14 Africa/Bissau,GW
15 Africa/Blantyre,MZ,BI,BW,CD,MW,RW,ZM,ZW
16 Africa/Brazzaville,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
17 Africa/Bujumbura,MZ,BI,BW,CD,MW,RW,ZM,ZW
18 Africa/Cairo,EG
19 Africa/Casablanca,MA
20 Africa/Ceuta,ES
21 Africa/Conakry,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
22 Africa/Dakar,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
23 Africa/Dar_es_Salaam,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
24 Africa/Djibouti,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
25 Africa/Douala,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
26 Africa/El_Aaiun,EH
27 Africa/Freetown,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
28 Africa/Gaborone,MZ,BI,BW,CD,MW,RW,ZM,ZW
29 Africa/Harare,MZ,BI,BW,CD,MW,RW,ZM,ZW
30 Africa/Johannesburg,ZA,LS,SZ
31 Africa/Juba,SS
32 Africa/Kampala,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
33 Africa/Khartoum,SD
34 Africa/Kigali,MZ,BI,BW,CD,MW,RW,ZM,ZW
35 Africa/Kinshasa,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
36 Africa/Lagos,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
37 Africa/Libreville,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
38 Africa/Lome,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
39 Africa/Luanda,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
40 Africa/Lubumbashi,MZ,BI,BW,CD,MW,RW,ZM,ZW
41 Africa/Lusaka,MZ,BI,BW,CD,MW,RW,ZM,ZW
42 Africa/Malabo,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
43 Africa/Maputo,MZ,BI,BW,CD,MW,RW,ZM,ZW
44 Africa/Maseru,ZA,LS,SZ
45 Africa/Mbabane,ZA,LS,SZ
46 Africa/Mogadishu,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
47 Africa/Monrovia,LR
48 Africa/Nairobi,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
49 Africa/Ndjamena,TD
50 Africa/Niamey,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
51 Africa/Nouakchott,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
52 Africa/Ouagadougou,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
53 Africa/Porto-Novo,NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE
54 Africa/Sao_Tome,ST
55 Africa/Timbuktu,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
56 Africa/Tripoli,LY
57 Africa/Tunis,TN
58 Africa/Windhoek,NA
59 America/Adak,US
60 America/Anchorage,US
61 America/Anguilla,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
62 America/Antigua,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
63 America/Araguaina,BR
64 America/Argentina/Buenos_Aires,AR
65 America/Argentina/Catamarca,AR
66 America/Argentina/ComodRivadavia,AR
67 America/Argentina/Cordoba,AR
68 America/Argentina/Jujuy,AR
69 America/Argentina/La_Rioja,AR
70 America/Argentina/Mendoza,AR
71 America/Argentina/Rio_Gallegos,AR
72 America/Argentina/Salta,AR
73 America/Argentina/San_Juan,AR
74 America/Argentina/San_Luis,AR
75 America/Argentina/Tucuman,AR
76 America/Argentina/Ushuaia,AR
77 America/Aruba,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
78 America/Asuncion,PY
79 America/Atikokan,PA,CA,KY
80 America/Atka,US
81 America/Bahia,BR
82 America/Bahia_Banderas,MX
83 America/Barbados,BB
84 America/Belem,BR
85 America/Belize,BZ
86 America/Blanc-Sablon,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
87 America/Boa_Vista,BR
88 America/Bogota,CO
89 America/Boise,US
90 America/Buenos_Aires,AR
91 America/Cambridge_Bay,CA
92 America/Campo_Grande,BR
93 America/Cancun,MX
94 America/Caracas,VE
95 America/Catamarca,AR
96 America/Cayenne,GF
97 America/Cayman,PA,CA,KY
98 America/Chicago,US
99 America/Chihuahua,MX
100 America/Ciudad_Juarez,MX
101 America/Coral_Harbour,PA,CA,KY
102 America/Cordoba,AR
103 America/Costa_Rica,CR
104 America/Coyhaique,CL
105 America/Creston,US,CA
106 America/Cuiaba,BR
107 America/Curacao,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
108 America/Danmarkshavn,GL
109 America/Dawson,CA
110 America/Dawson_Creek,CA
111 America/Denver,US
112 America/Detroit,US
113 America/Dominica,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
114 America/Edmonton,CA
115 America/Eirunepe,BR
116 America/El_Salvador,SV
117 America/Ensenada,MX
118 America/Fort_Nelson,CA
119 America/Fort_Wayne,US
120 America/Fortaleza,BR
121 America/Glace_Bay,CA
122 America/Godthab,GL
123 America/Goose_Bay,CA
124 America/Grand_Turk,TC
125 America/Grenada,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
126 America/Guadeloupe,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
127 America/Guatemala,GT
128 America/Guayaquil,EC
129 America/Guyana,GY
130 America/Halifax,CA
131 America/Havana,CU
132 America/Hermosillo,MX
133 America/Indiana/Indianapolis,US
134 America/Indiana/Knox,US
135 America/Indiana/Marengo,US
136 America/Indiana/Petersburg,US
137 America/Indiana/Tell_City,US
138 America/Indiana/Vevay,US
139 America/Indiana/Vincennes,US
140 America/Indiana/Winamac,US
141 America/Indianapolis,US
142 America/Inuvik,CA
143 America/Iqaluit,CA
144 America/Jamaica,JM
145 America/Jujuy,AR
146 America/Juneau,US
147 America/Kentucky/Louisville,US
148 America/Kentucky/Monticello,US
149 America/Knox_IN,US
150 America/Kralendijk,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
151 America/La_Paz,BO
152 America/Lima,PE
153 America/Los_Angeles,US
154 America/Louisville,US
155 America/Lower_Princes,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
156 America/Maceio,BR
157 America/Managua,NI
158 America/Manaus,BR
159 America/Marigot,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
160 America/Martinique,MQ
161 America/Matamoros,MX
162 America/Mazatlan,MX
163 America/Mendoza,AR
164 America/Menominee,US
165 America/Merida,MX
166 America/Metlakatla,US
167 America/Mexico_City,MX
168 America/Miquelon,PM
169 America/Moncton,CA
170 America/Monterrey,MX
171 America/Montevideo,UY
172 America/Montreal,CA,BS
173 America/Montserrat,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
174 America/Nassau,CA,BS
175 America/New_York,US
176 America/Nipigon,CA,BS
177 America/Nome,US
178 America/Noronha,BR
179 America/North_Dakota/Beulah,US
180 America/North_Dakota/Center,US
181 America/North_Dakota/New_Salem,US
182 America/Nuuk,GL
183 America/Ojinaga,MX
184 America/Panama,PA,CA,KY
185 America/Pangnirtung,CA
186 America/Paramaribo,SR
187 America/Phoenix,US,CA
188 America/Port-au-Prince,HT
189 America/Port_of_Spain,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
190 America/Porto_Acre,BR
191 America/Porto_Velho,BR
192 America/Puerto_Rico,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
193 America/Punta_Arenas,CL
194 America/Rainy_River,CA
195 America/Rankin_Inlet,CA
196 America/Recife,BR
197 America/Regina,CA
198 America/Resolute,CA
199 America/Rio_Branco,BR
200 America/Rosario,AR
201 America/Santa_Isabel,MX
202 America/Santarem,BR
203 America/Santiago,CL
204 America/Santo_Domingo,DO
205 America/Sao_Paulo,BR
206 America/Scoresbysund,GL
207 America/Shiprock,US
208 America/Sitka,US
209 America/St_Barthelemy,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
210 America/St_Johns,CA
211 America/St_Kitts,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
212 America/St_Lucia,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
213 America/St_Thomas,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
214 America/St_Vincent,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
215 America/Swift_Current,CA
216 America/Tegucigalpa,HN
217 America/Thule,GL
218 America/Thunder_Bay,CA,BS
219 America/Tijuana,MX
220 America/Toronto,CA,BS
221 America/Tortola,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
222 America/Vancouver,CA
223 America/Virgin,PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI
224 America/Whitehorse,CA
225 America/Winnipeg,CA
226 America/Yakutat,US
227 America/Yellowknife,CA
228 Antarctica/Casey,AQ
229 Antarctica/Davis,AQ
230 Antarctica/DumontDUrville,PG,AQ,FM
231 Antarctica/Macquarie,AU
232 Antarctica/Mawson,AQ
233 Antarctica/McMurdo,NZ,AQ
234 Antarctica/Palmer,AQ
235 Antarctica/Rothera,AQ
236 Antarctica/South_Pole,NZ,AQ
237 Antarctica/Syowa,SA,AQ,KW,YE
238 Antarctica/Troll,AQ
239 Antarctica/Vostok,AQ
240 Arctic/Longyearbyen,DE,DK,NO,SE,SJ
241 Asia/Aden,SA,AQ,KW,YE
242 Asia/Almaty,KZ
243 Asia/Amman,JO
244 Asia/Anadyr,RU
245 Asia/Aqtau,KZ
246 Asia/Aqtobe,KZ
247 Asia/Ashgabat,TM
248 Asia/Ashkhabad,TM
249 Asia/Atyrau,KZ
250 Asia/Baghdad,IQ
251 Asia/Bahrain,QA,BH
252 Asia/Baku,AZ
253 Asia/Bangkok,TH,CX,KH,LA,VN
254 Asia/Barnaul,RU
255 Asia/Beirut,LB
256 Asia/Bishkek,KG
257 Asia/Brunei,MY,BN
258 Asia/Calcutta,IN
259 Asia/Chita,RU
260 Asia/Choibalsan,MN
261 Asia/Chongqing,CN
262 Asia/Chungking,CN
263 Asia/Colombo,LK
264 Asia/Dacca,BD
265 Asia/Damascus,SY
266 Asia/Dhaka,BD
267 Asia/Dili,TL
268 Asia/Dubai,AE,OM,RE,SC,TF
269 Asia/Dushanbe,TJ
270 Asia/Famagusta,CY
271 Asia/Gaza,PS
272 Asia/Harbin,CN
273 Asia/Hebron,PS
274 Asia/Ho_Chi_Minh,VN
275 Asia/Hong_Kong,HK
276 Asia/Hovd,MN
277 Asia/Irkutsk,RU
278 Asia/Istanbul,TR
279 Asia/Jakarta,ID
280 Asia/Jayapura,ID
281 Asia/Jerusalem,IL
282 Asia/Kabul,AF
283 Asia/Kamchatka,RU
284 Asia/Karachi,PK
285 Asia/Kashgar,CN
286 Asia/Kathmandu,NP
287 Asia/Katmandu,NP
288 Asia/Khandyga,RU
289 Asia/Kolkata,IN
290 Asia/Krasnoyarsk,RU
291 Asia/Kuala_Lumpur,SG,AQ,MY
292 Asia/Kuching,MY,BN
293 Asia/Kuwait,SA,AQ,KW,YE
294 Asia/Macao,MO
295 Asia/Macau,MO
296 Asia/Magadan,RU
297 Asia/Makassar,ID
298 Asia/Manila,PH
299 Asia/Muscat,AE,OM,RE,SC,TF
300 Asia/Nicosia,CY
301 Asia/Novokuznetsk,RU
302 Asia/Novosibirsk,RU
303 Asia/Omsk,RU
304 Asia/Oral,KZ
305 Asia/Phnom_Penh,TH,CX,KH,LA,VN
306 Asia/Pontianak,ID
307 Asia/Pyongyang,KP
308 Asia/Qatar,QA,BH
309 Asia/Qostanay,KZ
310 Asia/Qyzylorda,KZ
311 Asia/Rangoon,MM,CC
312 Asia/Riyadh,SA,AQ,KW,YE
313 Asia/Saigon,VN
314 Asia/Sakhalin,RU
315 Asia/Samarkand,UZ
316 Asia/Seoul,KR
317 Asia/Shanghai,CN
318 Asia/Singapore,SG,AQ,MY
319 Asia/Srednekolymsk,RU
320 Asia/Taipei,TW
321 Asia/Tashkent,UZ
322 Asia/Tbilisi,GE
323 Asia/Tehran,IR
324 Asia/Tel_Aviv,IL
325 Asia/Thimbu,BT
326 Asia/Thimphu,BT
327 Asia/Tokyo,JP,AU
328 Asia/Tomsk,RU
329 Asia/Ujung_Pandang,ID
330 Asia/Ulaanbaatar,MN
331 Asia/Ulan_Bator,MN
332 Asia/Urumqi,CN
333 Asia/Ust-Nera,RU
334 Asia/Vientiane,TH,CX,KH,LA,VN
335 Asia/Vladivostok,RU
336 Asia/Yakutsk,RU
337 Asia/Yangon,MM,CC
338 Asia/Yekaterinburg,RU
339 Asia/Yerevan,AM
340 Atlantic/Azores,PT
341 Atlantic/Bermuda,BM
342 Atlantic/Canary,ES
343 Atlantic/Cape_Verde,CV
344 Atlantic/Faeroe,FO
345 Atlantic/Faroe,FO
346 Atlantic/Jan_Mayen,DE,DK,NO,SE,SJ
347 Atlantic/Madeira,PT
348 Atlantic/Reykjavik,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
349 Atlantic/South_Georgia,GS
350 Atlantic/St_Helena,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
351 Atlantic/Stanley,FK
352 Australia/ACT,AU
353 Australia/Adelaide,AU
354 Australia/Brisbane,AU
355 Australia/Broken_Hill,AU
356 Australia/Canberra,AU
357 Australia/Currie,AU
358 Australia/Darwin,AU
359 Australia/Eucla,AU
360 Australia/Hobart,AU
361 Australia/LHI,AU
362 Australia/Lindeman,AU
363 Australia/Lord_Howe,AU
364 Australia/Melbourne,AU
365 Australia/NSW,AU
366 Australia/North,AU
367 Australia/Perth,AU
368 Australia/Queensland,AU
369 Australia/South,AU
370 Australia/Sydney,AU
371 Australia/Tasmania,AU
372 Australia/Victoria,AU
373 Australia/West,AU
374 Australia/Yancowinna,AU
375 Brazil/Acre,BR
376 Brazil/DeNoronha,BR
377 Brazil/East,BR
378 Brazil/West,BR
379 CET,BE,LU,NL
380 Canada/Atlantic,CA
381 Canada/Central,CA
382 Canada/Eastern,CA,BS
383 Canada/Mountain,CA
384 Canada/Newfoundland,CA
385 Canada/Pacific,CA
386 Canada/Saskatchewan,CA
387 Canada/Yukon,CA
388 Chile/Continental,CL
389 Chile/EasterIsland,CL
390 Cuba,CU
391 EET,GR
392 Egypt,EG
393 Eire,IE
394 Europe/Amsterdam,BE,LU,NL
395 Europe/Andorra,AD
396 Europe/Astrakhan,RU
397 Europe/Athens,GR
398 Europe/Belfast,GB,GG,IM,JE
399 Europe/Belgrade,RS,BA,HR,ME,MK,SI
400 Europe/Berlin,DE,DK,NO,SE,SJ
401 Europe/Bratislava,CZ,SK
402 Europe/Brussels,BE,LU,NL
403 Europe/Bucharest,RO
404 Europe/Budapest,HU
405 Europe/Busingen,CH,DE,LI
406 Europe/Chisinau,MD
407 Europe/Copenhagen,DE,DK,NO,SE,SJ
408 Europe/Dublin,IE
409 Europe/Gibraltar,GI
410 Europe/Guernsey,GB,GG,IM,JE
411 Europe/Helsinki,FI,AX
412 Europe/Isle_of_Man,GB,GG,IM,JE
413 Europe/Istanbul,TR
414 Europe/Jersey,GB,GG,IM,JE
415 Europe/Kaliningrad,RU
416 Europe/Kiev,UA
417 Europe/Kirov,RU
418 Europe/Kyiv,UA
419 Europe/Lisbon,PT
420 Europe/Ljubljana,RS,BA,HR,ME,MK,SI
421 Europe/London,GB,GG,IM,JE
422 Europe/Luxembourg,BE,LU,NL
423 Europe/Madrid,ES
424 Europe/Malta,MT
425 Europe/Mariehamn,FI,AX
426 Europe/Minsk,BY
427 Europe/Monaco,FR,MC
428 Europe/Moscow,RU
429 Europe/Nicosia,CY
430 Europe/Oslo,DE,DK,NO,SE,SJ
431 Europe/Paris,FR,MC
432 Europe/Podgorica,RS,BA,HR,ME,MK,SI
433 Europe/Prague,CZ,SK
434 Europe/Riga,LV
435 Europe/Rome,IT,SM,VA
436 Europe/Samara,RU
437 Europe/San_Marino,IT,SM,VA
438 Europe/Sarajevo,RS,BA,HR,ME,MK,SI
439 Europe/Saratov,RU
440 Europe/Simferopol,RU,UA
441 Europe/Skopje,RS,BA,HR,ME,MK,SI
442 Europe/Sofia,BG
443 Europe/Stockholm,DE,DK,NO,SE,SJ
444 Europe/Tallinn,EE
445 Europe/Tirane,AL
446 Europe/Tiraspol,MD
447 Europe/Ulyanovsk,RU
448 Europe/Uzhgorod,UA
449 Europe/Vaduz,CH,DE,LI
450 Europe/Vatican,IT,SM,VA
451 Europe/Vienna,AT
452 Europe/Vilnius,LT
453 Europe/Volgograd,RU
454 Europe/Warsaw,PL
455 Europe/Zagreb,RS,BA,HR,ME,MK,SI
456 Europe/Zaporozhye,UA
457 Europe/Zurich,CH,DE,LI
458 GB,GB,GG,IM,JE
459 GB-Eire,GB,GG,IM,JE
460 Hongkong,HK
461 Iceland,CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG
462 Indian/Antananarivo,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
463 Indian/Chagos,IO
464 Indian/Christmas,TH,CX,KH,LA,VN
465 Indian/Cocos,MM,CC
466 Indian/Comoro,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
467 Indian/Kerguelen,MV,TF
468 Indian/Mahe,AE,OM,RE,SC,TF
469 Indian/Maldives,MV,TF
470 Indian/Mauritius,MU
471 Indian/Mayotte,KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT
472 Indian/Reunion,AE,OM,RE,SC,TF
473 Iran,IR
474 Israel,IL
475 Jamaica,JM
476 Japan,JP,AU
477 Kwajalein,MH
478 Libya,LY
479 MET,BE,LU,NL
480 Mexico/BajaNorte,MX
481 Mexico/BajaSur,MX
482 Mexico/General,MX
483 NZ,NZ,AQ
484 NZ-CHAT,NZ
485 Navajo,US
486 PRC,CN
487 Pacific/Apia,WS
488 Pacific/Auckland,NZ,AQ
489 Pacific/Bougainville,PG
490 Pacific/Chatham,NZ
491 Pacific/Chuuk,PG,AQ,FM
492 Pacific/Easter,CL
493 Pacific/Efate,VU
494 Pacific/Enderbury,KI
495 Pacific/Fakaofo,TK
496 Pacific/Fiji,FJ
497 Pacific/Funafuti,KI,MH,TV,UM,WF
498 Pacific/Galapagos,EC
499 Pacific/Gambier,PF
500 Pacific/Guadalcanal,SB,FM
501 Pacific/Guam,GU,MP
502 Pacific/Honolulu,US
503 Pacific/Johnston,US
504 Pacific/Kanton,KI
505 Pacific/Kiritimati,KI
506 Pacific/Kosrae,FM
507 Pacific/Kwajalein,MH
508 Pacific/Majuro,KI,MH,TV,UM,WF
509 Pacific/Marquesas,PF
510 Pacific/Midway,AS,UM
511 Pacific/Nauru,NR
512 Pacific/Niue,NU
513 Pacific/Norfolk,NF
514 Pacific/Noumea,NC
515 Pacific/Pago_Pago,AS,UM
516 Pacific/Palau,PW
517 Pacific/Pitcairn,PN
518 Pacific/Pohnpei,SB,FM
519 Pacific/Ponape,SB,FM
520 Pacific/Port_Moresby,PG,AQ,FM
521 Pacific/Rarotonga,CK
522 Pacific/Saipan,GU,MP
523 Pacific/Samoa,AS,UM
524 Pacific/Tahiti,PF
525 Pacific/Tarawa,KI,MH,TV,UM,WF
526 Pacific/Tongatapu,TO
527 Pacific/Truk,PG,AQ,FM
528 Pacific/Wake,KI,MH,TV,UM,WF
529 Pacific/Wallis,KI,MH,TV,UM,WF
530 Pacific/Yap,PG,AQ,FM
531 Poland,PL
532 Portugal,PT
533 ROK,KR
534 Singapore,SG,AQ,MY
535 Turkey,TR
536 US/Alaska,US
537 US/Aleutian,US
538 US/Arizona,US,CA
539 US/Central,US
540 US/East-Indiana,US
541 US/Eastern,US
542 US/Hawaii,US
543 US/Indiana-Starke,US
544 US/Michigan,US
545 US/Mountain,US
546 US/Pacific,US
547 US/Samoa,AS,UM
548 W-SU,RU
549 WET,PT
@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Unmodifiable;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/// Verifies exact time zone lookup and preservation of candidate regions.
@NotNullByDefault
final class RegionUtilsTest {
/// Reads candidates in source order and exposes an unmodifiable list.
@Test
void loadsUnmodifiableCandidateMapping() throws IOException {
assertEquals(List.of("CN"), RegionUtils.findRegionsByTimeZone("Asia/Shanghai"));
@Unmodifiable List<String> candidates = RegionUtils.findRegionsByTimeZone("Asia/Tokyo");
assertEquals(List.of("JP", "AU"), candidates);
assertThrows(UnsupportedOperationException.class, () -> candidates.add("US"));
}
/// Does not match missing identifiers, partial names, or region fields.
@Test
void matchesCompleteTimeZoneField() throws IOException {
for (String zoneId : List.of("Etc/UTC", "Asia/Shang", "CN", "")) {
assertEquals(List.of(), RegionUtils.findRegionsByTimeZone(zoneId));
}
}
/// Finds system time zone aliases without dropping the target's candidate countries.
@Test
void loadsCompatibilityAliases() throws IOException {
assertEquals(List.of("BE", "LU", "NL"), RegionUtils.findRegionsByTimeZone("Europe/Amsterdam"));
assertEquals(List.of("DE", "DK", "NO", "SE", "SJ"), RegionUtils.findRegionsByTimeZone("Europe/Stockholm"));
}
}
@@ -29,7 +29,7 @@ import static org.jackhuang.hmcl.util.io.NetworkUtils.decodeURL;
/**
* @author yushijinhun
* @see <a href="https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#dnd-%E6%96%B9%E5%BC%8F%E6%B7%BB%E5%8A%A0-yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF">Launcher Technical Specification for Authlib-Injector</a>
* @see <a href="https://yushijinhun.github.io/authlib-injector/en/launcher-technical-specification.html#configuration-via-drag-and-drop">Launcher Technical Specification for Authlib-Injector</a>
*/
public final class AuthlibInjectorDnD {
@@ -1,48 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.event;
import org.jetbrains.annotations.NotNull;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author huangyuhui
public final class EventBus extends ClassValue<EventManager<?>> {
public static final EventBus EVENT_BUS = new EventBus();
private EventBus() {
}
@Override
protected EventManager<?> computeValue(@NotNull Class<?> type) {
return new EventManager<>();
}
@SuppressWarnings("unchecked")
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
return (EventManager<T>) get(clazz);
}
@SuppressWarnings("unchecked")
public Event.Result fireEvent(Event obj) {
LOG.info(obj + " gets fired");
return ((EventManager<Event>) get(obj.getClass())).fireEvent(obj);
}
}
@@ -1,56 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
/**
* This event gets fired when we launch the JVM and it got crashed.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class JVMLaunchFailedEvent extends Event {
private final ManagedProcess process;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
* @param process the crashed process.
*/
public JVMLaunchFailedEvent(Object source, ManagedProcess process) {
super(source);
this.process = process;
}
public ManagedProcess getProcess() {
return process;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("process", process)
.toString();
}
}
@@ -1,56 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
/**
* This event gets fired when a JavaProcess exited abnormally and the exit code is not zero.
*
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public final class ProcessExitedAbnormallyEvent extends Event {
private final ManagedProcess process;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
* @param process The process that exited abnormally.
*/
public ProcessExitedAbnormallyEvent(Object source, ManagedProcess process) {
super(source);
this.process = process;
}
public ManagedProcess getProcess() {
return process;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("process", process)
.toString();
}
}
@@ -1,47 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
/**
* This event gets fired when minecraft process exited successfully and the exit code is 0.
* <br>
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
*/
public class ProcessStoppedEvent extends Event {
private final ManagedProcess process;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
* @param process minecraft process
*/
public ProcessStoppedEvent(Object source, ManagedProcess process) {
super(source);
this.process = process;
}
public ManagedProcess getProcess() {
return process;
}
}
@@ -307,8 +307,10 @@ public abstract class DefaultGameRepository implements GameRepository {
/// Loads one instance directory without renaming on-disk JSON or jar files.
///
/// When the conventional `versions/<id>/<id>.json` is missing but the directory contains exactly
/// one JSON file, that manifest path is recorded on the instance. The primary jar is derived as
/// the sibling path with the same base name.
/// one JSON file, that manifest path is recorded on the instance. If there are multiple JSON
/// files, selects the first parseable manifest in file-name order with a non-blank main class,
/// a parent instance, or non-empty patches. The primary jar is derived as the sibling path
/// with the same base name.
///
/// @param snapshot the unsealed snapshot that will own the instance
/// @param dir the instance directory under `versions/`
@@ -332,12 +334,13 @@ public abstract class DefaultGameRepository implements GameRepository {
json = conventionalJson;
} else {
List<Path> jsons = FileUtils.listFilesByExtension(dir, "json");
if (jsons.size() != 1) {
@Nullable Path candidate = jsons.size() == 1 ? jsons.get(0) : findInstanceManifest(jsons);
if (candidate == null) {
LOG.info("No available json file found, ignoring instance " + id);
return null;
}
json = jsons.get(0);
json = candidate;
if (!json.equals(conventionalJson)) {
manifestFileOverride = json;
}
@@ -361,8 +364,35 @@ public abstract class DefaultGameRepository implements GameRepository {
return createInstance(snapshot, id, manifest, manifestFileOverride);
}
/// Finds the first parseable manifest in file-name order with a non-blank main class,
/// a parent instance, or non-empty patches, skipping unreadable and malformed candidates.
///
/// @param jsons the candidate JSON files; the list is not modified
/// @return the selected path, or `null` if no candidate matches
private static @Nullable Path findInstanceManifest(List<Path> jsons) {
for (Path json : jsons.stream().sorted(Comparator.comparing(FileUtils::getName)).toList()) {
try {
GameInstanceManifest manifest = readInstanceManifest(json);
@Nullable String mainClass = manifest.mainClass();
if ((mainClass != null && !mainClass.isBlank())
|| manifest.inheritsFrom() != null
|| !manifest.getPatches().isEmpty()) {
return json;
}
} catch (IOException | RuntimeException ignored) {
}
}
return null;
}
/// Reads a non-null instance manifest from a JSON file.
///
/// @param json the manifest file
/// @return the parsed manifest
/// @throws IOException if the file cannot be read
/// @throws JsonParseException if the JSON is malformed or does not describe a manifest
private static GameInstanceManifest readInstanceManifest(Path json) throws IOException, JsonParseException {
GameInstanceManifest manifest = JsonUtils.fromJsonFile(json, GameInstanceManifest.class);
@Nullable GameInstanceManifest manifest = JsonUtils.fromJsonFile(json, GameInstanceManifest.class);
if (manifest == null) {
throw new JsonParseException("Manifest is null");
}
@@ -140,16 +140,18 @@ public enum JavaVersionConstraint {
},
// Minecraft>=1.13 may crash when generating world on Java [1.8,1.8.0_51)
VANILLA_JAVA_8_51(false, GameVersionNumber.atLeast("1.13"), VersionNumber.atLeast("1.8.0_51")),
// On Linux, JDK 9+ cannot launch Minecraft<=1.12.2, since JDK 9+ does not accept loading native library built in different arch.
// For example, JDK 9+ 64-bit cannot load 32-bit lwjgl native library.
/// Restricts legacy Linux x86-64 instances to Java 8 because newer JDKs reject mismatched native-library architectures.
/// An explicitly declared newer Java requirement takes precedence; Cleanroom remains exempt.
VANILLA_LINUX_JAVA_8(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) {
/// Applies the native-library restriction only when the manifest does not require newer Java.
@Override
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version,
@Nullable JavaRuntime java, @Nullable GameComponentAnalyzer analyzer) {
return OperatingSystem.CURRENT_OS == OperatingSystem.LINUX
&& Architecture.SYSTEM_ARCH == Architecture.X86_64
&& (java == null || java.getArchitecture() == Architecture.X86_64)
&& (analyzer == null || !analyzer.has(GameComponentType.CLEANROOM));
&& (analyzer == null || !analyzer.has(GameComponentType.CLEANROOM))
&& super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer);
}
@Override
@@ -17,10 +17,6 @@
*/
package org.jackhuang.hmcl.launch;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.JVMLaunchFailedEvent;
import org.jackhuang.hmcl.event.ProcessExitedAbnormallyEvent;
import org.jackhuang.hmcl.event.ProcessStoppedEvent;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
@@ -67,13 +63,10 @@ final class ExitWaiter implements Runnable {
"Could not create the Java Virtual Machine.",
"Error occurred during initialization of VM",
"A fatal exception has occurred. Program will exit.")) {
EventBus.EVENT_BUS.fireEvent(new JVMLaunchFailedEvent(this, process));
exitType = ProcessListener.ExitType.JVM_ERROR;
} else if (exitCode != 0 || StringUtils.containsOne(errorLines,
"Crash report saved to", "Could not save crash report to", "This crash report has been saved to:",
"Unable to launch", "An exception was thrown, the game will display an error screen and halt.")) {
EventBus.EVENT_BUS.fireEvent(new ProcessExitedAbnormallyEvent(this, process));
if (exitCode == 137 && OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
exitType = ProcessListener.ExitType.SIGKILL;
} else {
@@ -83,8 +76,6 @@ final class ExitWaiter implements Runnable {
exitType = ProcessListener.ExitType.NORMAL;
}
EventBus.EVENT_BUS.fireEvent(new ProcessStoppedEvent(this, process));
watcher.accept(exitCode, exitType);
} catch (InterruptedException e) {
watcher.accept(1, ProcessListener.ExitType.INTERRUPTED);
@@ -21,7 +21,6 @@ import org.glavo.url.WebURL;
import org.jackhuang.hmcl.download.DownloadCandidate;
import org.jackhuang.hmcl.download.DownloadCandidates;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.*;
@@ -587,7 +586,7 @@ public abstract class FetchTask<T extends @UnknownNullability Object> extends Ta
private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
private static final AtomicLong downloadSpeed = new AtomicLong(0L);
public static final EventManager<SpeedEvent> SPEED_EVENT = EventBus.EVENT_BUS.channel(SpeedEvent.class);
public static final EventManager<SpeedEvent> SPEED_EVENT = new EventManager<>();
static {
timer.schedule(new TimerTask() {
@@ -0,0 +1,97 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.Objects;
/// An immutable byte sequence with content-based equality and a cached hash code.
/// Use [#copyOf(byte[])] to copy mutable input, or [#wrap(byte[])] to retain an
/// array that the caller guarantees will not be modified.
@NotNullByDefault
public final class ByteArrayKey {
/// The retained bytes, which must not change after construction.
private final byte @Unmodifiable [] bytes;
/// The content hash computed during construction.
private final int hashCode;
/// Retains the supplied bytes without copying and computes their content hash.
/// The array must not be modified after construction.
private ByteArrayKey(byte @Unmodifiable [] bytes) {
this.bytes = Objects.requireNonNull(bytes);
this.hashCode = Arrays.hashCode(bytes);
}
/// Creates a key by parsing pairs of hexadecimal digits without separators.
/// Both uppercase and lowercase ASCII digits are accepted; an empty string creates an empty key.
/// The decoded array is owned by the key and is not copied again.
///
/// @param hex the hexadecimal representation of the bytes
/// @throws IllegalArgumentException if the length is odd or a character is not a hexadecimal digit
/// @throws NullPointerException if the string is null
public static ByteArrayKey fromHexString(String hex) {
return new ByteArrayKey(HexFormat.of().parseHex(hex));
}
/// Creates a key containing a copy of the supplied bytes, which may be empty.
/// Subsequent changes to the input array do not affect the key.
///
/// @param bytes the bytes to copy
/// @throws NullPointerException if the array is null
public static ByteArrayKey copyOf(byte[] bytes) {
return new ByteArrayKey(bytes.clone());
}
/// Creates a key that retains the supplied array without copying it.
/// The caller must ensure that the array is never modified after this call, including
/// through other references. Modifying it violates the key's immutability and can make
/// its cached hash inconsistent with its contents, invalidating hash-collection lookups.
/// Use [#copyOf(byte[])] when this requirement cannot be met.
///
/// @param bytes the bytes to retain, which may be empty
/// @throws NullPointerException if the array is null
public static ByteArrayKey wrap(byte @Unmodifiable [] bytes) {
return new ByteArrayKey(bytes);
}
/// Returns a new array containing this key's bytes.
/// Changes to the returned array do not affect this key.
public byte[] toByteArray() {
return bytes.clone();
}
/// Returns whether the other object is a [ByteArrayKey] with the same byte sequence.
/// Hash collisions do not make distinct byte sequences equal.
@Override
public boolean equals(@Nullable Object other) {
return this == other || other instanceof ByteArrayKey key
&& hashCode == key.hashCode && Arrays.equals(bytes, key.bytes);
}
/// Returns the cached value of [Arrays#hashCode(byte[])] for this key's bytes.
@Override
public int hashCode() {
return hashCode;
}
}
@@ -1,60 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jetbrains.annotations.NotNullByDefault;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.Semaphore;
/// @author Glavo
@NotNullByDefault
public abstract class ResourceCleaner implements AutoCloseable {
private static final VarHandle CLEANED_HANDLE;
public static ResourceCleaner acquire(Semaphore semaphore) throws InterruptedException {
semaphore.acquire();
return new ResourceCleaner() {
@Override
protected void clean() {
semaphore.release();
}
};
}
static {
try {
CLEANED_HANDLE = MethodHandles.lookup()
.findVarHandle(ResourceCleaner.class, "cleaned", boolean.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
private volatile boolean cleaned = false;
@Override
public void close() {
if (!(boolean) CLEANED_HANDLE.getAndSet(this, true)) {
clean();
}
}
protected abstract void clean();
}
@@ -18,10 +18,6 @@
package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
@@ -31,9 +27,6 @@ import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -48,29 +41,6 @@ public final class LocaleUtils {
public static final Locale SYSTEM_DEFAULT = Locale.getDefault();
public static final boolean IS_CHINA_MAINLAND = isChinaMainland();
private static boolean isChinaMainland() {
if ("Asia/Shanghai".equals(ZoneId.systemDefault().getId()))
return true;
// Check if the time zone is UTC+8
if (ZonedDateTime.now().getOffset().getTotalSeconds() == Duration.ofHours(8).toSeconds()) {
if ("CN".equals(LocaleUtils.SYSTEM_DEFAULT.getCountry()))
return true;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && NativeUtils.USE_JNA) {
Kernel32 kernel32 = Kernel32.INSTANCE;
// https://learn.microsoft.com/windows/win32/intl/table-of-geographical-locations
if (kernel32 != null && kernel32.GetUserGeoID(WinConstants.GEOCLASS_NATION) == 45) // China
return true;
}
}
return false;
}
public static final Locale LOCALE_ZH_HANS = Locale.forLanguageTag("zh-Hans");
public static final Locale LOCALE_ZH_HANT = Locale.forLanguageTag("zh-Hant");
@@ -0,0 +1,259 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.jackhuang.hmcl.util.ByteArrayKey;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import javax.net.ssl.CertPathTrustManagerParameters;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertPathValidatorException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.TrustAnchor;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// Installs TLS certificate validation that rejects known compromised CA public keys.
@NotNullByDefault
public final class TLSSecurity {
/// Prevents instantiation.
private TLSSecurity() {
}
/// Replaces the process-wide default TLS context and HTTPS socket factory.
/// Uses the JSSE default trust store, including `javax.net.ssl.trustStore` overrides,
/// and the `com.sun.net.ssl.checkRevocation` setting. Client credentials use the JSSE
/// `javax.net.ssl.keyStore` properties. Normal PKIX validation and HTTPS hostname
/// verification remain enabled. Existing connections and explicitly
/// configured TLS contexts are not changed.
///
/// Uses the configured default trust manager algorithm, which must support PKIX parameters.
/// Preinstalled default contexts and socket factories are replaced, not wrapped.
/// This method must be called after logging is initialized and before network activity.
/// If configuration fails, logs the cause and installs a context that rejects all certificate
/// authentication so that offline startup can continue without unprotected TLS requests.
///
/// @throws IllegalStateException if even the rejecting TLS context cannot be installed
public static void install() {
SSLContext context;
try {
context = SSLContext.getInstance("TLS");
TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
factory.init(new CertPathTrustManagerParameters(createParameters(null)));
context.init(createKeyManagers(), factory.getTrustManagers(), null);
} catch (GeneralSecurityException | IOException | RuntimeException failure) {
LOG.warning("TLS configuration failed; default TLS connections will be rejected", failure);
try {
context = SSLContext.getInstance("TLS");
context.init(new KeyManager[0], new X509TrustManager[]{new RejectingTrustManager(failure)}, null);
} catch (GeneralSecurityException e) {
e.addSuppressed(failure);
throw new IllegalStateException("Failed to disable unprotected TLS connections", e);
}
}
SSLContext.setDefault(context);
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
}
/// Rejects certificate authentication after TLS configuration fails.
@NotNullByDefault
private static final class RejectingTrustManager implements X509TrustManager {
/// The configuration failure retained as the cause of handshake errors.
private final Exception failure;
/// Retains the configuration failure for subsequent handshake diagnostics.
private RejectingTrustManager(Exception failure) {
this.failure = failure;
}
/// Always rejects client certificate authentication with the configuration failure.
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
throw new CertificateException("TLS configuration failed", failure);
}
/// Always rejects server certificate authentication with the configuration failure.
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
throw new CertificateException("TLS configuration failed", failure);
}
/// Returns an empty array because no certificate authorities are trusted.
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
/// Loads client credentials using the JSSE key store path, type, provider, and password properties.
/// An empty path or `NONE` loads the store without a file; PKCS11 requires `NONE`.
/// An empty type delegates store selection to the default key manager factory.
///
/// @throws GeneralSecurityException if the store or key managers cannot be initialized
/// @throws IOException if the store cannot be read or its password is incorrect
static KeyManager[] createKeyManagers() throws GeneralSecurityException, IOException {
String path = System.getProperty("javax.net.ssl.keyStore", "");
String type = System.getProperty("javax.net.ssl.keyStoreType", KeyStore.getDefaultType());
String provider = System.getProperty("javax.net.ssl.keyStoreProvider", "");
String passwordProperty = System.getProperty("javax.net.ssl.keyStorePassword", "");
boolean pkcs11 = "PKCS11".equals(type);
if (pkcs11 && !"NONE".equals(path))
throw new GeneralSecurityException("PKCS11 key stores require javax.net.ssl.keyStore=NONE");
char @Nullable [] password = passwordProperty.isEmpty() ? null : passwordProperty.toCharArray();
try {
@Nullable KeyStore store = null;
try (@Nullable InputStream input = path.isEmpty() || "NONE".equals(path) ? null : new FileInputStream(path)) {
if (!type.isEmpty()) {
store = provider.isEmpty() ? KeyStore.getInstance(type) : KeyStore.getInstance(type, provider);
store.load(input, password);
}
}
KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
// Token providers obtain private keys without a separate key password.
factory.init(store, pkcs11 ? null : password);
return factory.getKeyManagers();
} finally {
if (password != null)
Arrays.fill(password, '\0');
}
}
/// Creates PKIX parameters with compromised trust anchors removed and a path checker.
/// A null store selects the JSSE default store. Revocation follows the JSSE system property.
///
/// @throws GeneralSecurityException if initialization fails or no usable trust anchors remain
static PKIXBuilderParameters createParameters(@Nullable KeyStore store) throws GeneralSecurityException {
TrustManagerFactory defaults = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
defaults.init(store);
Set<TrustAnchor> anchors = new HashSet<>();
BlockedKeyChecker checker = new BlockedKeyChecker();
for (var manager : defaults.getTrustManagers()) {
if (manager instanceof X509TrustManager x509) {
for (X509Certificate certificate : x509.getAcceptedIssuers()) {
// PKIX path checkers do not inspect trust anchors.
if (!checker.isBlocked(certificate))
anchors.add(new TrustAnchor(certificate, null));
}
}
}
PKIXBuilderParameters parameters = new PKIXBuilderParameters(anchors, new X509CertSelector());
parameters.setRevocationEnabled(Boolean.getBoolean("com.sun.net.ssl.checkRevocation"));
parameters.addCertPathChecker(checker);
return parameters;
}
/// Rejects compromised public keys in trust anchors and constructed certificate paths.
/// Instances are stateless and may be used concurrently.
@NotNullByDefault
private static final class BlockedKeyChecker extends PKIXCertPathChecker {
/// DER SubjectPublicKeyInfo encodings of compromised TLS and code-signing CA public keys.
/// Cross-signed certificates are covered because they share the same public keys.
/// Sources: [Mozilla incident announcement](https://groups.google.com/a/mozilla.org/g/dev-security-policy/c/0R3XLJL6FuE/m/Kp1v6CPuAAAJ) and
/// [pinned certificate list](https://github.com/galadran/we-have-a-problem.com/blob/e2786ff8b707b4951f0db1f83ccaf8ac9af2f7e6/impacted-intermediates.json).
private static final @Unmodifiable Set<ByteArrayKey> BLOCKED_KEYS = Set.of(
// EM DV TLS CA - G3B
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B8104002203620004E79ED9C04A9773C4735E51111DA85AC2"
+ "BD5E64AB374B80E12CBC2038EC1E4F0E3B812FC9859E1E67AFB04E2F8BC97F7485BA65AA911EE8A4"
+ "E45B62D5CC03D56BE678B3C38CB4DC937CA2460E238453C892935D29D44F91E15606B63CCEDAB468"),
// EM OV TLS CA - G3B
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B810400220362000473897AA0EB3CFDCA8B12402FFAF0ABC4"
+ "E1212BAF8DEE778AC9EE2D6DB83FAC280C5E05638AE7A2ACD71C322A8E361CDEB9F69C4302B9DD8D"
+ "73D1CC6CAAB2CC0AEE4E216CE979DD518A4AF4B626E5684CDD658037ED0C854E4CC12F4CF8646D0D"),
// EM EV TLS CA - G3B
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B810400220362000425DBA85A64D397DA0B1052E9EA546E48"
+ "7BEFEA9C9D9D68A0C5EADCFEEF75A69BB5D6FC01F42BFBCA6EB40BCD64C0767E36FFD05D97A813C5"
+ "AA0C6C9530E79CF5F0866468F13806420AA5C851840B1E252540E790E4B654C3852ED64AF1E8657D"),
// EM DV TLS CA - G3C
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B81040022036200043A7A7862C8C44D324DDD69033A6782C0"
+ "EFEEFE4055E68A123FA0371BA3BD9194514EE172A433E3630F02BD188037D9E5149F9B77DB94A281"
+ "DF987B2FEA888B74BDFE8FDA4C1099EE6CA19566FE3EF0E5F6B09A556CCC8BA50561DF1B2A441D01"),
// EM OV TLS CA - G3C
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B8104002203620004138251CD52AC9298C1C8AAD977321DEB"
+ "97E709BD0B4CA0ACA55DC8AD51DCFC9D1589A1597E3A5120E1EFD631C63E1835CACAE29869A62E16"
+ "31E8A28181AB56616DC45D918ABC09F3AB0E63CF792AA4DCED7387BE37BBA569549F1C02B270ED67"),
// EM EV TLS CA - G3C
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B81040022036200042D56E829A6FB82C64A4697D960AF951E"
+ "9067AE91F926D34306DA48A8AAEB7869B6A67E6D27426E0442AA03B36E1988DFF636514EE1CF07F5"
+ "BA4B3FF2A06645AE7D31D5866AD31AA3D246EB0B3177EE99CB7566C516CE74808A668F91F0C47FCA"),
// EM OV CS CA - G3B
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B8104002203620004AABB0FB0666ACFCF01FAD52BFCFD4C2B"
+ "F2CB3C3497DE73950E5819B61FE695098741C8C491439B34A0431CAFFE5BF9B4C1C8E8AE0001F9F4"
+ "DC8AD82C59F3639E96CD6C3D4DB0CEFEC26C3D62517B37761372743F8E8ADD77B10064F2ABE5E050"),
// EM OV CS CA - G3C
ByteArrayKey.fromHexString("3076301006072A8648CE3D020106052B8104002203620004B9F0E0634702A69CA6AADD443CFD62C4"
+ "1B9BA4B20924697D7974E7691A126FB96AFB3E34C2FD3BC5F483061F265DC2A58436347323510436"
+ "B28F04F3862363067980E2503E02532D6546235A7165A259A1EF496F741B5FDEE7787208E4B8FAEF")
);
/// Returns whether the certificate contains an exact blocked SubjectPublicKeyInfo encoding.
///
/// @throws CertPathValidatorException if the public key cannot be encoded
private boolean isBlocked(Certificate certificate) throws CertPathValidatorException {
byte @Nullable [] encoded = certificate.getPublicKey().getEncoded();
if (encoded == null)
throw new CertPathValidatorException("Certificate public key cannot be encoded");
return BLOCKED_KEYS.contains(ByteArrayKey.wrap(encoded));
}
@Override
public void init(boolean forward) {
}
@Override
public boolean isForwardCheckingSupported() {
return true;
}
@Override
public @Unmodifiable Set<String> getSupportedExtensions() {
return Set.of();
}
/// Rejects a blocked public key without consuming any critical extensions.
///
/// @throws CertPathValidatorException if the public key is blocked or cannot be encoded
@Override
public void check(Certificate certificate, @Nullable Collection<String> unresolvedCritExts)
throws CertPathValidatorException {
if (isBlocked(certificate))
throw new CertPathValidatorException("Certificate uses a blocked CA public key");
}
}
}
@@ -64,13 +64,25 @@ public final class GraphicsCard {
private final @Nullable String driverVersion;
private final @Unmodifiable List<Path> vulkanDriverFiles;
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion, List<Path> vulkanDriverFiles) {
/// Whether the detector identified this adapter as a known virtual display adapter.
private final boolean virtualDisplayAdapter;
/// Creates an adapter with the supplied metadata and detection result.
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion, @Unmodifiable List<Path> vulkanDriverFiles, boolean virtualDisplayAdapter) {
this.name = Objects.requireNonNull(name);
this.vendor = vendor;
this.type = type;
this.driver = driver;
this.driverVersion = driverVersion;
this.vulkanDriverFiles = vulkanDriverFiles;
this.virtualDisplayAdapter = virtualDisplayAdapter;
}
/// Returns whether this adapter was identified as a known virtual display adapter.
/// A false result also includes adapters whose classification is unknown;
/// it does not establish that the adapter is physical.
public boolean isVirtualDisplayAdapter() {
return virtualDisplayAdapter;
}
public String getName() {
@@ -117,8 +129,12 @@ public final class GraphicsCard {
private @Nullable String driverVersion;
private @Nullable List<Path> vulkanDriverFiles;
/// Whether the adapter was identified as a known virtual display adapter; initially false.
private boolean virtualDisplayAdapter;
/// Creates an adapter, supplying a vendor-based or unknown name if no name was set.
public GraphicsCard build() {
String name = this.name;
@Nullable String name = this.name;
if (name == null) {
if (vendor != null)
name = vendor + " Graphics";
@@ -128,7 +144,17 @@ public final class GraphicsCard {
List<Path> vulkanDriverFiles = Objects.requireNonNullElse(this.vulkanDriverFiles, List.of());
return new GraphicsCard(name, vendor, type, driver, driverVersion, List.copyOf(vulkanDriverFiles));
return new GraphicsCard(name, vendor, type, driver, driverVersion, List.copyOf(vulkanDriverFiles), virtualDisplayAdapter);
}
/// Sets whether the detector identified the adapter as a known virtual display adapter.
///
/// @param virtualDisplayAdapter true for an identified virtual display adapter;
/// false otherwise, including when classification is unknown
/// @return this builder
public Builder setVirtualDisplayAdapter(boolean virtualDisplayAdapter) {
this.virtualDisplayAdapter = virtualDisplayAdapter;
return this;
}
public @Nullable String getName() {
@@ -1,42 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.Pointer;
import com.sun.jna.win32.StdCallLibrary;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
/// Provides mappings for the OLE/COM functions used by HMCL.
///
/// @author Glavo
@NotNullByDefault
public interface Ole32 extends StdCallLibrary {
/// The loaded Ole32 library, or `null` when JNA is unavailable or the current platform is not Windows.
@Nullable Ole32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
? NativeUtils.load("ole32", Ole32.class)
: null;
/// Frees a block of task memory previously allocated through the COM task allocator.
///
/// @param pv the memory block to free; ignored when `null`
/// @see <a href="https://learn.microsoft.com/windows/win32/api/combaseapi/nf-combaseapi-cotaskmemfree">CoTaskMemFree function</a>
void CoTaskMemFree(@Nullable Pointer pv);
}
@@ -42,16 +42,6 @@ public interface Shell32 extends StdCallLibrary {
/// @see <a href="https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-setcurrentprocessexplicitappusermodelid">SetCurrentProcessExplicitAppUserModelID function</a>
int SetCurrentProcessExplicitAppUserModelID(WString appID);
/// Retrieves the application-defined AppUserModelID for the current process.
///
/// The caller must free the returned string with [`Ole32#CoTaskMemFree(com.sun.jna.Pointer)`]
/// when it is no longer needed.
///
/// @param ppszAppID receives a pointer to the process AppUserModelID string
/// @return `S_OK` if the operation succeeds; otherwise a failure `HRESULT`
/// @see <a href="https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-getcurrentprocessexplicitappusermodelid">GetCurrentProcessExplicitAppUserModelID function</a>
int GetCurrentProcessExplicitAppUserModelID(PointerByReference ppszAppID);
/// Retrieves an [`IPropertyStore`] interface for the given window.
///
/// @param hwnd the window whose property store is requested
@@ -124,6 +124,9 @@ final class WindowsGPUDetector {
}
}
/// Reads graphics card information from the display adapter registry keys.
/// Uses the driver description when the adapter name is missing or blank;
/// if both are missing or blank, leaves the name to the builder's fallback.
private static List<GraphicsCard> detectByRegistry(WinReg reg) {
final WinReg.HKEY hkey = WinReg.HKEY.HKEY_LOCAL_MACHINE;
final String displayDevices = "SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\";
@@ -134,14 +137,18 @@ final class WindowsGPUDetector {
if (!graphicsCardPattern.matcher(subkey).find())
continue;
String name = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.AdapterString"));
String vendor = regValueToString(reg.queryValue(hkey, subkey, "ProviderName"));
String driverVersion = regValueToString(reg.queryValue(hkey, subkey, "DriverVersion"));
String dacType = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.DacType"));
Object vulkanDriverName = reg.queryValue(hkey, subkey, "VulkanDriverName");
@Nullable String name = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.AdapterString"));
if (StringUtils.isBlank(name))
name = regValueToString(reg.queryValue(hkey, subkey, "DriverDesc"));
@Nullable String vendor = regValueToString(reg.queryValue(hkey, subkey, "ProviderName"));
@Nullable String driverVersion = regValueToString(reg.queryValue(hkey, subkey, "DriverVersion"));
@Nullable String dacType = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.DacType"));
@Nullable Object vulkanDriverName = reg.queryValue(hkey, subkey, "VulkanDriverName");
@Nullable String matchingDeviceId = regValueToString(reg.queryValue(hkey, subkey, "MatchingDeviceId"));
GraphicsCard.Builder builder = GraphicsCard.builder();
if (name != null)
builder.setVirtualDisplayAdapter("Root\\GameViewerIddDriver".equalsIgnoreCase(matchingDeviceId));
if (StringUtils.isNotBlank(name))
builder.setName(GraphicsCard.cleanName(name));
if (vendor != null)
builder.setVendor(HardwareVendor.of(vendor));
@@ -1,3 +1,5 @@
# IANA registry version: 2026-09-17
# Generated from https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
Arab,ar,fa,ps,ur
Armn,hy
Beng,as,bn
1 Arab,ar,fa,ps,ur # IANA registry version: 2026-09-17
1 # IANA registry version: 2026-09-17
2 # Generated from https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
3 Arab,ar,fa,ps,ur Arab,ar,fa,ps,ur
4 Armn,hy Armn,hy
5 Beng,as,bn Beng,as,bn
@@ -1,3 +1,5 @@
# IANA registry version: 2026-09-17
# Generated from https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
ak,tw,fat
ar,aao,abh,abv,acm,acq,acw,acx,acy,adf,aeb,aec,afb,apc,apd,arb,arq,ars,ary,arz,auz,avl,ayh,ayl,ayn,ayp,pga,shu,ssh
ay,ayc,ayr
1 ak,tw,fat # IANA registry version: 2026-09-17
1 # IANA registry version: 2026-09-17
2 # Generated from https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
3 ak,tw,fat ak,tw,fat
4 ar,aao,abh,abv,acm,acq,acw,acx,acy,adf,aeb,aec,afb,apc,apd,arb,arq,ars,ary,arz,auz,avl,ayh,ayl,ayn,ayp,pga,shu,ssh ar,aao,abh,abv,acm,acq,acw,acx,acy,adf,aeb,aec,afb,apc,apd,arb,arq,ars,ary,arz,auz,avl,ayh,ayl,ayn,ayp,pga,shu,ssh
5 ay,ayc,ayr ay,ayc,ayr
@@ -49,6 +49,40 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
@NotNullByDefault
public final class DefaultGameRepositoryDraftTest {
/// Finds a manifest among unrelated and malformed JSON files and preserves its storage path.
@Test
public void testRefreshSelectsManifestAmongMultipleJsonFiles(@TempDir Path tempDirectory) throws IOException {
TestRepository repository = new TestRepository(tempDirectory);
GameInstanceID id = new GameInstanceID("instance");
Path root = Files.createDirectories(repository.getLayout().getInstanceRoot(id));
Files.writeString(root.resolve("a-broken.json"), "{");
Files.writeString(root.resolve("b-settings.json"), "{\"id\":\"settings\",\"mainClass\":42}");
Path selected = root.resolve("c-game.json");
Files.writeString(selected, "{\"id\":\"original\",\"mainClass\":\"example.Main\"}");
Files.writeString(root.resolve("d-game.json"), "{\"id\":\"other\",\"mainClass\":\"other.Main\"}");
repository.refresh();
assertEquals(selected, repository.getInstance(id).getManifestFile());
assertEquals(id, repository.getInstance(id).getManifest().id());
assertEquals("example.Main", repository.getInstance(id).getManifest().mainClass());
assertFalse(Files.exists(repository.getLayout().getInstanceJson(id)));
}
/// Ignores directories whose multiple JSON files do not contain an instance manifest.
@Test
public void testRefreshIgnoresUnrelatedJsonFiles(@TempDir Path tempDirectory) throws IOException {
TestRepository repository = new TestRepository(tempDirectory);
GameInstanceID id = new GameInstanceID("instance");
Path root = Files.createDirectories(repository.getLayout().getInstanceRoot(id));
Files.writeString(root.resolve("settings.json"), "{\"id\":\"settings\"}");
Files.writeString(root.resolve("empty.json"), "null");
repository.refresh();
assertFalse(repository.hasInstance(id));
}
/// Rejects path-segment instance ids before they can reach repository deletion code.
@Test
public void testRejectsSpecialPathSegmentIds() {
@@ -0,0 +1,99 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/// Tests manifest precedence over the legacy Linux Java ceiling.
@NotNullByDefault
public final class JavaVersionConstraintTest {
/// The affected Minecraft version.
private static final GameVersionNumber GAME = GameVersionNumber.asGameVersion("1.7.10");
/// Synthetic Java 8 metadata; no executable is launched.
private static final JavaRuntime JAVA_8 = new JavaRuntime(Path.of("java8"),
new JavaInfo(Platform.LINUX_X86_64, "1.8.0_402", "test"), false, true);
/// Synthetic Java 25 metadata; no executable is launched.
private static final JavaRuntime JAVA_25 = new JavaRuntime(Path.of("java25"),
new JavaInfo(Platform.LINUX_X86_64, "25", "test"), false, true);
/// Modern manifest requirements suppress the ceiling for both discovery and explicit runtime validation.
@Test
public void testModernRequirementsTakePrecedence() {
assumeTrue(OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64);
for (int major : List.of(17, 25)) {
GameInstanceManifest manifest = new GameInstanceManifest(new GameInstanceID("modern"))
.withJavaVersion(new GameJavaVersion("test", major));
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, GAME);
JavaRuntime runtime = new JavaRuntime(Path.of("java" + major),
new JavaInfo(Platform.LINUX_X86_64, String.valueOf(major), "test"), false, true);
assertFalse(JavaVersionConstraint.VANILLA_LINUX_JAVA_8.appliesToVersion(GAME, manifest, runtime, analyzer));
assertFalse(JavaVersionConstraint.VANILLA_LINUX_JAVA_8.appliesToVersion(GAME, manifest, null, analyzer));
assertTrue(JavaVersionConstraint.GAME_JSON.appliesToVersion(GAME, manifest, runtime, analyzer));
assertFalse(JavaVersionConstraint.GAME_JSON.checkJava(GAME, manifest, JAVA_8, analyzer));
assertTrue(JavaVersionConstraint.GAME_JSON.checkJava(GAME, manifest, runtime, analyzer));
}
}
/// Missing metadata and Java 8 declarations retain the platform-specific ceiling.
@Test
public void testLegacyRequirementsRetainCeiling() {
GameInstanceManifest manifest = new GameInstanceManifest(new GameInstanceID("legacy"));
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, GAME);
boolean affectedPlatform = OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64;
for (int major : List.of(0, 8, -1)) {
@Nullable GameInstanceManifest candidate = major == -1 ? null
: major == 0 ? manifest : manifest.withJavaVersion(GameJavaVersion.JAVA_8);
assertEquals(affectedPlatform, JavaVersionConstraint.VANILLA_LINUX_JAVA_8.appliesToVersion(GAME, candidate, JAVA_25, analyzer));
assertEquals(affectedPlatform, JavaVersionConstraint.VANILLA_LINUX_JAVA_8.appliesToVersion(GAME, candidate, null, analyzer));
}
assertTrue(JavaVersionConstraint.VANILLA_LINUX_JAVA_8.checkJava(GAME, manifest, JAVA_8, analyzer));
assertFalse(JavaVersionConstraint.VANILLA_LINUX_JAVA_8.checkJava(GAME, manifest, JAVA_25, analyzer));
}
/// Newer games, other runtime architectures, and Cleanroom remain exempt.
@Test
public void testExistingExemptions() {
assumeTrue(OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64);
GameInstanceManifest manifest = new GameInstanceManifest(new GameInstanceID("legacy"));
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, GAME);
assertFalse(JavaVersionConstraint.VANILLA_LINUX_JAVA_8.appliesToVersion(
GameVersionNumber.asGameVersion("1.13"), manifest, JAVA_25, analyzer));
JavaRuntime armRuntime = new JavaRuntime(Path.of("java-arm"),
new JavaInfo(Platform.LINUX_ARM64, "25", "test"), false, true);
assertFalse(JavaVersionConstraint.VANILLA_LINUX_JAVA_8.appliesToVersion(GAME, manifest, armRuntime, analyzer));
GameInstanceManifest cleanroom = manifest.withLibraries(List.of(new Library("com.cleanroommc", "cleanroom", "0.3.0")));
GameComponentAnalyzer cleanroomAnalyzer = GameComponentAnalyzer.analyze(cleanroom, GAME);
assertTrue(cleanroomAnalyzer.has(GameComponentType.CLEANROOM));
assertFalse(JavaVersionConstraint.VANILLA_LINUX_JAVA_8.appliesToVersion(GAME, cleanroom, JAVA_25, cleanroomAnalyzer));
}
}
@@ -0,0 +1,106 @@
/*
* 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 org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.*;
/// Verifies byte-array ownership and equality when keys are used in hash collections.
@NotNullByDefault
class ByteArrayKeyTest {
/// Keeps map lookup and the cached hash stable after input and output arrays are modified.
@Test
void ownsItsBytes() {
byte[] input = {1, 2, 3};
ByteArrayKey key = ByteArrayKey.copyOf(input);
int hash = key.hashCode();
var map = new HashMap<ByteArrayKey, String>();
map.put(key, "value");
input[0] = 9;
byte[] output = key.toByteArray();
output[1] = 9;
assertArrayEquals(new byte[]{1, 2, 3}, key.toByteArray());
assertEquals(hash, key.hashCode());
assertEquals("value", map.get(ByteArrayKey.copyOf(new byte[]{1, 2, 3})));
assertNotSame(output, key.toByteArray());
}
/// Creates equal keys through all factories and keeps the retained array hidden on output.
@Test
void supportsAllFactories() {
byte[] bytes = {0, 10, -1};
ByteArrayKey wrapped = ByteArrayKey.wrap(bytes);
ByteArrayKey copied = ByteArrayKey.copyOf(bytes);
ByteArrayKey parsed = ByteArrayKey.fromHexString("000aFF");
assertEquals(copied, wrapped);
assertEquals(copied, parsed);
assertEquals(copied.hashCode(), wrapped.hashCode());
assertEquals(copied.hashCode(), parsed.hashCode());
wrapped.toByteArray()[0] = 1;
assertEquals(parsed, wrapped);
assertEquals(ByteArrayKey.copyOf(new byte[0]), ByteArrayKey.fromHexString(""));
assertEquals(ByteArrayKey.copyOf(new byte[0]), ByteArrayKey.wrap(new byte[0]));
}
/// Rejects invalid hexadecimal strings and absent inputs.
@Test
void rejectsInvalidFactoryArguments() {
for (String hex : new String[]{"0", "0g", "00 ff", "0x00"})
assertThrows(IllegalArgumentException.class, () -> ByteArrayKey.fromHexString(hex));
assertThrows(NullPointerException.class, () -> ByteArrayKey.fromHexString(null));
assertThrows(NullPointerException.class, () -> ByteArrayKey.copyOf(null));
assertThrows(NullPointerException.class, () -> ByteArrayKey.wrap(null));
}
/// Distinguishes different byte sequences even when their cached hash codes collide.
@Test
void resolvesHashCollisionsByContent() {
ByteArrayKey first = ByteArrayKey.copyOf(new byte[]{0, 31});
ByteArrayKey second = ByteArrayKey.copyOf(new byte[]{1, 0});
assertEquals(first.hashCode(), second.hashCode());
assertNotEquals(first, second);
var map = new HashMap<ByteArrayKey, String>();
map.put(first, "first");
map.put(second, "second");
assertEquals(2, map.size());
assertEquals("first", map.get(ByteArrayKey.copyOf(new byte[]{0, 31})));
assertEquals("second", map.get(ByteArrayKey.copyOf(new byte[]{1, 0})));
}
/// Supports empty sequences and enforces equality and null-input contracts.
@Test
void comparesByContent() {
ByteArrayKey first = ByteArrayKey.copyOf(new byte[]{-128, 0, 127});
ByteArrayKey second = ByteArrayKey.copyOf(new byte[]{-128, 0, 127});
assertEquals(first, first);
assertEquals(first, second);
assertEquals(second, first);
assertEquals(Arrays.hashCode(new byte[]{-128, 0, 127}), first.hashCode());
assertFalse(first.equals(null));
assertFalse(first.equals(new byte[]{-128, 0, 127}));
assertNotEquals(first, ByteArrayKey.copyOf(new byte[]{-128, 0}));
assertEquals(ByteArrayKey.copyOf(new byte[0]), ByteArrayKey.copyOf(new byte[0]));
}
}
@@ -0,0 +1,418 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.io;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import javax.net.ssl.CertPathTrustManagerParameters;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedTrustManager;
import javax.net.ssl.X509KeyManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.Security;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
/// Exercises compromised and independent test certificates with offline PKIX validation.
///
/// `test-chain.pem` contains a locally generated leaf, intermediate, and self-signed root,
/// in that order, without private keys. The certificates use EC `secp256r1` keys and were
/// generated with `keytool`, a start date of `2026/01/01 00:00:00`, and a validity of 3650 days.
/// The leaf permits server and client authentication; both CAs permit certificate signing.
///
/// The compromised certificates also match the original Mozilla report attachments:
/// [original issuances](https://bugzilla.mozilla.org/attachment.cgi?id=9664066) and
/// [cross-signs](https://bugzilla.mozilla.org/attachment.cgi?id=9664067).
///
/// Public certificate fixtures come from the following pinned source files.
/// SHA-256 digests refer to the original downloaded bytes.
///
/// | Source | Local fixtures | SHA-256 |
/// | --- | --- | --- |
/// | [impacted-intermediates.json](https://github.com/galadran/we-have-a-problem.com/blob/e2786ff8b707b4951f0db1f83ccaf8ac9af2f7e6/impacted-intermediates.json) | `compromised.pem` | `f67b3abe9b4879f4e91874ed041beccd71257f1db41c588c724ccf12aa90c59e` |
/// | [roots.json](https://github.com/galadran/we-have-a-problem.com/blob/e2786ff8b707b4951f0db1f83ccaf8ac9af2f7e6/roots.json) | `roots.pem` | `119f32cd81a6b5a4e3c4c2df5fe07daa90dc43cafa123a62f0394f1895d92380` |
/// | [chains.zip](https://github.com/galadran/we-have-a-problem.com/blob/e2786ff8b707b4951f0db1f83ccaf8ac9af2f7e6/chains.zip) | The 12 TLS chain PEM files | `8a2edc81b6c51e3ad27d45ca1e441ec84c961e2dee0fc547f2a02c0f74315f95` |
@NotNullByDefault
class TLSSecurityTest {
/// Loads an ephemeral client identity and fails closed for invalid key store settings.
@Test
void loadsConfiguredClientCredentials(@TempDir Path directory) throws Exception {
SSLContext previousContext = SSLContext.getDefault();
SSLSocketFactory previousFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
Path storePath = directory.resolve("client.p12");
Path output = directory.resolve("keytool.log");
String executable = System.getProperty("os.name").startsWith("Windows") ? "keytool.exe" : "keytool";
Process process = new ProcessBuilder(Path.of(System.getProperty("java.home"), "bin", executable).toString(),
"-genkeypair", "-alias", "client", "-keyalg", "EC", "-groupname", "secp256r1",
"-dname", "CN=TLS test client", "-validity", "1", "-storetype", "PKCS12",
"-keystore", storePath.toString(), "-storepass", "test-password", "-keypass", "test-password")
.redirectErrorStream(true).redirectOutput(output.toFile()).start();
try {
assertTrue(process.waitFor(30, TimeUnit.SECONDS), "Keytool timed out");
assertEquals(0, process.exitValue(), new String(Files.readAllBytes(output),
Charset.forName(System.getProperty("native.encoding", "UTF-8"))));
} finally {
process.destroyForcibly();
}
String previousAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
Map<String, @Nullable String> previous = new HashMap<>();
for (String store : List.of("keyStore", "trustStore")) {
for (String suffix : List.of("", "Type", "Provider", "Password")) {
String property = "javax.net.ssl." + store + suffix;
previous.put(property, System.getProperty(property));
}
}
try {
System.setProperty("javax.net.ssl.keyStore", storePath.toString());
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");
System.setProperty("javax.net.ssl.keyStoreProvider", "SUN");
System.setProperty("javax.net.ssl.keyStorePassword", "test-password");
var keyManagers = TLSSecurity.createKeyManagers();
SSLContext serverContext = SSLContext.getInstance("TLS");
serverContext.init(keyManagers, null, null);
X509KeyManager manager = (X509KeyManager) keyManagers[0];
@Nullable String alias = manager.chooseClientAlias(new String[]{"EC"}, null, null);
assertNotNull(alias);
assertNotNull(manager.getPrivateKey(alias));
assertEquals("CN=TLS test client", manager.getCertificateChain(alias)[0].getSubjectX500Principal().getName());
System.setProperty("javax.net.ssl.keyStorePassword", "wrong-password");
assertThrows(IOException.class, TLSSecurity::createKeyManagers);
assertDoesNotThrow(TLSSecurity::install);
assertNotSame(previousContext, SSLContext.getDefault());
assertNotSame(previousFactory, HttpsURLConnection.getDefaultSSLSocketFactory());
assertRejectsHandshake(serverContext);
System.setProperty("javax.net.ssl.keyStore", directory.resolve("missing.p12").toString());
assertThrows(IOException.class, TLSSecurity::createKeyManagers);
assertDoesNotThrow(TLSSecurity::install);
assertRejectsHandshake(serverContext);
System.setProperty("javax.net.ssl.keyStoreType", "PKCS11");
assertThrows(GeneralSecurityException.class, TLSSecurity::createKeyManagers);
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");
System.clearProperty("javax.net.ssl.keyStorePassword");
for (String path : List.of("", "NONE")) {
System.setProperty("javax.net.ssl.keyStore", path);
X509KeyManager empty = (X509KeyManager) TLSSecurity.createKeyManagers()[0];
assertNull(empty.chooseClientAlias(new String[]{"EC"}, null, null));
}
Security.setProperty("ssl.TrustManagerFactory.algorithm", "SunX509");
assertDoesNotThrow(TLSSecurity::install);
assertRejectsHandshake(serverContext);
Security.setProperty("ssl.TrustManagerFactory.algorithm", previousAlgorithm);
Path emptyStore = directory.resolve("empty.p12");
try (var outputStream = Files.newOutputStream(emptyStore)) {
trustStore().store(outputStream, "test-password".toCharArray());
}
System.setProperty("javax.net.ssl.trustStore", emptyStore.toString());
System.setProperty("javax.net.ssl.trustStoreType", KeyStore.getDefaultType());
System.setProperty("javax.net.ssl.trustStoreProvider", "");
System.setProperty("javax.net.ssl.trustStorePassword", "test-password");
assertDoesNotThrow(TLSSecurity::install);
assertRejectsHandshake(serverContext);
} finally {
Security.setProperty("ssl.TrustManagerFactory.algorithm", previousAlgorithm);
SSLContext.setDefault(previousContext);
HttpsURLConnection.setDefaultSSLSocketFactory(previousFactory);
previous.forEach((property, value) -> {
if (value == null)
System.clearProperty(property);
else
System.setProperty(property, value);
});
}
}
/// Confirms that the installed default context refuses a real local TLS handshake.
private static void assertRejectsHandshake(SSLContext serverContext) throws Exception {
var executor = Executors.newSingleThreadExecutor();
try (SSLServerSocket server = (SSLServerSocket) serverContext.getServerSocketFactory().createServerSocket(0)) {
server.setSoTimeout(5000);
Future<?> handshake = executor.submit(() -> {
try (SSLSocket socket = (SSLSocket) server.accept()) {
socket.setSoTimeout(5000);
socket.startHandshake();
} catch (IOException expected) {
// The client aborts when the rejecting trust manager is invoked.
}
});
try (SSLSocket client = (SSLSocket) SSLContext.getDefault().getSocketFactory()
.createSocket("localhost", server.getLocalPort())) {
client.setSoTimeout(5000);
SSLException failure = assertThrows(SSLException.class, client::startHandshake);
assertTrue(failure.getMessage().contains("TLS configuration failed"), failure.toString());
}
handshake.get(10, TimeUnit.SECONDS);
} finally {
executor.shutdownNow();
}
}
/// Reads an independent array of public certificates from a bundled PEM fixture.
private static X509Certificate[] readCertificates(String name) throws Exception {
try (InputStream input = Objects.requireNonNull(TLSSecurityTest.class.getResourceAsStream("tls/" + name))) {
return CertificateFactory.getInstance("X.509").generateCertificates(input).toArray(X509Certificate[]::new);
}
}
/// Creates a trust store containing exactly the supplied certificates.
private static KeyStore trustStore(X509Certificate... certificates) throws Exception {
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
store.load(null, null);
for (int i = 0; i < certificates.length; i++)
store.setCertificateEntry("certificate-" + i, certificates[i]);
return store;
}
/// Creates an extended trust manager at a fixed date within the fixtures' validity periods.
private static X509ExtendedTrustManager trustManager(PKIXBuilderParameters parameters) throws Exception {
assertEquals(5, parameters.getMaxPathLength());
parameters.setDate(Date.from(Instant.parse("2026-10-22T00:00:00Z")));
parameters.setRevocationEnabled(false);
TrustManagerFactory factory = TrustManagerFactory.getInstance("PKIX");
factory.init(new CertPathTrustManagerParameters(parameters));
return (X509ExtendedTrustManager) factory.getTrustManagers()[0];
}
/// Returns an initialized checker from the application's PKIX configuration.
private static PKIXCertPathChecker newChecker() throws Exception {
PKIXCertPathChecker checker = TLSSecurity.createParameters(trustStore(readCertificates("roots.pem")))
.getCertPathCheckers().get(0);
checker.init(false);
return checker;
}
/// Covers all eight keys and their cross-signed certificates, including code-signing CAs.
@Test
void blocksAllPublishedCertificates() throws Exception {
X509Certificate[] certificates = readCertificates("compromised.pem");
assertEquals(16, certificates.length);
PKIXCertPathChecker checker = newChecker();
Set<String> fingerprints = new HashSet<>();
for (X509Certificate certificate : certificates) {
fingerprints.add(HexFormat.of().withUpperCase().formatHex(
MessageDigest.getInstance("SHA-256").digest(certificate.getPublicKey().getEncoded())));
assertThrows(CertPathValidatorException.class, () -> checker.check(certificate),
certificate.getSubjectX500Principal().toString());
}
assertEquals(Set.of(
"EBED6475C2C2D80FCB199F3FE8DD6E6B3CE60B77EB15D4E42C3DEC0C6F5B5105",
"43BFFA19D6EF651451F6841E019931E96F6F2EAEFF28AB4B6F1CE4576DE2717B",
"E0254C7FCDC265E2F0F8A428DABB75AB4AE039B4872D62F5EE57F0877D5A8A41",
"49DD353DC2B919A3770645117D0E60C16115E7D1BC259EEFE734B00EA434FE5F",
"63ECEFEE431D49523BC1136C4E72D0206A477086CAF606CD5CF025686C109CF8",
"5C0660A2B164910D8FF589C18E14552E9E0EC237DCEFE2CB736D41A4811F81A0",
"EC4FFA1AD0AC88081314B7AAFFEF6A91F97CD57291BB5BA686C5415F5EB4F6B6",
"92C063D67E8F14DB71D288E11266FFDAF69CBF115F7BAB14F3BC2F68D25E0E30"
), fingerprints);
}
/// Matches every certificate to the full-certificate fingerprints in Mozilla's disclosure.
/// Source: [Mozilla disclosure](https://bugzilla.mozilla.org/attachment.cgi?id=9664786).
@Test
void matchesMozillaDisclosure() throws Exception {
Set<String> fingerprints = new HashSet<>();
for (X509Certificate certificate : readCertificates("compromised.pem")) {
fingerprints.add(HexFormat.of().withUpperCase().formatHex(
MessageDigest.getInstance("SHA-256").digest(certificate.getEncoded())));
}
assertEquals(Set.of(
"E7568FE6E12331F4FD7DE61DA4C7559D0F8EB192A218D4741A3BA75184333838",
"98B896625C0C230ADEEC3EB2CF6AAD5001ECF9156A68644E82374CE5833BC25B",
"42C4ACCD12C4F59597925E1A08A7A90102608543809308D50EC427E118BA1493",
"432EC25F0D28DE9006D97BF752F17BBB2CD41F669933FCB072D9A1148D0261F3",
"CB6E15165AC802E9B3F993F16C25D41F4E81C21EBDE71AE1B012AC00BF9C7C3F",
"0164D7CEAE5BB909C272AF151FF44B1177AE9B47D9424B94F1D9558C30934D19",
"9E39DE2881273ACC225A149C72BA3E0F51424963669AB633EE1DC7DF5DB067B4",
"4B2460A466F95FD40113BCDCCA2CEB29B3B093921DE45474DA7DF48874C630B1",
"579480AEA166290BD3C643589E795FC0D0E54E86505D180F1DFF99EFD3199512",
"5F1DED4DD590AF74631290CF073B122A2AAC6A1FA479778A0BD43579BD832699",
"45A883F349AEDFD5F3A649B5498FFB121712BA8B448F3F2A91DBC5A766FCDDBE",
"FD9D5C2DBBD4995AB69B8D50C439D9527B00529199C2FD01B2174EB58F07021F",
"7485B51C0ED192878FA1D2F948FB529AE1842CC5086C4B67F7BE5B2039DE9489",
"62B7F9868263F72F6911FC78CEAF72C1979158B7368BF648683BB2AA39B02E72",
"A496E23AC0CAB8155FC9B117B97EAE368EFCBB9041AEC8804D5A308D2D01C2D8",
"84112972B8E59D14560AE7E7119654474AAD119990CF3722BFC1E3B91615E1FD"
), fingerprints);
}
/// Rejects published TLS paths for both signing roots, including on JDKs that also block them.
@Test
void rejectsAllPublishedTlsChains() throws Exception {
for (String type : List.of("dv", "ov", "ev")) {
for (String generation : List.of("g3b", "g3c")) {
for (String crossSign : List.of("", "-aaa")) {
String name = type + "-" + generation + crossSign + "-emudhra.we-have-a-problem.com.pem";
X509Certificate[] chain = readCertificates(name);
PKIXBuilderParameters parameters = TLSSecurity.createParameters(trustStore(readCertificates("roots.pem")));
X509ExtendedTrustManager protectedManager = trustManager(parameters);
assertThrows(CertificateException.class, () -> protectedManager.checkServerTrusted(chain, "ECDHE_ECDSA"), name);
assertThrows(CertificateException.class, () -> protectedManager.checkServerTrusted(chain, "ECDHE_ECDSA", (Socket) null), name);
assertThrows(CertificateException.class, () -> protectedManager.checkServerTrusted(chain, "ECDHE_ECDSA", (SSLEngine) null), name);
assertThrows(CertificateException.class, () -> protectedManager.checkClientTrusted(chain, "EC"), name);
// JDK blocklists may independently reject these compromised certificates.
// Check HMCL's checker directly so JDK rejection cannot mask a missing key.
assertThrows(CertPathValidatorException.class, () -> newChecker().check(chain[1]), name);
}
}
}
}
/// Removes compromised certificates even when explicitly installed as trust anchors.
@Test
void filtersCompromisedTrustAnchors() throws Exception {
X509Certificate[] compromised = readCertificates("compromised.pem");
X509Certificate root = readCertificates("roots.pem")[2];
KeyStore store = trustStore(compromised);
store.setCertificateEntry("unaffected-root", root);
PKIXBuilderParameters parameters = TLSSecurity.createParameters(store);
assertEquals(1, parameters.getTrustAnchors().size());
assertEquals(root, parameters.getTrustAnchors().iterator().next().getTrustedCert());
assertDoesNotThrow(() -> trustManager(parameters).checkServerTrusted(new X509Certificate[]{root}, "RSA"));
assertThrows(InvalidAlgorithmParameterException.class, () -> TLSSecurity.createParameters(trustStore(compromised)));
}
/// Checks intermediates supplied by the path builder rather than by the peer.
@Test
void rejectsIntermediateFromCertStore() throws Exception {
X509Certificate[] chain = readCertificates("dv-g3b-aaa-emudhra.we-have-a-problem.com.pem");
PKIXBuilderParameters parameters = TLSSecurity.createParameters(trustStore(readCertificates("roots.pem")));
parameters.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(chain))));
X509Certificate[] leaf = {chain[0]};
assertThrows(CertificateException.class, () -> trustManager(parameters).checkServerTrusted(leaf, "ECDHE_ECDSA"));
}
/// Accepts an independent test chain with peer-supplied or locally stored intermediates.
/// Also verifies that a missing intermediate still prevents path construction.
@Test
void acceptsUnblockedTlsChain() throws Exception {
X509Certificate[] chain = readCertificates("test-chain.pem");
assertEquals(3, chain.length);
PKIXBuilderParameters parameters = TLSSecurity.createParameters(trustStore(chain[2]));
X509ExtendedTrustManager manager = trustManager(parameters);
assertDoesNotThrow(() -> manager.checkServerTrusted(chain, "ECDHE_ECDSA"));
assertDoesNotThrow(() -> manager.checkServerTrusted(chain, "ECDHE_ECDSA", (Socket) null));
assertDoesNotThrow(() -> manager.checkServerTrusted(chain, "ECDHE_ECDSA", (SSLEngine) null));
assertDoesNotThrow(() -> manager.checkClientTrusted(chain, "EC"));
X509Certificate[] leaf = {chain[0]};
assertThrows(CertificateException.class, () -> manager.checkServerTrusted(leaf, "ECDHE_ECDSA"));
parameters.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(chain[1]))));
assertDoesNotThrow(() -> trustManager(parameters).checkServerTrusted(leaf, "ECDHE_ECDSA"));
}
/// Exercises concurrent checks on a shared instance and its shallow clones.
@Test
void supportsConcurrentChecksAndClones() throws Exception {
X509Certificate[] blocked = readCertificates("compromised.pem");
X509Certificate[] roots = readCertificates("roots.pem");
PKIXCertPathChecker original = newChecker();
original.check(roots[0]);
var executor = Executors.newFixedThreadPool(4);
try {
List<Future<?>> tasks = new ArrayList<>();
for (int i = 0; i < 4; i++) {
PKIXCertPathChecker checker = i % 2 == 0 ? original : (PKIXCertPathChecker) original.clone();
tasks.add(executor.submit(() -> {
for (int iteration = 0; iteration < 100; iteration++) {
X509Certificate root = roots[iteration % roots.length];
for (X509Certificate certificate : blocked) {
assertThrows(CertPathValidatorException.class, () -> checker.check(certificate));
assertDoesNotThrow(() -> checker.check(root));
}
assertDoesNotThrow(() -> checker.init(true));
}
}));
}
for (Future<?> task : tasks)
task.get();
assertDoesNotThrow(() -> original.check(roots[0]));
assertThrows(CertPathValidatorException.class, () -> original.check(blocked[0]));
} finally {
executor.shutdownNow();
}
}
/// Installs both process defaults and restores them after the assertion.
@Test
void installsProcessDefaults() throws Exception {
SSLContext previousContext = SSLContext.getDefault();
SSLSocketFactory previousFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
try {
TLSSecurity.install();
assertNotSame(previousContext, SSLContext.getDefault());
assertNotSame(previousFactory, HttpsURLConnection.getDefaultSSLSocketFactory());
} finally {
SSLContext.setDefault(previousContext);
HttpsURLConnection.setDefaultSSLSocketFactory(previousFactory);
}
}
/// Keeps ordinary validation active for a leaf that does not contain a blocked key.
@Test
void rejectsUntrustedLeafWithoutIntermediate() throws Exception {
X509Certificate[] chain = readCertificates("dv-g3b-aaa-emudhra.we-have-a-problem.com.pem");
assertDoesNotThrow(() -> newChecker().check(chain[0]));
X509ExtendedTrustManager manager = trustManager(TLSSecurity.createParameters(trustStore(readCertificates("roots.pem"))));
assertThrows(CertificateException.class, () -> manager.checkServerTrusted(new X509Certificate[]{chain[0]}, "ECDHE_ECDSA"));
}
}
@@ -0,0 +1,350 @@
-----BEGIN CERTIFICATE-----
MIIDaDCCAu6gAwIBAgIQAKSu5njyM2K0wORgt3LyFDAKBggqhkjOPQQDAzBWMQsw
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTU1
NDA4WhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11
ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAE557ZwEqXc8RzXlERHahawr1eZKs3S4DhLLwgOOweTw47gS/J
hZ4eZ6+wTi+LyX90hbplqpEe6KTkW2LVzAPVa+Z4s8OMtNyTfKJGDiOEU8iSk10p
1E+R4VYGtjzO2rRoo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
WUIgkFQwHQYDVR0OBBYEFOIeL+ccGTkOVUtiwSAkVzUkiiUUMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
aHR0cDovL29jc3AtYi5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQi5wN2MwNwYDVR0f
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
AwMDaAAwZQIwLmsGXi6oFMJ2klkcEMXMug6gvxQWZWkewiDWVzQyl2L6z6DuQfPO
Ibh1grvdULDCAjEAxZvLlxUavG69J6wnSjC8XBbqNIQsJ6CE2QdNOe84Ptxb6VG8
gt2TlfJbHii+KNvK
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
MRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO
L4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2
PM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h
AQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB
QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF
BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB
AQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE
wqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM
QkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ
7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl
bN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V
GUyRCEIDhF5TBUuKF4E3kf1h
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDZjCCAu2gAwIBAgIPbK2jZ/scEACIEg6iAtVoMAoGCCqGSM49BAMDMFYxCzAJ
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNjAw
MTJaFw00MDA0MjgxODMwMDBaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVk
aHJhIEJWMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIB
BgUrgQQAIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK
56Ks1xwyKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDft
DIVOTMEvTPhkbQ2jggGWMIIBkjAfBgNVHSMEGDAWgBThOqshfPHjTVSu1HK3/YtZ
QiCQVDAdBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowHQYDVR0lBBYwFAYI
KwYBBQUHAwEGCCsGAQUFBwMCMF0GA1UdIARWMFQwOQYLKwYBBAGDjiEBAAEwKjAo
BggrBgEFBQcCARYcaHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTANBgsrBgEE
AYOOIQECbjAIBgZngQwBAgIwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho
dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv
c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1PVlRMU0NBRzNCLnA3YzA3BgNVHR8E
MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy
bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD
AwNnADBkAjBAk+5BizdJhrED3wb7F2Gm7bWI60dlNd075nghT2TrR4NWKZtJZcbc
28xxIY/NONICMF6mPdsjosaqmvEMWqKcBh4vsIvXSKnQj5o8zrFB4oKS+accjYTe
8Dz3bQjauvy9uw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
MRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy
Ko42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv
TPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h
AQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB
QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF
BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB
AQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj
CeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y
mtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb
bERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl
CCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x
0k9mldz2dqI9nhCiY+TTIOOR
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu2gAwIBAgIQAMfEHv6S2CXwXq6EGkqCwTAKBggqhkjOPQQDAzBWMQsw
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTYw
MzMxWhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11
ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRVYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEJduoWmTTl9oLEFLp6lRuSHvv6pydnWigxerc/u91ppu11vwB
9Cv7ym60C81kwHZ+Nv/QXZeoE8WqDGyVMOec9fCGZGjxOAZCCqXIUYQLHiUlQOeQ
5LZUw4Uu1krx6GV9o4IBlTCCAZEwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
WUIgkFQwHQYDVR0OBBYEFD7cINmJRpiHe4c/2IFHslwV2EcGMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjBcBgNVHSAEVTBTMDkGCysGAQQBg44hAQABMCow
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
BAGDjiEBAngwBwYFZ4EMAQEwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho
dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv
c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1FVlRMU0NBRzNCLnA3YzA3BgNVHR8E
MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy
bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD
AwNpADBmAjEA9Jq999oaOUKhFwopw33iCmt0OHIn3gjT43GfuOHR/YckXdwoNdDA
lIEhz0NgRO3/AjEAvY/a3OMvktIZLYjn+WLKmp0QhrIMV+e6EpS7j4GVeOKouNp6
MN/P+ByGeiRMghzB
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL
zWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W
SvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB
AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB
Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF
BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB
AJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM
7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf
FVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk
2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM
m7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet
bAlwhwFT6AP8uIVMGUHNw24=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1
MDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk
aHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy
pDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV
bMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
WUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
AwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg
yuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd
AEKwaJ0mVzScy2+6
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzzCCAregAwIBAgIRAJHzR/Glt9OZYrleDuTimdEwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
YzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE
ACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFypDPjYw8C
vRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpVbMyLpQVh
3xsqRB0Bo4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw
HQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMA4GA1UdDwEB/wQEAwIBBjAS
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAxBgNVHSAEKjAoMAgGBmeBDAECATANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO
IQECZDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B
QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC
AQEAr7b6Mx11TIJgVzHt6EH5qGNWeiiqoMrp8E5/k8s+jvupx1n083pZ5XOzUzND
wEUe0W4C4FIGUHNQ4XDwqOw0yK42xqm2dvCKgsko2UvDlT0haZVtTCeWMQOe5J/J
ziaMfZwsZNfZ3aaI6XPtWDZtwY8AaNfyaeCpbtm5GcGnyNWk4jSNpbtpo6NjfXuP
CFOm3rTjJ4zytP7Tso6iMm0L01cYFOlTmoPv4515R2r3SPnKDtijoB/D/vgxzojv
DafgyG9afX8+laO9NbMMz4sqC1vZx3984i/sBNFiugm3Vvdq5OdJRjuGC1QILIs6
dg/K3i8VQqRpbfaM9kIlMzZ3dQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu6gAwIBAgIPKWkmwiejoVJjkrGUgqUEMAoGCCqGSM49BAMDMFYxCzAJ
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM3
NDFaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk
aHJhIEluYzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZ
fjpRIOHv1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+
N7ulaVSfHAKycO1no4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
WUIgkFQwHQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
BAGDjiEBAm4wCAYGZ4EMAQICMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNT1ZUTFNDQUczQy5wN2MwNwYDVR0f
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
AwMDaAAwZQIxAMSZt2xsPeumRdeSMSQ3CbzIZeQ/q7GDCWfeE1Pq+mhgA6gRM44c
NVF3xJ+wsxJk+wIwFJbaDMokD8Ei6eNOiA8yUMYvfzQyngmrhZQMdPCK1SewrR79
4rSb9vGqxZzelQth
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzzCCAregAwIBAgIRAI1aSBGTnvRH13ERzdmEmukwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
YzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE
ACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZfjpRIOHv
1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+N7ulaVSf
HAKycO1no4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw
HQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMA4GA1UdDwEB/wQEAwIBBjAS
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAxBgNVHSAEKjAoMAgGBmeBDAECAjANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO
IQECbjBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B
QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC
AQEAHlbhZpjhkPEAQVhPHRtBDaHqiisVD20pQ1TUeSx3l62Qe7NQ0b7vWQy7DN0i
KOu7BGpP5EnIjvXbz9FhpfPJ4FhKEt3tQ51rqXz2oD8vRaryvExWPobDUz3j7HN6
OzBsVMIe3PD7AAkpky0x8zMSi/ep5R9OMzybmNptbtppyuB5ytgq+h7lQdRIP+CQ
TaJX3qTgfMqzntYre5GljmEB5hU2/rSn8zoSU2APkJidlZFpghtgbD4I2VRHZvCw
FFr/SrFM9pwrgbJNZ1MtyTSVgI5NlNb0aMfLR+/r6jJt3ROeY2yh2QQ2dx+Yqgdb
uL5ChFWOQU+ErSDnUq7yr+pjBQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz
OTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11
ZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49
AgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+
bSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm
xRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9
i1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU
BggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq
MCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG
AQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
AwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc
9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f
Aal1GNxsOutLnr0Y
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzTCCArWgAwIBAgIQBfB4dS3s56G7hZXTAxZxcTANBgkqhkiG9w0BAQsFADB7
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4
MTIzMTIzNTk1OVowQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5j
MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAAQtVugppvuCxkpGl9lgr5UekGeukfkm00MG2kioqut4abamfm0nQm4EQqoD
s24ZiN/2NlFO4c8H9bpLP/KgZkWufTHVhmrTGqPSRusLMXfumct1ZsUWznSAimaP
kfDEf8qjggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQUpiVegKlNt/OWfBvHkd+SvO5L618wDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB
AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB
Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF
BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB
AIFfHiaMSjnrdZygRYV4ekRBmG7ybcFk4hGQWMMyMFi0cAEk2+dGfz5bl8IaIbLq
z+uVJzhOgVEX7WqIfgbWfFsfPYHYjPHBmy7SlZ3BwA86IQ6qF/etpX2/qL9A5yRN
N3nVowUpaqV6H3aCpkAh3wR2ZNkvAbLDvPz3J0l+sEWjWv+ZISOV4R+hXDTdPXV0
1lKxnbwqEsdo/sE2QkCDKwFrZh/0uKjWspydJj+jSaDLTrGNx50Dgmn7VEcD1ZI5
FXjknVUiO25YlmS1dtVZSBbyUKSjlrP/yDwPcZunoAsmCAAtCKsFLviLaymzvsKI
eSSDGxpSySp2VCxompGDk8g=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDWzCCAuCgAwIBAgIPeBiTP4+FtGGaKzX7qD0KMAoGCCqGSM49BAMDMFUxCzAJ
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
MR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2MDgy
N1oXDTQwMDQyODE4MzAwMFowPjELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRo
cmEgQlYxGjAYBgNVBAMMEUVNIE9WIENTIENBIC0gRzNCMHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAEqrsPsGZqz88B+tUr/P1MK/LLPDSX3nOVDlgZth/mlQmHQcjEkUOb
NKBDHK/+W/m0wcjorgAB+fTcitgsWfNjnpbNbD1NsM7+wmw9YlF7N3YTcnQ/jord
d7EAZPKr5eBQo4IBizCCAYcwHwYDVR0jBBgwFoAUaHQD+IXKjVRQVx0W4MjeRQ2r
zS0wHQYDVR0OBBYEFHn66OzP2w11jaRY1p+27dx8Sd7mMBMGA1UdJQQMMAoGCCsG
AQUFBwMDMF4GA1UdIARXMFUwOQYLKwYBBAGDjiEBAAEwKjAoBggrBgEFBQcCARYc
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTAOBgwrBgEEAYOOIQECgUgwCAYG
Z4EMAQQBMHQGCCsGAQUFBwEBBGgwZjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3At
Yi5lbXNpZ24uY29tMD4GCCsGAQUFBzAChjJodHRwOi8vcmVwb3NpdG9yeS5lbXNp
Z24uY29tL2NlcnRzL0VNT1ZDU0NBRzNCLnA3YzA2BgNVHR8ELzAtMCugKaAnhiVo
dHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3Rjc2NhZzMuY3JsMA4GA1UdDwEB/wQE
AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMAoGCCqGSM49BAMDA2kAMGYCMQC/zr03
YEELsNKqzUtH2dVFi4BaKFdyrr/6G1dIWp8KAVgsw2/u4lyyboUDaC9jDvICMQC9
HTxku84tNX9dmHOtH7aC2Nu/JM0jTf6KjZAvovCJzzv1IFWSZpVLZibynMLXOrM=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDwzCCAqugAwIBAgIQQeMrc6wYXWzg10gLrPhOaTANBgkqhkiG9w0BAQsFADB7
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4
MTIzMTIzNTk1OVowPjELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYx
GjAYBgNVBAMMEUVNIE9WIENTIENBIC0gRzNCMHYwEAYHKoZIzj0CAQYFK4EEACID
YgAEqrsPsGZqz88B+tUr/P1MK/LLPDSX3nOVDlgZth/mlQmHQcjEkUObNKBDHK/+
W/m0wcjorgAB+fTcitgsWfNjnpbNbD1NsM7+wmw9YlF7N3YTcnQ/jordd7EAZPKr
5eBQo4IBLDCCASgwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD
VR0OBBYEFHn66OzP2w11jaRY1p+27dx8Sd7mMA4GA1UdDwEB/wQEAwIBBjASBgNV
HRMBAf8ECDAGAQH/AgEAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDIGA1UdIAQrMCkw
CAYGZ4EMAQQBMA0GCysGAQQBg44hAQABMA4GDCsGAQQBg44hAQKBSDBDBgNVHR8E
PDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9BQUFDZXJ0aWZpY2F0
ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6
Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAIh0OzWsdBYhY
7+yxPGyosRL+uUSN6/Mrd30fBuougs5FVWQ3cRgLhImnUUgXADY9buugWhMmfT3c
yU5T1MFXZXClCILm5mfg+oqm/ikUWJl3Ayf2GvYf2O++6vzqvkn418nI5Lpdt0O7
Li524YS/3CZHDA+zZQ7rrHpZXgQ9Hmp8AUQ4FneaGjNanFoYxiZxwCyW5z4u7AoK
0bmOVqKtcDFoVNz5++G9TYDJdhWOXmKKgzqRo1MK0PjazNT7MGHqknP93SqMVJ1g
mX/qNWxYteAjlvFanQxBNX/+TRxDjuFsEHO/7BvqSpIIck0UEEfizhN8zNpWLbcl
EqVgl0g39A==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDWzCCAuGgAwIBAgIPVEt/kiwxriIKSpIpFvbmMAoGCCqGSM49BAMDMFUxCzAJ
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
MR8wHQYDVQQDDBZlbVNpZ24gUm9vdCBDUyBDQSAtIEczMB4XDTI1MDQzMDE2Mzg0
MFoXDTQwMDQyODE4MzAwMFowPzELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRo
cmEgSW5jMRowGAYDVQQDDBFFTSBPViBDUyBDQSAtIEczQzB2MBAGByqGSM49AgEG
BSuBBAAiA2IABLnw4GNHAqacpqrdRDz9YsQbm6SyCSRpfXl052kaEm+5avs+NML9
O8X0gwYfJl3CpYQ2NHMjUQQ2so8E84YjYwZ5gOJQPgJTLWVGI1pxZaJZoe9Jb3Qb
X97neHII5Lj676OCAYswggGHMB8GA1UdIwQYMBaAFGh0A/iFyo1UUFcdFuDI3kUN
q80tMB0GA1UdDgQWBBTub3MpjW+/p7Mbw5U09hVWRHhmtzATBgNVHSUEDDAKBggr
BgEFBQcDAzBeBgNVHSAEVzBVMDkGCysGAQQBg44hAQABMCowKAYIKwYBBQUHAgEW
HGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDgYMKwYBBAGDjiEBAoFIMAgG
BmeBDAEEATB0BggrBgEFBQcBAQRoMGYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw
LWMuZW1zaWduLmNvbTA+BggrBgEFBQcwAoYyaHR0cDovL3JlcG9zaXRvcnkuZW1z
aWduLmNvbS9jZXJ0cy9FTU9WQ1NDQUczQy5wN2MwNgYDVR0fBC8wLTAroCmgJ4Yl
aHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290Y3NjYWczLmNybDAOBgNVHQ8BAf8E
BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQDAwNoADBlAjEA8yPg
8sntOhLwV2QXJ5LEaKSpo/fnE3uOgTadK73fDhyjiqr0Io9GVuQTYqBPbQ2TAjBg
euApmBS486+iei03lRrp139dPw6k28jjGNvWsAU7A1N86J1zPJVDEyVH+nMAWC0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIRANal72E7OQ8y75zfNJ93KEEwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
YzEaMBgGA1UEAwwRRU0gT1YgQ1MgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAAS58OBjRwKmnKaq3UQ8/WLEG5uksgkkaX15dOdpGhJvuWr7PjTC/TvF9IMG
HyZdwqWENjRzI1EENrKPBPOGI2MGeYDiUD4CUy1lRiNacWWiWaHvSW90G1/e53hy
COS4+u+jggEsMIIBKDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQU7m9zKY1vv6ezG8OVNPYVVkR4ZrcwDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwMgYDVR0gBCsw
KTAIBgZngQwBBAEwDQYLKwYBBAGDjiEBAAEwDgYMKwYBBAGDjiEBAoFIMEMGA1Ud
HwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FBQUNlcnRpZmlj
YXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0
cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBgGbZ+NDkn
2Fr5dCVs/51+6cT5iEPP1stglX4TI2UaNh9LmPdltxXw3crV8gK5L8i+d+ScGibF
y0obtBHNTldXDgLnAHIzY3n+pceyKj7O2ae9CYnyuu5RBifeI3XYKowWE+oXnVGF
Oy0CvCJ/JsdP6aYgoCa9hGnAYicYQQL6MOMQZ5PuhcSM8t4giIzrpgrEkUP93GYE
emDG5Z8BYwKs80lLeQNBnwg/VHJ3iEF/o84RXYwrlrKqJ+33C094o6LNikMJCqOK
/WYHZSR8Xyhf8GXFAj5pZuI6Bcpug2V/E7i42j8OBhc25aNYDqK+B0qmb90sjNUB
g3tJeSIb/599
-----END CERTIFICATE-----
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIICQDCCAcagAwIBAgIUU3mgjls/8ZJ8upmukdlSQR1IF/UwCgYIKoZIzj0EAwIw
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
IERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDFaFw0yNzA1MDMwMjI3NDFa
MDMxMTAvBgNVBAMMKGR2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl
bS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATK3sfcOxxtM2Vdq9Qbw/GR
NsHlJcnqINEInXO3fkWhw/FYT80hkqGk0jSN/EKib3YZJsr5GyfkYcpEiA2rwMgm
o4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMDMGA1UdEQQsMCqCKGR2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl
LWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFDggfJFz1eeToKmzFBSYkMCvpAu0MB8G
A1UdIwQYMBaAFOIeL+ccGTkOVUtiwSAkVzUkiiUUMAoGCCqGSM49BAMCA2gAMGUC
MDqDDOt7K313QIH0rhn/ZKnzFu8+/9W8OlTuFtDfVCTRDfRmqdyfsCfLsEk7xqHq
uQIxAJqyLERABmU98U46OCuT5bp5h0ytuSEIGM7orTpKci5S1u1B/AOmwv162U7R
TcVPHA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzjCCAragAwIBAgIRAKAk/fS6Zlx4XOSgIEg4WLcwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
MRswGQYDVQQDDBJFTSBEViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAATnntnASpdzxHNeUREdqFrCvV5kqzdLgOEsvCA47B5PDjuBL8mFnh5nr7BO
L4vJf3SFumWqkR7opORbYtXMA9Vr5nizw4y03JN8okYOI4RTyJKTXSnUT5HhVga2
PM7atGijggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQU4h4v5xwZOQ5VS2LBICRXNSSKJRQwDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDEGA1UdIAQqMCgwCAYGZ4EMAQIBMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h
AQJkMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB
QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF
BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB
AQBzQ1IoqpdotM6gFrR4Q3YrKifOLeaj5XXunldpVE/vZn2TxC2y0s8oYiLR5ICE
wqVXSB7N6BM1Mp+Nif7Jbhb2IswAfSR4Iik8+B0iRmi7lYoNlycCgHjmsjOmZaxM
QkPmjXAJbG5f4V41OsvJZmS7lI3r0IuvEDVX3GAZ13on6IImSs9ybNpHQ7NCqkMQ
7wH4uAO1dtpFycgJSdHaCOhHjrox2BTYGLdpUrYwY3EOAjlZ8UGgsxs7mG+xquMl
bN1LFURwfnkVMP3T/DBIZ/ys2+lNFyxK3FlArrV4/000uTnVB1lO8oGORYdXoT5V
GUyRCEIDhF5TBUuKF4E3kf1h
-----END CERTIFICATE-----
@@ -0,0 +1,56 @@
-----BEGIN CERTIFICATE-----
MIICOTCCAb6gAwIBAgIUY1R/PHrtXShV0uhLJ5EyjsHvMi0wCgYIKoZIzj0EAwIw
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
IERWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMDU5MzZaFw0yNzA1MDMwMTA0MzZa
MC8xLTArBgNVBAMMJGR2LWczYi1lbXVkaHJhLndlLWhhdmUtYS1wcm9ibGVtLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFpE24ACBFHQLzpg+7D767thJ3+Q
NIoJJugUMT/OftEz6G6uDdxlS+9VfXYBDglHjJirjEPFNglOGJoQNQuiz7Gjgacw
gaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
BQUHAwEwLwYDVR0RBCgwJoIkZHYtZzNiLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
ZW0uY29tMB0GA1UdDgQWBBT4r5v4etYNuG21BqBqs273OkcHiDAfBgNVHSMEGDAW
gBTiHi/nHBk5DlVLYsEgJFc1JIolFDAKBggqhkjOPQQDAgNpADBmAjEAlxNF+/8l
APpgibawACy15yn5bcvTksz2Zw0R9z9nUK9e3MSSk+hXagg7LVb7V4NtAjEA4qyR
laFPkaokrnTjqQieq8CgDQ6tIz3FjBvj3D7vkhKXHaml6zD2zT5ih2Y+lD2h
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu6gAwIBAgIQAKSu5njyM2K0wORgt3LyFDAKBggqhkjOPQQDAzBWMQsw
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTU1
NDA4WhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11
ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAE557ZwEqXc8RzXlERHahawr1eZKs3S4DhLLwgOOweTw47gS/J
hZ4eZ6+wTi+LyX90hbplqpEe6KTkW2LVzAPVa+Z4s8OMtNyTfKJGDiOEU8iSk10p
1E+R4VYGtjzO2rRoo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
WUIgkFQwHQYDVR0OBBYEFOIeL+ccGTkOVUtiwSAkVzUkiiUUMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
aHR0cDovL29jc3AtYi5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQi5wN2MwNwYDVR0f
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
AwMDaAAwZQIwLmsGXi6oFMJ2klkcEMXMug6gvxQWZWkewiDWVzQyl2L6z6DuQfPO
Ibh1grvdULDCAjEAxZvLlxUavG69J6wnSjC8XBbqNIQsJ6CE2QdNOe84Ptxb6VG8
gt2TlfJbHii+KNvK
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
-----END CERTIFICATE-----
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIICQjCCAcegAwIBAgIUMH3z62Z6agEULBZqrfhpw9/ZpCAwCgYIKoZIzj0EAwIw
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
TSBEViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQxWhcNMjcwNTAzMDIyNzQx
WjAzMTEwLwYDVQQDDChkdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
ZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAMPpHsQqEqIerrj/durO
EN7gkUVJSVgIPN6+VJ2sEGXDyqXAN1fnNzGuASP/4WEr0RIhht3cYvuUYp2SYofr
w6OBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
BggrBgEFBQcDATAzBgNVHREELDAqgihkdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2
ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBT8CNQz4OLVnIwrQYZBF+++9jTCvzAf
BgNVHSMEGDAWgBSufhBcPCmWwahHJwVZ8eA91OUK8zAKBggqhkjOPQQDAgNpADBm
AjEAp3ZLdfBvKcZxgR2p8LZaxWzP6uB7zU/EZ9BA49Ds2P5ByYkRodwK5qcwt4J/
VL/vAjEAnnxrcrDX7SzoJxuho604GBh6eyBLJLCdmcy+qb+LA3u5oxP/OjAzCA4x
v1kw6Tm4
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzzCCAregAwIBAgIRAJHzR/Glt9OZYrleDuTimdEwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
YzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE
ACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFypDPjYw8C
vRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpVbMyLpQVh
3xsqRB0Bo4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw
HQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMA4GA1UdDwEB/wQEAwIBBjAS
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAxBgNVHSAEKjAoMAgGBmeBDAECATANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO
IQECZDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B
QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC
AQEAr7b6Mx11TIJgVzHt6EH5qGNWeiiqoMrp8E5/k8s+jvupx1n083pZ5XOzUzND
wEUe0W4C4FIGUHNQ4XDwqOw0yK42xqm2dvCKgsko2UvDlT0haZVtTCeWMQOe5J/J
ziaMfZwsZNfZ3aaI6XPtWDZtwY8AaNfyaeCpbtm5GcGnyNWk4jSNpbtpo6NjfXuP
CFOm3rTjJ4zytP7Tso6iMm0L01cYFOlTmoPv4515R2r3SPnKDtijoB/D/vgxzojv
DafgyG9afX8+laO9NbMMz4sqC1vZx3984i/sBNFiugm3Vvdq5OdJRjuGC1QILIs6
dg/K3i8VQqRpbfaM9kIlMzZ3dQ==
-----END CERTIFICATE-----
@@ -0,0 +1,56 @@
-----BEGIN CERTIFICATE-----
MIICODCCAb+gAwIBAgIUbWvtwSNK/Qro223vAqyz323+VBAwCgYIKoZIzj0EAwIw
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
TSBEViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDA1OTM2WhcNMjcwNTAzMDEwNDM2
WjAvMS0wKwYDVQQDDCRkdi1nM2MtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxlbS5j
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAScdBQpNUOOCcag6YEVFccNRSFe
UIl02iH53vDfoWPJFS005WzqF3CPDclB/kygP2gWF4pg/r5gPYdnI3/6Vdzro4Gn
MIGkMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG
AQUFBwMBMC8GA1UdEQQoMCaCJGR2LWczYy1lbXVkaHJhLndlLWhhdmUtYS1wcm9i
bGVtLmNvbTAdBgNVHQ4EFgQUBj2yGO/TzDOE/u/6QeUvcW8y1iMwHwYDVR0jBBgw
FoAUrn4QXDwplsGoRycFWfHgPdTlCvMwCgYIKoZIzj0EAwIDZwAwZAIwBQOOQqG9
ccdzM/7XIlXLtota1esALaLmgEN/ZweosBIvk4xabHcERZInWVIwO45NAjBos8Ue
NwDe1X3ahdK2lkxqBEoBMtUHJN18TnJKs2nK1gmXpMbmY5oKnAQQgCEZPA4=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu6gAwIBAgIPWIsuPCu4zaE+GhIJJUMhMAoGCCqGSM49BAMDMFYxCzAJ
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNTM1
MDlaFw00MDA0MjgxODMwMDBaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVk
aHJhIEluYzEbMBkGA1UEAwwSRU0gRFYgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEOnp4YsjETTJN3WkDOmeCwO/u/kBV5ooSP6A3G6O9kZRRTuFy
pDPjYw8CvRiAN9nlFJ+bd9uUooHfmHsv6oiLdL3+j9pMEJnubKGVZv4+8OX2sJpV
bMyLpQVh3xsqRB0Bo4IBljCCAZIwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
WUIgkFQwHQYDVR0OBBYEFK5+EFw8KZbBqEcnBVnx4D3U5QrzMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjBdBgNVHSAEVjBUMDkGCysGAQQBg44hAQABMCow
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
BAGDjiEBAmQwCAYGZ4EMAQIBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRFZUTFNDQUczQy5wN2MwNwYDVR0f
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
AwMDaAAwZQIxAMEN2HWMocbuWdy1d33pH6JpMvP/V/DIS5PtZbv1Q7TXEbxrcyxg
yuddOJscYDJJlAIwB4sH1BpbScDzkdD4tJBSwrG/1Mx82vb8YaySehNuu1+1oXNd
AEKwaJ0mVzScy2+6
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
-----END CERTIFICATE-----
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIICQDCCAcagAwIBAgIUUYu3O8bpN1GPvCIHqWpT/FYUNDowCgYIKoZIzj0EAwIw
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
IEVWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDFaFw0yNzA1MDMwMjI3NDFa
MDMxMTAvBgNVBAMMKGV2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl
bS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASjDLpnVdmvXM6R3UB2o42a
1GnrYfxRXo7UII36mBl9mIS7p9MkAryhPdxitFHkgTrrbypVtzpydNtJIGGGXUwE
o4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMDMGA1UdEQQsMCqCKGV2LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl
LWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFEVBY1ecTN9bLMDl4ifdnN2WZL+WMB8G
A1UdIwQYMBaAFD7cINmJRpiHe4c/2IFHslwV2EcGMAoGCCqGSM49BAMCA2gAMGUC
MQCyvr9gktgmtsuTuFleFV+YExNNKLf3GZieuUprQcERrzh798oYKavTqZeYf9Kz
RhECMCVs1B7KPh7/lgHRly14Z8RJA8ApR7e2cRREW5uPKFOSDlZ/RIezxsfEzsQ4
lAODeA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzTCCArWgAwIBAgIRANj4aF5PK9nYJAKoWhwetzEwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAAQl26haZNOX2gsQUunqVG5Ie+/qnJ2daKDF6tz+73Wmm7XW/AH0K/vKbrQL
zWTAdn42/9Bdl6gTxaoMbJUw55z18IZkaPE4BkIKpchRhAseJSVA55DktlTDhS7W
SvHoZX2jggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQUPtwg2YlGmId7hz/YgUeyXBXYRwYwDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB
AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB
Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF
BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB
AJNFuAFW9Gnw/8+kCO4KIlZdnM1Sh2u+8cK0EYAHhXpb5FlGvYkcARigo5oS7VoM
7FOYlTueV6yICZKNAYawZPJPhNCGK+rp3cQBEz6d0n8OZEYZov9lb3ll8ORgSDuf
FVIW6Gagg/UnOamLBTOR8eD5V73SCme/B2cHUDw+m5E8cc+0Ujzz3Cn3ex5JrhXk
2T3s8PoVzEmhTgGboFfcWO1EYhPPrYwdxPYRQIXAXUXhLEOVNL9ASbZJLFI8E1IM
m7NwxVN2s38jWpaCUaaF0tg7hfiy7oe13rzJfHsdqldJVTTUTPWVx8TC4ju0VLet
bAlwhwFT6AP8uIVMGUHNw24=
-----END CERTIFICATE-----
@@ -0,0 +1,56 @@
-----BEGIN CERTIFICATE-----
MIICNzCCAb6gAwIBAgIUPfthO0PYGAYKs9lQ4gp1IOkOr2MwCgYIKoZIzj0EAwIw
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
IEVWIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMDU5MzZaFw0yNzA1MDMwMTA0MzZa
MC8xLTArBgNVBAMMJGV2LWczYi1lbXVkaHJhLndlLWhhdmUtYS1wcm9ibGVtLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPZPwlA3io/AX/pMwu0dNl9WMxrr
C9j/xMLHgG72cWVzoX62REmvp9zyXQ04kHP9/sW6KnCcm5ZZ5i4YoJbDPASjgacw
gaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
BQUHAwEwLwYDVR0RBCgwJoIkZXYtZzNiLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
ZW0uY29tMB0GA1UdDgQWBBSvX4QR4ERW5uNUaMq0yYzXCZf0ZjAfBgNVHSMEGDAW
gBQ+3CDZiUaYh3uHP9iBR7JcFdhHBjAKBggqhkjOPQQDAgNnADBkAjACy/uJR01M
/O9IEZyRHX1LSU28bmg542K0IX9jDRTrZXUqkDWfiZIL7xfnrRrBpHoCMFbBIaOa
WwWtFtDg9ZqQvktsWS950u5qfW1sChgWNya/E3IMKFgtoHm2xrZsmfwLSQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu2gAwIBAgIQAMfEHv6S2CXwXq6EGkqCwTAKBggqhkjOPQQDAzBWMQsw
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTYw
MzMxWhcNNDAwNDI4MTgzMDAwWjA/MQswCQYDVQQGEwJOTDETMBEGA1UECgwKZU11
ZGhyYSBCVjEbMBkGA1UEAwwSRU0gRVYgVExTIENBIC0gRzNCMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAEJduoWmTTl9oLEFLp6lRuSHvv6pydnWigxerc/u91ppu11vwB
9Cv7ym60C81kwHZ+Nv/QXZeoE8WqDGyVMOec9fCGZGjxOAZCCqXIUYQLHiUlQOeQ
5LZUw4Uu1krx6GV9o4IBlTCCAZEwHwYDVR0jBBgwFoAU4TqrIXzx401UrtRyt/2L
WUIgkFQwHQYDVR0OBBYEFD7cINmJRpiHe4c/2IFHslwV2EcGMB0GA1UdJQQWMBQG
CCsGAQUFBwMBBggrBgEFBQcDAjBcBgNVHSAEVTBTMDkGCysGAQQBg44hAQABMCow
KAYIKwYBBQUHAgEWHGh0dHA6Ly9yZXBvc2l0b3J5LmVtc2lnbi5jb20wDQYLKwYB
BAGDjiEBAngwBwYFZ4EMAQEwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho
dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv
c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1FVlRMU0NBRzNCLnA3YzA3BgNVHR8E
MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy
bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD
AwNpADBmAjEA9Jq999oaOUKhFwopw33iCmt0OHIn3gjT43GfuOHR/YckXdwoNdDA
lIEhz0NgRO3/AjEAvY/a3OMvktIZLYjn+WLKmp0QhrIMV+e6EpS7j4GVeOKouNp6
MN/P+ByGeiRMghzB
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
-----END CERTIFICATE-----
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIICQDCCAcegAwIBAgIUTMUrGzSU+0BySFdrN3ALZq9Spv8wCgYIKoZIzj0EAwIw
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
TSBFViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQyWhcNMjcwNTAzMDIyNzQy
WjAzMTEwLwYDVQQDDChldi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
ZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEglClDbotLFFeY9SpryT4
k958tvriY2DDnaPsmjQaecRhB6+L/8hLw6FMt+HT8SLvjIZnFY+AH92cBjedcNST
AKOBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
BggrBgEFBQcDATAzBgNVHREELDAqgihldi1nM2MtYWFhLWVtdWRocmEud2UtaGF2
ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBSVtGbn6xUT1iosUeaGA15Nq640aTAf
BgNVHSMEGDAWgBSmJV6AqU2385Z8G8eR35K87kvrXzAKBggqhkjOPQQDAgNnADBk
AjAg7b5bIK9X50QEX0U6G8fQh12BnFQ079Iluzd0Y2TJWCjtVXZi3nOciM8RzNKM
tZECMCBf0JOyMt8RNS93Wj8kYbWkCtoKoUrqx7G3JTfGReKci+DdTvkF8/3F6doi
9KB22Q==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzTCCArWgAwIBAgIQBfB4dS3s56G7hZXTAxZxcTANBgkqhkiG9w0BAQsFADB7
MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTI1MDUxNDAwMDAwMFoXDTI4
MTIzMTIzNTk1OVowQDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5j
MRswGQYDVQQDDBJFTSBFViBUTFMgQ0EgLSBHM0MwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAAQtVugppvuCxkpGl9lgr5UekGeukfkm00MG2kioqut4abamfm0nQm4EQqoD
s24ZiN/2NlFO4c8H9bpLP/KgZkWufTHVhmrTGqPSRusLMXfumct1ZsUWznSAimaP
kfDEf8qjggE0MIIBMDAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQUpiVegKlNt/OWfBvHkd+SvO5L618wDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDAGA1UdIAQpMCcwBwYFZ4EMAQEwDQYLKwYBBAGDjiEBAAEwDQYLKwYBBAGDjiEB
AngwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFB
Q2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUF
BzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQADggEB
AIFfHiaMSjnrdZygRYV4ekRBmG7ybcFk4hGQWMMyMFi0cAEk2+dGfz5bl8IaIbLq
z+uVJzhOgVEX7WqIfgbWfFsfPYHYjPHBmy7SlZ3BwA86IQ6qF/etpX2/qL9A5yRN
N3nVowUpaqV6H3aCpkAh3wR2ZNkvAbLDvPz3J0l+sEWjWv+ZISOV4R+hXDTdPXV0
1lKxnbwqEsdo/sE2QkCDKwFrZh/0uKjWspydJj+jSaDLTrGNx50Dgmn7VEcD1ZI5
FXjknVUiO25YlmS1dtVZSBbyUKSjlrP/yDwPcZunoAsmCAAtCKsFLviLaymzvsKI
eSSDGxpSySp2VCxompGDk8g=
-----END CERTIFICATE-----
@@ -0,0 +1,56 @@
-----BEGIN CERTIFICATE-----
MIICOTCCAb+gAwIBAgIUOcw5N4XGLiB/3GvB05bJuGoZPR0wCgYIKoZIzj0EAwIw
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
TSBFViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDA1OTM3WhcNMjcwNTAzMDEwNDM3
WjAvMS0wKwYDVQQDDCRldi1nM2MtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxlbS5j
b20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQo7U02MAUO8EEMomXwIl+kSQzm
b+nvXKIZ/hC4rZ1GpXchv4SzTrV1SB/Qg+pD6OBEXbBBnPmQNkePNQTlujqRo4Gn
MIGkMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG
AQUFBwMBMC8GA1UdEQQoMCaCJGV2LWczYy1lbXVkaHJhLndlLWhhdmUtYS1wcm9i
bGVtLmNvbTAdBgNVHQ4EFgQUqxe+7qS+MxNyioJZhQsL5MEEw6swHwYDVR0jBBgw
FoAUpiVegKlNt/OWfBvHkd+SvO5L618wCgYIKoZIzj0EAwIDaAAwZQIwC/CxSIov
wcar3sV98B8cL+QuR2kkJdLdXsFKzg/HDSJ1MOjqTGm924LB6r9nl0f7AjEAzcwr
WQOYY9eycqIz+LG2daYS6LOctj+CjoAk/CLugWPxGxp4Aerfau2P1z1h5jAA
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDaDCCAu6gAwIBAgIQAIAJkopkEHYBrwX6LxnxCTAKBggqhkjOPQQDAzBWMQsw
CQYDVQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl
ZDEgMB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwHhcNMjUwNDMwMTUz
OTMxWhcNNDAwNDI4MTgzMDAwWjBAMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZU11
ZGhyYSBJbmMxGzAZBgNVBAMMEkVNIEVWIFRMUyBDQSAtIEczQzB2MBAGByqGSM49
AgEGBSuBBAAiA2IABC1W6Cmm+4LGSkaX2WCvlR6QZ66R+SbTQwbaSKiq63hptqZ+
bSdCbgRCqgOzbhmI3/Y2UU7hzwf1uks/8qBmRa59MdWGatMao9JG6wsxd+6Zy3Vm
xRbOdICKZo+R8MR/yqOCAZUwggGRMB8GA1UdIwQYMBaAFOE6qyF88eNNVK7Ucrf9
i1lCIJBUMB0GA1UdDgQWBBSmJV6AqU2385Z8G8eR35K87kvrXzAdBgNVHSUEFjAU
BggrBgEFBQcDAQYIKwYBBQUHAwIwXAYDVR0gBFUwUzA5BgsrBgEEAYOOIQEAATAq
MCgGCCsGAQUFBwIBFhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMA0GCysG
AQQBg44hAQJ4MAcGBWeBDAEBMHUGCCsGAQUFBwEBBGkwZzAkBggrBgEFBQcwAYYY
aHR0cDovL29jc3AtYy5lbXNpZ24uY29tMD8GCCsGAQUFBzAChjNodHRwOi8vcmVw
b3NpdG9yeS5lbXNpZ24uY29tL2NlcnRzL0VNRVZUTFNDQUczQy5wN2MwNwYDVR0f
BDAwLjAsoCqgKIYmaHR0cDovL2NybC5lbXNpZ24uY29tLz9yb290dGxzY2FnMy5j
cmwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwCgYIKoZIzj0E
AwMDaAAwZQIwBy1wTnzH4lXDTy2PD91ysLjvnlVN3JbJmc1BIMEU8TOLkYVHibMc
9HeyonikFSfmAjEA6uE3QRxlrZTz/1/lXPTEMUhoauAtycagtlNFp79kjhvNa2/f
Aal1GNxsOutLnr0Y
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
-----END CERTIFICATE-----
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIICPzCCAcagAwIBAgIUH0gWfBRnOiSjcduti/cukjDGAjIwCgYIKoZIzj0EAwIw
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
IE9WIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMjIyNDJaFw0yNzA1MDMwMjI3NDJa
MDMxMTAvBgNVBAMMKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZlLWEtcHJvYmxl
bS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ3EfjVE+6fUm9gr7UoUv6N
YPttIdILR9OMZnAdTj+RWT1Z4fp7dpC5mat1gbIo4J6EDelcOQQz5/oGeB3gFJKw
o4GrMIGoMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMDMGA1UdEQQsMCqCKG92LWczYi1hYWEtZW11ZGhyYS53ZS1oYXZl
LWEtcHJvYmxlbS5jb20wHQYDVR0OBBYEFKzQPp6gTBOWb0TBxZbkL5zsQn3oMB8G
A1UdIwQYMBaAFJvYZVGv5+csN60I7mkZ2b83hK4KMAoGCCqGSM49BAMCA2cAMGQC
MARKrRRBEWY2Ix1d80DGUvC7bWENP1Q9pf6BOGF4+84UiwV5+nAuht/+hdpAanWB
sQIwJF2dW8tpPN6lTm1kDerzG6yJAJOz4drotaTthMF2+efuv5W2FL7Dr8qZSd1z
201o
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzjCCAragAwIBAgIRAOlCI73pL5FOWrmsq/2sNxAwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVkaHJhIEJW
MRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK56Ks1xwy
Ko42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDftDIVOTMEv
TPhkbQ2jggE1MIIBMTAfBgNVHSMEGDAWgBSgEQojPpbxB+zirynvgqV/0DCktDAd
BgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MDEGA1UdIAQqMCgwCAYGZ4EMAQICMA0GCysGAQQBg44hAQABMA0GCysGAQQBg44h
AQJuMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0FB
QUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEF
BQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBCwUAA4IB
AQALRoOSvkzRa+tN5Rje9W5GiXd/qsicd9N6Gnh0SWC7VyWH+HycIqAnFCMYZznj
CeGusomFYHE42SHWr85JDIbgzy6y3dO4i9n9w0Bx7yrXWBbDzFS8VgA3+EY4Cg7y
mtoC3RPOJVtaXXPZ+68X804g1Mj+3vCSHj2lNPVgco0OzSIOJ/KMg1R9MLBkiKzb
bERAGZHejY0HBhYd76IINZN8z6bBNah2UdFYxsOisLynPQUkol6gRLum024/FzVl
CCDfnUusrkr+mAfPAvD/Vb8bhpA7jYtepuU+2mIdShj5lLlvAwZphn+ypWycuo2x
0k9mldz2dqI9nhCiY+TTIOOR
-----END CERTIFICATE-----
@@ -0,0 +1,56 @@
-----BEGIN CERTIFICATE-----
MIICODCCAb6gAwIBAgIUPXjC97k7buQHZ7p5ehDU8QNyrR4wCgYIKoZIzj0EAwIw
PzELMAkGA1UEBhMCTkwxEzARBgNVBAoMCmVNdWRocmEgQlYxGzAZBgNVBAMMEkVN
IE9WIFRMUyBDQSAtIEczQjAeFw0yNjEwMjAwMDU5MzdaFw0yNzA1MDMwMTA0Mzda
MC8xLTArBgNVBAMMJG92LWczYi1lbXVkaHJhLndlLWhhdmUtYS1wcm9ibGVtLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABA1haD+95Albk0UN6YtDCPpqevTv
YUmEhsIVpb/hnhsbBlH0yLthSE2JLH14ywiQJXttoc/pDWC9eoICW6CJXL2jgacw
gaQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYB
BQUHAwEwLwYDVR0RBCgwJoIkb3YtZzNiLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
ZW0uY29tMB0GA1UdDgQWBBQUhzc4s202Hn62G0srmgguYdY2MTAfBgNVHSMEGDAW
gBSb2GVRr+fnLDetCO5pGdm/N4SuCjAKBggqhkjOPQQDAgNoADBlAjEAoJ6d4zzA
xE349yGHHrwKZpVCC/HBFutcOpOfzQoe8ikbmaaBTdtKai5TEllwt3dBAjBeHGiX
VheDhJfewbei99cw1MfYB5tPwdfuY5pe1MJz0Tg9zJ4JgxBvF6LTT0OOXZ4=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDZjCCAu2gAwIBAgIPbK2jZ/scEACIEg6iAtVoMAoGCCqGSM49BAMDMFYxCzAJ
BgNVBAYTAklOMSUwIwYDVQQKDBxlTXVkaHJhIFRlY2hub2xvZ2llcyBMaW1pdGVk
MSAwHgYDVQQDDBdlbVNpZ24gUm9vdCBUTFMgQ0EgLSBHMzAeFw0yNTA0MzAxNjAw
MTJaFw00MDA0MjgxODMwMDBaMD8xCzAJBgNVBAYTAk5MMRMwEQYDVQQKDAplTXVk
aHJhIEJWMRswGQYDVQQDDBJFTSBPViBUTFMgQ0EgLSBHM0IwdjAQBgcqhkjOPQIB
BgUrgQQAIgNiAARziXqg6zz9yosSQC/68KvE4SErr43ud4rJ7i1tuD+sKAxeBWOK
56Ks1xwyKo42HN659pxDArndjXPRzGyqsswK7k4hbOl53VGKSvS2JuVoTN1lgDft
DIVOTMEvTPhkbQ2jggGWMIIBkjAfBgNVHSMEGDAWgBThOqshfPHjTVSu1HK3/YtZ
QiCQVDAdBgNVHQ4EFgQUm9hlUa/n5yw3rQjuaRnZvzeErgowHQYDVR0lBBYwFAYI
KwYBBQUHAwEGCCsGAQUFBwMCMF0GA1UdIARWMFQwOQYLKwYBBAGDjiEBAAEwKjAo
BggrBgEFBQcCARYcaHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbTANBgsrBgEE
AYOOIQECbjAIBgZngQwBAgIwdQYIKwYBBQUHAQEEaTBnMCQGCCsGAQUFBzABhhho
dHRwOi8vb2NzcC1iLmVtc2lnbi5jb20wPwYIKwYBBQUHMAKGM2h0dHA6Ly9yZXBv
c2l0b3J5LmVtc2lnbi5jb20vY2VydHMvRU1PVlRMU0NBRzNCLnA3YzA3BgNVHR8E
MDAuMCygKqAohiZodHRwOi8vY3JsLmVtc2lnbi5jb20vP3Jvb3R0bHNjYWczLmNy
bDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAKBggqhkjOPQQD
AwNnADBkAjBAk+5BizdJhrED3wb7F2Gm7bWI60dlNd075nghT2TrR4NWKZtJZcbc
28xxIY/NONICMF6mPdsjosaqmvEMWqKcBh4vsIvXSKnQj5o8zrFB4oKS+accjYTe
8Dz3bQjauvy9uw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDdDCCAvmgAwIBAgIQAJYaPvsTWVdu0EAFtGhKWzAKBggqhkjOPQQDAzBrMQsw
CQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhy
YSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290
IENBIC0gRzMwHhcNMjQwNzExMTU0MzM2WhcNMzkwNzA5MTgzMDAwWjBWMQswCQYD
VQQGEwJJTjElMCMGA1UECgwcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg
MB4GA1UEAwwXZW1TaWduIFJvb3QgVExTIENBIC0gRzMwdjAQBgcqhkjOPQIBBgUr
gQQAIgNiAARfD9w/q1cWxL6D9bMGH6CQ4u+gAV6GN9s+PwQx9Bl4hC+0iU8QOk8m
WO/RwXQy10DCSxT8vSIJClHKp+Ao4qJKdYeV0SwZ7LMQziyLfxIUOY6Grja/sQFw
LKYf8EdDXAyjggF1MIIBcTAfBgNVHSMEGDAWgBR8XQKEE9TMipuBzhccLikenEhj
QjAdBgNVHQ4EFgQU4TqrIXzx401UrtRyt/2LWUIgkFQwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIB
FhxodHRwOi8vcmVwb3NpdG9yeS5lbXNpZ24uY29tMHgGCCsGAQUFBwEBBGwwajAi
BggrBgEFBQcwAYYWaHR0cDovL29jc3AuZW1zaWduLmNvbTBEBggrBgEFBQcwAoY4
aHR0cDovL3JlcG9zaXRvcnkuZW1zaWduLmNvbS9jZXJ0cy9lbVNpZ25FQ0NSb290
Q0FHMy5jcnQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5lbXNpZ24uY29t
P1Jvb3RDQUczLmNybDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB
ATAKBggqhkjOPQQDAwNpADBmAjEAt9CIpRGp46QHQYDUBnLKw+vvK0/h9vmilEAg
TroUl/anH4x2rXHqiCtBTBfgKDegAjEA8WpG1LTZdAIL0FSYqxXeREaeTSxEofM0
1jJH3jGvCXC79LU9UPJ2o3TU+UIj2gMV
-----END CERTIFICATE-----
@@ -0,0 +1,38 @@
-----BEGIN CERTIFICATE-----
MIICQTCCAcegAwIBAgIULuWnWW6kFV6QZxp2ls8r18NCjXkwCgYIKoZIzj0EAwIw
QDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC2VNdWRocmEgSW5jMRswGQYDVQQDDBJF
TSBPViBUTFMgQ0EgLSBHM0MwHhcNMjYxMDIwMDIyMjQyWhcNMjcwNTAzMDIyNzQy
WjAzMTEwLwYDVQQDDChvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2ZS1hLXByb2Js
ZW0uY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb7+5g2YHNYwH2eQERrZc
TmVuf/RmvzsGGMSwXmZke6vn35Ut5HME7UOOU3Muy+BSfjfpo6fjL5KOs3Ndz67d
OKOBqzCBqDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
BggrBgEFBQcDATAzBgNVHREELDAqgihvdi1nM2MtYWFhLWVtdWRocmEud2UtaGF2
ZS1hLXByb2JsZW0uY29tMB0GA1UdDgQWBBQ7oBzq3jpTuvntbIkzMV12Ps3O0TAf
BgNVHSMEGDAWgBQ06uoM68kK2vnQxeLtm3jJh54bbDAKBggqhkjOPQQDAgNoADBl
AjEA/Y7AZ4M2MYbN5Od9tREaUfFjZKoOjczfIKPjKNMjtGxTdL9WUXvAT4QDgktn
/IJdAjAbORTkAXD5hggyG6K7p0MBz3lFZJb2l/LndZlrttJWBLP8QGrc44hrZZtK
Iq0d1dE=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDzzCCAregAwIBAgIRAI1aSBGTnvRH13ERzdmEmukwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNTA1MTQwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaMEAxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtlTXVkaHJhIElu
YzEbMBkGA1UEAwwSRU0gT1YgVExTIENBIC0gRzNDMHYwEAYHKoZIzj0CAQYFK4EE
ACIDYgAEE4JRzVKskpjByKrZdzId65fnCb0LTKCspV3IrVHc/J0ViaFZfjpRIOHv
1jHGPhg1ysrimGmmLhYx6KKBgatWYW3EXZGKvAnzqw5jz3kqpNztc4e+N7ulaVSf
HAKycO1no4IBNTCCATEwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQw
HQYDVR0OBBYEFDTq6gzryQra+dDF4u2beMmHnhtsMA4GA1UdDwEB/wQEAwIBBjAS
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
AjAxBgNVHSAEKjAoMAgGBmeBDAECAjANBgsrBgEEAYOOIQEAATANBgsrBgEEAYOO
IQECbjBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9B
QUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDA0BggrBgEFBQcBAQQoMCYwJAYIKwYB
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQsFAAOC
AQEAHlbhZpjhkPEAQVhPHRtBDaHqiisVD20pQ1TUeSx3l62Qe7NQ0b7vWQy7DN0i
KOu7BGpP5EnIjvXbz9FhpfPJ4FhKEt3tQ51rqXz2oD8vRaryvExWPobDUz3j7HN6
OzBsVMIe3PD7AAkpky0x8zMSi/ep5R9OMzybmNptbtppyuB5ytgq+h7lQdRIP+CQ
TaJX3qTgfMqzntYre5GljmEB5hU2/rSn8zoSU2APkJidlZFpghtgbD4I2VRHZvCw
FFr/SrFM9pwrgbJNZ1MtyTSVgI5NlNb0aMfLR+/r6jJt3ROeY2yh2QQ2dx+Yqgdb
uL5ChFWOQU+ErSDnUq7yr+pjBQ==
-----END CERTIFICATE-----
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more