Compare commits

..
48 Commits
Author SHA1 Message Date
Glavo 69f19115ea Bump HMCLauncher to 3.7.0.1 (#4681) 2025-10-16 15:29:50 +08:00
Glavo ecb4eedd12 指定使用 UTF-8 编码编译 buildSrc 子项目 (#4677) 2025-10-15 21:11:00 +08:00
Glavo df6f35bc8e Fix #4670: 修复不在场景中的按钮尝试取消自身焦点时触发 NPE 的问题 (#4674) 2025-10-15 15:05:21 +08:00
Glavo 58d3780553 Fix #4663: 排序模组时应忽略大小写 (#4665) 2025-10-13 15:13:46 +08:00
Glavo 58b357d4b8 修复模组下载页面图标模糊的问题 (#4660) 2025-10-12 21:14:18 +08:00
辞庐andGlavo d69d27af57 修复导出游戏崩溃信息后弹窗没有正常弹出的问题 (#4624)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-10-12 21:08:42 +08:00
Glavo f9a39f1c90 优化对无法识别的模组的提示 (#4659) 2025-10-12 20:57:47 +08:00
3gf8jv4dv f8a4261480 chore(l10n): add preview update option translation to spanish (#4599) 2025-10-12 20:54:53 +08:00
Glavo 2c428faa95 修复无法识别部分旧版本 Forge 模组的问题 (#4658) 2025-10-12 20:42:55 +08:00
Glavo da91fe15df 更新陶瓦联机页面文本 (#4607) 2025-10-12 20:05:14 +08:00
辞庐 1df962884c Fix #4613 修复本地安装升级 Cleanroom 相关问题 (#4625) 2025-10-11 21:37:33 +08:00
Glavo 8ad5d83946 修复无法识别部分 Forge 模组信息的问题 (#4654) 2025-10-11 21:27:48 +08:00
Glavo 74878bda0c 对于安装了信雅互联的实例抑制对 Fabric 模组的警告 (#4653) 2025-10-11 20:57:53 +08:00
Glavo b32d3304b0 支持读取 META-INF/jarjar/metadata.json (#4652) 2025-10-11 20:45:27 +08:00
Glavo 60375c9632 Add net.neoforged.fml.startup.Client to FORGE_OPTIFINE_MAIN (#4649) 2025-10-11 20:06:14 +08:00
Glavo 33877691f8 使用 Gradle Task 检查 I18N 中文件的全角括号 (#4650) 2025-10-10 22:02:23 +08:00
3gf8jv4dv 109d6874ab Fix #4645: mc 1.21.9+ instances with neoforge do not show their icons (#4646) 2025-10-10 21:43:26 +08:00
Burning_TNT a225e7fac4 更改陶瓦联机反馈表入口展示方式 (#4623) 2025-10-10 21:43:08 +08:00
Glavo 7dad43570e Fix #4647: 修复总是将同时兼容 Forge 和 NeoForge 的模组识别为 Forge 模组的问题 (#4648) 2025-10-10 21:35:11 +08:00
Glavo 8a166266d8 添加 SupportedLocale#getTextDirection() (#4644) 2025-10-10 21:34:14 +08:00
Glavo c53fbd0efb Close #4640: 支持读取 Embedded-Dependencies-Mod (#4641) 2025-10-10 20:32:34 +08:00
Glavo 1e74567a22 在游戏下载界面提示版本支持状态 (#4471) 2025-10-10 16:45:18 +08:00
3gf8jv4dv 0b9616f460 chore(docs&l10n): adjust preview update option text (#4598) 2025-10-10 15:55:54 +08:00
3gf8jv4dv 0b89612ec5 chore(l10n): fix loader mismatch warning on chinese (traditional) (#4642) 2025-10-10 15:54:22 +08:00
3gf8jv4dv 642893d2e9 chore(docs&l10n): highlight support for cleanroom! (#4615) 2025-10-10 15:53:21 +08:00
Glavo 2e878e43cb 修复模组列表页面加载模组图标相关问题 (#4638)
1. 修复高分屏上模组图标模糊的问题;
2. 修复快速滚动时可能会显示其他模组的图标的问题;
3. 修复快速滚动时可能重复加载图标的问题。
2025-10-09 21:33:35 +08:00
Glavo c8edf4be62 优化模组中文译名展示方式 (#4636)
1. 隐藏不包含中文字符的译名
2. 过滤译名中的 Emoji
2025-10-09 16:34:33 +08:00
Glavo 96f16382c0 将模组中文译名中的小括号统一为半角括号 (#4635) 2025-10-09 15:49:27 +08:00
Glavo 11c45e0ab8 优化模组管理页面模组信息的展示方式 (#4621) 2025-10-09 15:21:28 +08:00
Glavo 08f6f973c0 修复颠倒英语中括号显示错误的问题 (#4631) 2025-10-09 15:19:12 +08:00
Glavo 35e63fc8c8 重构本地化支持 (#4632) 2025-10-09 15:18:45 +08:00
Zkitefly 23037b8afc 使用 subname 匹配模组翻译 (#3231) 2025-10-08 22:36:19 +08:00
Glavo 25323dc385 清理 ModTranslations (#4634) 2025-10-08 21:09:19 +08:00
Glavo 496b403eeb 使用 checktyle 检查对 toLowerCase()/toUpperCase() 的误用 (#4633) 2025-10-08 16:27:57 +08:00
Burning_TNT 7be00f2408 修复 Terracotta 和部分 MacOS 版本的兼容性 (#4619) 2025-10-07 23:07:53 +08:00
Glavo 08d7974e27 Update versions.txt to 1.21.10 (#4626) 2025-10-07 22:50:53 +08:00
辞庐 f16cbed5b4 Bump authlib-injector to 1.2.6 (#4617) 2025-10-07 22:46:03 +08:00
Glavo 0988dc53cd 优化模组加载器检测机制 (#4622) 2025-10-07 16:26:26 +08:00
Wulian233 f1c8791961 优化模组信息对话框 UI 宽高 (#4616) 2025-10-06 16:20:06 +08:00
Glavo 07e5ac9e0e 添加 ReleaseSchedule.md (#4606) 2025-10-05 20:36:17 +08:00
Burning_TNT c98dbe6694 Feature: Display feedback form when updating Terracotta. (#4611) 2025-10-05 20:35:49 +08:00
Burning_TNT 460134cc1e 隐藏空的 Terracotta 玩家列表 (#4609) 2025-10-05 20:12:51 +08:00
Burning_TNT 9dd117c454 Bump Terracotta to 0.3.10 (#4608)
https://github.com/burningtnt/Terracotta/compare/v0.3.9-rc.10...v0.3.10
2025-10-04 23:24:43 +08:00
Wulian233 c60af8675f 修复部分按钮点击后始终保持焦点状态的问题 (#4585) 2025-10-04 21:16:35 +08:00
Burning_TNT 411efbe774 在全局设置中储存是否曾向用户展示过陶瓦联机用户须知 (#4601) 2025-10-04 21:15:22 +08:00
Glavo 2e09a40b69 修复 FetchTask 不遵循代理设置的问题 (#4596) 2025-10-04 21:11:44 +08:00
Glavo ba47c2d10d Update badges (#4595) 2025-10-03 21:46:09 +08:00
Glavo fd90ca8afe Add README_en_Qabs.md (#4594) 2025-10-03 21:41:18 +08:00
78 changed files with 1945 additions and 773 deletions

No files matched your search

+2
View File
@@ -16,6 +16,8 @@ NVIDIA
minecraft-exported-crash-info*
hmcl-exported-logs-*
/.java/
/.local/
/.cache/
# gradle build
/build/
@@ -191,7 +191,7 @@ public final class LauncherHelper {
LaunchOptions launchOptions = repository.getLaunchOptions(
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10));
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
return new HMCLGameLauncher(
repository,
@@ -48,6 +48,8 @@ public final class GlobalConfig implements Observable {
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
private final IntegerProperty logRetention = new SimpleIntegerProperty();
@@ -94,6 +96,18 @@ public final class GlobalConfig implements Observable {
this.agreementVersion.set(agreementVersion);
}
public int getTerracottaAgreementVersion() {
return terracottaAgreementVersion.get();
}
public IntegerProperty terracottaAgreementVersionProperty() {
return terracottaAgreementVersion;
}
public void setTerracottaAgreementVersion(int terracottaAgreementVersion) {
this.terracottaAgreementVersion.set(terracottaAgreementVersion);
}
public int getPlatformPromptVersion() {
return platformPromptVersion.get();
}
@@ -153,6 +167,7 @@ public final class GlobalConfig implements Observable {
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"terracottaAgreementVersion",
"platformPromptVersion",
"logRetention",
"userJava",
@@ -169,6 +184,7 @@ public final class GlobalConfig implements Observable {
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("terracottaAgreementVersion", context.serialize(src.getTerracottaAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
@@ -196,6 +212,7 @@ public final class GlobalConfig implements Observable {
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setTerracottaAgreementVersion(Optional.ofNullable(obj.get("terracottaAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.setting;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.ui.FXUtils;
public enum VersionIconType {
@@ -39,6 +40,18 @@ public enum VersionIconType {
// Please append new items at last
public static VersionIconType getIconType(ModLoaderType modLoaderType) {
return switch (modLoaderType) {
case FORGE -> VersionIconType.FORGE;
case NEO_FORGED -> VersionIconType.NEO_FORGE;
case FABRIC -> VersionIconType.FABRIC;
case QUILT -> VersionIconType.QUILT;
case LITE_LOADER -> VersionIconType.CHICKEN;
case CLEANROOM -> VersionIconType.CLEANROOM;
default -> VersionIconType.COMMAND;
};
}
private final String resourceUrl;
VersionIconType(String resourceUrl) {
@@ -48,8 +61,4 @@ public enum VersionIconType {
public Image getIcon() {
return FXUtils.newBuiltinImage(resourceUrl);
}
public Image getIcon(int size) {
return FXUtils.newBuiltinImage(resourceUrl, size, size, true, true);
}
}
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.terracotta.provider.MacOSProvider;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OSVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -86,7 +87,11 @@ public final class TerracottaMetadata {
public static final ITerracottaProvider PROVIDER;
public static final String PACKAGE_NAME;
private static final List<Link> PACKAGE_LINKS;
public static final List<Link> PACKAGE_LINKS;
public static final String FEEDBACK_LINK = NetworkUtils.withQuery("https://docs.hmcl.net/multiplayer/feedback.html", Map.of(
"v", "v1",
"launcher_version", Metadata.VERSION
));
private static final Pattern LEGACY;
private static final List<String> RECENT;
@@ -131,10 +136,6 @@ public final class TerracottaMetadata {
}
}
public static List<Link> getPackageLinks() {
return PACKAGE_LINKS;
}
@Nullable
private static ProviderContext locateProvider(Config config) {
String architecture = switch (Architecture.SYSTEM_ARCH) {
@@ -66,7 +66,7 @@ public final class MacOSProvider implements ITerracottaProvider {
"osascript",
"-e",
String.format(
"do shell script \"installer -pkg %s -target /Applications\" with prompt \"%s\" with administrator privileges",
"do shell script \"installer -pkg %s -target /\" with prompt \"%s\" with administrator privileges",
installer.getPath(),
i18n("terracotta.sudo_installing")
)
@@ -36,6 +36,7 @@ import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -1387,6 +1388,16 @@ public final class FXUtils {
});
}
public static void clearFocus(Node node) {
Scene scene = node.getScene();
if (scene != null) {
Parent root = scene.getRoot();
if (root != null) {
root.requestFocus();
}
}
}
public static void copyOnDoubleClick(Labeled label) {
label.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
@@ -41,9 +41,12 @@ import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.platform.*;
import java.io.IOException;
@@ -59,9 +62,9 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class GameCrashWindow extends Stage {
private final Version version;
@@ -283,7 +286,7 @@ public class GameCrashWindow extends Stage {
alert.showAndWait();
return null;
});
}, Schedulers.javafx());
}
private final class View extends VBox {
@@ -100,7 +100,10 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
ret.getStyleClass().add("jfx-tool-bar-button");
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1)));
ret.setText(text);
ret.setOnAction(e -> onClick.run());
ret.setOnAction(e -> {
onClick.run();
FXUtils.clearFocus(ret);
});
return ret;
}
@@ -33,9 +33,8 @@ import org.jackhuang.hmcl.util.AggregatedObservableList;
public class TwoLineListItem extends VBox {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
public static Label createTagLabel(String tag) {
private static Label createTagLabel(String tag) {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
@@ -111,7 +110,15 @@ public class TwoLineListItem extends VBox {
}
public void addTag(String tag) {
getTags().add(createTagLabel(tag));
Label tagLabel = createTagLabel(tag);
tagLabel.getStyleClass().add("tag");
getTags().add(tagLabel);
}
public void addTagWarning(String tag) {
Label tagLabel = createTagLabel(tag);
tagLabel.getStyleClass().add("tag-warning");
getTags().add(tagLabel);
}
public ObservableList<Label> getTags() {
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.*;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
@@ -49,14 +50,20 @@ 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.*;
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.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.NativePatcher;
import org.jackhuang.hmcl.util.SettingsMap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.Locale;
@@ -65,8 +72,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class VersionsPage extends Control implements WizardPage, Refreshable {
private final String gameVersion;
@@ -172,7 +179,10 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
{
if ("game".equals(control.libraryId)) {
JFXButton wikiButton = newToggleButton4(SVG.GLOBE_BOOK);
wikiButton.setOnAction(event -> onOpenWiki());
wikiButton.setOnAction(event -> {
onOpenWiki();
FXUtils.clearFocus(wikiButton);
});
FXUtils.installFastTooltip(wikiButton, i18n("wiki.tooltip"));
actions.getChildren().add(wikiButton);
}
@@ -233,13 +243,14 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
if (remoteVersion instanceof GameRemoteVersion) {
RemoteVersion.Type versionType = remoteVersion.getVersionType();
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(remoteVersion.getGameVersion());
switch (versionType) {
case RELEASE:
case RELEASE -> {
twoLineListItem.addTag(i18n("version.game.release"));
imageView.setImage(VersionIconType.GRASS.getIcon());
break;
case PENDING:
case SNAPSHOT:
}
case PENDING, SNAPSHOT -> {
if (versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
twoLineListItem.addTag(i18n("version.game.april_fools"));
@@ -248,11 +259,16 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
twoLineListItem.addTag(i18n("version.game.snapshot"));
imageView.setImage(VersionIconType.COMMAND.getIcon());
}
break;
default:
}
default -> {
twoLineListItem.addTag(i18n("version.game.old"));
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
break;
}
}
switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) {
case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested"));
case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported"));
}
} else {
VersionIconType iconType;
@@ -361,18 +377,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
);
}
}
// {
// HBox actionsBox = new HBox(8);
// GridPane.setColumnSpan(actionsBox, 4);
// actionsBox.setAlignment(Pos.CENTER_RIGHT);
//
// JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh"));
// refreshButton.setOnAction(event -> control.onRefresh());
//
// actionsBox.getChildren().setAll(refreshButton);
//
// searchPane.addRow(rowIndex++, actionsBox);
// }
}
{
@@ -448,21 +452,16 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
if (filter != null)
versions = versions.filter(it -> {
RemoteVersion.Type versionType = it.getVersionType();
switch (filter) {
case RELEASE:
return versionType == RemoteVersion.Type.RELEASE;
case SNAPSHOTS:
return versionType == RemoteVersion.Type.SNAPSHOT
|| versionType == RemoteVersion.Type.PENDING;
case APRIL_FOOLS:
return versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
case OLD:
return versionType == RemoteVersion.Type.OLD;
case ALL:
default:
return true;
}
return switch (filter) {
case RELEASE -> versionType == RemoteVersion.Type.RELEASE;
case SNAPSHOTS -> versionType == RemoteVersion.Type.SNAPSHOT
|| versionType == RemoteVersion.Type.PENDING;
case APRIL_FOOLS -> versionType == RemoteVersion.Type.SNAPSHOT
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
case OLD -> versionType == RemoteVersion.Type.OLD;
// case ALL,
default -> true;
};
});
String nameQuery = nameField.getText();
@@ -152,6 +152,7 @@ public abstract class SettingsView extends StackPane {
previewPane = new OptionToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
settingsPane.getContent().add(previewPane);
}
@@ -22,6 +22,8 @@ import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections;
@@ -41,6 +43,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Profile;
@@ -79,10 +83,14 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ThreadLocalRandom;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TerracottaControllerPage extends StackPane {
private static final String FEEDBACK_TIP = "terracotta-feedback";
private static final ObjectProperty<TerracottaState> UI_STATE = new SimpleObjectProperty<>();
static {
@@ -177,13 +185,25 @@ public class TerracottaControllerPage extends StackPane {
download.setSubtitle(i18n("terracotta.status.uninitialized.desc"));
download.setRightIcon(SVG.ARROW_FORWARD);
FXUtils.onClicked(download, () -> {
if (uninitialized.hasLegacy()) {
if (globalConfig().getTerracottaAgreementVersion() >= 1) {
TerracottaState.Preparing s = TerracottaManager.install(null);
if (s != null) {
UI_STATE.set(s);
}
if (uninitialized.hasLegacy() && I18n.isUseChinese()) {
Object feedback = config().getShownTips().get(FEEDBACK_TIP);
if (!(feedback instanceof Number number) || number.intValue() < 1) {
Controllers.confirm(i18n("terracotta.feedback.desc"), i18n("terracotta.feedback.title"), () -> {
FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK);
config().getShownTips().put(FEEDBACK_TIP, 1);
}, () -> {
});
}
}
} else {
Controllers.confirmActionDanger(i18n("terracotta.confirm.desc"), i18n("terracotta.confirm.title"), () -> {
globalConfig().setTerracottaAgreementVersion(1);
TerracottaState.Preparing s = TerracottaManager.install(null);
if (s != null) {
UI_STATE.set(s);
@@ -265,7 +285,18 @@ public class TerracottaControllerPage extends StackPane {
});
});
nodesProperty.setAll(flow, host, guest);
if (ThreadLocalRandom.current().nextDouble() < 0.02D) {
LineButton feedback = LineButton.of();
feedback.setLeftIcon(SVG.FEEDBACK);
feedback.setTitle(i18n("terracotta.feedback.title"));
feedback.setSubtitle(i18n("terracotta.feedback.desc"));
feedback.setRightIcon(SVG.OPEN_IN_NEW);
FXUtils.onClicked(feedback, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
nodesProperty.setAll(flow, host, guest, feedback);
} else {
nodesProperty.setAll(flow, host, guest);
}
} else if (state instanceof TerracottaState.HostScanning) {
statusProperty.set(i18n("terracotta.status.scanning"));
progressProperty.set(-1);
@@ -303,7 +334,11 @@ public class TerracottaControllerPage extends StackPane {
nodesProperty.setAll(room);
} else if (state instanceof TerracottaState.HostOK hostOK) {
if (hostOK.isForkOf(legacyState)) {
((PlayerProfileUI) nodesProperty.get(nodesProperty.size() - 1)).updateProfiles(hostOK.getProfiles());
if (nodesProperty.get(nodesProperty.size() - 1) instanceof PlayerProfileUI profileUI) {
profileUI.updateProfiles(hostOK.getProfiles());
} else { // Should NOT happen
nodesProperty.add(new PlayerProfileUI(hostOK.getProfiles()));
}
return;
} else {
String cs = hostOK.getCode();
@@ -357,7 +392,11 @@ public class TerracottaControllerPage extends StackPane {
}
});
nodesProperty.setAll(code, copy, back, new PlayerProfileUI(hostOK.getProfiles()));
if (hostOK.getProfiles().isEmpty()) {
nodesProperty.setAll(code, copy, back);
} else {
nodesProperty.setAll(code, copy, back, new PlayerProfileUI(hostOK.getProfiles()));
}
}
} else if (state instanceof TerracottaState.GuestStarting) {
statusProperty.set(i18n("terracotta.status.guest_starting"));
@@ -377,7 +416,11 @@ public class TerracottaControllerPage extends StackPane {
nodesProperty.setAll(room);
} else if (state instanceof TerracottaState.GuestOK guestOK) {
if (guestOK.isForkOf(legacyState)) {
((PlayerProfileUI) nodesProperty.get(nodesProperty.size() - 1)).updateProfiles(guestOK.getProfiles());
if (nodesProperty.get(nodesProperty.size() - 1) instanceof PlayerProfileUI profileUI) {
profileUI.updateProfiles(guestOK.getProfiles());
} else { // Should NOT happen
nodesProperty.add(new PlayerProfileUI(guestOK.getProfiles()));
}
return;
} else {
statusProperty.set(i18n("terracotta.status.guest_ok"));
@@ -398,7 +441,11 @@ public class TerracottaControllerPage extends StackPane {
}
});
nodesProperty.setAll(tutorial, back, new PlayerProfileUI(guestOK.getProfiles()));
if (guestOK.getProfiles().isEmpty()) {
nodesProperty.setAll(tutorial, back);
} else {
nodesProperty.setAll(tutorial, back, new PlayerProfileUI(guestOK.getProfiles()));
}
}
} else if (state instanceof TerracottaState.Exception exception) {
statusProperty.set(i18n("terracotta.status.exception.desc." + exception.getType().name().toLowerCase(Locale.ROOT)));
@@ -530,7 +577,7 @@ public class TerracottaControllerPage extends StackPane {
header.setSubtitle(i18n("terracotta.from_local.desc"));
locals.setHeaderLeft(header);
for (TerracottaMetadata.Link link : TerracottaMetadata.getPackageLinks()) {
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
HBox node = new HBox();
node.setAlignment(Pos.CENTER_LEFT);
node.setPadding(new Insets(10, 16, 10, 16));
@@ -557,9 +604,10 @@ public class TerracottaControllerPage extends StackPane {
private static final class LineButton extends RipplerContainer {
private final WeakListenerHolder holder = new WeakListenerHolder();
private final TwoLineListItem middle = new TwoLineListItem();
private final ObjectProperty<Node> left = new SimpleObjectProperty<>();
private final ObjectProperty<Node> right = new SimpleObjectProperty<>();
private final ObjectProperty<Node> left = new SimpleObjectProperty<>(this, "left");
private final ObjectProperty<Node> right = new SimpleObjectProperty<>(this, "right");
private final StringProperty title = new SimpleStringProperty(this, "title", "");
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle", "");
public static LineButton of() {
return of(true);
@@ -584,7 +632,35 @@ public class TerracottaControllerPage extends StackPane {
nodes.add(left);
}
nodes.add(button.middle);
{
// FIXME: It's sucked to have the following TwoLineListItem-liked logic whose subtitle is a TextFlow.
VBox middle = new VBox();
middle.getStyleClass().add("two-line-list-item");
middle.setMouseTransparent(true);
{
HBox firstLine = new HBox();
firstLine.getStyleClass().add("first-line");
{
Label lblTitle = new Label(button.title.get());
lblTitle.getStyleClass().add("title");
firstLine.getChildren().setAll(lblTitle);
}
HBox secondLine = new HBox();
{
Text text = new Text(button.subTitle.get());
text.setFill(new Color(0, 0, 0, 0.5));
TextFlow lblSubtitle = new TextFlow(text);
lblSubtitle.getStyleClass().add("subtitle");
secondLine.getChildren().setAll(lblSubtitle);
}
middle.getChildren().setAll(firstLine, secondLine);
}
nodes.add(middle);
}
nodes.add(spacing);
Node right = button.right.get();
@@ -593,7 +669,7 @@ public class TerracottaControllerPage extends StackPane {
}
container.getChildren().setAll(nodes);
}, button.middle.titleProperty(), button.middle.subtitleProperty(), button.left, button.right));
}, button.title, button.subTitle, button.left, button.right));
button.getProperties().put("ComponentList.noPadding", true);
return button;
@@ -604,11 +680,11 @@ public class TerracottaControllerPage extends StackPane {
}
public void setTitle(String title) {
this.middle.setTitle(title);
this.title.set(title);
}
public void setSubtitle(String subtitle) {
this.middle.setSubtitle(subtitle);
this.subTitle.set(subtitle);
}
public void setLeftImage(Image left) {
@@ -648,10 +724,7 @@ public class TerracottaControllerPage extends StackPane {
TwoLineListItem item = new TwoLineListItem();
item.setTitle(profile.getName());
item.setSubtitle(profile.getVendor());
item.getTags().setAll(TwoLineListItem.createTagLabel(
i18n("terracotta.player_kind." + profile.getType().name().toLowerCase(Locale.ROOT)))
);
item.addTag(i18n("terracotta.player_kind." + profile.getType().name().toLowerCase(Locale.ROOT)));
pane.getChildren().add(item);
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.terracotta;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -52,6 +53,12 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
});
AdvancedListItem feedback = new AdvancedListItem();
feedback.setLeftGraphic(wrap(SVG.FEEDBACK));
feedback.setActionButtonVisible(false);
feedback.setTitle(i18n("terracotta.feedback.title"));
feedback.setOnAction(e -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
AdvancedListItem chatItem = new AdvancedListItem();
chatItem.setLeftGraphic(wrap(SVG.CHAT));
chatItem.setActionButtonVisible(false);
@@ -67,6 +74,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerTab(tab, statusPage, i18n("terracotta.status"), SVG.TUNE)
.startCategory(i18n("help").toUpperCase(Locale.ROOT))
.add(feedback)
.add(chatItem)
.add(easytierItem);
FXUtils.setLimitWidth(sideBar, 200);
@@ -532,6 +532,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
container.setAlignment(Pos.CENTER_LEFT);
pane.getChildren().add(container);
imageView.setFitWidth(40);
imageView.setFitHeight(40);
container.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
HBox.setHgrow(content, Priority.ALWAYS);
}
@@ -547,7 +550,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
.map(category -> getSkinnable().getLocalizedCategory(category))
.forEach(content::addTag);
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 80, 80, true, true));
}
}
});
@@ -219,8 +219,10 @@ public class DownloadPage extends Control implements DecoratorPage {
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
{
ImageView imageView = new ImageView();
imageView.setFitWidth(40);
imageView.setFitHeight(40);
if (StringUtils.isNotBlank(getSkinnable().addon.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 40, 40, true, true));
imageView.imageProperty().bind(FXUtils.newRemoteImage(getSkinnable().addon.getIconUrl(), 80, 80, true, true));
}
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
@@ -341,6 +343,8 @@ public class DownloadPage extends Control implements DecoratorPage {
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
ImageView imageView = new ImageView();
imageView.setFitWidth(40);
imageView.setFitHeight(40);
pane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content);
RipplerContainer container = new RipplerContainer(pane);
@@ -358,12 +362,12 @@ public class DownloadPage extends Control implements DecoratorPage {
.map(page::getLocalizedCategory)
.forEach(content::addTag);
if (StringUtils.isNotBlank(addon.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 80, 80, true, true));
}
} else {
content.setTitle(i18n("mods.broken_dependency.title"));
content.setSubtitle(i18n("mods.broken_dependency.desc"));
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@8x.png", 40, 40, true, true));
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@4x.png"));
}
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList;
@@ -27,6 +26,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
@@ -44,19 +44,22 @@ import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.concurrent.locks.ReentrantLock;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private final ReentrantLock lock = new ReentrantLock();
private ModManager modManager;
private LibraryAnalyzer libraryAnalyzer;
private Profile profile;
private String versionId;
private String instanceId;
private String gameVersion;
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
public ModListPage() {
FXUtils.applyDragListener(this, it -> Arrays.asList("jar", "zip", "litemod").contains(FileUtils.getExtension(it)), mods -> {
@@ -83,34 +86,82 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
@Override
public void loadVersion(Profile profile, String id) {
this.profile = profile;
this.versionId = id;
this.instanceId = id;
HMCLGameRepository repository = profile.getRepository();
Version resolved = repository.getResolvedPreservingPatchesVersion(id);
libraryAnalyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null));
modded.set(libraryAnalyzer.hasModLoader());
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
modded.set(analyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
}
private CompletableFuture<?> loadMods(ModManager modManager) {
private void loadMods(ModManager modManager) {
setLoading(true);
this.modManager = modManager;
return CompletableFuture.supplyAsync(() -> {
CompletableFuture.supplyAsync(() -> {
lock.lock();
try {
synchronized (ModListPage.this) {
runInFX(() -> loadingProperty().set(true));
modManager.refreshMods();
return new ArrayList<>(modManager.getMods());
}
modManager.refreshMods();
return modManager.getMods().stream().map(ModListPageSkin.ModInfoObject::new).toList();
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
lock.unlock();
}
}, Schedulers.defaultScheduler()).whenCompleteAsync((list, exception) -> {
loadingProperty().set(false);
if (exception == null)
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).sorted().collect(Collectors.toList()));
else
getProperties().remove(ModListPage.class);
}, Platform::runLater);
}, Schedulers.io()).whenCompleteAsync((list, exception) -> {
updateSupportedLoaders(modManager);
if (exception == null) {
getItems().setAll(list);
} else {
LOG.warning("Failed to load mods", exception);
getItems().clear();
}
setLoading(false);
}, Schedulers.javafx());
}
private void updateSupportedLoaders(ModManager modManager) {
supportedLoaders.clear();
LibraryAnalyzer analyzer = modManager.getLibraryAnalyzer();
if (analyzer == null) {
Collections.addAll(supportedLoaders, ModLoaderType.values());
return;
}
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
if (type.isModLoader() && analyzer.has(type)) {
ModLoaderType modLoaderType = type.getModLoaderType();
if (modLoaderType != null) {
supportedLoaders.add(modLoaderType);
if (modLoaderType == ModLoaderType.CLEANROOM)
supportedLoaders.add(ModLoaderType.FORGE);
}
}
}
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
supportedLoaders.add(ModLoaderType.FORGE);
}
if (analyzer.has(LibraryAnalyzer.LibraryType.QUILT)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
supportedLoaders.add(ModLoaderType.FORGE);
supportedLoaders.add(ModLoaderType.NEO_FORGED);
}
// Sinytra Connector
if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGED)
|| "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
supportedLoaders.add(ModLoaderType.FABRIC);
}
}
public void add() {
@@ -148,7 +199,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}).start();
}
public void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
void removeSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
try {
modManager.removeMods(selectedItems.stream()
.filter(Objects::nonNull)
@@ -160,14 +211,14 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
}
public void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
void enableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
selectedItems.stream()
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
.forEach(info -> info.setActive(true));
}
public void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
void disableSelected(ObservableList<ModListPageSkin.ModInfoObject> selectedItems) {
selectedItems.stream()
.filter(Objects::nonNull)
.map(ModListPageSkin.ModInfoObject::getModInfo)
@@ -175,13 +226,13 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void openModFolder() {
FXUtils.openFolder(profile.getRepository().getRunDirectory(versionId).resolve("mods"));
FXUtils.openFolder(profile.getRepository().getRunDirectory(instanceId).resolve("mods"));
}
public void checkUpdates() {
Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> {
Optional<String> gameVersion = profile.getRepository().getGameVersion(versionId);
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
if (gameVersion.isPresent()) {
return new ModCheckUpdatesTask(gameVersion.get(), modManager.getMods());
}
@@ -199,7 +250,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
.withStagesHint(Collections.singletonList("mods.check_updates")),
i18n("update.checking"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(versionId)) {
if (profile.getRepository().isModpack(instanceId)) {
Controllers.confirm(
i18n("mods.update_modpack_mod.warning"), null,
MessageDialogPane.MessageType.WARNING,
@@ -210,7 +261,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void download() {
Controllers.getDownloadPage().showModDownloads().selectVersion(versionId);
Controllers.getDownloadPage().showModDownloads().selectVersion(instanceId);
Controllers.navigate(Controllers.getDownloadPage());
}
@@ -239,7 +290,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
return this.profile;
}
public String getVersionId() {
return this.versionId;
public String getInstanceId() {
return this.instanceId;
}
}
@@ -18,18 +18,17 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SkinBase;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
@@ -37,6 +36,7 @@ import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
@@ -55,32 +55,28 @@ 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.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -97,6 +93,9 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
// FXThread
private boolean isSearching = false;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Boolean> holder;
ModListPageSkin(ModListPage skinnable) {
super(skinnable);
@@ -108,6 +107,12 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
this.holder = FXUtils.onWeakChange(skinnable.loadingProperty(), loading -> {
if (!loading) {
listView.scrollTo(0);
}
});
{
toolbarPane = new TransitionPane();
@@ -275,19 +280,41 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|| predicate.test(modInfo.getGameVersion())
|| predicate.test(modInfo.getId())
|| predicate.test(Objects.toString(modInfo.getModLoaderType()))
|| predicate.test((item.getMod() != null ? item.getMod().getDisplayName() : null))) {
|| predicate.test((item.getModTranslations() != null ? item.getModTranslations().getDisplayName() : null))) {
listView.getItems().add(item);
}
}
}
}
private static Task<Image> loadModIcon(LocalModFile modFile, int size) {
return Task.supplyAsync(() -> {
static final class ModInfoObject {
private final BooleanProperty active;
private final LocalModFile localModFile;
private final @Nullable ModTranslations.Mod modTranslations;
private SoftReference<CompletableFuture<Image>> iconCache;
ModInfoObject(LocalModFile localModFile) {
this.localModFile = localModFile;
this.active = localModFile.activeProperty();
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
}
LocalModFile getModInfo() {
return localModFile;
}
public @Nullable ModTranslations.Mod getModTranslations() {
return modTranslations;
}
@FXThread
private Image loadIcon() {
List<String> iconPaths = new ArrayList<>();
if (StringUtils.isNotBlank(modFile.getLogoPath())) {
iconPaths.add(modFile.getLogoPath());
if (StringUtils.isNotBlank(this.localModFile.getLogoPath())) {
iconPaths.add(this.localModFile.getLogoPath());
}
iconPaths.addAll(List.of(
@@ -311,7 +338,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
"resources/mod_icon.png"
));
String modId = modFile.getId();
String modId = this.localModFile.getId();
if (StringUtils.isNotBlank(modId)) {
iconPaths.addAll(List.of(
"assets/" + modId + "/icon.png",
@@ -330,14 +357,12 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
));
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.getFile())) {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
for (String path : iconPaths) {
Path iconPath = fs.getPath(path);
if (Files.exists(iconPath)) {
Image image = FXUtils.loadImage(iconPath, size, size, true, true);
if (!image.isError() &&
image.getWidth() > 0 &&
image.getHeight() > 0 &&
Image image = FXUtils.loadImage(iconPath, 80, 80, true, true);
if (!image.isError() && image.getWidth() > 0 && image.getHeight() > 0 &&
Math.abs(image.getWidth() - image.getHeight()) < 1) {
return image;
}
@@ -347,102 +372,68 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
LOG.warning("Failed to load mod icons", e);
}
VersionIconType defaultIcon = switch (modFile.getModLoaderType()) {
case FORGE -> VersionIconType.FORGE;
case NEO_FORGED -> VersionIconType.NEO_FORGE;
case FABRIC -> VersionIconType.FABRIC;
case QUILT -> VersionIconType.QUILT;
case LITE_LOADER -> VersionIconType.CHICKEN;
case CLEANROOM -> VersionIconType.CLEANROOM;
default -> VersionIconType.COMMAND;
};
return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
}
return defaultIcon.getIcon(size);
});
}
static class ModInfoObject extends RecursiveTreeObject<ModInfoObject> implements Comparable<ModInfoObject> {
private final BooleanProperty active;
private final LocalModFile localModFile;
private final String title;
private final String message;
private final ModTranslations.Mod mod;
private SoftReference<Image> iconCache;
ModInfoObject(LocalModFile localModFile) {
this.localModFile = localModFile;
this.active = localModFile.activeProperty();
this.title = localModFile.getName();
List<String> parts = new ArrayList<>();
if (isNotBlank(localModFile.getId())) {
parts.add(localModFile.getId());
public void loadIcon(ImageView imageView, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
Image image = imageFuture.getNow(null);
if (image != null) {
imageView.setImage(image);
return;
}
} else {
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
this.iconCache = new SoftReference<>(imageFuture);
}
if (isNotBlank(localModFile.getVersion())) {
parts.add(localModFile.getVersion());
}
if (isNotBlank(localModFile.getGameVersion())) {
parts.add(i18n("game.version") + ": " + localModFile.getGameVersion());
}
this.message = String.join(", ", parts);
this.mod = ModTranslations.MOD.getModById(localModFile.getId());
}
imageView.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon());
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
// The current ListCell has already switched to another object
return;
}
}
String getTitle() {
return title;
}
String getSubtitle() {
return message;
}
LocalModFile getModInfo() {
return localModFile;
}
public ModTranslations.Mod getMod() {
return mod;
}
@Override
public int compareTo(@NotNull ModListPageSkin.ModInfoObject o) {
return localModFile.getFileName().toLowerCase().compareTo(o.localModFile.getFileName().toLowerCase());
imageView.setImage(image);
}, Schedulers.javafx());
}
}
class ModInfoDialog extends JFXDialogLayout {
final class ModInfoDialog extends JFXDialogLayout {
ModInfoDialog(ModInfoObject modInfo) {
HBox titleContainer = new HBox();
titleContainer.setSpacing(8);
Stage stage = Controllers.getStage();
maxWidthProperty().bind(stage.widthProperty().multiply(0.7));
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 40, 40);
loadModIcon(modInfo.getModInfo(), 40)
.whenComplete(Schedulers.javafx(), (image, exception) -> {
imageView.setImage(image);
}).start();
modInfo.loadIcon(imageView, null);
TwoLineListItem title = new TwoLineListItem();
title.setTitle(modInfo.getModInfo().getName());
if (modInfo.getMod() != null) {
title.addTag(modInfo.getMod().getDisplayName());
}
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
title.setTitle(modInfo.getModTranslations().getDisplayName());
else
title.setTitle(modInfo.getModInfo().getName());
List<String> subtitleParts = new ArrayList<>();
subtitleParts.add(FileUtils.getName(modInfo.getModInfo().getFile()));
StringJoiner subtitle = new StringJoiner(" | ");
subtitle.add(FileUtils.getName(modInfo.getModInfo().getFile()));
if (StringUtils.isNotBlank(modInfo.getModInfo().getGameVersion())) {
subtitleParts.add(modInfo.getModInfo().getGameVersion());
subtitle.add(modInfo.getModInfo().getGameVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
subtitleParts.add(modInfo.getModInfo().getVersion());
subtitle.add(modInfo.getModInfo().getVersion());
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getAuthors())) {
subtitleParts.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
subtitle.add(i18n("archive.author") + ": " + modInfo.getModInfo().getAuthors());
}
title.setSubtitle(String.join(", ", subtitleParts));
title.setSubtitle(subtitle.toString());
titleContainer.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), title);
setHeading(titleContainer);
@@ -456,6 +447,11 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
descriptionPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
descriptionPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
descriptionPane.setFitToWidth(true);
description.heightProperty().addListener((obs, oldVal, newVal) -> {
double maxHeight = stage.getHeight() * 0.5;
double targetHeight = Math.min(newVal.doubleValue(), maxHeight);
descriptionPane.setPrefViewportHeight(targetHeight);
});
setBody(descriptionPane);
@@ -507,7 +503,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
Controllers.navigate(new DownloadPage(
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
remoteMod,
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getVersionId()),
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
(profile, version, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
));
});
@@ -530,7 +526,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
getActions().add(officialPageButton);
}
if (modInfo.getMod() == null || StringUtils.isBlank(modInfo.getMod().getMcmod())) {
if (modInfo.getModTranslations() == null || StringUtils.isBlank(modInfo.getModTranslations().getMcmod())) {
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
searchButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
@@ -545,7 +541,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
JFXHyperlink mcmodButton = new JFXHyperlink(i18n("mods.mcmod.page"));
mcmodButton.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getMod()));
FXUtils.openLink(ModTranslations.MOD.getMcmodUrl(modInfo.getModTranslations()));
});
getActions().add(mcmodButton);
}
@@ -564,6 +560,8 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
final class ModInfoListCell extends MDListCell<ModInfoObject> {
private static final PseudoClass WARNING = PseudoClass.getPseudoClass("warning");
JFXCheckBox checkBox = new JFXCheckBox();
ImageView imageView = new ImageView();
TwoLineListItem content = new TwoLineListItem();
@@ -572,9 +570,13 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
JFXButton revealButton = new JFXButton();
BooleanProperty booleanProperty;
Tooltip warningTooltip;
ModInfoListCell(JFXListView<ModInfoObject> listView, Holder<Object> lastCell) {
super(listView, lastCell);
this.getStyleClass().add("mod-info-list-cell");
HBox container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
@@ -585,7 +587,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
imageView.setFitWidth(24);
imageView.setFitHeight(24);
imageView.setPreserveRatio(true);
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 24, 24, true, true));
imageView.setImage(VersionIconType.COMMAND.getIcon());
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24), 24, 24));
@@ -606,68 +608,113 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
@Override
protected void updateControl(ModInfoObject dataItem, boolean empty) {
pseudoClassStateChanged(WARNING, false);
if (warningTooltip != null) {
Tooltip.uninstall(this, warningTooltip);
warningTooltip = null;
}
if (empty) return;
SoftReference<Image> iconCache = dataItem.iconCache;
Image icon;
if (iconCache != null && (icon = iconCache.get()) != null) {
imageView.setImage(icon);
} else {
loadModIcon(dataItem.getModInfo(), 24)
.whenComplete(Schedulers.javafx(), (image, exception) -> {
dataItem.iconCache = new SoftReference<>(image);
imageView.setImage(image);
}).start();
List<String> warning = new ArrayList<>();
content.getTags().clear();
LocalModFile modInfo = dataItem.getModInfo();
ModTranslations.Mod modTranslations = dataItem.getModTranslations();
ModLoaderType modLoaderType = modInfo.getModLoaderType();
dataItem.loadIcon(imageView, new WeakReference<>(this.itemProperty()));
String displayName = modInfo.getName();
if (modTranslations != null && I18n.isUseChinese()) {
String chineseName = modTranslations.getName();
if (StringUtils.containsChinese(chineseName)) {
if (StringUtils.containsEmoji(chineseName)) {
StringBuilder builder = new StringBuilder();
chineseName.codePoints().forEach(ch -> {
if (ch < 0x1F300 || ch > 0x1FAFF)
builder.appendCodePoint(ch);
});
chineseName = builder.toString().trim();
}
if (StringUtils.isNotBlank(chineseName) && !displayName.equalsIgnoreCase(chineseName)) {
displayName = displayName + " (" + chineseName + ")";
}
}
}
content.setTitle(displayName);
StringJoiner joiner = new StringJoiner(" | ");
if (modLoaderType != ModLoaderType.UNKNOWN && StringUtils.isNotBlank(modInfo.getId()))
joiner.add(modInfo.getId());
joiner.add(FileUtils.getName(modInfo.getFile()));
content.setSubtitle(joiner.toString());
if (modLoaderType == ModLoaderType.UNKNOWN) {
content.addTagWarning(i18n("mods.unknown"));
} else if (!ModListPageSkin.this.getSkinnable().supportedLoaders.contains(modLoaderType)) {
warning.add(i18n("mods.warning.loader_mismatch"));
switch (dataItem.getModInfo().getModLoaderType()) {
case FORGE:
content.addTagWarning(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.addTagWarning(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
content.addTagWarning(i18n("install.installer.neoforge"));
break;
case FABRIC:
content.addTagWarning(i18n("install.installer.fabric"));
break;
case LITE_LOADER:
content.addTagWarning(i18n("install.installer.liteloader"));
break;
case QUILT:
content.addTagWarning(i18n("install.installer.quilt"));
break;
}
}
content.setTitle(dataItem.getTitle());
content.getTags().clear();
switch (dataItem.getModInfo().getModLoaderType()) {
case FORGE:
content.addTag(i18n("install.installer.forge"));
break;
case CLEANROOM:
content.addTag(i18n("install.installer.cleanroom"));
break;
case NEO_FORGED:
content.addTag(i18n("install.installer.neoforge"));
break;
case FABRIC:
content.addTag(i18n("install.installer.fabric"));
break;
case LITE_LOADER:
content.addTag(i18n("install.installer.liteloader"));
break;
case QUILT:
content.addTag(i18n("install.installer.quilt"));
break;
}
if (dataItem.getMod() != null && I18n.isUseChinese()) {
if (isNotBlank(dataItem.getSubtitle())) {
content.setSubtitle(dataItem.getSubtitle() + ", " + dataItem.getMod().getDisplayName());
} else {
content.setSubtitle(dataItem.getMod().getDisplayName());
}
} else {
content.setSubtitle(dataItem.getSubtitle());
String modVersion = modInfo.getVersion();
if (StringUtils.isNotBlank(modVersion) && !"${version}".equals(modVersion)) {
content.addTag(modVersion);
}
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.active);
restoreButton.setVisible(!dataItem.getModInfo().getMod().getOldFiles().isEmpty());
restoreButton.setVisible(!modInfo.getMod().getOldFiles().isEmpty());
restoreButton.setOnAction(e -> {
menu.get().getContent().setAll(dataItem.getModInfo().getMod().getOldFiles().stream()
menu.get().getContent().setAll(modInfo.getMod().getOldFiles().stream()
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(),
() -> getSkinnable().rollback(dataItem.getModInfo(), localModFile),
() -> getSkinnable().rollback(modInfo, localModFile),
popup.get()))
.collect(Collectors.toList())
.toList()
);
popup.get().show(restoreButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, 0, restoreButton.getHeight());
});
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(dataItem.getModInfo().getFile()));
revealButton.setOnAction(e -> FXUtils.showFileInExplorer(modInfo.getFile()));
infoButton.setOnAction(e -> Controllers.dialog(new ModInfoDialog(dataItem)));
if (!warning.isEmpty()) {
pseudoClassStateChanged(WARNING, true);
//noinspection ConstantValue
this.warningTooltip = warning.size() == 1
? new Tooltip(warning.get(0))
: new Tooltip(String.join("\n", warning));
FXUtils.installFastTooltip(this, warningTooltip);
}
}
}
}
@@ -20,13 +20,13 @@ package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -57,54 +57,220 @@ public enum ModTranslations {
};
public static ModTranslations getTranslationsByRepositoryType(RemoteModRepository.Type type) {
switch (type) {
case MOD:
return MOD;
case MODPACK:
return MODPACK;
default:
return EMPTY;
return switch (type) {
case MOD -> MOD;
case MODPACK -> MODPACK;
default -> EMPTY;
};
}
@SuppressWarnings("StatementWithEmptyBody")
private static String cleanSubname(String subname) {
if (StringUtils.isBlank(subname))
return "";
StringBuilder builder = new StringBuilder(subname.length());
for (int i = 0; i < subname.length(); ) {
int ch = subname.codePointAt(i);
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|| ".+\\".indexOf(ch) >= 0) {
builder.appendCodePoint(ch);
} else if (Character.isWhitespace(ch)
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0
|| ch >= 0x1F300 && ch <= 0x1FAFF) {
// Remove these unnecessary characters from subname
} else {
// The subname contains unsupported characters, so we do not use this subname to match the mod
return "";
}
i += Character.charCount(ch);
}
return builder.length() == subname.length() ? subname : builder.toString();
}
private final String resourceName;
private List<Mod> mods;
private Map<String, Mod> modIdMap; // mod id -> mod
private Map<String, Mod> curseForgeMap; // curseforge id -> mod
private List<Pair<String, Mod>> keywords;
private int maxKeywordLength = -1;
private volatile List<Mod> mods;
private volatile Map<String, Mod> modIdMap; // mod id -> mod
private volatile Map<String, Mod> subnameMap;
private volatile Map<String, Mod> curseForgeMap; // curseforge id -> mod
private volatile List<Pair<String, Mod>> keywords;
private volatile int maxKeywordLength = -1;
ModTranslations(String resourceName) {
this.resourceName = resourceName;
}
@Nullable
public Mod getModByCurseForgeId(String id) {
if (StringUtils.isBlank(id) || !loadCurseForgeMap()) return null;
public @NotNull List<Mod> getMods() {
List<Mod> mods = this.mods;
if (mods != null)
return mods;
return curseForgeMap.get(id);
synchronized (this) {
mods = this.mods;
if (mods != null)
return mods;
if (StringUtils.isBlank(resourceName)) {
return this.mods = List.of();
}
//noinspection DataFlowIssue
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8))) {
return this.mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::new).toList();
} catch (Exception e) {
LOG.warning("Failed to load " + resourceName, e);
return this.mods = List.of();
}
}
}
private @NotNull Map<String, Mod> getModIdMap() {
Map<String, Mod> modIdMap = this.modIdMap;
if (modIdMap != null)
return modIdMap;
synchronized (this) {
modIdMap = this.modIdMap;
if (modIdMap != null)
return modIdMap;
List<Mod> mods = getMods();
modIdMap = new HashMap<>(mods.size());
for (Mod mod : mods) {
for (String id : mod.getModIds()) {
if (StringUtils.isNotBlank(id)) {
modIdMap.putIfAbsent(id, mod);
}
}
}
return this.modIdMap = modIdMap;
}
}
private @NotNull Map<String, Mod> getSubnameMap() {
Map<String, Mod> subnameMap = this.subnameMap;
if (subnameMap != null)
return subnameMap;
synchronized (this) {
subnameMap = this.subnameMap;
if (subnameMap != null)
return subnameMap;
subnameMap = new HashMap<>();
List<Mod> mods = getMods();
for (Mod mod : mods) {
String subname = cleanSubname(mod.getSubname());
if (StringUtils.isNotBlank(subname)) {
subnameMap.putIfAbsent(subname, mod);
}
}
return this.subnameMap = subnameMap;
}
}
private @NotNull Map<String, Mod> getCurseForgeMap() {
Map<String, Mod> curseForgeMap = this.curseForgeMap;
if (curseForgeMap != null)
return curseForgeMap;
synchronized (this) {
curseForgeMap = this.curseForgeMap;
if (curseForgeMap != null)
return curseForgeMap;
List<Mod> mods = getMods();
curseForgeMap = new HashMap<>(mods.size());
for (Mod mod : mods) {
if (StringUtils.isNotBlank(mod.getCurseforge())) {
curseForgeMap.putIfAbsent(mod.getCurseforge(), mod);
}
}
return this.curseForgeMap = curseForgeMap;
}
}
private @NotNull List<Pair<String, Mod>> getKeywords() {
List<Pair<String, Mod>> keywords = this.keywords;
if (keywords != null)
return keywords;
synchronized (this) {
keywords = this.keywords;
if (keywords != null)
return keywords;
List<Mod> mods = getMods();
keywords = new ArrayList<>();
int maxKeywordLength = -1;
for (Mod mod : mods) {
if (StringUtils.isNotBlank(mod.getName())) {
keywords.add(pair(mod.getName(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length());
}
if (StringUtils.isNotBlank(mod.getSubname())) {
keywords.add(pair(mod.getSubname(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length());
}
if (StringUtils.isNotBlank(mod.getAbbr())) {
keywords.add(pair(mod.getAbbr(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length());
}
}
this.maxKeywordLength = maxKeywordLength;
return this.keywords = keywords;
}
}
private int getMaxKeywordLength() {
int maxKeywordLength = this.maxKeywordLength;
if (maxKeywordLength >= 0)
return maxKeywordLength;
// Ensure maxKeywordLength is initialized
getKeywords();
return this.maxKeywordLength;
}
@Nullable
public Mod getModById(String id) {
if (StringUtils.isBlank(id) || !loadModIdMap()) return null;
public Mod getModByCurseForgeId(String id) {
if (StringUtils.isBlank(id)) return null;
return modIdMap.get(id);
return getCurseForgeMap().get(id);
}
@Nullable
public Mod getMod(String id, String subname) {
subname = cleanSubname(subname);
if (StringUtils.isNotBlank(subname)) {
Mod mod = getSubnameMap().get(subname);
if (mod != null && (StringUtils.isBlank(id) || mod.getModIds().contains(id)))
return mod;
}
if (StringUtils.isNotBlank(id))
return getModIdMap().get(id);
return null;
}
public abstract String getMcmodUrl(Mod mod);
public List<Mod> searchMod(String query) {
if (!loadKeywords()) return Collections.emptyList();
StringBuilder newQuery = ((CharSequence) query).chars()
StringBuilder newQuery = query.chars()
.filter(ch -> !Character.isSpaceChar(ch))
.collect(StringBuilder::new, (sb, value) -> sb.append((char) value), StringBuilder::append);
query = newQuery.toString();
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxKeywordLength);
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), getMaxKeywordLength());
List<Pair<Integer, Mod>> modList = new ArrayList<>();
for (Pair<String, Mod> keyword : keywords) {
for (Pair<String, Mod> keyword : getKeywords()) {
int value = lcs.calc(query, keyword.getKey());
if (value >= Math.max(1, query.length() - 3)) {
modList.add(pair(value, keyword.getValue()));
@@ -113,92 +279,7 @@ public enum ModTranslations {
return modList.stream()
.sorted((a, b) -> -a.getKey().compareTo(b.getKey()))
.map(Pair::getValue)
.collect(Collectors.toList());
}
private boolean loadFromResource() {
if (mods != null) return true;
if (StringUtils.isBlank(resourceName)) {
mods = Collections.emptyList();
return true;
}
//noinspection DataFlowIssue
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8))) {
mods = reader.lines().filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
return true;
} catch (Exception e) {
LOG.warning("Failed to load " + resourceName, e);
return false;
}
}
private boolean loadCurseForgeMap() {
if (curseForgeMap != null) {
return true;
}
if (mods == null) {
if (!loadFromResource()) return false;
}
curseForgeMap = new HashMap<>();
for (Mod mod : mods) {
if (StringUtils.isNotBlank(mod.getCurseforge())) {
curseForgeMap.put(mod.getCurseforge(), mod);
}
}
return true;
}
private boolean loadModIdMap() {
if (modIdMap != null) {
return true;
}
if (mods == null) {
if (!loadFromResource()) return false;
}
modIdMap = new HashMap<>();
for (Mod mod : mods) {
for (String id : mod.getModIds()) {
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) {
modIdMap.put(id, mod);
}
}
}
return true;
}
private boolean loadKeywords() {
if (keywords != null) {
return true;
}
if (mods == null) {
if (!loadFromResource()) return false;
}
keywords = new ArrayList<>();
maxKeywordLength = -1;
for (Mod mod : mods) {
if (StringUtils.isNotBlank(mod.getName())) {
keywords.add(pair(mod.getName(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getName().length());
}
if (StringUtils.isNotBlank(mod.getSubname())) {
keywords.add(pair(mod.getSubname(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getSubname().length());
}
if (StringUtils.isNotBlank(mod.getAbbr())) {
keywords.add(pair(mod.getAbbr(), mod));
maxKeywordLength = Math.max(maxKeywordLength, mod.getAbbr().length());
}
}
return true;
.toList();
}
public static final class Mod {
@@ -217,7 +298,7 @@ public enum ModTranslations {
curseforge = items[0];
mcmod = items[1];
modIds = Collections.unmodifiableList(Arrays.asList(items[2].split(",")));
modIds = List.of(items[2].split(","));
name = items[3];
subname = items[4];
abbr = items[5];
@@ -45,8 +45,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
* @author Glavo
*/
public final class NativePatcher {
private NativePatcher() {
}
private static final Library NONEXISTENT_LIBRARY = new Library(null);
@@ -196,4 +194,84 @@ public final class NativePatcher {
return null;
}
}
public static SupportStatus checkSupportedStatus(GameVersionNumber gameVersion, Platform platform,
OSVersion systemVersion) {
if (platform.equals(Platform.WINDOWS_X86_64)) {
if (!systemVersion.isAtLeast(OSVersion.WINDOWS_10) && gameVersion.isAtLeast("1.20.5", "24w14a"))
return SupportStatus.UNSUPPORTED;
return SupportStatus.OFFICIAL_SUPPORTED;
}
if (platform.equals(Platform.MACOS_X86_64) || platform.equals(Platform.LINUX_X86_64))
return SupportStatus.OFFICIAL_SUPPORTED;
if (platform.equals(Platform.WINDOWS_X86) || platform.equals(Platform.LINUX_X86)) {
if (gameVersion.isAtLeast("1.20.5", "24w14a"))
return SupportStatus.UNSUPPORTED;
else
return SupportStatus.OFFICIAL_SUPPORTED;
}
if (platform.equals(Platform.WINDOWS_ARM64) || platform.equals(Platform.MACOS_ARM64)) {
if (gameVersion.compareTo("1.19") >= 0)
return SupportStatus.OFFICIAL_SUPPORTED;
String minVersion = platform.getOperatingSystem() == OperatingSystem.WINDOWS
? "1.8"
: "1.6";
return gameVersion.compareTo(minVersion) >= 0
? SupportStatus.LAUNCHER_SUPPORTED
: SupportStatus.TRANSLATION_SUPPORTED;
}
String minVersion = null;
String maxVersion = null;
if (platform.equals(Platform.FREEBSD_X86_64)) {
minVersion = "1.13";
} else if (platform.equals(Platform.LINUX_ARM64)) {
minVersion = "1.6";
} else if (platform.equals(Platform.LINUX_RISCV64)) {
minVersion = "1.8";
} else if (platform.equals(Platform.LINUX_LOONGARCH64)) {
minVersion = "1.6";
} else if (platform.equals(Platform.LINUX_LOONGARCH64_OW)) {
minVersion = "1.6";
maxVersion = "1.20.1";
} else if (platform.equals(Platform.LINUX_MIPS64EL) || platform.equals(Platform.LINUX_ARM32)) {
minVersion = "1.8";
maxVersion = "1.20.1";
}
if (minVersion != null) {
if (gameVersion.compareTo(minVersion) >= 0) {
if (maxVersion != null && gameVersion.compareTo(maxVersion) >= 0)
return SupportStatus.UNSUPPORTED;
String[] defaultGameVersions = GameVersionNumber.getDefaultGameVersions();
if (defaultGameVersions.length > 0 && gameVersion.compareTo(defaultGameVersions[0]) > 0) {
return SupportStatus.UNTESTED;
}
return SupportStatus.LAUNCHER_SUPPORTED;
} else {
return SupportStatus.UNSUPPORTED;
}
}
return SupportStatus.UNTESTED;
}
public enum SupportStatus {
OFFICIAL_SUPPORTED,
LAUNCHER_SUPPORTED,
TRANSLATION_SUPPORTED,
UNTESTED,
UNSUPPORTED,
}
private NativePatcher() {
}
}
@@ -101,14 +101,14 @@ public final class SelfDependencyPatcher {
private static final class DependencyDescriptor {
private static final String DEPENDENCIES_LIST_FILE = "/assets/openjfx-dependencies.json";
private static final Path DEPENDENCIES_DIR_PATH = Metadata.DEPENDENCIES_DIRECTORY.resolve(Platform.getPlatform().toString()).resolve("openjfx");
private static final Path DEPENDENCIES_DIR_PATH = Metadata.DEPENDENCIES_DIRECTORY.resolve(Platform.CURRENT_PLATFORM.toString()).resolve("openjfx");
static List<DependencyDescriptor> readDependencies() {
//noinspection ConstantConditions
try (Reader reader = new InputStreamReader(SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE), UTF_8)) {
Map<String, Map<String, List<DependencyDescriptor>>> allDependencies =
JsonUtils.GSON.fromJson(reader, mapTypeOf(String.class, mapTypeOf(String.class, listTypeOf(DependencyDescriptor.class))));
Map<String, List<DependencyDescriptor>> platform = allDependencies.get(Platform.getPlatform().toString());
Map<String, List<DependencyDescriptor>> platform = allDependencies.get(Platform.CURRENT_PLATFORM.toString());
if (platform == null)
return null;
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.i18n.translator.Translator;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -55,6 +55,10 @@ public final class I18n {
return locale.getResourceBundle();
}
public static Translator getTranslator() {
return locale.getTranslator();
}
public static String i18n(String key, Object... formatArgs) {
return locale.i18n(key, formatArgs);
}
@@ -64,22 +68,11 @@ public final class I18n {
}
public static String formatDateTime(TemporalAccessor time) {
return locale.formatDateTime(time);
return getTranslator().formatDateTime(time);
}
public static String getDisplaySelfVersion(RemoteVersion version) {
if (locale.getLocale().getLanguage().equals("lzh")) {
if (version instanceof GameRemoteVersion)
return WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion(version.getSelfVersion()));
else
return WenyanUtils.translateGenericVersion(version.getSelfVersion());
}
if (LocaleUtils.isEnglish(locale.getLocale()) && "Qabs".equals(LocaleUtils.getScript(locale.getLocale()))) {
return UpsideDownUtils.translate(version.getSelfVersion());
}
return version.getSelfVersion();
return getTranslator().getDisplayVersion(version);
}
/// Find the builtin localized resource with given name and suffix.
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.util.i18n.translator.Translator_lzh;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.Locale;
@@ -36,9 +37,9 @@ public final class MinecraftWiki {
if (wikiVersion.startsWith("2.0"))
translatedVersion = "二點〇";
else if (wikiVersion.startsWith("1.0.0-rc2"))
translatedVersion = WenyanUtils.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2"));
translatedVersion = Translator_lzh.translateGameVersion(GameVersionNumber.asGameVersion("1.0.0-rc2"));
else
translatedVersion = WenyanUtils.translateGameVersion(gameVersion);
translatedVersion = Translator_lzh.translateGameVersion(gameVersion);
if (translatedVersion.equals(gameVersion.toString()) || gameVersion instanceof GameVersionNumber.Old) {
return getWikiLink(SupportedLocale.getLocale(LocaleUtils.LOCALE_ZH_HANT), version);
@@ -15,7 +15,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
import com.google.gson.annotations.JsonAdapter;
@@ -24,12 +23,13 @@ import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.i18n.translator.Translator;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -72,10 +72,12 @@ public final class SupportedLocale {
private final boolean isDefault;
private final String name;
private final Locale locale;
private final TextDirection textDirection;
private ResourceBundle resourceBundle;
private ResourceBundle localeNamesBundle;
private DateTimeFormatter dateTimeFormatter;
private List<Locale> candidateLocales;
private Translator translator;
SupportedLocale() {
this.isDefault = true;
@@ -85,12 +87,14 @@ public final class SupportedLocale {
this.locale = StringUtils.isBlank(language)
? LocaleUtils.SYSTEM_DEFAULT
: Locale.forLanguageTag(language);
this.textDirection = LocaleUtils.getTextDirection(locale);
}
SupportedLocale(Locale locale) {
this.isDefault = false;
this.name = locale.toLanguageTag();
this.locale = locale;
this.textDirection = LocaleUtils.getTextDirection(locale);
}
public boolean isDefault() {
@@ -105,6 +109,10 @@ public final class SupportedLocale {
return locale;
}
public TextDirection getTextDirection() {
return textDirection;
}
public String getDisplayName(SupportedLocale inLocale) {
if (isDefault()) {
try {
@@ -200,23 +208,6 @@ public final class SupportedLocale {
}
}
public String formatDateTime(TemporalAccessor time) {
DateTimeFormatter formatter = dateTimeFormatter;
if (formatter == null) {
if (LocaleUtils.isEnglish(locale) && "Qabs".equals(locale.getScript())) {
return UpsideDownUtils.formatDateTime(time);
}
if (locale.getLanguage().equals("lzh")) {
return WenyanUtils.formatDateTime(time);
}
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("datetime.format"))
.withZone(ZoneId.systemDefault());
}
return formatter.format(time);
}
public String getFcMatchPattern() {
String language = locale.getLanguage();
String region = locale.getCountry();
@@ -252,6 +243,31 @@ public final class SupportedLocale {
return region.isEmpty() ? language : language + "-" + region;
}
public Translator getTranslator() {
Translator translator = this.translator;
if (translator != null)
return translator;
List<Locale> candidateLocales = getCandidateLocales();
for (Locale candidateLocale : candidateLocales) {
String className = DefaultResourceBundleControl.INSTANCE.toBundleName(Translator.class.getSimpleName(), candidateLocale);
if (Translator.class.getResource(className + ".class") != null) {
try {
Class<?> clazz = Class.forName(Translator.class.getPackageName() + "." + className);
MethodHandle constructor = MethodHandles.publicLookup()
.findConstructor(clazz, MethodType.methodType(void.class, SupportedLocale.class));
return this.translator = (Translator) constructor.invoke(this);
} catch (Throwable e) {
LOG.warning("Failed to create instance for " + className, e);
}
}
}
return this.translator = new Translator(this);
}
public boolean isSameLanguage(SupportedLocale other) {
return LocaleUtils.getRootLanguage(this.getLocale())
.equals(LocaleUtils.getRootLanguage(other.getLocale()));
@@ -0,0 +1,62 @@
/*
* 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.i18n.translator;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;
/// @author Glavo
public class Translator {
protected final SupportedLocale supportedLocale;
protected final Locale locale;
public Translator(SupportedLocale supportedLocale) {
this.supportedLocale = supportedLocale;
this.locale = supportedLocale.getLocale();
}
public final SupportedLocale getSupportedLocale() {
return supportedLocale;
}
public final Locale getLocale() {
return locale;
}
public String getDisplayVersion(RemoteVersion remoteVersion) {
return remoteVersion.getSelfVersion();
}
/// @see [#formatDateTime(TemporalAccessor)]
protected DateTimeFormatter dateTimeFormatter;
public String formatDateTime(TemporalAccessor time) {
DateTimeFormatter formatter = dateTimeFormatter;
if (formatter == null) {
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(supportedLocale.getResourceBundle().getString("datetime.format"))
.withZone(ZoneId.systemDefault());
}
return formatter.format(time);
}
}
@@ -15,7 +15,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
package org.jackhuang.hmcl.util.i18n.translator;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import java.io.IOException;
import java.io.InputStream;
@@ -30,13 +33,16 @@ import java.util.Map;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public final class UpsideDownUtils {
public class Translator_en_Qabs extends Translator {
private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
.withZone(ZoneId.systemDefault());
private static final Map<Integer, Integer> MAPPER = loadMap();
private static Map<Integer, Integer> loadMap() {
var map = new LinkedHashMap<Integer, Integer>();
InputStream inputStream = UpsideDownUtils.class.getResourceAsStream("/assets/lang/upside_down.txt");
InputStream inputStream = Translator_en_Qabs.class.getResourceAsStream("/assets/lang/upside_down.txt");
if (inputStream != null) {
try (inputStream) {
new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> {
@@ -65,13 +71,17 @@ public final class UpsideDownUtils {
return builder.reverse().toString();
}
private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
.withZone(ZoneId.systemDefault());
public Translator_en_Qabs(SupportedLocale locale) {
super(locale);
}
public static String formatDateTime(TemporalAccessor time) {
@Override
public String getDisplayVersion(RemoteVersion remoteVersion) {
return translate(remoteVersion.getSelfVersion());
}
@Override
public String formatDateTime(TemporalAccessor time) {
return translate(BASE_FORMATTER.format(time));
}
private UpsideDownUtils() {
}
}
@@ -15,8 +15,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
package org.jackhuang.hmcl.util.i18n.translator;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.time.Instant;
@@ -27,10 +30,8 @@ import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Glavo
*/
public final class WenyanUtils {
/// @author Glavo
public class Translator_lzh extends Translator {
private static final String DOT = "點";
private static final String[] NUMBERS = {
@@ -79,37 +80,8 @@ public final class WenyanUtils {
builder.append(hour % 2 == 0 ? '正' : '初');
}
public static String formatDateTime(TemporalAccessor time) {
LocalDateTime localDateTime;
if (time instanceof Instant)
localDateTime = ((Instant) time).atZone(ZoneId.systemDefault()).toLocalDateTime();
else
localDateTime = LocalDateTime.from(time);
StringBuilder builder = new StringBuilder(16);
appendYear(builder, localDateTime.getYear());
builder.append('年');
builder.append(numberToString(localDateTime.getMonthValue()));
builder.append('月');
builder.append(numberToString(localDateTime.getDayOfMonth()));
builder.append('日');
builder.append(' ');
appendHour(builder, localDateTime.getHour());
builder.append(numberToString(localDateTime.getMinute()));
builder.append('分');
builder.append(numberToString(localDateTime.getSecond()));
builder.append('秒');
return builder.toString();
}
public static String translateGameVersion(GameVersionNumber gameVersion) {
if (gameVersion instanceof GameVersionNumber.Release) {
var release = (GameVersionNumber.Release) gameVersion;
if (gameVersion instanceof GameVersionNumber.Release release) {
StringBuilder builder = new StringBuilder();
appendDigitByDigit(builder, String.valueOf(release.getMajor()));
builder.append(DOT);
@@ -120,6 +92,7 @@ public final class WenyanUtils {
appendDigitByDigit(builder, String.valueOf(release.getPatch()));
}
//noinspection StatementWithEmptyBody
if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) {
// do nothing
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) {
@@ -134,9 +107,7 @@ public final class WenyanUtils {
}
return builder.toString();
} else if (gameVersion instanceof GameVersionNumber.Snapshot) {
var snapshot = (GameVersionNumber.Snapshot) gameVersion;
} else if (gameVersion instanceof GameVersionNumber.Snapshot snapshot) {
StringBuilder builder = new StringBuilder();
appendDigitByDigit(builder, String.valueOf(snapshot.getYear()));
@@ -152,35 +123,20 @@ public final class WenyanUtils {
return builder.toString();
} else if (gameVersion instanceof GameVersionNumber.Special) {
String version = gameVersion.toString();
switch (version.toLowerCase(Locale.ROOT)) {
case "2.0":
return "二點〇";
case "2.0_blue":
return "二點〇藍";
case "2.0_red":
return "二點〇赤";
case "2.0_purple":
return "二點〇紫";
case "1.rv-pre1":
return "一點真視之預一";
case "3d shareware v1.34":
return "躍然享件一點三四";
case "20w14infinite":
case "20w14~":
case "20w14∞":
return "二〇週一四宇";
case "22w13oneblockatatime":
return "二二週一三典";
case "23w13a_or_b":
return "二三週一三暨";
case "24w14potato":
return "二四週一四芋";
case "25w14craftmine":
return "二五週一四礦";
default:
return version;
}
return switch (version.toLowerCase(Locale.ROOT)) {
case "2.0" -> "二點〇";
case "2.0_blue" -> "二點〇藍";
case "2.0_red" -> "二點〇赤";
case "2.0_purple" -> "二點〇紫";
case "1.rv-pre1" -> "一點真視之預一";
case "3d shareware v1.34" -> "躍然享件一點三四";
case "20w14infinite", "20w14~", "20w14∞" -> "二〇週一四宇";
case "22w13oneblockatatime" -> "二二週一三典";
case "23w13a_or_b" -> "二三週一三暨";
case "24w14potato" -> "二四週一四芋";
case "25w14craftmine" -> "二五週一四礦";
default -> version;
};
} else {
return gameVersion.toString();
}
@@ -211,6 +167,43 @@ public final class WenyanUtils {
return version;
}
private WenyanUtils() {
public Translator_lzh(SupportedLocale locale) {
super(locale);
}
@Override
public String getDisplayVersion(RemoteVersion remoteVersion) {
if (remoteVersion instanceof GameRemoteVersion)
return translateGameVersion(GameVersionNumber.asGameVersion(remoteVersion.getSelfVersion()));
else
return translateGenericVersion(remoteVersion.getSelfVersion());
}
@Override
public String formatDateTime(TemporalAccessor time) {
LocalDateTime localDateTime;
if (time instanceof Instant instant)
localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
else
localDateTime = LocalDateTime.from(time);
StringBuilder builder = new StringBuilder(16);
appendYear(builder, localDateTime.getYear());
builder.append('年');
builder.append(numberToString(localDateTime.getMonthValue()));
builder.append('月');
builder.append(numberToString(localDateTime.getDayOfMonth()));
builder.append('日');
builder.append(' ');
appendHour(builder, localDateTime.getHour());
builder.append(numberToString(localDateTime.getMinute()));
builder.append('分');
builder.append(numberToString(localDateTime.getSecond()));
builder.append('秒');
return builder.toString();
}
}
@@ -293,6 +293,14 @@
-fx-font-size: 12px;
}
.two-line-list-item > .first-line > .tag-warning {
-fx-text-fill: #d34336;;
-fx-background-color: #f1aeb5;
-fx-padding: 2;
-fx-font-weight: normal;
-fx-font-size: 12px;
}
.two-line-item-second-large {
}
@@ -918,6 +926,10 @@
-fx-background-color: derive(-fx-base-color, 60%);
}
.mod-info-list-cell:warning {
-fx-background-color: #F8D7DA;
}
.options-sublist {
-fx-background-color: white;
}
@@ -717,10 +717,10 @@ install.installer.game=Minecraft
install.installer.incompatible=Incompatible with %s
install.installer.install=Installing %s
install.installer.install_offline=Install/Update from Local File
install.installer.install_offline.extension=(Neo)Forge/OptiFine installer
install.installer.install_offline.tooltip=We support using the local (Neo)Forge/OptiFine installer.
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine installer
install.installer.install_offline.tooltip=We support using the local (Neo)Forge, Cleanroom, and OptiFine installer.
install.installer.install_online=Online Install
install.installer.install_online.tooltip=We currently support Forge, NeoForge, OptiFine, Fabric, Quilt, and LiteLoader.
install.installer.install_online.tooltip=We currently support Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt, and LiteLoader.
install.installer.liteloader=LiteLoader
install.installer.not_installed=Not installed
install.installer.optifine=OptiFine
@@ -1090,8 +1090,10 @@ mods.not_modded=You must install a modloader (Forge, NeoForge, Fabric, Quilt, or
mods.restore=Restore
mods.url=Official Page
mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot launch. Are you sure you want to update?
mods.warning.loader_mismatch=Mod loader mismatch
mods.install=Install
mods.save_as=Save As
mods.unknown=Unknown Mod
nbt.entries=%s entries
nbt.open.failed=Failed to open file
@@ -1418,6 +1420,8 @@ terracotta.easytier=About EasyTier
terracotta.terracotta=Terracotta | Multiplayer
terracotta.status=Lobby
terracotta.back=Exit
terracotta.feedback.title=Fill Out Feedback Form
terracotta.feedback.desc=As HMCL updates Multiplayer Core, we hope you can take 10 seconds to fill out the feedback form.
terracotta.sudo_installing=HMCL must verify your password before installing Multiplayer Core
terracotta.from_local.title=Third-party download channels for Multiplayer Core
terracotta.from_local.desc=In some areas, the built-in default download channel may be unstable.
@@ -1432,7 +1436,7 @@ terracotta.status.uninitialized.update=Multiplayer Core: Update Available
terracotta.status.uninitialized.update.title=Update Multiplayer Core (~ 8MiB)
terracotta.status.uninitialized.desc=You legally promise to strictly abide by all laws and regulations of your country or region during the multiplayer process.
terracotta.confirm.title=User Notice
terracotta.confirm.desc=Terracotta is a third-party open source free software, which has little relationship with HMCL.\n\
terracotta.confirm.desc=Terracotta is a third-party open source free software. Please provide feedback on any issues you encounter through the relevant channels during use.\n\
Terracotta is based on P2P, so that the final experience depends greatly on your network condition.\n\
You promise to strictly abide by all laws and regulations in your country or region when playing online.
terracotta.status.preparing=Multiplayer Core: Downloading (DO NOT exit HMCL)
@@ -1513,7 +1517,8 @@ update.note=Beta and Nightly channels may have more features or fixes, but they
update.latest=This is the latest version
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
update.tooltip=Update
update.preview=Try out the HMCL version
update.preview=Preview HMCL releases early
update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release.
version=Games
version.name=Instance Name
@@ -1529,6 +1534,8 @@ version.game.release=Release
version.game.releases=Releases
version.game.snapshot=Snapshot
version.game.snapshots=Snapshots
version.game.support_status.unsupported=Unsupported
version.game.support_status.untested=Untested
version.game.type=Type
version.launch=Launch Game
version.launch.empty=Start Game
@@ -723,10 +723,10 @@ install.installer.game=Minecraft
install.installer.incompatible=Incompatible con %s
install.installer.install=Instalando %s
install.installer.install_offline=Instalar/actualizar desde archivo local
install.installer.install_offline.extension=Instalador de (Neo)Forge/OptiFine
install.installer.install_offline.tooltip=Apoyamos el uso del instalador local de (Neo)Forge/OptiFine.
install.installer.install_offline.extension=Instalador de (Neo)Forge/Cleanroom/OptiFine
install.installer.install_offline.tooltip=Apoyamos el uso del instalador local de (Neo)Forge/Cleanroom/OptiFine.
install.installer.install_online=Instalación en línea
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, OptiFine, Fabric, Quilt y LiteLoader.
install.installer.install_online.tooltip=Actualmente soportamos Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader y OptiFine.
install.installer.liteloader=LiteLoader
install.installer.not_installed=No está instalado
install.installer.optifine=OptiFine
@@ -1444,6 +1444,8 @@ update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden ten
update.latest=Esta es la última versión.
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
update.tooltip=Actualización
update.preview=Vista previa de actualizaciones anticipadas
update.preview.tooltip=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
version=Juegos
version.name=Nombre de instancia
@@ -532,10 +532,10 @@ install.installer.game=礦藝
install.installer.incompatible=與 %s 不兼也
install.installer.install=裝 %s
install.installer.install_offline=取私案而置
install.installer.install_offline.extension= (新) 鍛/OptiFine 裝者
install.installer.install_offline.tooltip=可錄 (新) 鍛/Optifine 裝者
install.installer.install_offline.extension= (新) 鍛/Cleanroom/OptiFine 裝者
install.installer.install_offline.tooltip=可錄 (新) 鍛/Cleanroom/OptiFine 裝者
install.installer.install_online=網取而置
install.installer.install_online.tooltip=可置鍛、新鍛、緞、褥、輕載與 Optifine
install.installer.install_online.tooltip=可置鍛、新鍛、Cleanroom、緞、褥、輕載與 OptiFine
install.installer.liteloader=輕載
install.installer.not_installed=未裝
install.installer.optifine=OptiFine
@@ -714,10 +714,10 @@ install.installer.game=Minecraft
install.installer.incompatible=Несовместимо с %s
install.installer.install=Установить %s
install.installer.install_offline=Установить/обновить из локального файла
install.installer.install_offline.extension=Установщик (Neo)Forge/OptiFine
install.installer.install_offline.tooltip=Поддерживаются установщики (Neo)Forge/OptiFine.
install.installer.install_offline.extension=Установщик (Neo)Forge/Cleanroom/OptiFine
install.installer.install_offline.tooltip=Поддерживаются установщики (Neo)Forge/Cleanroom/OptiFine.
install.installer.install_online=Установить с интернета
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, OptiFine, Fabric, Quilt и LiteLoader.
install.installer.install_online.tooltip=Поддерживаются Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader и OptiFine.
install.installer.liteloader=LiteLoader
install.installer.not_installed=Не установлено
install.installer.optifine=OptiFine
@@ -659,10 +659,10 @@ install.installer.game=Minecraft
install.installer.incompatible=Несумісний з %s
install.installer.install=Встановлення %s
install.installer.install_offline=Встановити/Оновити з локального файлу
install.installer.install_offline.extension=Інсталятор (Neo)Forge/OptiFine
install.installer.install_offline.tooltip=Ми підтримуємо використання локального інсталятора (Neo)Forge/OptiFine.
install.installer.install_offline.extension=Інсталятор (Neo)Forge/Cleanroom/OptiFine
install.installer.install_offline.tooltip=Ми підтримуємо використання локального інсталятора (Neo)Forge/Cleanroom/OptiFine.
install.installer.install_online=Онлайн встановлення
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, OptiFine, Fabric, Quilt та LiteLoader.
install.installer.install_online.tooltip=Наразі ми підтримуємо Forge, NeoForge, Cleanroom, OptiFine, Fabric, Quilt та LiteLoader.
install.installer.liteloader=LiteLoader
install.installer.not_installed=Не встановлено
install.installer.optifine=OptiFine
@@ -524,10 +524,10 @@ install.installer.game=Minecraft
install.installer.incompatible=與 %s 不相容
install.installer.install=安裝 %s
install.installer.install_offline=從本機檔案安裝或升級
install.installer.install_offline.extension=(Neo)Forge/OptiFine 安裝器
install.installer.install_offline.tooltip=支援匯入已經下載好的 (Neo)Forge/OptiFine 安裝器
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安裝器
install.installer.install_offline.tooltip=支援匯入已經下載好的 (Neo)Forge、Cleanroom 及 OptiFine 安裝器
install.installer.install_online=線上安裝
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.install_online.tooltip=支援安裝 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.liteloader=LiteLoader
install.installer.not_installed=未安裝
install.installer.optifine=OptiFine
@@ -538,7 +538,7 @@ install.installer.external_version=%s [由外部安裝的版本,無法解除
install.installing=安裝
install.modpack=安裝模組包
install.modpack.installation=安裝模組包
install.name.invalid=名稱中包含特殊字元(如 Emoji 表情或中文字元)。\n建議修改名稱。名稱建議僅包含英文字母、數字和底線,以防啟動遊戲時出現問題。是否繼續安裝?
install.name.invalid=名稱中包含特殊字元 (如 Emoji 表情或中文字元)。\n建議修改名稱。名稱建議僅包含英文字母、數字和底線,以防啟動遊戲時出現問題。是否繼續安裝?
install.new_game=安裝新實例
install.new_game.already_exists=此實例已經存在,請重新命名
install.new_game.current_game_version=目前遊戲實例
@@ -748,7 +748,7 @@ modpack.wizard.step.3.title=選取模組包匯出類型
modpack.wizard.step.initialization.exported_version=要匯出的遊戲實例
modpack.wizard.step.initialization.force_update=強制升級模組包至最新版本 (需要自建伺服器)
modpack.wizard.step.initialization.include_launcher=包含啟動器
modpack.wizard.step.initialization.modrinth.info=在模組包建立過程中,啟動器將匹配 CurseForge/Modrinth 遠端資源替代本機檔案(包括模組、資源包和光影包)以縮減模組包大小,並將副檔名為「.disabled」的檔案標註為「安裝時可選項」。
modpack.wizard.step.initialization.modrinth.info=在模組包建立過程中,啟動器將匹配 CurseForge/Modrinth 遠端資源替代本機檔案 (包括模組、資源包和光影包) 以縮減模組包大小,並將副檔名為「.disabled」的檔案標註為「安裝時可選項」。
modpack.wizard.step.initialization.no_create_remote_files=不匹配遠端檔案
modpack.wizard.step.initialization.save=選取要匯出到的遊戲模組包位置
modpack.wizard.step.initialization.skip_curseforge_remote_files=不匹配 CurseForge 遠端資源
@@ -887,8 +887,10 @@ mods.not_modded=你需要先在「自動安裝」頁面安裝 Forge、NeoForge
mods.restore=回退
mods.url=官方頁面
mods.update_modpack_mod.warning=更新模組包中的模組可能導致模組包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎?
mods.warning.loader_mismatch=模組載入器不匹配
mods.install=安裝到目前實例
mods.save_as=下載到本機目錄
mods.unknown=未知模組
nbt.entries=%s 個條目
nbt.open.failed=開啟檔案失敗
@@ -1206,6 +1208,8 @@ terracotta.easytier=關於 EasyTier
terracotta.terracotta=Terracotta | 陶瓦聯機
terracotta.status=聯機大廳
terracotta.back=退出
terracotta.feedback.title=填寫回饋表
terracotta.feedback.desc=在 HMCL 更新聯機核心時,我們歡迎您用 10 秒時間填寫聯機品質回饋收集表。
terracotta.sudo_installing=HMCL 需要驗證您的密碼才能安裝線上核心
terracotta.from_local.title=線上核心第三方下載管道
terracotta.from_local.desc=在部分地區,內建的預設下載管道可能不穩定或連線緩慢
@@ -1215,15 +1219,15 @@ terracotta.export_log=匯出線上核心日誌
terracotta.export_log.desc=為分析錯誤提供更多信息
terracotta.status.bootstrap=正在收集資訊
terracotta.status.uninitialized.not_exist=未下載聯機核心
terracotta.status.uninitialized.not_exist.title=下載聯機核心(約 8MiB)
terracotta.status.uninitialized.not_exist.title=下載聯機核心 (約 8MiB)
terracotta.status.uninitialized.update=需更新聯機核心
terracotta.status.uninitialized.update.title=更新聯機核心(約 8MiB)
terracotta.status.uninitialized.update.title=更新聯機核心 (約 8MiB)
terracotta.status.uninitialized.desc=您承諾,在多人聯機全過程中,您將嚴格遵守您所在國家或地區的全部法律法規
terracotta.confirm.title=使用者須知
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,與 HMCL 無強關聯性。\n\
terracotta.confirm.desc=陶瓦聯機是第三方開源自由軟體,您在使用過程中所遇到的問題請通過相關管道進行回饋。\n\
多人連線基於 p2p,最終線上體驗和您的網路情況有較大關係。\n\
您承諾,在多人連線全過程中,您將嚴格遵守您所在國家或地區的全部法律法規。
terracotta.status.preparing=正在下載聯機核心(請勿退出啟動器)
terracotta.status.preparing=正在下載聯機核心 (請勿退出啟動器)
terracotta.status.launching=正在初始化聯機核心
terracotta.status.unknown=正在初始化聯機核心
terracotta.status.waiting=聯機核心已就緒
@@ -1242,7 +1246,7 @@ terracotta.status.scanning.back=這將同時停止掃描區域網路世界。
terracotta.status.host_starting=正在建立房間
terracotta.status.host_starting.back=這將會取消建立房間。
terracotta.status.host_ok=已建立房間
terracotta.status.host_ok.code=邀請碼(已自動複製到剪貼簿)
terracotta.status.host_ok.code=邀請碼 (已自動複製到剪貼簿)
terracotta.status.host_ok.code.copy=複製邀請碼
terracotta.status.host_ok.code.desc=請提醒您的朋友在 HMCL 或 PCL CE 多人遊戲功能中選擇房客模式,並輸入該邀請碼。
terracotta.status.host_ok.back=這將同時徹底關閉房間,其他房客將退出並不再能重新加入該房間。
@@ -1297,7 +1301,8 @@ update.note=開發版與預覽版包含更多的功能以及錯誤修復,但
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
update.preview=提前預覽 HMCL 版本
update.preview=提前測試 HMCL 預覽版本
update.preview.tooltip=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
version=遊戲
version.name=遊戲實例名稱
@@ -1313,6 +1318,8 @@ version.game.release=正式版
version.game.releases=正式版
version.game.snapshot=快照
version.game.snapshots=快照
version.game.support_status.unsupported=不支援
version.game.support_status.untested=未經測試
version.game.type=版本類型
version.launch=啟動遊戲
version.launch.empty=開始遊戲
@@ -534,10 +534,10 @@ install.installer.game=Minecraft
install.installer.incompatible=与 %s 不兼容
install.installer.install=安装 %s
install.installer.install_offline=从本地文件安装/升级
install.installer.install_offline.extension=(Neo)Forge/OptiFine 安装器
install.installer.install_offline.tooltip=支持导入已经下载好的 (Neo)Forge/OptiFine 安装器
install.installer.install_offline.extension=(Neo)Forge/Cleanroom/OptiFine 安装器
install.installer.install_offline.tooltip=支持导入已经下载好的 (Neo)Forge、Cleanroom 和 OptiFine 安装器
install.installer.install_online=在线安装
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.install_online.tooltip=支持安装 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine
install.installer.liteloader=LiteLoader
install.installer.not_installed=未安装
install.installer.optifine=OptiFine
@@ -548,7 +548,7 @@ install.installer.external_version=%s (由外部安装的版本,无法卸载
install.installing=安装
install.modpack=安装整合包
install.modpack.installation=安装整合包
install.name.invalid=名称中包含特殊字符(如 Emoji 表情或中文字符)。\n建议修改名称。名称建议仅包含英文字母、数字和下划线,以防启动游戏时出现问题。是否继续安装?
install.name.invalid=名称中包含特殊字符 (如 Emoji 表情或中文字符)。\n建议修改名称。名称建议仅包含英文字母、数字和下划线,以防启动游戏时出现问题。是否继续安装?
install.new_game=安装新游戏
install.new_game.already_exists=此实例已经存在,请换一个名字
install.new_game.current_game_version=当前游戏实例
@@ -758,7 +758,7 @@ modpack.wizard.step.3.title=选择整合包导出类型
modpack.wizard.step.initialization.exported_version=要导出的游戏实例
modpack.wizard.step.initialization.force_update=强制升级整合包至最新版本 (需要自建服务器)
modpack.wizard.step.initialization.include_launcher=包含启动器
modpack.wizard.step.initialization.modrinth.info=在整合包创建过程中,启动器将匹配 CurseForge/Modrinth 远程资源替代本地文件(包括模组、资源包和光影包)以缩减整合包体积,并将扩展名为“.disabled”的文件标注为“安装时可选项”。
modpack.wizard.step.initialization.modrinth.info=在整合包创建过程中,启动器将匹配 CurseForge/Modrinth 远程资源替代本地文件 (包括模组、资源包和光影包) 以缩减整合包体积,并将扩展名为“.disabled”的文件标注为“安装时可选项”。
modpack.wizard.step.initialization.no_create_remote_files=不匹配远程文件
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
modpack.wizard.step.initialization.skip_curseforge_remote_files=不匹配 CurseForge 远程资源
@@ -897,8 +897,10 @@ mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge
mods.restore=回退
mods.url=官方页面
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
mods.warning.loader_mismatch=模组加载器不匹配
mods.install=安装到当前实例
mods.save_as=下载到本地文件夹
mods.unknown=未知模组
nbt.entries=%s 个条目
nbt.open.failed=打开文件失败
@@ -1216,6 +1218,8 @@ terracotta.easytier=关于 EasyTier
terracotta.terracotta=Terracotta | 陶瓦联机
terracotta.status=联机大厅
terracotta.back=退出
terracotta.feedback.title=填写反馈表
terracotta.feedback.desc=在 HMCL 更新联机核心时,我们欢迎您用 10 秒时间填写联机质量反馈收集表。
terracotta.sudo_installing=HMCL 需要验证您的密码才能安装联机核心
terracotta.from_local.title=联机核心第三方下载渠道
terracotta.from_local.desc=在部分地区,HMCL 内置的默认下载渠道可能不稳定或连接缓慢
@@ -1225,15 +1229,15 @@ terracotta.export_log=导出联机核心日志
terracotta.export_log.desc=为分析错误提供更多信息
terracotta.status.bootstrap=正在收集信息
terracotta.status.uninitialized.not_exist=未下载联机核心
terracotta.status.uninitialized.not_exist.title=下载联机核心(约 8MiB)
terracotta.status.uninitialized.not_exist.title=下载联机核心 (约 8MiB)
terracotta.status.uninitialized.update=需更新联机核心
terracotta.status.uninitialized.update.title=更新联机核心(约 8MiB)
terracotta.status.uninitialized.update.title=更新联机核心 (约 8MiB)
terracotta.status.uninitialized.desc=您承诺,在多人联机全过程中,您将严格遵守您所在国家或地区的全部法律法规
terracotta.confirm.title=用户须知
terracotta.confirm.desc=陶瓦联机是第三方开源自由软件,与 HMCL 无强关联性。\n\
terracotta.confirm.desc=陶瓦联机是第三方开源自由软件,您在使用过程中所遇到的问题请通过相关渠道进行反馈。\n\
多人联机基于 p2p,最终联机体验和您的网络情况有较大关系。\n\
在多人联机全过程中,您将严格遵守您所在国家或地区的全部法律法规。
terracotta.status.preparing=正在下载联机核心(请勿退出启动器)
terracotta.status.preparing=正在下载联机核心 (请勿退出启动器)
terracotta.status.launching=正在初始化联机核心
terracotta.status.unknown=正在初始化联机核心
terracotta.status.waiting=联机核心已就绪
@@ -1252,7 +1256,7 @@ terracotta.status.scanning.back=这将同时停止扫描局域网世界。
terracotta.status.host_starting=正在启动房间
terracotta.status.host_starting.back=这将会取消创建房间。
terracotta.status.host_ok=已启动房间
terracotta.status.host_ok.code=邀请码(已自动复制到剪贴板)
terracotta.status.host_ok.code=邀请码 (已自动复制到剪贴板)
terracotta.status.host_ok.code.copy=复制邀请码
terracotta.status.host_ok.code.desc=请提醒您的朋友在 HMCL 或 PCL CE 多人联机功能中选择房客模式,并输入该邀请码。
terracotta.status.host_ok.back=这将同时彻底关闭房间,其他房客将退出并不再能重新加入该房间。
@@ -1308,6 +1312,7 @@ update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
update.preview=提前预览 HMCL 版本
update.preview.tooltip=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
version=游戏
version.name=游戏实例名称
@@ -1323,6 +1328,8 @@ version.game.release=正式版
version.game.releases=正式版
version.game.snapshot=快照
version.game.snapshots=快照
version.game.support_status.unsupported=不支持
version.game.support_status.untested=未经测试
version.game.type=版本类型
version.launch=启动游戏
version.launch.empty=开始游戏
+38 -38
View File
@@ -100,7 +100,7 @@ craftguide;106;craftguide;G键合成表;CraftGuide;
;107;mobdictionary;生物图鉴;Mob Dictionary;
biomes-o-plenty;108;biomesoplenty;超多生物群系;Biomes O' Plenty;BOP
;109;BambooMod;竹;Bamboo;
;110;tdmg;立体机动装置(Ibemu版);立体機動装置MOD / Three-Dimensional Maneuver Gear;TDMG
;110;tdmg;立体机动装置 (Ibemu版);立体機動装置MOD / Three-Dimensional Maneuver Gear;TDMG
enhanced-portals-3;111;enhancedportals;增强传送门3;Enhanced Portals 3;
compact-solars;112;compactsolars,CompactSolars;压缩太阳能阵列;CompactSolars / Compact Solar Arrays;
bibliocraft;113;BiblioCraft,bibliocraft;收藏馆;BiblioCraft;
@@ -662,7 +662,7 @@ compact-machines;712;compactmachines3,cm2,compactmachines,CompactMachines;更多
ex-nihilo-omnia;714;exnihiloomnia;无中生有-世间万物;Ex Nihilo Omnia;
last-sword-you-will-ever-need-mo;715;theonlychaosmodforyou;最终之剑;The Last Sword You Will Ever Need Mod;
the-last-sword-you-will-ever-need-reboot;716;tlsywen;最终之剑:重生;The Last Sword You Will Ever Need Reboot;
ftb-library-legacy-forge;718;ftblib,FTBL,ftbl;FTB Library(旧版);FTB Library (Forge) (Legacy);FTBL
ftb-library-legacy-forge;718;ftblib,FTBL,ftbl;FTB Library (旧版);FTB Library (Forge) (Legacy);FTBL
;719;InventoryScan;库存扫描;InventoryScan;IS
;720;godzillamod;哥斯拉;Godzilla;
thaumic-potatoes-2;721;thaumicpotatoes;神秘土豆2;Thaumic Potatoes 2;TP2
@@ -684,7 +684,7 @@ aether-aspects;737;AetherAspects;天境要素扫描;Aether Aspects;
arcane-arteries;738;arcanearteries;奥法献祭学;Arcane Arteries;
thaumic-palmistry;739;thaumicpalmistry;Thaumic Palmistry;;
one-click-crafting;740;oneclickcrafting;一键合成;One click crafting;OCC
essential-thaumaturgy-unofficial;741;;源质神秘学(非官方版);Essential Thaumaturgy Unofficial;
essential-thaumaturgy-unofficial;741;;源质神秘学 (非官方版);Essential Thaumaturgy Unofficial;
wireless-crafting-terminal;742;wct,ae2wct;无线合成终端;Wireless Crafting Terminal;WCT
waila-plugins;743;wailaplugins;Waila 插件;Waila Plugins;WP
appleskin;744;appleskin;苹果皮;AppleSkin;AS
@@ -704,7 +704,7 @@ hammer-lib;756;hammercore,hammerlib;锤子核心;Hammer Core/Hammer Lib;
thermal-casting;758;ThermalCasting;热力铸造室;Thermal Casting;
thermal-smeltery-redux;759;;热力冶炼厂重置版;Thermal Smeltery Redux;
brcore;760;BRCore;BRCore;;
industrial-expansion-te-addon;761;industrialexpansion;工业扩充(TE附属);Industrial Expansion [TE Addon];
industrial-expansion-te-addon;761;industrialexpansion;工业扩充 (TE附属);Industrial Expansion [TE Addon];
;762;gtveinlocator,detravscannermod;格雷科技矿脉定位器;GTVeinLocator;
;763;gtneioreplugin;格雷矿物NEI插件;GTNEIOrePlugin;GTNOP
spmodapi;764;spmodapi;SpmodAPI;;
@@ -2219,7 +2219,7 @@ lollipop;2328;lollipop;棒棒糖;Lollipop;
enemyz;2329;enemyz;敌意显示;Enemyz;
thaumic-restoration;2330;thaumicrestoration;神秘复辟;Thaumic Restoration;
;2331;vimi;连锁矿工数据包;Vein Miner Datapack;
;2332;;快速落叶(伪);Fast Leaf Decay;
;2332;;快速落叶 (伪);Fast Leaf Decay;
primal-boat;2333;primalboat;原始船舶;Primal Boat;
eyes-in-the-darkness;2334;eyesinthedarkness;Eyes in the Darkness;;
thaumic-augmentation;2335;thaumicaugmentation;神秘进阶;Thaumic Augmentation;TA
@@ -2910,7 +2910,7 @@ riddle-chests;3041;riddlechests;谜题箱;Riddle Chests;
;3042;cinfinitelib;CinfiniteLib;;CiL
totem-plus;3043;;图腾增强;Totem Plus;
blockmeterfabric;3044;blockmeter;方块计量表;Block Meter Fabric;
;3045;kamenrider3dmod;假面骑士(3D版);KamenRider3D;KR3D
;3045;kamenrider3dmod;假面骑士 (3D版);KamenRider3D;KR3D
fluid-drawers;3046;fluiddrawers;储液抽屉;Fluid Drawers;
creepypastacraft-reborn;3047;creepypastacraft;蠕动意面重置版;CreepyPastaCraft Reborn;
itemzoom;3048;itemzoom;物品缩放;ItemZoom;
@@ -3324,7 +3324,7 @@ eidolon;3469;eidolon;幻梦;Eidolon;
rats-ratlantis;3470;ratlantis;老鼠:鼠西洲;Rats: Ratlantis;
wthit;3471;wthit;What The Hell Is That;;WTHIT
modern-industrialization;3472;modern_industrialization;现代工业化;Modern Industrialization;MI
chunk-pregenerator-fabric;3473;chunkpregen;区块预生成器(Fabric);Fabric Chunk Pregenerator;
chunk-pregenerator-fabric;3473;chunkpregen;区块预生成器 (Fabric);Fabric Chunk Pregenerator;
jumploader;3474;jumploader;Jumploader;;
unloader;3475;unloader;Unloader;;
better-animal-models;3476;betteranimals;更真实的动物;Better animal models;
@@ -3920,7 +3920,7 @@ gender;4082;gender;Gender & Age Mod;;
;4083;mtm;Magical Talismans Mod;;
ticktock;4084;servertick;Server Tick;;
;4085;synthesis_formula_added;行尸走肉弹夹合成配方添加;;
fps-reducre-for-fabric;4086;;FPS减速器(Fabric);FPS Reducer (For Fabric);
fps-reducre-for-fabric;4086;;FPS减速器 (Fabric);FPS Reducer (For Fabric);
art-artillery;4087;art_artillery,aa2;艺术?火炮!;Art? Artillery!;AA
minemenufabric;4088;modmenu;我的菜单Fabric版;MineMenuFabric;
;4089;tabtps-fabric;TabTPS;;
@@ -4061,7 +4061,7 @@ firmalife;4226;firmalife;FirmaLife;;
supermartijn642s-core-lib;4227;supermartijn642corelib;SuperMartijn642's Core Lib;;
cherished-worlds;4228;cherishedworlds;存档置顶;Cherished Worlds;
ironfist;4229;IronFist;铁拳;Ironfist;
;4230;mining_dimension_cave_version;挖矿维度(矿洞版);Mining dimension(cave version);
;4230;mining_dimension_cave_version;挖矿维度 (矿洞版);Mining dimension(cave version);
simply-acceleration;4231;simplyacceleration;简单加速;Simply Acceleration;
creeper-habitats;4232;creeperhabitats;Creeper Habitats;;
husbandry;4233;husbandry;Husbandry;;
@@ -4269,9 +4269,9 @@ dungeons-mobs;4436;dungeons_mobs;地下城生物;Dungeons Mobs;
;4439;meteorrandomidea;流年年的随想作;MeteorRandomIdea;
;4440;fortyyears;四十年;Miyazaki Hayao Forty Years;
;4441;flycycle;Flycycle;;
;4442;civilizationswars;三体工业(TeaCon预览版);Civilizations Wars;
;4442;civilizationswars;三体工业 (TeaCon预览版);Civilizations Wars;
;4443;king_and_knight;明日方舟;King and knight;
;4444;ashihara;苇原(芝之章~百里乡野行纪);Ashihara;ASHR
;4444;ashihara;苇原 (芝之章~百里乡野行纪);Ashihara;ASHR
uusi-aurinko;4445;uusi-aurinko;新日;Uusi Aurinko;
;4446;serializableconsciousness;Serializable Consciousness;;SeriCons
;4448;elytra_booster;推进鞘翅;Elytra Booster;
@@ -4332,7 +4332,7 @@ capsule;4503;capsule;Capsule;;
isometric-renders;4504;isometric-renders;等轴渲染;Isometric Renders;IR
;4505;;Abstract's Recraft;;
enchanting-secretaire;4506;enchantnote;附魔记录;Enchanting Secretaire;
sync-fabric;4507;sync;克隆(Fabric);Sync (Fabric);
sync-fabric;4507;sync;克隆 (Fabric);Sync (Fabric);
tale-of-kingdoms-a-new-conquest;4508;taleofkingdoms;王国:新征程;Tale of Kingdoms: A new Conquest;
;4509;networktitle;网络窗口标题;NetworkTitle;
schwarz;4510;schwarz;黑;Schwarz;
@@ -6502,7 +6502,7 @@ uncle-js;6698;;Uncle J's;;
wither-bones;6699;wither_bones;凋灵之骨;Wither Bones;
sleep-warp;6700;sleepwarp;SleepWarp;;
diagonal-fences;6701;diagonalfences;Diagonal Fences;;DF
jumpy-boats-fabric;6702;jumpyboat;跳跃的船(Fabric/Quilt);Jumpy Boats(Fabric/Quilt);
jumpy-boats-fabric;6702;jumpyboat;跳跃的船 (Fabric/Quilt);Jumpy Boats(Fabric/Quilt);
dyeable-redstone-signal;6703;dyeable_redstone_signal;染色红石信号;Dyeable Redstone Signal;DRS
velocity-based-damage;6704;veldmg;Velocity Based Damage;;
no-telemetry;6705;no-telemetry;No Telemetry;;
@@ -7925,8 +7925,8 @@ fallout-wastelands;8133;fallout_wastelands;Fallout Wastelands;;
;8136;letitsnow;Let It Snow;;
dons-lightning-rod-mod;8137;dlr;Don's Lightning Rod;;
rainglow;8138;rainglow;Rainglow;;
nethers-delight-fabric;8139;nethersdelight;下界乐事(Fabric 版);Nether's Delight (Fabric);
quicksand-neoforge;8140;quicksand;流沙(NeoForge);Quicksand (NeoForge);
nethers-delight-fabric;8139;nethersdelight;下界乐事 (Fabric 版);Nether's Delight (Fabric);
quicksand-neoforge;8140;quicksand;流沙 (NeoForge);Quicksand (NeoForge);
axolotl-bucket-fix-forge;8141;axolotlbucketfix;美西螈桶修复 Forge 版;Axolotl Bucket Fix (Forge);
scape-and-run-parasites-survival-addon-by-nocube;8142;nocubessrpsurvival;Scape and Run: Parasites Survival Addon;;
scape-and-run-parasites-nests-addon-by-nocube;8143;nocubessrpnests;Scape and Run: Parasites Nests Addon;;
@@ -8587,7 +8587,7 @@ vanity-metallurgical;8803;vanity_metallurgical;Vanity: Metallurgical;;
minespaceex;8804;minespaceex;MineSpaceEX;;MSEX
gregious-maximus;8805;gregiousmaximus;Gregious Maximus;;
shut-up-model-loader;8806;shutupmodelloader;Shut Up, Model Loader!;;SUML
cultural-delights-fabric;8807;culturaldelights;多元乐事(Fabric 版);Cultural Delights [Fabric];
cultural-delights-fabric;8807;culturaldelights;多元乐事 (Fabric 版);Cultural Delights [Fabric];
collectors-reap;8808;collectorsreap;Collector's Reap;;
bones-and-swords;8809;bonesandswords;Bones And Swords;;
vanity-shedds-mystical-toolsets-pack;8810;sheddmersopal;Vanity: Shedd's Mystical Toolsets Pack;;
@@ -8669,7 +8669,7 @@ waymaker;8888;waymaker;Waystone Waypoint Maker;;
;8889;mixingasm;Mixingasm;;
dmod;8890;dmod;D-Mod;;
extra-redstone-craft;8891;extracraft;更多红石合成;Extra Redstone Craft;EXRS
farmers-respite-fabric;8892;farmersrespite;农夫暇事(Fabric 版);Farmer's Respite (Fabric);
farmers-respite-fabric;8892;farmersrespite;农夫暇事 (Fabric 版);Farmer's Respite (Fabric);
wizards;8893;wizards;魔法师;Wizards;
basketcase;8894;basketcase;Basketcase;;
primal-lib;8895;primallib;Primal Lib;;
@@ -9027,7 +9027,7 @@ universal-enchants-forge;9252;universalenchants;通用附魔;Universal Enchants;
aa-farmer-compat;9253;farmeradditions;AA Farmer Compat;;
;9254;sctz_ywsj;只要听话就不会死!;;
;9255;configurable-firework-explosion;可配置的烟花爆炸;Configurable Firework Explosion;CFE
;9256;;内修炼体(属性加点);;
;9256;;内修炼体 (属性加点);;
better-crates;9257;bettercrates;Better Crates;;
vanillaicecreamfix;9259;vanillaicecreamfix;VanillaIcecreamFix;;
actually-unbreaking-forge;9260;actuallyunbreaking;Actually Unbreaking;;
@@ -9235,7 +9235,7 @@ cute-hermit-crabs;9466;cutehermitcrabs;Cute Hermit Crabs;;
;9467;ulib;uLib;;
receiver-mod;9468;rm;Receiver Gun Mod;;
chargepads;9469;ChargePads;充电座;ChargePads;
snow-real-magic-potato-edition;9470;;雪!真实的魔法!(土豆版);Snow Real Magic Potato Edition;
snow-real-magic-potato-edition;9470;;雪!真实的魔法!(土豆版);Snow Real Magic Potato Edition;
;9471;depthstrider;Depth Strider;;
ravenous-void;9472;rv;Ravenous Void;;
immersive-engineering-blueprint-tweaker;9473;iebpt;Immersive Engineering Blueprint Tweaker;;IEBPT
@@ -9434,7 +9434,7 @@ splatcraft;9665;splatcraft;斯普拉遁;Splatcraft;
beneath-the-snow;9666;beneath_thesnow_mr;Beneath The Snow;;
sea-creeps;9667;aquacreepermod;Sea Creeps;;
skills-v;9668;skillsvanilla;技能:香草;Skills: Vanilla;
;9669;teleportation;瞬身术(Fabric);Teleportation (Fabric);
;9669;teleportation;瞬身术 (Fabric);Teleportation (Fabric);
flower-foxes;9670;flower_foxes;Flower Foxes!;;
pack-it-up;9671;pack_it_up;Pack It Up!;;
mine-trading-cards-open-source-edition;9672;is_mtc;Mine Trading Cards Open Source Edition;;
@@ -11746,7 +11746,7 @@ stairdoors-extended;11999;stairdoors;StairDoors Extended;;
into-the-rend;12000;therenddimension;Into The Rendv / The Rend Dimension;;
buff-frog-mod;12001;buff_frog_mod;Buff Frogs;;
asphere;12002;asphere;Asphere;;
autoclick-machine;12003;clickmachine;自动连点器(Fabric);Click Machine (Fabric);
autoclick-machine;12003;clickmachine;自动连点器 (Fabric);Click Machine (Fabric);
harvestcraft-tweaker;12004;harvestcrafttweaker;HarvestCraft Tweaker;;
the-legend-of-the-mudman;12005;legend_of_the_mudman;The Legend of the Mudman;;
mutagenesis;12006;mutagenesis;Mutagenesis;;
@@ -11842,7 +11842,7 @@ the-digimod-beta;12095;thedigimod;方块数码宝贝;The Digimod;
ex-deorum;12096;exdeorum;无中生有:神赐;Ex Deorum;
entity-detectors;12097;entitydetectors;Entity Detectors;;
;12098;horsearmorstandmod;Horse Armor Stand;;
decorative-blocks-fork;12099;decorative_blocks;装饰方块(分支版);Decorative Blocks (Fork);
decorative-blocks-fork;12099;decorative_blocks;装饰方块 (分支版);Decorative Blocks (Fork);
above-and-below;12100;above_and_below;Above and Below;;
;12101;ppcore;PPcore;;
freeze-it-and-heat-it;12102;fiahi;冷汗:食物冷冻与加热;Freeze It, and Heat It!;FIAHI
@@ -11974,10 +11974,10 @@ rustic-furniture-compat;12227;rusticcompat;Rustic Furniture Compat;;
;12228;norealmsbutton;Realms按钮移除;No Realms Button;
;12229;benchantments;BEnchantments;;
epic-fight-indestructible;12230;indestructible;坚不可摧;Epic Fight - Indestructible;
;12231;set_bonus;Set Bonus(TeamPancake);;
;12232;chat_bar_time_display;聊天栏时间显示(TeamPancake 版);When Was That Again(TeamPancke);
;12231;set_bonus;Set Bonus (TeamPancake);;
;12232;chat_bar_time_display;聊天栏时间显示 (TeamPancake 版);When Was That Again(TeamPancke);
;12233;;逃离塔块夫-重铸;Escape From Tarcraft Reforged;EFTR
;12234;;右键文本清除(TeamPancake 版);Right Click Clear(TeamPancake);
;12234;;右键文本清除 (TeamPancake 版);Right Click Clear(TeamPancake);
catwalks-llc;12235;catwalksinc;Catwalks LLC.;;
divine-quest-rpg;12236;tales_of_sunnrya,divine_quest;Tales of Sunnrya / Divine Quest RPG;;
;12237;carpet-org-addition;Carpet Org Addition;;
@@ -12021,7 +12021,7 @@ rusticwitchcraft;12274;rusticwitchcraft;Rustic Witchcraft;;
;12275;unif.logger;UniF-Logger;;
sterling-and-black;12276;sterlingblack;Sterling and Black;;
the-devourer-of-skulls-datapack;12277;;The Devourer of Skulls 『𝔹𝕠𝕤𝕤 𝔽𝕚𝕘𝕙𝕥』;;
;12278;birds_nests;鸟巢(TeamPancake 版);Birds Nests(Team Pancake);
;12278;birds_nests;鸟巢 (TeamPancake 版);Birds Nests(Team Pancake);
boss-and-magic-raid-compat;12279;;Boss And Magic: Raid Compat;;
animated-mobs;12280;animatedmobsmod;Boss And Magic / AME's Mobs / Animated Mobs;;
;12281;GTQTCore;量子跃迁:纠缠核心;GTQTcore;GTC
@@ -13701,12 +13701,12 @@ the-super-god-road;13962;TheSuperGodRoad;超神之路;TheSuperGodRoad;SGR
;13965;exblades;ExBlades;;
storage-delight-forge;13966;storagedelight;存储乐事;Storage Delight;
lightshield;13967;lightshield;光之护盾;LightShield;LShield
sophisticated-core-unofficial-fabric-port;13968;;精妙核心(非官方 Fabric 移植);Sophisticated Core (Unofficial Fabric port);
sophisticated-backpacks-unofficial-fabric-port;13969;;精妙背包(非官方 Fabric 分支);Sophisticated Backpacks (Unofficial Fabric port);
sophisticated-core-unofficial-fabric-port;13968;;精妙核心 (非官方 Fabric 移植);Sophisticated Core (Unofficial Fabric port);
sophisticated-backpacks-unofficial-fabric-port;13969;;精妙背包 (非官方 Fabric 分支);Sophisticated Backpacks (Unofficial Fabric port);
red-power;13970;red_power;Red Power;;
;13971;hdykwd;你怎么知道我做了什么;How do you know what I done;HDYKWD
;13972;minihudextra;MiniHUD Extra;;MHEx
sophisticated-storage-unofficial-fabric-port;13973;;精妙存储(非官方 Fabric 分支);Sophisticated Storage (Unofficial Fabric port);
sophisticated-storage-unofficial-fabric-port;13973;;精妙存储 (非官方 Fabric 分支);Sophisticated Storage (Unofficial Fabric port);
;13974;oldblockdrops,oldoredropsforge;Old Ore Drops;;
rage-mod;13975;rage;兴怒;Rage Mod;
;13976;disableallstructers;Disable All Structures;;
@@ -14500,7 +14500,7 @@ the-rings-of-power;14766;trop;力量之戒/魔戒众戒;The Rings of Power;TRoP
suspicious-zombification;14770;suszombification;Suspicious Zombification;;
;14771;wmis;Where Is My Stuff-Forge;;WIMSF
ivtoolkit-arcana-rpg-continuation-edition;14772;IvToolkit;IvToolkit:延续;IvToolkit Arcana Rpg Continuation Edition;
farmer-s-delight-bedrock-unofficial;14773;FarmersDelightBedrockUnofficial;农夫乐事基岩版(Unofficial);Farmer's Delight Bedrock (Unofficial);
farmer-s-delight-bedrock-unofficial;14773;FarmersDelightBedrockUnofficial;农夫乐事基岩版 (Unofficial);Farmer's Delight Bedrock (Unofficial);
;14774;byzh-picgen;byzh's 麦块画图;BYZH's Picgen;
tropicraft-continuation;14775;tropicraft;热带世界:延续;Tropicraft Continuation;
battle-tower-continuation;14776;AS_BattleTowers;战斗高塔:延续;Battle Tower Continuation;
@@ -14676,9 +14676,9 @@ skeletalband;14947;skeletalband;骷髅乐队乐谱;SkeletalBand;
alexs-caves-entropy;14948;entropy;Alex 的洞穴:熵;Alex's Caves:Entropy;ACE
mekanism-explosives;14949;mekanismexplosives;通用机械:炸药;Mekanism Explosives;
;14950;minimalcoordshud;Minimal Coords HUD;;
;14951;;存储乐事基岩版(Unofficial);Storage Delight Bedrock (Unofficial);
;14952;;蟹农乐事基岩版(Unofficial);Crabber's Delight Bedrock (Unofficial);
;14953;;末地乐事基岩版(Unofficial);End's Delight Bedrock (Unofficial);
;14951;;存储乐事基岩版 (Unofficial);Storage Delight Bedrock (Unofficial);
;14952;;蟹农乐事基岩版 (Unofficial);Crabber's Delight Bedrock (Unofficial);
;14953;;末地乐事基岩版 (Unofficial);End's Delight Bedrock (Unofficial);
avaritiareborn;14954;avaritia;无尽贪婪:复兴;AvaritiaReborn;
cherry-shrimp;14955;cherry_shrimp;Cherry Shrimp;;
pick-command;14956;pickcommand;俺拾的指令;Pick Command;PiC
@@ -18132,7 +18132,7 @@ deimos-fabric-forge-neoforge;18436;deimos;Deimos;;
excavation;18438;excavation;挖掘;Excavation;
basic-end-ores;18439;beo;基础末地矿石;Basic End Ores;BEO
;18440;portable_crafting;Portable Crafting;;
;18441;gofish;一起钓鱼吧!(Forge 版);Go Fish Forge;
;18441;gofish;一起钓鱼吧!(Forge 版);Go Fish Forge;
simple-bbq-unofficial;18442;simplebbq;简易烧烤非官方版;Simple BBQ Unofficial;
;18443;carystanley_extremefarming;Extreme Farming;;
eggy-goodness;18444;EggyGoodness;Eggy Goodness;;
@@ -18332,7 +18332,7 @@ auto-pillar-generator;18637;apg;一键柱子生成;Auto Pillar Generator;APG
vanilla-delight;18638;vanilladelight;Vanilla Delight;;
jade-vs;18639;jade_vs;Jade VS;;
;18640;custom_ender_dragon;Draconic Ascension - Custom Ender Dragon Fight;;
vics-modern-warfare-patched;18641;;维克的现代战争(修复版);Vic's Modern Warfare (Patched);VMW
vics-modern-warfare-patched;18641;;维克的现代战争 (修复版);Vic's Modern Warfare (Patched);VMW
waterframesbackported;18642;waterframes;WaterFramesBackported;;
;18643;sihywtcamc-unofficial-port;So I heard you were talking crap about Minecraft's combat? - 非官方移植;sihywtcamc unofficial port;sihywtcamc
tweakeroo-update-port;18644;tweakeroo;Tweakeroo Update Port;;
@@ -18777,8 +18777,8 @@ exporbrecall;19084;exporbrecall;经验球召回;ExpOrbRecall;
functional-economy;19085;functional_economy;功能性钱币;FunctionalEconomy;FE
ranckors-rings-of-power;19086;the_rings_of_power;Ranckor的力量之戒;Ranckor's Rings of Power;
lotr-improvements-lite;19087;lotrimprovementslite;魔戒改进:轻量版;LOTR Improvements Lite;
;19088;;海洋乐事基岩版(Unofficial);Ocean's Delight Bedrock (Unofficial);
;19089;;幽匿乐事基岩版(unofficial);Silent's Delight Bedrock (Unofficial);
;19088;;海洋乐事基岩版 (Unofficial);Ocean's Delight Bedrock (Unofficial);
;19089;;幽匿乐事基岩版 (unofficial);Silent's Delight Bedrock (Unofficial);
astounding;19090;astounding;Astounding;;AUD
carpets-core;19091;carpetscore;Carpets Core;;
carpet-variants;19092;carpetvariants;Carpet Variants;;
@@ -19206,7 +19206,7 @@ baublesreforked;19516;baublesreforked;饰品重铸;BaublesReforked;
tmrv;19518;tmrv;TooManyRecipeViewers;;TMRV
;19519;codmodern;COD击杀信息显示;COD Modern Warfare Information;CMW
twilight-treehouses;19520;twilight_treehouses;Twilight Treehouses;;
improved-rails;19521;irails;更好的铁轨(可转弯の动力铁轨!);Improved Rail (TURNABLE POWERED RAILS);IRail
improved-rails;19521;irails;更好的铁轨 (可转弯の动力铁轨!);Improved Rail (TURNABLE POWERED RAILS);IRail
redstone-signal-bind-wand;19522;wlrt;非Create版无线红石;Redstone Signal Bind Wand;RSBW
;19523;mr_custom_packetaggregates;定制数据包集合体;Custom packet aggregates;CPA
;19524;trator;游戏内聊天翻译;In game Translator;IGTL
+11 -11
View File
@@ -1,18 +1,18 @@
{
"version_legacy": "0\\.3\\.[89]-rc.[0-9]",
"version_legacy": "0\\.3\\.[89]-rc\\.([0-9]|10)",
"version_recent": [
"0.3.9-rc.9"
"0.3.9-rc.10"
],
"version_latest": "0.3.9-rc.10",
"version_latest": "0.3.10",
"classifiers": {
"linux-arm64": "sha256:d5d386e9936d99dd8c31cdab2098a9db04bf55263faf5cd27d388139a42dee0b",
"linux-x86_64": "sha256:c43f111460c777315f12081d934729c08cc67b1a2048fdf3ae451e2682157dd8",
"macos-arm64": "sha256:49cbf5880aa94551951abda0e225bbbd9c43b9078ead8a738f5b995085d6022c",
"macos-arm64.pkg": "sha256:756831f50d67ed2382f8507dfcc94fcc32d6082e087cd4f6a2fc40ae39f25cdf",
"macos-x86_64": "sha256:fdf519f018ce4b3d5b4be06b86dcf9990e94b4010db9dac391175d917214cc01",
"macos-x86_64.pkg": "sha256:fc030f0205aa3667033c79ac514045bb7c94ae0175dc5946ea920eefee7ad997",
"windows-arm64.exe": "sha256:4b395093b0acaea85a36ce1e569c206d9dfa7e11bfe70409f00bc6c6100df88e",
"windows-x86_64.exe": "sha256:2b8add8f6330452d29faaa944ac63c35452e61c5a4ff009069d2c3017c7186c6"
"linux-arm64": "sha256:001b6b1e220e98dffd84198a3e16855644cad27900562c78d610b0bf50138d17",
"linux-x86_64": "sha256:b133ddb796ff52879fb0c38e68ef7ea968d3eaec77f27ea274c701888e795782",
"macos-arm64": "sha256:4bb2eb432ea0ee3e3dca6b1019d8864aa14bedfe20b3d804865e216e36e856df",
"macos-arm64.pkg": "sha256:e4bf3d2014e417565e3ab0df579f4280e0af42bc28516f640067a52dca77098f",
"macos-x86_64": "sha256:17a2cc46d884951a88da6f3f0725e38ffca6fdfd1da021807121c29b944bd830",
"macos-x86_64.pkg": "sha256:a4f08a706605262485c1889dfae00554fba753141f6d6ada76e29bfc3e0338aa",
"windows-arm64.exe": "sha256:245e0faf2ee1cdc5e430b5c18fbab4c4e5e3f333f0de51a621ae772db3a16924",
"windows-x86_64.exe": "sha256:ab99ddb729860c939b2085d8852fd62007b86d97568b687cf2a7147718b64eea"
},
"downloads": [
"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}",
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.i18n;
package org.jackhuang.hmcl.util.i18n.translator;
import org.junit.jupiter.api.Test;
@@ -26,33 +26,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Glavo
*/
public final class WenyanUtilsTest {
public final class TranslatorTest {
private static void assertYearToString(String value, int year) {
//region lzh
private static void assertYearToLZH(String value, int year) {
StringBuilder builder = new StringBuilder(2);
WenyanUtils.appendYear(builder, year);
Translator_lzh.appendYear(builder, year);
assertEquals(value, builder.toString());
}
@Test
public void testYearToString() {
assertYearToString("甲子", 1984);
assertYearToString("乙巳", 2025);
assertYearToString("甲子", -2996);
assertYearToString("庚子", 1000);
public void testYearToLZH() {
assertYearToLZH("甲子", 1984);
assertYearToLZH("乙巳", 2025);
assertYearToLZH("甲子", -2996);
assertYearToLZH("庚子", 1000);
}
@Test
public void testHourToString() {
public void testHourToLZH() {
List<String> list = List.of(
"子正", "丑初", "丑正", "寅初", "寅正", "卯初", "卯正", "辰初", "辰正", "巳初", "巳正", "午初",
"午正", "未初", "未正", "申初", "申正", "酉初", "酉正", "戌初", "戌正", "亥初", "亥正", "子初"
);
for (int hour = 0; hour < list.size(); hour++) {
StringBuilder builder = new StringBuilder(2);
WenyanUtils.appendHour(builder, hour);
Translator_lzh.appendHour(builder, hour);
assertEquals(list.get(hour), builder.toString());
}
}
//endregion
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.download.cleanroom.CleanroomInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
@@ -182,6 +183,11 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
return Task
.composeAsync(() -> {
try {
return CleanroomInstallTask.install(this, oldVersion, installer);
} catch (IOException ignore) {
}
try {
return NeoForgeInstallTask.install(this, oldVersion, installer);
} catch (IOException ignore) {
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.download;
import org.intellij.lang.annotations.Language;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jackhuang.hmcl.util.versioning.VersionRange;
@@ -172,6 +171,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
String mainClass = resolvedVersion.getMainClass();
return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass)
|| mainClass.startsWith("net.minecraftforge")
|| mainClass.startsWith("net.neoforged")
|| mainClass.startsWith("top.outlands") //Cleanroom
|| mainClass.startsWith("net.fabricmc")
|| mainClass.startsWith("org.quiltmc")
@@ -361,14 +361,16 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
public static final String MOD_LAUNCHER_MAIN = "cpw.mods.modlauncher.Launcher";
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
public static final String FORGE_BOOTSTRAP_MAIN = "net.minecraftforge.bootstrap.ForgeBootstrap";
public static final String NEO_FORGED_BOOTSTRAP_MAIN = "net.neoforged.fml.startup.Client";
public static final Set<String> FORGE_OPTIFINE_MAIN = new HashSet<>(Lang.immutableListOf(
public static final Set<String> FORGE_OPTIFINE_MAIN = Set.of(
LibraryAnalyzer.VANILLA_MAIN,
LibraryAnalyzer.LAUNCH_WRAPPER_MAIN,
LibraryAnalyzer.MOD_LAUNCHER_MAIN,
LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN,
LibraryAnalyzer.FORGE_BOOTSTRAP_MAIN
));
LibraryAnalyzer.FORGE_BOOTSTRAP_MAIN,
LibraryAnalyzer.NEO_FORGED_BOOTSTRAP_MAIN
);
public static final VersionRange<VersionNumber> FORGE_OPTIFINE_BROKEN_RANGE = VersionNumber.between("48.0.0", "49.0.50");
@@ -21,30 +21,48 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.UnsupportedInstallationException;
import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile;
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
public final class CleanroomInstallTask extends Task<Version> {
private final DefaultDependencyManager dependencyManager;
private final Version version;
private Path installer;
private final CleanroomRemoteVersion remote;
private Path installer;
private FileDownloadTask dependent;
private Task<Version> task;
private String selfVersion;
public CleanroomInstallTask(DefaultDependencyManager dependencyManager, Version version, CleanroomRemoteVersion remoteVersion) {
this.dependencyManager = dependencyManager;
this.version = version;
this.remote = remoteVersion;
setSignificance(TaskSignificance.MODERATE);
}
public CleanroomInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) {
this.dependencyManager = dependencyManager;
this.version = version;
this.selfVersion = selfVersion;
this.remote = null;
this.installer = installer;
setSignificance(TaskSignificance.MODERATE);
}
@@ -55,14 +73,16 @@ public final class CleanroomInstallTask extends Task<Version> {
@Override
public void preExecute() throws Exception {
installer = Files.createTempFile("cleanroom-installer", ".jar");
if (installer == null) {
installer = Files.createTempFile("cleanroom-installer", ".jar");
dependent = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
installer, null);
dependent.setCacheRepository(dependencyManager.getCacheRepository());
dependent.setCaching(true);
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
dependent = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
installer, null);
dependent.setCacheRepository(dependencyManager.getCacheRepository());
dependent.setCaching(true);
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
}
}
@Override
@@ -78,7 +98,7 @@ public final class CleanroomInstallTask extends Task<Version> {
@Override
public Collection<Task<?>> getDependents() {
return Collections.singleton(dependent);
return dependent == null ? Collections.emptySet() : Collections.singleton(dependent);
}
@Override
@@ -88,6 +108,31 @@ public final class CleanroomInstallTask extends Task<Version> {
@Override
public void execute() throws IOException, VersionMismatchException, UnsupportedInstallationException {
task = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId()));
if (selfVersion == null) {
task = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId()));
} else {
task = new ForgeNewInstallTask(dependencyManager, version, selfVersion, installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId()));
}
}
public static Task<Version> install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException {
Optional<String> gameVersion = dependencyManager.getGameRepository().getGameVersion(version);
if (gameVersion.isEmpty()) throw new IOException();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
String installProfileText = Files.readString(fs.getPath("install_profile.json"));
Map<?, ?> installProfile = JsonUtils.fromNonNullJson(installProfileText, Map.class);
if (LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId().equals(installProfile.get("profile"))) {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
if (!gameVersion.get().equals(profile.getMinecraft()))
throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get());
return new CleanroomInstallTask(dependencyManager, version, modifyVersion(profile.getVersion()), installer);
} else {
throw new IOException();
}
}
}
private static String modifyVersion(String version) {
return version.replace("cleanroom-", "");
}
}
@@ -149,6 +149,11 @@ public class DefaultGameRepository implements GameRepository {
return getVersionRoot(id).resolve("natives-" + platform);
}
@Override
public Path getModsDirectory(String id) {
return getRunDirectory(id).resolve("mods");
}
@Override
public Path getVersionRoot(String id) {
return getBaseDirectory().resolve("versions/" + id);
@@ -131,6 +131,12 @@ public interface GameRepository extends VersionProvider {
*/
Path getNativeDirectory(String id, Platform platform);
/// Get the directory for placing mod files.
///
/// @param id instance id
/// @return the mods directory
Path getModsDirectory(String id);
/**
* Get minecraft jar
*
@@ -189,7 +189,7 @@ public final class LocalModFile implements Comparable<LocalModFile> {
@Override
public int compareTo(LocalModFile o) {
return getFileName().compareTo(o.getFileName());
return getFileName().compareToIgnoreCase(o.getFileName());
}
@Override
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.mod;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.mod.modinfo.*;
import org.jackhuang.hmcl.util.Pair;
@@ -25,39 +26,46 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModManager {
@FunctionalInterface
private interface ModMetadataReader {
LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException;
}
private static final Map<String, Pair<ModMetadataReader[], String>> READERS;
private static final Map<String, List<Pair<ModMetadataReader, ModLoaderType>>> READERS;
static {
TreeMap<String, Pair<ModMetadataReader[], String>> readers = new TreeMap<>();
readers.put("zip", Pair.pair(new ModMetadataReader[]{
ForgeOldModMetadata::fromFile,
ForgeNewModMetadata::fromFile,
FabricModMetadata::fromFile,
QuiltModMetadata::fromFile,
PackMcMeta::fromFile,
}, ""));
readers.put("jar", readers.get("zip"));
readers.put("litemod", Pair.pair(new ModMetadataReader[]{
LiteModMetadata::fromFile
}, "LiteLoader Mod"));
READERS = Collections.unmodifiableMap(readers);
var map = new HashMap<String, List<Pair<ModMetadataReader, ModLoaderType>>>();
var zipReaders = List.<Pair<ModMetadataReader, ModLoaderType>>of(
pair(ForgeNewModMetadata::fromForgeFile, ModLoaderType.FORGE),
pair(ForgeNewModMetadata::fromNeoForgeFile, ModLoaderType.NEO_FORGED),
pair(ForgeOldModMetadata::fromFile, ModLoaderType.FORGE),
pair(FabricModMetadata::fromFile, ModLoaderType.FABRIC),
pair(QuiltModMetadata::fromFile, ModLoaderType.QUILT),
pair(PackMcMeta::fromFile, ModLoaderType.PACK)
);
map.put("zip", zipReaders);
map.put("jar", zipReaders);
map.put("litemod", List.of(pair(LiteModMetadata::fromFile, ModLoaderType.LITE_LOADER)));
READERS = map;
}
private final GameRepository repository;
private final String id;
private final TreeSet<LocalModFile> localModFiles = new TreeSet<>();
private final HashMap<LocalMod, LocalMod> localMods = new HashMap<>();
private final HashMap<Pair<String, ModLoaderType>, LocalMod> localMods = new HashMap<>();
private LibraryAnalyzer analyzer;
private boolean loaded = false;
@@ -70,57 +78,104 @@ public final class ModManager {
return repository;
}
public String getVersion() {
public String getInstanceId() {
return id;
}
public Path getModsDirectory() {
return repository.getRunDirectory(id).resolve("mods");
return repository.getModsDirectory(id);
}
public LocalMod getLocalMod(String id, ModLoaderType modLoaderType) {
return localMods.computeIfAbsent(new LocalMod(id, modLoaderType), x -> x);
public LibraryAnalyzer getLibraryAnalyzer() {
return analyzer;
}
public LocalMod getLocalMod(String modId, ModLoaderType modLoaderType) {
return localMods.computeIfAbsent(pair(modId, modLoaderType),
x -> new LocalMod(x.getKey(), x.getValue()));
}
public boolean hasMod(String modId, ModLoaderType modLoaderType) {
return localMods.containsKey(pair(modId, modLoaderType));
}
private void addModInfo(Path file) {
try {
LocalModFile localModFile = getModInfo(file);
if (!localModFile.isOld()) {
localModFiles.add(localModFile);
}
} catch (IllegalArgumentException ignore) {
}
}
public LocalModFile getModInfo(Path modFile) {
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
String fileName = StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION);
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
Pair<ModMetadataReader[], String> currentReader = READERS.get(extension);
if (currentReader == null) {
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
List<Pair<ModMetadataReader, ModLoaderType>> readersMap = READERS.get(extension);
if (readersMap == null) {
// Is not a mod file.
return;
}
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
for (ModMetadataReader reader : currentReader.getKey()) {
Set<ModLoaderType> modLoaderTypes = analyzer.getModLoaders();
var supportedReaders = new ArrayList<ModMetadataReader>();
var unsupportedReaders = new ArrayList<ModMetadataReader>();
for (Pair<ModMetadataReader, ModLoaderType> reader : readersMap) {
if (modLoaderTypes.contains(reader.getValue())) {
supportedReaders.add(reader.getKey());
} else {
unsupportedReaders.add(reader.getKey());
}
}
LocalModFile modInfo = null;
List<Exception> exceptions = new ArrayList<>();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file)) {
for (ModMetadataReader reader : supportedReaders) {
try {
return reader.fromFile(this, modFile, fs);
} catch (Exception ignore) {
modInfo = reader.fromFile(this, file, fs);
break;
} catch (Exception e) {
exceptions.add(e);
}
}
} catch (Exception ignored) {
if (modInfo == null) {
for (ModMetadataReader reader : unsupportedReaders) {
try {
modInfo = reader.fromFile(this, file, fs);
break;
} catch (Exception ignored) {
}
}
}
} catch (Exception e) {
LOG.warning("Failed to open mod file " + file, e);
}
return new LocalModFile(this,
getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.UNKNOWN),
modFile,
FileUtils.getNameWithoutExtension(modFile),
new LocalModFile.Description(currentReader.getValue())
);
if (modInfo == null) {
Exception exception = new Exception("Failed to read mod metadata");
for (Exception e : exceptions) {
exception.addSuppressed(e);
}
LOG.warning("Failed to read mod metadata", exception);
String fileNameWithoutExtension = FileUtils.getNameWithoutExtension(file);
modInfo = new LocalModFile(this,
getLocalMod(fileNameWithoutExtension, ModLoaderType.UNKNOWN),
file,
fileNameWithoutExtension,
new LocalModFile.Description("litemod".equals(extension) ? "LiteLoader Mod" : "")
);
}
if (!modInfo.isOld()) {
localModFiles.add(modInfo);
}
}
public void refreshMods() throws IOException {
localModFiles.clear();
localMods.clear();
analyzer = LibraryAnalyzer.analyze(getRepository().getResolvedPreservingPatchesVersion(id), null);
if (Files.isDirectory(getModsDirectory())) {
try (DirectoryStream<Path> modsDirectoryStream = Files.newDirectoryStream(getModsDirectory())) {
for (Path subitem : modsDirectoryStream) {
@@ -140,10 +195,10 @@ public final class ModManager {
loaded = true;
}
public Collection<LocalModFile> getMods() throws IOException {
public @Unmodifiable List<LocalModFile> getMods() throws IOException {
if (!loaded)
refreshMods();
return localModFiles;
return List.copyOf(localModFiles);
}
public void addMod(Path file) throws IOException {
@@ -138,7 +138,7 @@ public final class CurseCompletionTask extends Task<Void> {
.collect(Collectors.toList()));
JsonUtils.writeToJsonFile(root.resolve("manifest.json"), newManifest);
Path versionRoot = repository.getVersionRoot(modManager.getVersion());
Path versionRoot = repository.getVersionRoot(modManager.getInstanceId());
Path resourcePacksRoot = versionRoot.resolve("resourcepacks");
Path shaderPacksRoot = versionRoot.resolve("shaderpacks");
finished.set(0);
@@ -139,7 +139,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
return new SearchResult(response.getData().stream().map(CurseAddon::toMod).map(remoteMod -> {
String lowerCaseResult = remoteMod.getTitle().toLowerCase();
String lowerCaseResult = remoteMod.getTitle().toLowerCase(Locale.ROOT);
int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult);
for (String s : StringUtils.tokenize(lowerCaseResult)) {
@@ -273,7 +273,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
@Nullable
private Path getFilePath(McbbsModpackManifest.File file) {
if (file instanceof McbbsModpackManifest.AddonFile) {
return modManager.getRepository().getRunDirectory(modManager.getVersion()).resolve(((McbbsModpackManifest.AddonFile) file).getPath());
return modManager.getRepository().getRunDirectory(modManager.getInstanceId()).resolve(((McbbsModpackManifest.AddonFile) file).getPath());
} else if (file instanceof McbbsModpackManifest.CurseFile) {
String fileName = ((McbbsModpackManifest.CurseFile) file).getFileName();
if (fileName == null) return null;
@@ -6,12 +6,15 @@ import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.jar.Attributes;
@@ -111,25 +114,45 @@ public final class ForgeNewModMetadata {
}
}
private static final int ACC_FORGE = 0x01;
public static LocalModFile fromForgeFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException {
return fromFile(modManager, modFile, fs, ModLoaderType.FORGE);
}
private static final int ACC_NEO_FORGED = 0x02;
public static LocalModFile fromNeoForgeFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException {
return fromFile(modManager, modFile, fs, ModLoaderType.NEO_FORGED);
}
private static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs, ModLoaderType modLoaderType) throws IOException {
if (modLoaderType != ModLoaderType.FORGE && modLoaderType != ModLoaderType.NEO_FORGED) {
throw new IOException("Invalid mod loader: " + modLoaderType);
}
if (modLoaderType == ModLoaderType.NEO_FORGED) {
try {
return fromFile0("META-INF/neoforge.mods.toml", modLoaderType, modManager, modFile, fs);
} catch (Exception ignored) {
}
}
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
try {
return fromFile0("META-INF/mods.toml", ACC_FORGE | ACC_NEO_FORGED, ModLoaderType.FORGE, modManager, modFile, fs);
return fromFile0("META-INF/mods.toml", modLoaderType, modManager, modFile, fs);
} catch (Exception ignored) {
}
try {
return fromFile0("META-INF/neoforge.mods.toml", ACC_NEO_FORGED, ModLoaderType.NEO_FORGED, modManager, modFile, fs);
return fromEmbeddedMod(modManager, modFile, fs, modLoaderType);
} catch (Exception ignored) {
}
throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod.");
}
private static LocalModFile fromFile0(String tomlPath, int loaderACC, ModLoaderType defaultLoader, ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
private static LocalModFile fromFile0(
String tomlPath,
ModLoaderType modLoaderType,
ModManager modManager,
Path modFile,
FileSystem fs) throws IOException, JsonParseException {
Path modToml = fs.getPath(tomlPath);
if (Files.notExists(modToml))
throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod.");
@@ -149,7 +172,7 @@ public final class ForgeNewModMetadata {
}
}
ModLoaderType type = analyzeLoader(toml, mod.getModId(), loaderACC, defaultLoader);
ModLoaderType type = analyzeLoader(toml, mod.getModId(), modLoaderType);
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
@@ -157,31 +180,131 @@ public final class ForgeNewModMetadata {
metadata.getLogoFile());
}
private static ModLoaderType analyzeLoader(Toml toml, String modID, int loaderACC, ModLoaderType defaultLoader) throws IOException {
private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, FileSystem fs, ModLoaderType modLoaderType) throws IOException {
Path manifestFile = fs.getPath("META-INF/MANIFEST.MF");
if (!Files.isRegularFile(manifestFile))
throw new IOException("Missing MANIFEST.MF in file " + manifestFile);
Manifest manifest;
try (InputStream input = Files.newInputStream(manifestFile)) {
manifest = new Manifest(input);
}
List<Path> embeddedModFiles = List.of();
String embeddedDependenciesMod = manifest.getMainAttributes().getValue("Embedded-Dependencies-Mod");
if (embeddedDependenciesMod != null) {
Path embeddedModFile = fs.getPath(embeddedDependenciesMod);
if (!Files.isRegularFile(embeddedModFile)) {
LOG.warning("Missing embedded-dependencies-mod: " + embeddedModFile);
throw new IOException();
}
embeddedModFiles = List.of(embeddedModFile);
} else {
Path jarInJarMetadata = fs.getPath("META-INF/jarjar/metadata.json");
if (Files.isRegularFile(jarInJarMetadata)) {
JarInJarMetadata metadata = JsonUtils.fromJsonFile(jarInJarMetadata, JarInJarMetadata.class);
if (metadata == null)
throw new IOException("Invalid metadata file: " + jarInJarMetadata);
metadata.validate();
embeddedModFiles = new ArrayList<>();
for (EmbeddedJarMetadata jar : metadata.jars) {
Path path = fs.getPath(jar.path);
if (Files.isRegularFile(path)) {
embeddedModFiles.add(path);
} else {
LOG.warning("Missing embedded-dependencies-mod: " + path);
}
}
}
}
if (embeddedModFiles.isEmpty()) {
throw new IOException("Missing embedded mods");
}
Path tempFile = Files.createTempFile("hmcl-", ".zip");
try {
for (Path embeddedModFile : embeddedModFiles) {
Files.copy(embeddedModFile, tempFile, StandardCopyOption.REPLACE_EXISTING);
try (FileSystem embeddedFs = CompressingUtils.createReadOnlyZipFileSystem(tempFile)) {
return fromFile(modManager, modFile, embeddedFs, modLoaderType);
} catch (Exception ignored) {
}
}
} finally {
Files.deleteIfExists(tempFile);
}
throw new IOException();
}
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) throws IOException {
List<HashMap<String, Object>> dependencies = toml.getList("dependencies." + modID);
if (dependencies == null) {
dependencies = toml.getList("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
try {
dependencies = toml.getList("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
} catch (ClassCastException e) {
try {
Toml table = toml.getTable("dependencies");
if (table == null)
return loader;
dependencies = table.getList(modID);
} catch (Throwable ignored) {
}
}
if (dependencies == null) {
return defaultLoader;
return loader;
}
}
ModLoaderType result = null;
loop:
for (HashMap<String, Object> dependency : dependencies) {
switch ((String) dependency.get("modId")) {
case "forge": return checkLoaderACC(loaderACC, ACC_FORGE, ModLoaderType.FORGE);
case "neoforge": return checkLoaderACC(loaderACC, ACC_NEO_FORGED, ModLoaderType.NEO_FORGED);
case "forge":
result = ModLoaderType.FORGE;
break loop;
case "neoforge":
result = ModLoaderType.NEO_FORGED;
break loop;
}
}
// ??? I have no idea why some of the Forge mods doesn't provide this key.
return defaultLoader;
}
private static ModLoaderType checkLoaderACC(int current, int target, ModLoaderType res) throws IOException {
if ((target & current) != 0) {
return res;
} else {
throw new IOException("Mismatched loader.");
if (result == loader)
return result;
else if (result != null)
throw new IOException("Loader mismatch");
else {
LOG.warning("Cannot determine the mod loader for mod " + modID + ", expected " + loader);
return loader;
}
}
@JsonSerializable
private record JarInJarMetadata(List<EmbeddedJarMetadata> jars) implements Validation {
@Override
public void validate() throws JsonParseException {
Validation.requireNonNull(jars, "jars");
for (EmbeddedJarMetadata jar : jars) {
jar.validate();
}
}
}
@JsonSerializable
private record EmbeddedJarMetadata(
String path,
boolean isObfuscated
) implements Validation {
@Override
public void validate() throws JsonParseException {
Validation.requireNonNull(path, "path");
}
}
}
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.mod.modinfo;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager;
@@ -125,9 +127,27 @@ public final class ForgeOldModMetadata {
Path mcmod = fs.getPath("mcmod.info");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod.");
List<ForgeOldModMetadata> modList = JsonUtils.fromJsonFile(mcmod, listTypeOf(ForgeOldModMetadata.class));
List<ForgeOldModMetadata> modList;
try (var reader = Files.newBufferedReader(mcmod);
var jsonReader = new JsonReader(reader)) {
JsonToken firstToken = jsonReader.peek();
if (firstToken == JsonToken.BEGIN_ARRAY)
modList = JsonUtils.GSON.fromJson(jsonReader, listTypeOf(ForgeOldModMetadata.class));
else if (firstToken == JsonToken.BEGIN_OBJECT) {
ForgeOldModMetadataLst list = JsonUtils.GSON.fromJson(jsonReader, ForgeOldModMetadataLst.class);
if (list == null)
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed");
modList = list.modList();
} else {
throw new JsonParseException("Unexpected first token: " + firstToken);
}
}
if (modList == null || modList.isEmpty())
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed..");
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed");
ForgeOldModMetadata metadata = modList.get(0);
String authors = metadata.getAuthor();
if (StringUtils.isBlank(authors) && metadata.getAuthors().length > 0)
@@ -0,0 +1,29 @@
/*
* 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.mod.modinfo;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import java.util.List;
/// @author Glavo
@JsonSerializable
public record ForgeOldModMetadataLst(
int modListVersion,
List<ForgeOldModMetadata> modList) {
}
@@ -45,16 +45,6 @@ import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class FetchTask<T> extends Task<T> {
private static final HttpClient HTTP_CLIENT;
static {
boolean useHttp2 = !"false".equalsIgnoreCase(System.getProperty("hmcl.http2"));
HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(NetworkUtils.TIME_OUT))
.version(useHttp2 ? HttpClient.Version.HTTP_2 : HttpClient.Version.HTTP_1_1)
.build();
}
protected static final int DEFAULT_RETRY = 3;
@@ -221,7 +211,7 @@ public abstract class FetchTask<T> extends Task<T> {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(currentURI);
requestBuilder.timeout(Duration.ofMillis(NetworkUtils.TIME_OUT));
headers.forEach(requestBuilder::header);
response = HTTP_CLIENT.send(requestBuilder.build(), BODY_HANDLER);
response = Holder.HTTP_CLIENT.send(requestBuilder.build(), BODY_HANDLER);
bmclapiHash = response.headers().firstValue("x-bmclapi-hash").orElse(null);
if (DigestUtils.isSha1Digest(bmclapiHash)) {
@@ -509,4 +499,21 @@ public abstract class FetchTask<T> extends Task<T> {
public static int getDownloadExecutorConcurrency() {
return downloadExecutorConcurrency;
}
/// Ensure that [#HTTP_CLIENT] is initialized after ProxyManager has been initialized.
private static final class Holder {
private static final HttpClient HTTP_CLIENT;
static {
boolean useHttp2 = !"false".equalsIgnoreCase(System.getProperty("hmcl.http2"));
HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(NetworkUtils.TIME_OUT))
.version(useHttp2 ? HttpClient.Version.HTTP_2 : HttpClient.Version.HTTP_1_1)
.build();
}
private Holder() {
}
}
}
@@ -87,7 +87,7 @@ public final class StringUtils {
builder.append(str, start, i);
}
builder.append(' ');
i = whitespaceEnd ;
i = whitespaceEnd;
continue;
}
}
@@ -261,6 +261,19 @@ public final class StringUtils {
return false;
}
public static boolean containsEmoji(String str) {
for (int i = 0; i < str.length(); ) {
int ch = str.codePointAt(i);
if (ch >= 0x1F300 && ch <= 0x1FAFF)
return true;
i += Character.charCount(ch);
}
return false;
}
private static boolean isVarNameStart(char ch) {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
}
@@ -27,14 +27,7 @@ import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;
@@ -55,6 +48,7 @@ public final class LocaleUtils {
private static final Map<String, String> subLanguageToParent = new HashMap<>();
private static final Map<String, String> iso3To2 = new HashMap<>();
private static final Set<String> rtl = new HashSet<>();
static {
try {
@@ -96,6 +90,17 @@ public final class LocaleUtils {
} catch (Throwable e) {
LOG.warning("Failed to load iso_languages.csv", e);
}
try {
for (String line : Lang.toIterable(IOUtils.readFullyAsString(LocaleUtils.class.getResourceAsStream("/assets/lang/rtl.txt")).lines())) {
if (line.startsWith("#") || line.isBlank()) {
continue;
}
rtl.add(line.trim());
}
} catch (Throwable e) {
LOG.warning("Failed to load rtl.txt", e);
}
}
private static Locale getInstance(String language, String script, String region,
@@ -163,6 +168,21 @@ public final class LocaleUtils {
return locale.getScript();
}
public static @NotNull TextDirection getTextDirection(Locale locale) {
TextDirection direction = rtl.contains(getRootLanguage(locale))
? TextDirection.RIGHT_TO_LEFT
: TextDirection.LEFT_TO_RIGHT;
if ("Qabs".equals(getScript(locale))) {
direction = switch (direction) {
case RIGHT_TO_LEFT -> TextDirection.LEFT_TO_RIGHT;
case LEFT_TO_RIGHT -> TextDirection.RIGHT_TO_LEFT;
};
}
return direction;
}
private static final ConcurrentMap<Locale, List<Locale>> CANDIDATE_LOCALES = new ConcurrentHashMap<>();
public static @NotNull List<Locale> getCandidateLocales(Locale locale) {
@@ -0,0 +1,24 @@
/*
* 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.i18n;
/// @author Glavo
public enum TextDirection {
LEFT_TO_RIGHT,
RIGHT_TO_LEFT;
}
@@ -1,37 +1,31 @@
package org.jackhuang.hmcl.util.platform;
import java.util.Objects;
public final class Platform {
public record Platform(OperatingSystem os, Architecture arch) {
public static final Platform UNKNOWN = new Platform(OperatingSystem.UNKNOWN, Architecture.UNKNOWN);
public static final Platform WINDOWS_X86 = new Platform(OperatingSystem.WINDOWS, Architecture.X86);
public static final Platform WINDOWS_X86_64 = new Platform(OperatingSystem.WINDOWS, Architecture.X86_64);
public static final Platform WINDOWS_ARM64 = new Platform(OperatingSystem.WINDOWS, Architecture.ARM64);
public static final Platform LINUX_X86 = new Platform(OperatingSystem.LINUX, Architecture.X86);
public static final Platform LINUX_X86_64 = new Platform(OperatingSystem.LINUX, Architecture.X86_64);
public static final Platform LINUX_ARM64 = new Platform(OperatingSystem.LINUX, Architecture.ARM64);
public static final Platform LINUX_ARM32 = new Platform(OperatingSystem.LINUX, Architecture.ARM32);
public static final Platform LINUX_RISCV64 = new Platform(OperatingSystem.LINUX, Architecture.RISCV64);
public static final Platform LINUX_LOONGARCH64 = new Platform(OperatingSystem.LINUX, Architecture.LOONGARCH64);
public static final Platform LINUX_LOONGARCH64_OW = new Platform(OperatingSystem.LINUX, Architecture.LOONGARCH64_OW);
public static final Platform LINUX_MIPS64EL = new Platform(OperatingSystem.LINUX, Architecture.MIPS64EL);
public static final Platform MACOS_X86_64 = new Platform(OperatingSystem.MACOS, Architecture.X86_64);
public static final Platform MACOS_ARM64 = new Platform(OperatingSystem.MACOS, Architecture.ARM64);
public static final Platform FREEBSD_X86_64 = new Platform(OperatingSystem.FREEBSD, Architecture.X86);
public static final Platform CURRENT_PLATFORM = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.CURRENT_ARCH);
public static final Platform SYSTEM_PLATFORM = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.SYSTEM_ARCH);
public static boolean isCompatibleWithX86Java() {
return Architecture.SYSTEM_ARCH.isX86() || SYSTEM_PLATFORM == MACOS_ARM64 || SYSTEM_PLATFORM == WINDOWS_ARM64;
}
private final OperatingSystem os;
private final Architecture arch;
private Platform(OperatingSystem os, Architecture arch) {
this.os = os;
this.arch = arch;
}
public static Platform getPlatform() {
return CURRENT_PLATFORM;
return Architecture.SYSTEM_ARCH.isX86() || SYSTEM_PLATFORM.equals(MACOS_ARM64) || SYSTEM_PLATFORM.equals(WINDOWS_ARM64);
}
public static Platform getPlatform(OperatingSystem os, Architecture arch) {
@@ -74,23 +68,10 @@ public final class Platform {
return arch.getBits();
}
@Override
public int hashCode() {
return Objects.hash(os, arch);
}
public boolean equals(OperatingSystem os, Architecture arch) {
return this.os == os && this.arch == arch;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Platform)) return false;
Platform platform = (Platform) o;
return os == platform.os && arch == platform.arch;
}
@Override
public String toString() {
return os.getCheckedName() + "-" + arch.getCheckedName();
@@ -846,4 +846,6 @@
1.21.9-pre3
1.21.9-pre4
1.21.9-rc1
1.21.9
1.21.9
1.21.10-rc1
1.21.10
@@ -0,0 +1,6 @@
ar
fa
he
ps
ur
yi
@@ -68,4 +68,10 @@ _‾
!¡
//
\\
',
',
()
)(
[]
][
{}
}{
@@ -220,4 +220,18 @@ public final class LocaleUtilsTest {
assertNull(LocaleUtils.getParentLanguage("zh"));
assertNull(LocaleUtils.getParentLanguage("zho"));
}
@Test
public void testGetTextDirection() {
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("en")));
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("en-US")));
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("zh")));
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("zh-Hans")));
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("zh-CN")));
assertEquals(TextDirection.LEFT_TO_RIGHT, LocaleUtils.getTextDirection(Locale.forLanguageTag("ar-Qabs")));
assertEquals(TextDirection.RIGHT_TO_LEFT, LocaleUtils.getTextDirection(Locale.forLanguageTag("en-Qabs")));
assertEquals(TextDirection.RIGHT_TO_LEFT, LocaleUtils.getTextDirection(Locale.forLanguageTag("ar")));
assertEquals(TextDirection.RIGHT_TO_LEFT, LocaleUtils.getTextDirection(Locale.forLanguageTag("ara")));
}
}
+4
View File
@@ -17,6 +17,10 @@ java {
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
tasks.processResources {
into("org/jackhuang/hmcl/gradle/l10n") {
from(projectDir.resolve("../HMCLCore/src/main/resources/assets/lang/"))
@@ -23,11 +23,22 @@ import java.util.Locale;
/// @author Glavo
public enum DocumentLocale {
ENGLISH(Locale.ENGLISH, "") {
@Override
public String getSubLanguageDisplayName() {
return "Standard";
}
@Override
public List<DocumentLocale> getCandidates() {
return List.of(ENGLISH);
}
},
ENGLISH_UPSIDE_DOWN(Locale.forLanguageTag("en-Qabs"), "en-Qabs") {
@Override
public String getSubLanguageDisplayName() {
return "uʍoᗡ ǝpᴉsd∩";
}
},
SIMPLIFIED_CHINESE(Locale.forLanguageTag("zh-Hans"), "zh"),
TRADITIONAL_CHINESE("zh-Hant") {
@Override
@@ -63,6 +63,14 @@ public abstract class CheckTranslations extends DefaultTask {
checker.checkMisspelled(simplifiedChinese, key, value, "账户", "帐户");
checker.checkMisspelled(simplifiedChinese, key, value, "其他", "其它");
checker.checkMisspelled(simplifiedChinese, key, value, "(", "(");
checker.checkMisspelled(simplifiedChinese, key, value, ")", ")");
});
traditionalChinese.forEach((key, value) -> {
checker.checkMisspelled(traditionalChinese, key, value, "(", "(");
checker.checkMisspelled(traditionalChinese, key, value, ")", ")");
});
classicalChinese.forEach((key, value) -> {
@@ -23,6 +23,7 @@ import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -53,10 +54,7 @@ public abstract class UpsideDownTranslate extends DefaultTask {
Path inputFile = getInputFile().get().getAsFile().toPath();
Path outputFile = getOutputFile().get().getAsFile().toPath();
Properties english = new Properties();
try (var reader = Files.newBufferedReader(inputFile)) {
english.load(reader);
}
Properties english = PropertiesUtils.load(inputFile);
Properties output = new Properties();
Translator translator = new Translator();
@@ -52,8 +52,6 @@ public abstract class ParseModDataTask extends DefaultTask {
@OutputFile
public abstract RegularFileProperty getOutputFile();
// ---
private static final Logger LOGGER = Logging.getLogger(ParseModDataTask.class);
private static final String S = ";";
@@ -94,7 +92,33 @@ public abstract class ParseModDataTask extends DefaultTask {
return "";
}
private static final Set<String> skip = Set.of(
private static String cleanChineseName(String chineseName) {
if (chineseName == null || chineseName.isBlank())
return "";
chineseName = chineseName.trim();
StringBuilder builder = new StringBuilder(chineseName.length());
int[] codePoints = chineseName.codePoints().toArray();
for (int i = 0; i < codePoints.length; i++) {
int ch = codePoints[i];
int prev = i > 0 ? codePoints[i - 1] : 0;
switch (ch) {
case '(' -> {
if (Character.isWhitespace(prev) || prev == '!' || prev == '。')
builder.append('(');
else
builder.append(" (");
}
case ')' -> builder.append(')');
default -> builder.appendCodePoint(ch);
}
}
return builder.toString().trim();
}
private static final Set<String> SKIP = Set.of(
"Minecraft",
"The Building Game"
);
@@ -106,7 +130,6 @@ public abstract class ParseModDataTask extends DefaultTask {
Files.createDirectories(outputFile.getParent());
List<ModData> modDatas;
try (BufferedReader reader = Files.newBufferedReader(inputFile)) {
modDatas = new Gson().fromJson(reader, TypeToken.getParameterized(List.class, ModData.class).getType());
@@ -125,14 +148,13 @@ public abstract class ParseModDataTask extends DefaultTask {
String subName = mod.name.sub;
String abbr = mod.name.abbr;
if (chineseName == null)
chineseName = "";
chineseName = chineseName == null ? "" : cleanChineseName(chineseName);
if (subName == null)
subName = "";
if (abbr == null)
abbr = "";
if (skip.contains(subName)) {
if (SKIP.contains(subName)) {
continue;
}
@@ -208,7 +230,6 @@ public abstract class ParseModDataTask extends DefaultTask {
public Map<String, List<Link>> list;
}
public static final class ModIdDeserializer implements JsonDeserializer<List<String>> {
private static final Type STRING_LIST = TypeToken.getParameterized(List.class, String.class).getType();
+12
View File
@@ -104,5 +104,17 @@
<module name="RedundantImport"/> <!-- e.g. double import statements: http://checkstyle.sourceforge.net/config_imports.html#RedundantImport -->
<module name="UnusedImports"/> <!-- http://checkstyle.sourceforge.net/config_imports.html#UnusedImports -->
<module name="IllegalImport"/> <!-- checks if import sun.* is used http://checkstyle.sourceforge.net/config_imports.html#IllegalImport -->
<!-- Custom -->
<module name="RegexpSinglelineJava">
<property name="format" value="\.toLowerCase\(\)"/>
<property name="ignoreComments" value="true"/>
<property name="message" value="Should not use String#toLowerCase(), use String#toLowerCase(Locale.ROOT) instead."/>
</module>
<module name="RegexpSinglelineJava">
<property name="format" value="\.toUpperCase\(\)"/>
<property name="ignoreComments" value="true"/>
<property name="message" value="Should not use String#toUpperCase(), use String#toUpperCase(Locale.ROOT) instead."/>
</module>
</module>
</module>
+3 -4
View File
@@ -2,20 +2,19 @@
<!-- #BEGIN BLOCK -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END BLOCK -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
**English** | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
**English** (**Standard**, [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
<!-- #END LANGUAGE_SWITCHER -->
## Introduction
HMCL is an open-source, cross-platform Minecraft launcher that supports Mod Management, Game Customizing, ModLoader Installing (Forge, NeoForge, Fabric, Quilt, LiteLoader, and OptiFine), Modpack Creating, UI Customization, and more.
HMCL is an open-source, cross-platform Minecraft launcher that supports Mod Management, Game Customizing, ModLoader Installing (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader, and OptiFine), Modpack Creating, UI Customization, and more.
HMCL has amazing cross-platform capabilities. Not only does it run on different operating systems like Windows, Linux, macOS, and FreeBSD, but it also supports various CPU architectures such as x86, ARM, RISC-V, MIPS, and LoongArch. You can easily enjoy Minecraft across different platforms through HMCL.
+24
View File
@@ -0,0 +1,24 @@
# ɹǝɥɔunɐ˥ ¡ʇɟɐɹɔǝuᴉW ollǝH
[![pɐoꞁuʍoᗡ](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=p%C9%90o%EA%9E%81u%CA%8Do%E1%97%A1&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
<!-- #BEGIN BLOCK -->
<!-- #PROPERTY PROCESS_LINK=false -->
**ɥsᴉlƃuƎ** (**uʍoᗡ ǝpᴉsd∩** '[pɹɐpuɐʇS](README.md))
<!-- #END BLOCK -->
## uoᴉʇɔnpoɹʇuI
˙[ǝlqɐʇ sᴉɥʇ](PLATFORM.md) oʇ ɹǝɟǝɹ ǝsɐǝld '˥ƆWH ʎq pǝʇɹoddns sǝɹnʇɔǝʇᴉɥɔɹɐ ∩ԀƆ puɐ sɯǝʇsʎs ɹoℲ
˙˥ƆWH ɥƃnoɹɥʇ sɯɹoɟʇɐld ʇuǝɹǝɟɟᴉp ssoɹɔɐ ʇɟɐɹɔǝuᴉW ʎoɾuǝ ʎlᴉsɐǝ uɐɔ no⅄ ˙ɥɔɹ∀ƃuoo˥ puɐ 'SԀIW 'Λ-ƆSIɹ 'Wɹ∀ '98x sɐ ɥɔns sǝɹnʇɔǝʇᴉɥɔɹɐ ∩ԀƆ snoᴉɹɐʌ sʇɹoddns oslɐ ʇᴉ ʇnq 'pSqǝǝɹℲ puɐ 'SOɔɐɯ 'xnuᴉ˥ 'sʍopuᴉM ǝʞᴉl sɯǝʇsʎs ƃuᴉʇɐɹǝdo ʇuǝɹǝɟɟᴉp uo unɹ ʇᴉ sǝop ʎluo ʇoN ˙sǝᴉʇᴉlᴉqɐdɐɔ ɯɹoɟʇɐld-ssoɹɔ ƃuᴉzɐɯɐ sɐɥ ˥ƆWH
˙ǝɹoɯ puɐ 'uoᴉʇɐzᴉɯoʇsnƆ I∩ 'ƃuᴉʇɐǝɹƆ ʞɔɐdpoW '(ǝuᴉℲᴉʇdO puɐ 'ɹǝpɐo˥ǝʇᴉ˥ 'ʇlᴉnQ 'ɔᴉɹqɐℲ 'ɯooɹuɐǝlƆ 'ǝƃɹoℲoǝN 'ǝƃɹoℲ) ƃuᴉllɐʇsuI ɹǝpɐo˥poW 'ƃuᴉzᴉɯoʇsnƆ ǝɯɐפ 'ʇuǝɯǝƃɐuɐW poW sʇɹoddns ʇɐɥʇ ɹǝɥɔunɐl ʇɟɐɹɔǝuᴉW ɯɹoɟʇɐld-ssoɹɔ 'ǝɔɹnos-uǝdo uɐ sᴉ ˥ƆWH
## pɐoluʍoᗡ
˙ǝʌoqɐ pǝʇsᴉl sǝʇᴉsqǝʍ lɐᴉɔᴉɟɟo ǝɥʇ ɯoɹɟ sǝsɐǝlǝɹ pɐoluʍop oʇ ʎluo pǝpuǝɯɯoɔǝɹ sᴉ ʇᴉ 'ʎɹɐssǝɔǝu ʇou ɥƃnoɥʇl∀
˙[sǝsɐǝlǝɹ qnHʇᴉפ](https://github.com/HMCL-dev/HMCL/releases) uᴉ ˥ƆWH ɟo uoᴉsɹǝʌ ʇsǝʇɐl ǝɥʇ puᴉɟ oslɐ uɐɔ no⅄
˙[ǝʇᴉsqǝʍ lɐᴉɔᴉɟɟo](https://hmcl.huangyuhui.net/download) ǝɥʇ ɯoɹɟ uoᴉsɹǝʌ ʇsǝʇɐl ǝɥʇ pɐoluʍop
+3 -4
View File
@@ -2,8 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
@@ -11,12 +10,12 @@
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | **español** | [русский](README_ru.md) | [українська](README_uk.md)
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | **español** | [русский](README_ru.md) | [українська](README_uk.md)
<!-- #END LANGUAGE_SWITCHER -->
## Introducción
HMCL es un lanzador de Minecraft de código abierto y multiplataforma que admite la gestión de mods, personalización del juego, instalación de ModLoaders (Forge, NeoForge, Fabric, Quilt, LiteLoader y OptiFine), creación de modpacks, personalización de la interfaz de usuario y más.
HMCL es un lanzador de Minecraft de código abierto y multiplataforma que admite la gestión de mods, personalización del juego, instalación de ModLoaders (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader y OptiFine), creación de modpacks, personalización de la interfaz de usuario y más.
HMCL tiene increíbles capacidades multiplataforma. No solo funciona en diferentes sistemas operativos como Windows, Linux, macOS y FreeBSD, sino que también es compatible con varias arquitecturas de CPU como x86, ARM, RISC-V, MIPS y LoongArch. Puedes disfrutar fácilmente de Minecraft en diferentes plataformas a través de HMCL.
+3 -4
View File
@@ -2,20 +2,19 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
<!-- #END LANGUAGE_SWITCHER -->
## 紹介
HMCLはオープンソースでクロスプラットフォーム対応のMinecraftランチャーです。Mod管理、ゲームカスタマイズ、Modローダーのインストール(Forge、NeoForge、Fabric、Quilt、LiteLoader、OptiFine)、Modパック作成、UIカスタマイズなど、さまざまな機能をサポートしています。
HMCLはオープンソースでクロスプラットフォーム対応のMinecraftランチャーです。Mod管理、ゲームカスタマイズ、Modローダーのインストール(Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader、OptiFine)、Modパック作成、UIカスタマイズなど、さまざまな機能をサポートしています。
HMCLは優れたクロスプラットフォーム性能を持っています。Windows、Linux、macOS、FreeBSDなどの異なるオペレーティングシステムだけでなく、x86、ARM、RISC-V、MIPS、LoongArchなどのさまざまなCPUアーキテクチャにも対応しています。HMCLを使えば、さまざまなプラットフォームでMinecraftを簡単に楽しむことができます。
+3 -4
View File
@@ -2,20 +2,19 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
<!-- #END LANGUAGE_SWITCHER -->
### 概說
HMCL 者,開源之礦藝啟者也。能理改囊,善於遊戲之自定,且自動安裝諸如 Forge、NeoForge、Fabric、Quilt、LiteLoader、OptiFine 諸改囊,亦可製作改囊集,界面亦可隨意更易。
HMCL 者,開源之礦藝啟者也。能理改囊,善於遊戲之自定,且自動安裝諸如 Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader、OptiFine 諸改囊,亦可製作改囊集,界面亦可隨意更易。
HMCL 跨域甚廣。無論 Windows、Linux、macOS、FreeBSD 諸常見械綱,抑或 x86、ARM、RISC-V、MIPS、LoongArch 諸大構處理器,皆可運行。
君可憑此,於諸算機間自如遊戲。
+3 -4
View File
@@ -2,20 +2,19 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | **русский** | [українська](README_uk.md)
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | **русский** | [українська](README_uk.md)
<!-- #END LANGUAGE_SWITCHER -->
## Введение
HMCL — это открытый, кроссплатформенный лаунчер для Minecraft с поддержкой управления модами, настройки игры, установки загрузчиков модов (Forge, NeoForge, Fabric, Quilt, LiteLoader и OptiFine), создания модпаков, настройки интерфейса и многого другого.
HMCL — это открытый, кроссплатформенный лаунчер для Minecraft с поддержкой управления модами, настройки игры, установки загрузчиков модов (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader и OptiFine), создания модпаков, настройки интерфейса и многого другого.
HMCL обладает отличной кроссплатформенностью. Он работает не только на различных операционных системах, таких как Windows, Linux, macOS и FreeBSD, но и поддерживает различные архитектуры процессоров: x86, ARM, RISC-V, MIPS и LoongArch. Благодаря HMCL вы можете легко наслаждаться Minecraft на разных платформах.
+3 -4
View File
@@ -2,20 +2,19 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | **українська**
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | **українська**
<!-- #END LANGUAGE_SWITCHER -->
## Вступ
HMCL — це відкритий, кросплатформний лаунчер для Minecraft, який підтримує керування модами, налаштування гри, встановлення модлоадерів (Forge, NeoForge, Fabric, Quilt, LiteLoader та OptiFine), створення модпаків, налаштування інтерфейсу та багато іншого.
HMCL — це відкритий, кросплатформний лаунчер для Minecraft, який підтримує керування модами, налаштування гри, встановлення модлоадерів (Forge, NeoForge, Cleanroom, Fabric, Quilt, LiteLoader та OptiFine), створення модпаків, налаштування інтерфейсу та багато іншого.
HMCL має чудові кросплатформні можливості. Він працює не лише на різних операційних системах, таких як Windows, Linux, macOS і FreeBSD, а й підтримує різні архітектури процесорів, такі як x86, ARM, RISC-V, MIPS і LoongArch. Ви можете легко насолоджуватися Minecraft на різних платформах за допомогою HMCL.
+3 -4
View File
@@ -2,20 +2,19 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
<!-- #END LANGUAGE_SWITCHER -->
## 简介
HMCL 是一款开源、跨平台的 Minecraft 启动器,支持模组管理、游戏自定义、游戏自动安装 (Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine)、整合包创建、界面自定义等功能。
HMCL 是一款开源、跨平台的 Minecraft 启动器,支持模组管理、游戏自定义、游戏自动安装 (Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine)、整合包创建、界面自定义等功能。
HMCL 有着强大的跨平台能力。它不仅支持 Windows、Linux、macOS、FreeBSD 等常见的操作系统,同时也支持 x86、ARM、RISC-V、MIPS、LoongArch 等不同的 CPU 架构。你可以使用 HMCL 在不同平台上轻松地游玩 Minecraft。
+3 -4
View File
@@ -2,20 +2,19 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![Build Status](https://ci.huangyuhui.net/job/HMCL/badge/icon?.svg)](https://ci.huangyuhui.net/job/HMCL)
![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?style=flat)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
[![QQ Group](https://img.shields.io/badge/QQ-HMCL-bright?label=&logo=qq&logoColor=ffffff&color=1EBAFC&labelColor=1DB0EF&logoSize=auto)](https://docs.hmcl.net/groups.html)
<!-- #END COPY -->
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](README.md) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
English ([Standard](README.md), [uʍoᗡ ǝpᴉsd∩](README_en_Qabs.md)) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
<!-- #END LANGUAGE_SWITCHER -->
## 簡介
HMCL 是一款開源、跨平臺的 Minecraft 啟動器,支援模組管理、遊戲客製化、遊戲自動安裝 (Forge、NeoForge、Fabric、Quilt、LiteLoader 和 OptiFine)、模組包建立、介面客製化等功能。
HMCL 是一款開源、跨平臺的 Minecraft 啟動器,支援模組管理、遊戲客製化、遊戲自動安裝 (Forge、NeoForge、Cleanroom、Fabric、Quilt、LiteLoader 和 OptiFine)、模組包建立、介面客製化等功能。
HMCL 有著強大的跨平臺能力。它不僅支援 Windows、Linux、macOS、FreeBSD 等常見的作業系統,同時也支援 x86、ARM、RISC-V、MIPS、LoongArch 等不同的 CPU 架構。你可以使用 HMCL 在不同平臺上輕鬆地遊玩 Minecraft。
+121
View File
@@ -0,0 +1,121 @@
# Hello Minecraft! Launcher Release Schedule
<!-- #BEGIN LANGUAGE_SWITCHER -->
**English** | [中文](ReleaseSchedule_zh.md)
<!-- #END LANGUAGE_SWITCHER -->
This document describes the HMCL version release schedule starting from October 2025.
## Versioning Rules
### Version Branches
HMCL has multiple **version branches**, each named in the format `<major>.<minor>` (e.g., `3.7`).
The major version is incremented only when there are significant architectural changes in HMCL,
while the minor version is incremented regularly according to the release schedule.
### Version Types
HMCL has two types of versions: **Stable** and **Development**.
#### Stable Version
The HMCL stable version is suitable for users who prioritize software stability.
New features are merged into the stable version only after thorough testing.
The stable version number follows the naming rule `<version branch>.<build number>` (e.g., `3.7.1`).
The build number is calculated independently for each version branch.
#### Development Version
The HMCL development version is suitable for users who want to experience new features first.
The development version includes the latest features and bug fixes,
but may also have more issues due to insufficient testing.
The development version follows the naming rule `<version branch>.0.<build number>` (e.g., `3.7.0.300`).
The build number is shared globally across all version branches.
## Release Channels
HMCL has two main release channels: **Stable Channel** and **Development Channel**.
They are used to release the HMCL stable and development versions, respectively.
To test HMCL versions before official release, we will push updates to some users in advance.
Users can enable the "Preview HMCL releases early" option on the "Settings > General" page to receive preview updates from the corresponding channel.
## Release Model
```mermaid
gantt
title HMCL Version Lifecycle (Example)
section 3.9
Development Phase: a1, 2025-11-15, 30d
Preview Phase: a2, after a1, 16d
Stable Release: milestone, after a2, 0d
Maintenance Phase: a3, after a2, 31d
section 3.10
Development Phase: b1, after a1, 31d
Preview Phase: b2, after b1, 16d
Stable Release: milestone, after b2, 0d
Maintenance Phase: b3, after b2, 31d
section 3.11
Development Phase: c1, after b1, 31d
Preview Phase: c2, after c1, 16d
Stable Release: milestone, after c2, 0d
Maintenance Phase: c3, after c2, 30d
```
In general, we release a new version branch every month,
and each version branch `x.y` has a lifecycle of two and a half months.
The lifecycle of each version branch is divided into the following stages:
1. **Development Phase** (Mid-month M ~ Mid-month M+1)
During this phase, the version is developed in the Git branch `main`.
Typically, we release a development version `x.y.0.<build number>` based on this branch every week,
which includes all features and bug fixes merged during the week.
2. **Preview Phase** (Mid-month M+1 ~ End of month M+1)
In mid-month M+1, we fork the `main` branch to create the `release/x.y` branch,
which corresponds to the fixed version branch `x.y`.
At the same time, the version branch corresponding to the `main` branch is incremented to `x.y♯`,
entering the development phase of the next version branch.
After the fork, no new features will be added to this version branch;
only bug fixes and data updates will be made.
Within a few days, we will release a stable preview version `x.y.1` and
push it to users in the stable channel who have enabled preview updates.
Before the end of month M+1, the stable version corresponding to version branch `x.y` will remain in preview status.
If issues are found during the preview, we will release and push new stable preview versions (such as `x.y.2`, `x.y.3`, etc.) after fixing them.
3. **Stable Release** (End of month M+1)
At the end of month M+1, if there are no unexpected issues, we will promote the latest stable preview `x.y.z` to the official release and push it to all users in the stable channel.
4. **Maintenance Phase** (Month M+2)
In month M+2, version branch `x.y` enters the maintenance phase.
We will release stable updates irregularly based on the number and severity of issues fixed,
and push them to all users in the stable channel.
At the end of month M+2, after the official release of the stable version corresponding to version branch `x.y♯`, the lifecycle of version branch `x.y` ends, the Git branch `release/x.y` is
archived, and no longer receives updates.
## Long-Term Support Version Branches
Some special version branches are selected as Long-Term Support (LTS) branches.
Their lifecycle does not end with the official release of the next version branch's stable version.
We will continue to backport necessary patches to these branches for a longer period.
List of Long-Term Support Version Branches:
| Version Branch | Official Release Date | End of Lifecycle | Current Support Status | Notes |
|----------------|:---------------------:|:----------------:|:----------------------:|:-------------------------------------------------------------------------------------------------------------------------------------|
| 3.6 | November 23, 2024 | TBD | Supported | This is the last version branch that supports running on Java 8.<br>It is suitable for users on legacy platforms such as Windows XP. |
+106
View File
@@ -0,0 +1,106 @@
# Hello Minecraft! Launcher 发布计划
<!-- #BEGIN LANGUAGE_SWITCHER -->
[English](ReleaseSchedule.md) | **中文**
<!-- #END LANGUAGE_SWITCHER -->
本文介绍了自 2025 年 10 月起的 HMCL 版本发布计划。
## 版本规则
### 版本分支
HMCL 存在多个**版本分支**,每个版本分支都以 `<主版本号>.<次版本号>` 的形式命名 (例如 `3.7`)。
其中,主版本号仅在 HMCL 发生重大架构变化时递增,次版本号会根据发布计划定时递增。
### 版本类型
HMCL 具有两种版本类型: **稳定版**和**开发版**。
#### 稳定版
HMCL 稳定版适合优先追求软件稳定性的用户使用。新功能在经过充分测试后才会被合并到稳定版中。
HMCL 稳定版版本号遵循 `<版本分支>.<构建号>` 的命名规则 (例如 `3.7.1`)。其中构建号对每个版本分支独立计算。
#### 开发版
HMCL 开发版适合希望优先体验新功能的用户使用。开发版会包含最新功能和 BUG 修复,但因为未经过充分测试,也可能会存在更多问题。
HMCL 开发版遵循 `<版本分支>.0.<构建号>` 的命名规则 (例如 `3.7.0.300`)。其中构建号对所有版本分支全局共享。
## 发布通道
HMCL 具有两个主要发布通道:**稳定版通道**和**开发版通道**。
它们分别用于发布 HMCL 稳定版和开发版。
为了在正式发布前测试 HMCL 版本,我们会向部分用户优先推送更新。
用户可以在「设置 > 通用」页面中打开「提前预览 HMCL 版本」选项以接收到对应通道的预览更新。
## 发布模型
```mermaid
gantt
title HMCL 版本生命周期 (示例)
section 3.9
开发阶段: a1, 2025-11-15, 30d
预览阶段: a2, after a1, 16d
稳定版发布: milestone, after a2, 0d
维护阶段: a3, after a2, 31d
section 3.10
开发阶段: b1, after a1, 31d
预览阶段: b2, after b1, 16d
稳定版发布: milestone, after b2, 0d
维护阶段: b3, after b2, 31d
section 3.11
开发阶段: c1, after b1, 31d
预览阶段: c2, after c1, 16d
稳定版发布: milestone, after c2, 0d
维护阶段: c3, after c2, 30d
```
通常情况下,我们每个月发布一个版本分支,每个版本分支 `x.y` 生命周期为两个半月。
每个版本分支生命周期分为以下阶段:
1. **开发阶段** (M 月中旬 ~ M+1 月中旬)
在此阶段,该版本在 Git 分支 `main` 中进行开发。
通常情况下,我们每周都会基于该分支发布一个开发版 `x.y.0.<构建号>`,其中包含当周合并的所有功能和错误修复。
2. **预览阶段** (M+1 月中旬 ~ M+1 月底)
在 M+1 月中旬,我们会基于 Git 分支 `main` 分叉出分支 `release/x.y`,其对应的版本分支固定为 `x.y`。
与此同时,Git 分支 `main` 对应的版本分支递增至 `x.y♯`,进入下一个版本分支的开发版阶段。
在完成分叉后,该版本分支不会新增更多功能,只会进行错误修复和数据更新。
我们会在数日内发布稳定版预览 `x.y.1`,并将其推送给稳定版通道且接受预览更新的用户。
在 M+1 月底前,版本分支 `x.y` 对应的稳定版将一直保持预览状态。
若在预览过程中发现问题,我们会在修复后发布并推送新稳定版预览 (如 `x.y.2`、`x.y.3` 等)。
3. **稳定版发布** (M+1 月底)
在 M+1 月底,如果没有意外情况,我们会将最新的稳定版预览 `x.y.z` 提升为正式版本,推送给稳定版通道的全部用户。
4. **维护阶段** (M+2 月)
在 M+2 月,版本分支 `x.y` 进入维护阶段。我们会根据修复的问题数量和严重程度不定期地发布稳定版更新,并推送给稳定版通道的全部用户。
在 M+2 月底,版本分支 `x.y♯` 对应的稳定版正式发布后,版本分支 `x.y` 生命周期结束,Git 分支 `release/x.y` 被存档,不再接受更新。
## 长期支持版本分支
部分特殊版本分支会被我们选择为长期支持 (LTS) 版本分支,
其生命周期不会因为下一个版本分支对应的稳定版正式发布而结束。
我们会在更长的时间中继续为这些分支移植必要的补丁。
长期支持版本分支列表:
| 版本分支 | 正式发布日期 | 生命周期结束日期 | 当前受支持状态 | 备注 |
|------|:----------------:|:--------:|:-------:|:---------------------------------------------------------|
| 3.6 | 2024 年 11 月 23 日 | 未定 | 支持中 | 这是最后一个支持使用 Java 8 运行的版本分支,<br>该版本分支适用于 Windows XP 等旧平台用户 |
+2 -2
View File
@@ -1,5 +1,5 @@
[versions]
hmclauncher = "3.7.0.0"
hmclauncher = "3.7.0.1"
jetbrains-annotations = "26.0.1"
kala-compress = "1.27.1-1"
simple-png-javafx = "0.3.0"
@@ -16,7 +16,7 @@ twelvemonkeys = "3.12.0"
jna = "5.18.0"
pci-ids = "0.4.0"
java-info = "1.0"
authlib-injector = "1.2.5"
authlib-injector = "1.2.6"
# testing
junit = "5.13.4"