Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2d5a07053 | ||
|
|
e7526e39bf | ||
|
|
811b1fb5f4 | ||
|
|
72ae46c93d | ||
|
|
6b6276e6e2 | ||
|
|
60ff34a587 | ||
|
|
356960cc82 | ||
|
|
8708e7f8b1 | ||
|
|
962ee0d108 | ||
|
|
cf5957ffd1 | ||
|
|
f41b008b42 | ||
|
|
228a1e4887 | ||
|
|
913ce55d43 | ||
|
|
9ded2e489d | ||
|
|
c65e4d83a2 | ||
|
|
3184f91227 | ||
|
|
8eb17b6664 | ||
|
|
36ea1dac94 | ||
|
|
3fbe199aab | ||
|
|
8db8ade9c7 | ||
|
|
41e004f4d5 | ||
|
|
37ad445620 | ||
|
|
480f8b6890 |
No files matched your search
@@ -96,8 +96,8 @@ fun attachSignature(jar: File) {
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
sourceCompatibility = "17"
|
||||
targetCompatibility = "17"
|
||||
}
|
||||
|
||||
tasks.checkstyleMain {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.util.SwingUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
@@ -110,8 +109,14 @@ public final class EntryPoint {
|
||||
}
|
||||
|
||||
private static void initIcon() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
try {
|
||||
if (java.awt.Taskbar.isTaskbarSupported()) {
|
||||
var image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
|
||||
java.awt.Taskbar.getTaskbar().setIconImage(image);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to set application icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
|
||||
@@ -113,6 +113,10 @@ public final class Launcher extends Application {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ConfigHolder.isUnsupportedVersion()) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
|
||||
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public final class Metadata {
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
|
||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 11;
|
||||
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
|
||||
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
|
||||
public static final int RECOMMENDED_JAVA_VERSION = 21;
|
||||
|
||||
|
||||
@@ -111,36 +111,27 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
|
||||
private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) {
|
||||
String language = locale.getLanguage();
|
||||
String region = locale.getCountry();
|
||||
|
||||
switch (language) {
|
||||
case "ru":
|
||||
return "ru_RU";
|
||||
case "uk":
|
||||
return "uk_UA";
|
||||
case "es":
|
||||
return "es_ES";
|
||||
case "ja":
|
||||
return "ja_JP";
|
||||
case "lzh":
|
||||
return gameVersion.compareTo("1.16") >= 0
|
||||
? "lzh"
|
||||
: "";
|
||||
case "zh":
|
||||
default:
|
||||
if (LocaleUtils.isChinese(locale)) {
|
||||
String script = LocaleUtils.getScript(locale);
|
||||
if ("Hant".equals(script)) {
|
||||
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
|
||||
return "zh_HK";
|
||||
return "zh_TW";
|
||||
}
|
||||
return "zh_CN";
|
||||
}
|
||||
return switch (LocaleUtils.getISO1Language(locale)) {
|
||||
case "es" -> "es_ES";
|
||||
case "ja" -> "ja_JP";
|
||||
case "ru" -> "ru_RU";
|
||||
case "uk" -> "uk_UA";
|
||||
case "zh" -> {
|
||||
if ("lzh".equals(locale.getLanguage()) && gameVersion.compareTo("1.16") >= 0)
|
||||
yield "lzh";
|
||||
|
||||
return "";
|
||||
}
|
||||
String script = LocaleUtils.getScript(locale);
|
||||
if ("Hant".equals(script)) {
|
||||
if ((region.equals("HK") || region.equals("MO") && gameVersion.compareTo("1.16") >= 0))
|
||||
yield "zh_HK";
|
||||
yield "zh_TW";
|
||||
}
|
||||
yield "zh_CN";
|
||||
}
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -125,9 +125,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
} catch (IOException ex) {
|
||||
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
|
||||
@@ -26,8 +26,6 @@ import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
@@ -264,15 +262,8 @@ public final class TexturesLoader {
|
||||
int scale = (int) skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
|
||||
try {
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
|
||||
// In order to prevent the blurring caused by bilinear interpolation,
|
||||
// we use the self-implemented nearest neighbor interpolation to scale
|
||||
drawAvatarSlow(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatar(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
|
||||
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
@@ -284,37 +275,6 @@ public final class TexturesLoader {
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
PixelReader reader = skin.getPixelReader();
|
||||
PixelWriter writer = g.getPixelWriter();
|
||||
drawImage(writer, reader,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
drawImage(writer, reader,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
/*
|
||||
* Scale and draw image using nearest-neighbor interpolation
|
||||
*/
|
||||
private static void drawImage(PixelWriter writer, PixelReader reader,
|
||||
int sx, int sy, int sw, int sh,
|
||||
int dx, int dy, int dw, int dh) {
|
||||
double xScale = ((double) sw) / dw;
|
||||
double yScale = ((double) sh) / dh;
|
||||
|
||||
for (int xOffset = 0; xOffset < dw; xOffset++) {
|
||||
for (int yOffset = 0; yOffset < dh; yOffset++) {
|
||||
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
|
||||
|
||||
// Draw only non-transparent pixels
|
||||
if ((color >>> 24) != 0)
|
||||
writer.setArgb(dx + xOffset, dy + yOffset, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.java;
|
||||
import kala.compress.archivers.ArchiveEntry;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
||||
|
||||
@@ -31,6 +30,7 @@ import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -101,7 +101,7 @@ public final class JavaInstallTask extends Task<JavaManifest> {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
path.toFile().setExecutable(true);
|
||||
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
|
||||
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(HexFormat.of().formatHex(messageDigest.digest()), size));
|
||||
}
|
||||
nameStack.remove(nameStack.size() - 1);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import java.util.*;
|
||||
@JsonAdapter(value = Config.Adapter.class)
|
||||
public final class Config implements Observable {
|
||||
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
|
||||
public static final Gson CONFIG_GSON = new GsonBuilder()
|
||||
@@ -125,7 +126,7 @@ public final class Config implements Observable {
|
||||
// Properties
|
||||
|
||||
@SerializedName("_version")
|
||||
private final IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty configVersion = new SimpleIntegerProperty(CURRENT_VERSION);
|
||||
|
||||
public IntegerProperty configVersionProperty() {
|
||||
return configVersion;
|
||||
|
||||
@@ -45,6 +45,7 @@ public final class ConfigHolder {
|
||||
private static GlobalConfig globalConfigInstance;
|
||||
private static boolean newlyCreated;
|
||||
private static boolean ownerChanged = false;
|
||||
private static boolean unsupportedVersion = false;
|
||||
|
||||
public static Config config() {
|
||||
if (configInstance == null) {
|
||||
@@ -72,6 +73,10 @@ public final class ConfigHolder {
|
||||
return ownerChanged;
|
||||
}
|
||||
|
||||
public static boolean isUnsupportedVersion() {
|
||||
return unsupportedVersion;
|
||||
}
|
||||
|
||||
public static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
@@ -82,7 +87,8 @@ public final class ConfigHolder {
|
||||
LOG.info("Config location: " + configLocation);
|
||||
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
|
||||
if (!unsupportedVersion)
|
||||
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
|
||||
|
||||
globalConfigInstance = loadGlobalConfig();
|
||||
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
|
||||
@@ -163,7 +169,14 @@ public final class ConfigHolder {
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
} else {
|
||||
ConfigUpgrader.upgradeConfig(deserialized, content);
|
||||
int configVersion = deserialized.getConfigVersion();
|
||||
if (configVersion < Config.CURRENT_VERSION) {
|
||||
ConfigUpgrader.upgradeConfig(deserialized, content);
|
||||
} else if (configVersion > Config.CURRENT_VERSION) {
|
||||
unsupportedVersion = true;
|
||||
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", Config.CURRENT_VERSION, configVersion));
|
||||
}
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
|
||||
@@ -31,8 +31,6 @@ final class ConfigUpgrader {
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
private static final int CURRENT_VERSION = 2;
|
||||
|
||||
/**
|
||||
* This method is for the compatibility with old HMCL versions.
|
||||
*
|
||||
@@ -42,17 +40,10 @@ final class ConfigUpgrader {
|
||||
static void upgradeConfig(Config deserialized, String rawContent) {
|
||||
int configVersion = deserialized.getConfigVersion();
|
||||
|
||||
if (configVersion == CURRENT_VERSION) {
|
||||
if (configVersion >= Config.CURRENT_VERSION)
|
||||
return;
|
||||
}
|
||||
|
||||
if (configVersion > CURRENT_VERSION) {
|
||||
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
|
||||
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, Config.CURRENT_VERSION));
|
||||
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
|
||||
|
||||
if (configVersion < 1) {
|
||||
@@ -100,6 +91,6 @@ final class ConfigUpgrader {
|
||||
}
|
||||
}
|
||||
|
||||
deserialized.setConfigVersion(CURRENT_VERSION);
|
||||
deserialized.setConfigVersion(Config.CURRENT_VERSION);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -70,7 +71,10 @@ public final class ProxyManager {
|
||||
String password = config().getProxyPass();
|
||||
|
||||
if (username != null || password != null)
|
||||
return new SimpleAuthenticator(username, password.toCharArray());
|
||||
return new SimpleAuthenticator(
|
||||
Objects.requireNonNullElse(username, ""),
|
||||
Objects.requireNonNullElse(password, "").toCharArray()
|
||||
);
|
||||
else
|
||||
return null;
|
||||
} else
|
||||
|
||||
@@ -22,19 +22,13 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
@@ -57,23 +51,8 @@ public final class StyleSheets {
|
||||
config().themeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
|
||||
}
|
||||
|
||||
private static String toStyleSheetUri(String styleSheet, String fallback) {
|
||||
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
|
||||
// JavaFX 17+ support loading stylesheets from data URIs
|
||||
// https://bugs.openjdk.org/browse/JDK-8267554
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
else
|
||||
try {
|
||||
Path temp = Files.createTempFile("hmcl", ".css");
|
||||
// For JavaFX 17 or earlier, CssParser uses the default charset
|
||||
// https://bugs.openjdk.org/browse/JDK-8279328
|
||||
Files.writeString(temp, styleSheet, Charset.defaultCharset());
|
||||
temp.toFile().deleteOnExit();
|
||||
return temp.toUri().toString();
|
||||
} catch (IOException | NullPointerException e) {
|
||||
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
|
||||
return fallback;
|
||||
}
|
||||
private static String toStyleSheetUri(String styleSheet) {
|
||||
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String getFontStyleSheet() {
|
||||
@@ -123,7 +102,7 @@ public final class StyleSheets {
|
||||
|
||||
builder.append('}');
|
||||
|
||||
return toStyleSheetUri(builder.toString(), fontFamily);
|
||||
return toStyleSheetUri(builder.toString());
|
||||
}
|
||||
|
||||
private static String rgba(Color color, double opacity) {
|
||||
@@ -150,7 +129,7 @@ public final class StyleSheets {
|
||||
"-fx-base-disabled-text-fill:" + rgba(theme.getForegroundColor(), 0.7) + ";" +
|
||||
"-fx-base-text-fill:" + Theme.getColorDisplayName(theme.getForegroundColor()) + ";" +
|
||||
"-theme-thumb:" + rgba(theme.getPaint(), 0.7) + ";" +
|
||||
'}', blueCss);
|
||||
'}');
|
||||
}
|
||||
|
||||
public static void init(Scene scene) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class AwtUtils {
|
||||
private AwtUtils() {
|
||||
}
|
||||
|
||||
public static void setAppleIcon(Image image) {
|
||||
try {
|
||||
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@ import javafx.collections.ObservableMap;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventDispatcher;
|
||||
import javafx.event.EventType;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
@@ -48,6 +50,7 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
@@ -77,6 +80,7 @@ import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.*;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -110,6 +114,8 @@ public final class FXUtils {
|
||||
public static final @Nullable ObservableBooleanValue DARK_MODE;
|
||||
public static final @Nullable Boolean REDUCED_MOTION;
|
||||
|
||||
public static final @Nullable MethodHandle TEXT_TRUNCATED_PROPERTY;
|
||||
|
||||
static {
|
||||
String jfxVersion = System.getProperty("javafx.version");
|
||||
int majorVersion = -1;
|
||||
@@ -154,6 +160,20 @@ public final class FXUtils {
|
||||
PREFERENCES = preferences;
|
||||
DARK_MODE = darkMode;
|
||||
REDUCED_MOTION = reducedMotion;
|
||||
|
||||
MethodHandle textTruncatedProperty = null;
|
||||
if (JAVAFX_MAJOR_VERSION >= 23) {
|
||||
try {
|
||||
textTruncatedProperty = MethodHandles.publicLookup().findVirtual(
|
||||
Labeled.class,
|
||||
"textTruncatedProperty",
|
||||
MethodType.methodType(ReadOnlyBooleanProperty.class)
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to lookup textTruncatedProperty", e);
|
||||
}
|
||||
}
|
||||
TEXT_TRUNCATED_PROPERTY = textTruncatedProperty;
|
||||
}
|
||||
|
||||
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
|
||||
@@ -353,6 +373,22 @@ public final class FXUtils {
|
||||
ScrollUtils.addSmoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
/// If the current environment is JavaFX 23 or higher, this method returns [Labeled#textTruncatedProperty()];
|
||||
/// Otherwise, it returns `null`.
|
||||
public static @Nullable ReadOnlyBooleanProperty textTruncatedProperty(Labeled labeled) {
|
||||
if (TEXT_TRUNCATED_PROPERTY != null) {
|
||||
try {
|
||||
return (ReadOnlyBooleanProperty) TEXT_TRUNCATED_PROPERTY.invokeExact(labeled);
|
||||
} catch (RuntimeException | Error e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
|
||||
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
|
||||
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
|
||||
@@ -509,37 +545,35 @@ public final class FXUtils {
|
||||
if (link == null)
|
||||
return;
|
||||
|
||||
String uri = NetworkUtils.encodeLocation(link);
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", uri});
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("An exception occurred while calling rundll32", e);
|
||||
}
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
Path path = SystemUtils.which(browser);
|
||||
if (path != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{path.toString(), link});
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Runtime.getRuntime().exec(new String[]{"open", uri});
|
||||
return;
|
||||
} else {
|
||||
for (String browser : linuxBrowsers) {
|
||||
Path path = SystemUtils.which(browser);
|
||||
if (path != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{path.toString(), uri});
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG.warning("No known browser found");
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
LOG.warning("Unable to open link: " + link, ex);
|
||||
}
|
||||
LOG.warning("Failed to open link: " + link + ", fallback to java.awt.Desktop", e);
|
||||
}
|
||||
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(uri));
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to open link: " + link, e);
|
||||
}
|
||||
});
|
||||
@@ -1167,17 +1201,39 @@ public final class FXUtils {
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label truncatedLabel(String text, int limit) {
|
||||
Label label = new Label();
|
||||
if (text.length() <= limit) {
|
||||
label.setText(text);
|
||||
} else {
|
||||
label.setText(StringUtils.truncate(text, limit));
|
||||
installFastTooltip(label, text);
|
||||
}
|
||||
public static Label newSafeTruncatedLabel(String text) {
|
||||
Label label = new Label(text);
|
||||
label.setTextOverrun(OverrunStyle.CENTER_WORD_ELLIPSIS);
|
||||
showTooltipWhenTruncated(label);
|
||||
return label;
|
||||
}
|
||||
|
||||
private static final String LABEL_FULL_TEXT_PROP_KEY = FXUtils.class.getName() + ".LABEL_FULL_TEXT";
|
||||
|
||||
public static void showTooltipWhenTruncated(Labeled labeled) {
|
||||
ReadOnlyBooleanProperty textTruncatedProperty = textTruncatedProperty(labeled);
|
||||
if (textTruncatedProperty != null) {
|
||||
ChangeListener<Boolean> listener = (observable, oldValue, newValue) -> {
|
||||
var label = (Labeled) ((ReadOnlyProperty<?>) observable).getBean();
|
||||
var tooltip = (Tooltip) label.getProperties().get(LABEL_FULL_TEXT_PROP_KEY);
|
||||
|
||||
if (newValue) {
|
||||
if (tooltip == null) {
|
||||
tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(label.textProperty());
|
||||
label.getProperties().put(LABEL_FULL_TEXT_PROP_KEY, tooltip);
|
||||
}
|
||||
|
||||
FXUtils.installFastTooltip(label, tooltip);
|
||||
} else if (tooltip != null) {
|
||||
Tooltip.uninstall(label, tooltip);
|
||||
}
|
||||
};
|
||||
listener.changed(textTruncatedProperty, false, textTruncatedProperty.get());
|
||||
textTruncatedProperty.addListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
|
||||
applyDragListener(node, filter, callback, null);
|
||||
}
|
||||
@@ -1378,4 +1434,65 @@ public final class FXUtils {
|
||||
return new FileChooser.ExtensionFilter(i18n("extension.png"),
|
||||
IMAGE_EXTENSIONS.stream().map(ext -> "*." + ext).toArray(String[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Intelligently determines the popup position to prevent the menu from exceeding screen boundaries.
|
||||
* Supports multi-monitor setups by detecting the current screen where the component is located.
|
||||
* Now handles first-time popup display by forcing layout measurement.
|
||||
*
|
||||
* @param root the root node to calculate position relative to
|
||||
* @param popupInstance the popup instance to position
|
||||
* @return the optimal vertical position for the popup menu
|
||||
*/
|
||||
public static JFXPopup.PopupVPosition determineOptimalPopupPosition(Node root, JFXPopup popupInstance) {
|
||||
// Get the screen bounds in screen coordinates
|
||||
Bounds screenBounds = root.localToScreen(root.getBoundsInLocal());
|
||||
|
||||
// Convert Bounds to Rectangle2D for getScreensForRectangle method
|
||||
Rectangle2D boundsRect = new Rectangle2D(
|
||||
screenBounds.getMinX(), screenBounds.getMinY(),
|
||||
screenBounds.getWidth(), screenBounds.getHeight()
|
||||
);
|
||||
|
||||
// Find the screen that contains this component (supports multi-monitor)
|
||||
List<Screen> screens = Screen.getScreensForRectangle(boundsRect);
|
||||
Screen currentScreen = screens.isEmpty() ? Screen.getPrimary() : screens.get(0);
|
||||
Rectangle2D visualBounds = currentScreen.getVisualBounds();
|
||||
|
||||
double screenHeight = visualBounds.getHeight();
|
||||
double screenMinY = visualBounds.getMinY();
|
||||
double itemScreenY = screenBounds.getMinY();
|
||||
|
||||
// Calculate available space relative to the current screen
|
||||
double availableSpaceAbove = itemScreenY - screenMinY;
|
||||
double availableSpaceBelow = screenMinY + screenHeight - itemScreenY - root.getBoundsInLocal().getHeight();
|
||||
|
||||
// Get popup content and ensure it's properly measured
|
||||
Region popupContent = popupInstance.getPopupContent();
|
||||
|
||||
double menuHeight;
|
||||
if (popupContent.getHeight() <= 0) {
|
||||
// Force layout measurement if height is not yet available
|
||||
popupContent.autosize();
|
||||
popupContent.applyCss();
|
||||
popupContent.layout();
|
||||
|
||||
// Get the measured height, or use a reasonable fallback
|
||||
menuHeight = popupContent.getHeight();
|
||||
if (menuHeight <= 0) {
|
||||
// Fallback: estimate based on number of menu items
|
||||
// Each menu item is roughly 36px height + separators + padding
|
||||
menuHeight = 300; // Conservative estimate for the current menu structure
|
||||
}
|
||||
} else {
|
||||
menuHeight = popupContent.getHeight();
|
||||
}
|
||||
|
||||
// Add some margin for safety
|
||||
menuHeight += 20;
|
||||
|
||||
return (availableSpaceAbove > menuHeight && availableSpaceBelow < menuHeight)
|
||||
? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward
|
||||
: JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward
|
||||
}
|
||||
}
|
||||
@@ -78,15 +78,22 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
setActionButtonVisible(false);
|
||||
|
||||
setOnScroll(event -> {
|
||||
double deltaY = event.getDeltaY();
|
||||
if (deltaY == 0)
|
||||
return;
|
||||
|
||||
Account current = account.get();
|
||||
if (current == null) return;
|
||||
|
||||
ObservableList<Account> accounts = Accounts.getAccounts();
|
||||
int currentIndex = accounts.indexOf(account.get());
|
||||
if (event.getDeltaY() > 0) { // up
|
||||
int currentIndex = accounts.indexOf(current);
|
||||
if (currentIndex < 0) return;
|
||||
|
||||
if (deltaY > 0) // up
|
||||
currentIndex--;
|
||||
} else { // down
|
||||
else // down
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -21,7 +22,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
public class IconedTwoLineListItem extends HBox {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<String> tags = FXCollections.observableArrayList();
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
private final StringProperty externalLink = new SimpleStringProperty(this, "externalLink");
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
|
||||
@@ -62,7 +63,7 @@ public class IconedTwoLineListItem extends HBox {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public ObservableList<String> getTags() {
|
||||
public ObservableList<Label> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,16 +29,22 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.AggregatedObservableList;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final ObservableList<String> tags = FXCollections.observableArrayList();
|
||||
private final ObservableList<Label> tags = FXCollections.observableArrayList();
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
private final ObservableList<Node> tagLabels;
|
||||
private final AggregatedObservableList<Node> firstLineChildren;
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
@@ -58,16 +64,9 @@ public class TwoLineListItem extends VBox {
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
|
||||
tagLabels = MappedObservableList.create(tags, tag -> {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setText(tag);
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
});
|
||||
firstLineChildren = new AggregatedObservableList<>();
|
||||
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
|
||||
firstLineChildren.appendList(tagLabels);
|
||||
firstLineChildren.appendList(tags);
|
||||
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
|
||||
|
||||
Label lblSubtitle = new Label();
|
||||
@@ -111,7 +110,11 @@ public class TwoLineListItem extends VBox {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public ObservableList<String> getTags() {
|
||||
public void addTag(String tag) {
|
||||
getTags().add(createTagLabel(tag));
|
||||
}
|
||||
|
||||
public ObservableList<Label> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
||||
@@ -176,17 +176,12 @@ public class DecoratorController {
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// For JavaFX 12+
|
||||
MouseButton button = MouseButton.valueOf("BACK");
|
||||
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == button) {
|
||||
back();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
navigator.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.BACK) {
|
||||
back();
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Decorator getDecorator() {
|
||||
@@ -217,6 +212,8 @@ public class DecoratorController {
|
||||
|
||||
private Background getBackground() {
|
||||
EnumBackgroundImage imageType = config().getBackgroundImageType();
|
||||
if (imageType == null)
|
||||
imageType = EnumBackgroundImage.DEFAULT;
|
||||
|
||||
Image image = null;
|
||||
switch (imageType) {
|
||||
|
||||
@@ -229,27 +229,28 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
} else {
|
||||
twoLineListItem.setSubtitle(null);
|
||||
}
|
||||
twoLineListItem.getTags().clear();
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
switch (versionType) {
|
||||
case RELEASE:
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.release"));
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.april_fools"));
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.snapshot"));
|
||||
twoLineListItem.addTag(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.old"));
|
||||
twoLineListItem.addTag(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
break;
|
||||
}
|
||||
@@ -276,7 +277,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
if (twoLineListItem.getSubtitle() == null)
|
||||
twoLineListItem.setSubtitle(remoteVersion.getGameVersion());
|
||||
else
|
||||
twoLineListItem.getTags().setAll(remoteVersion.getGameVersion());
|
||||
twoLineListItem.addTag(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +180,9 @@ public final class JavaInstallPage extends WizardSinglePage {
|
||||
|
||||
pane.setLeft(new Label(name));
|
||||
|
||||
Label valueLabel = FXUtils.truncatedLabel(value, 60);
|
||||
Label valueLabel = FXUtils.newSafeTruncatedLabel(value);
|
||||
FXUtils.copyOnDoubleClick(valueLabel);
|
||||
BorderPane.setMargin(valueLabel, new Insets(0, 0, 0, 16));
|
||||
BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT);
|
||||
pane.setCenter(valueLabel);
|
||||
|
||||
|
||||
@@ -250,9 +250,9 @@ public final class JavaManagementPage extends ListPageBase<JavaManagementPage.Ja
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.setTitle((java.isJDK() ? "JDK" : "JRE") + " " + java.getVersion());
|
||||
item.setSubtitle(java.getBinary().toString());
|
||||
item.getTags().add(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
|
||||
item.addTag(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
|
||||
if (vendor != null)
|
||||
item.getTags().add(i18n("java.info.vendor") + ": " + vendor);
|
||||
item.addTag(i18n("java.info.vendor") + ": " + vendor);
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
center.getChildren().setAll(item);
|
||||
root.setCenter(center);
|
||||
|
||||
@@ -69,6 +69,7 @@ import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
@@ -79,7 +80,6 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.download.RemoteVersion.Type.RELEASE;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
@@ -209,15 +209,17 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
launchPane.setMaxWidth(230);
|
||||
launchPane.setMaxHeight(55);
|
||||
launchPane.setOnScroll(event -> {
|
||||
int index = IntStream.range(0, versions.size())
|
||||
.filter(i -> versions.get(i).getId().equals(getCurrentGame()))
|
||||
.findFirst().orElse(-1);
|
||||
double deltaY = event.getDeltaY();
|
||||
if (deltaY == 0)
|
||||
return;
|
||||
|
||||
String currentId = getCurrentGame();
|
||||
int index = Lang.indexWhere(versions, instance -> instance.getId().equals(currentId));
|
||||
if (index < 0) return;
|
||||
if (event.getDeltaY() > 0) {
|
||||
if (deltaY > 0) // up
|
||||
index--;
|
||||
} else {
|
||||
else // down
|
||||
index++;
|
||||
}
|
||||
profile.setSelectedVersion(versions.get((index + versions.size()) % versions.size()).getId());
|
||||
});
|
||||
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
|
||||
|
||||
@@ -74,9 +74,6 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
||||
Task.runAsync(datapack::loadFromDir)
|
||||
.withRunAsync(Schedulers.javafx(), () -> {
|
||||
setLoading(false);
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
@@ -523,9 +523,9 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
listView.setCellFactory(x -> new FloatListCell<RemoteMod>(listView) {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
ImageView imageView = new ImageView();
|
||||
listView.setCellFactory(x -> new FloatListCell<>(listView) {
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final ImageView imageView = new ImageView();
|
||||
|
||||
{
|
||||
HBox container = new HBox(8);
|
||||
@@ -542,10 +542,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(dataItem.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : dataItem.getTitle());
|
||||
content.setSubtitle(dataItem.getDescription());
|
||||
content.getTags().setAll(dataItem.getCategories().stream()
|
||||
content.getTags().clear();
|
||||
dataItem.getCategories().stream()
|
||||
.map(category -> getSkinnable().getLocalizedCategory(category))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
.forEach(content::addTag);
|
||||
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
|
||||
}
|
||||
|
||||
@@ -228,9 +228,9 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
ModTranslations.Mod mod = getSkinnable().translations.getModByCurseForgeId(getSkinnable().addon.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : getSkinnable().addon.getTitle());
|
||||
content.setSubtitle(getSkinnable().addon.getDescription());
|
||||
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
|
||||
getSkinnable().addon.getCategories().stream()
|
||||
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
||||
.collect(Collectors.toList()));
|
||||
.forEach(content::addTag);
|
||||
descriptionPane.getChildren().add(content);
|
||||
|
||||
if (getSkinnable().mod != null) {
|
||||
@@ -353,10 +353,9 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.getTitle());
|
||||
content.setSubtitle(addon.getDescription());
|
||||
content.getTags().setAll(addon.getCategories().stream()
|
||||
addon.getCategories().stream()
|
||||
.map(page::getLocalizedCategory)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
.forEach(content::addTag);
|
||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||
imageView.imageProperty().bind(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true));
|
||||
}
|
||||
@@ -389,15 +388,15 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
|
||||
switch (dataItem.getVersionType()) {
|
||||
case Alpha:
|
||||
content.getTags().add(i18n("mods.channel.alpha"));
|
||||
content.addTag(i18n("mods.channel.alpha"));
|
||||
graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE.createIcon(Theme.blackFill(), 24));
|
||||
break;
|
||||
case Beta:
|
||||
content.getTags().add(i18n("mods.channel.beta"));
|
||||
content.addTag(i18n("mods.channel.beta"));
|
||||
graphicPane.getChildren().setAll(SVG.BETA_CIRCLE.createIcon(Theme.blackFill(), 24));
|
||||
break;
|
||||
case Release:
|
||||
content.getTags().add(i18n("mods.channel.release"));
|
||||
content.addTag(i18n("mods.channel.release"));
|
||||
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE.createIcon(Theme.blackFill(), 24));
|
||||
break;
|
||||
}
|
||||
@@ -405,22 +404,22 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
for (ModLoaderType modLoaderType : dataItem.getLoaders()) {
|
||||
switch (modLoaderType) {
|
||||
case FORGE:
|
||||
content.getTags().add(i18n("install.installer.forge"));
|
||||
content.addTag(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.getTags().add(i18n("install.installer.cleanroom"));
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.getTags().add(i18n("install.installer.neoforge"));
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.getTags().add(i18n("install.installer.fabric"));
|
||||
content.addTag(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.getTags().add(i18n("install.installer.liteloader"));
|
||||
content.addTag(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.getTags().add(i18n("install.installer.quilt"));
|
||||
content.addTag(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,11 +47,9 @@ public class GameItemSkin extends SkinBase<GameItem> {
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
FXUtils.onChangeAndOperate(skinnable.tagProperty(), tag -> {
|
||||
if (StringUtils.isNotBlank(tag)) {
|
||||
item.getTags().setAll(tag);
|
||||
} else {
|
||||
item.getTags().clear();
|
||||
}
|
||||
item.getTags().clear();
|
||||
if (StringUtils.isNotBlank(tag))
|
||||
item.addTag(tag);
|
||||
});
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
|
||||
@@ -20,15 +20,12 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.stage.Screen;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -38,8 +35,7 @@ import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
@@ -105,7 +101,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
btnManage.setOnAction(e -> {
|
||||
currentSkinnable = skinnable;
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root);
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get());
|
||||
popup.get().show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight());
|
||||
});
|
||||
btnManage.getStyleClass().add("toggle-icon4");
|
||||
@@ -132,45 +128,9 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
currentSkinnable = skinnable;
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root);
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get());
|
||||
popup.get().show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Intelligently determines the popup position to prevent the menu from exceeding screen boundaries.
|
||||
* Supports multi-monitor setups by detecting the current screen where the component is located.
|
||||
*
|
||||
* @param root the root node to calculate position relative to
|
||||
* @return the optimal vertical position for the popup menu
|
||||
*/
|
||||
private static JFXPopup.PopupVPosition determineOptimalPopupPosition(BorderPane root) {
|
||||
// Get the screen bounds in screen coordinates
|
||||
Bounds screenBounds = root.localToScreen(root.getBoundsInLocal());
|
||||
|
||||
// Convert Bounds to Rectangle2D for getScreensForRectangle method
|
||||
Rectangle2D boundsRect = new Rectangle2D(
|
||||
screenBounds.getMinX(), screenBounds.getMinY(),
|
||||
screenBounds.getWidth(), screenBounds.getHeight()
|
||||
);
|
||||
|
||||
// Find the screen that contains this component (supports multi-monitor)
|
||||
List<Screen> screens = Screen.getScreensForRectangle(boundsRect);
|
||||
Screen currentScreen = screens.isEmpty() ? Screen.getPrimary() : screens.get(0);
|
||||
Rectangle2D visualBounds = currentScreen.getVisualBounds();
|
||||
|
||||
double screenHeight = visualBounds.getHeight();
|
||||
double screenMinY = visualBounds.getMinY();
|
||||
double itemScreenY = screenBounds.getMinY();
|
||||
|
||||
// Calculate available space relative to the current screen
|
||||
double availableSpaceAbove = itemScreenY - screenMinY;
|
||||
double availableSpaceBelow = screenMinY + screenHeight - itemScreenY - root.getHeight();
|
||||
double menuHeight = popup.get().getPopupContent().getHeight();
|
||||
|
||||
return (availableSpaceAbove > menuHeight && availableSpaceBelow < menuHeight)
|
||||
? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward
|
||||
: JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward
|
||||
}
|
||||
}
|
||||
@@ -110,9 +110,6 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
itemsProperty().setAll(list.stream().map(ModListPageSkin.ModInfoObject::new).sorted().collect(Collectors.toList()));
|
||||
else
|
||||
getProperties().remove(ModListPage.class);
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
}, Platform::runLater);
|
||||
}
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(modInfo.getModInfo().getName());
|
||||
if (StringUtils.isNotBlank(modInfo.getModInfo().getVersion())) {
|
||||
title.getTags().setAll(modInfo.getModInfo().getVersion());
|
||||
title.addTag(modInfo.getModInfo().getVersion());
|
||||
}
|
||||
title.setSubtitle(FileUtils.getName(modInfo.getModInfo().getFile()));
|
||||
|
||||
@@ -442,9 +442,10 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
List<String> tags = title.getTags();
|
||||
if (!tags.contains(loaderName)) {
|
||||
tags.add(loaderName);
|
||||
if (title.getTags()
|
||||
.stream()
|
||||
.noneMatch(it -> it.getText().equals(loaderName))) {
|
||||
title.addTag(loaderName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,26 +552,26 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
content.getTags().clear();
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE:
|
||||
content.getTags().add(i18n("install.installer.forge"));
|
||||
content.addTag(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.getTags().add(i18n("install.installer.cleanroom"));
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGED:
|
||||
content.getTags().add(i18n("install.installer.neoforge"));
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.getTags().add(i18n("install.installer.fabric"));
|
||||
content.addTag(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.getTags().add(i18n("install.installer.liteloader"));
|
||||
content.addTag(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.getTags().add(i18n("install.installer.quilt"));
|
||||
content.addTag(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
if (dataItem.getMod() != null && I18n.isUseChinese()) {
|
||||
content.getTags().add(dataItem.getMod().getDisplayName());
|
||||
content.addTag(dataItem.getMod().getDisplayName());
|
||||
}
|
||||
content.setSubtitle(dataItem.getSubtitle());
|
||||
if (booleanProperty != null) {
|
||||
|
||||
@@ -245,7 +245,8 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
||||
item.setTitle(parseColorEscapes(skinnable.getBackupWorld().getWorldName()));
|
||||
item.setSubtitle(formatDateTime(skinnable.getBackupTime()) + (skinnable.count == 0 ? "" : " (" + skinnable.count + ")"));
|
||||
|
||||
if (world.getGameVersion() != null) item.getTags().add(world.getGameVersion());
|
||||
if (world.getGameVersion() != null)
|
||||
item.addTag(world.getGameVersion());
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -75,9 +76,9 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
|
||||
|
||||
if (world.getGameVersion() != null)
|
||||
item.getTags().add(world.getGameVersion());
|
||||
item.addTag(world.getGameVersion());
|
||||
if (world.isLocked())
|
||||
item.getTags().add(i18n("world.locked"));
|
||||
item.addTag(i18n("world.locked"));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -137,6 +138,8 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup));
|
||||
|
||||
popup.show(root, JFXPopup.PopupVPosition.TOP, hPosition, initOffsetX, initOffsetY);
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
|
||||
|
||||
popup.show(root, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY);
|
||||
}
|
||||
}
|
||||
@@ -99,9 +99,6 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
|
||||
} else {
|
||||
LOG.warning("Failed to load world list page", exception);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/938
|
||||
System.gc();
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,11 +68,6 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", world.getWorldName())));
|
||||
this.header = new TabHeader(worldInfoTab, worldBackupsTab);
|
||||
|
||||
if (world.getGameVersion() != null && // old game will not write game version to level.dat
|
||||
GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
|
||||
header.getTabs().add(datapackTab);
|
||||
}
|
||||
|
||||
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
@@ -90,8 +85,14 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO)
|
||||
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE)
|
||||
.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION);
|
||||
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE);
|
||||
|
||||
if (world.getGameVersion() != null && // old game will not write game version to level.dat
|
||||
GameVersionNumber.asGameVersion(world.getGameVersion()).isAtLeast("1.13", "17w43a")) {
|
||||
header.getTabs().add(datapackTab);
|
||||
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION);
|
||||
}
|
||||
|
||||
left.setTop(sideBar);
|
||||
|
||||
AdvancedListBox toolbar = new AdvancedListBox();
|
||||
|
||||
@@ -28,10 +28,10 @@ import java.util.function.Function;
|
||||
|
||||
public final class AggregatedObservableList<T> {
|
||||
|
||||
protected final List<ObservableList<T>> lists = new ArrayList<>();
|
||||
final private List<Integer> sizes = new ArrayList<>();
|
||||
final private List<InternalListModificationListener> listeners = new ArrayList<>();
|
||||
final protected ObservableList<T> aggregatedList = FXCollections.observableArrayList();
|
||||
private final List<ObservableList<? extends T>> lists = new ArrayList<>();
|
||||
private final List<Integer> sizes = new ArrayList<>();
|
||||
private final List<InternalListModificationListener> listeners = new ArrayList<>();
|
||||
private final ObservableList<T> aggregatedList = FXCollections.observableArrayList();
|
||||
|
||||
public AggregatedObservableList() {
|
||||
|
||||
@@ -46,7 +46,7 @@ public final class AggregatedObservableList<T> {
|
||||
return aggregatedList;
|
||||
}
|
||||
|
||||
public void appendList(@NotNull ObservableList<T> list) {
|
||||
public void appendList(@NotNull ObservableList<? extends T> list) {
|
||||
assert !lists.contains(list) : "List is already contained: " + list;
|
||||
lists.add(list);
|
||||
final InternalListModificationListener listener = new InternalListModificationListener(list);
|
||||
@@ -59,7 +59,7 @@ public final class AggregatedObservableList<T> {
|
||||
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
|
||||
}
|
||||
|
||||
public void prependList(@NotNull ObservableList<T> list) {
|
||||
public void prependList(@NotNull ObservableList<? extends T> list) {
|
||||
assert !lists.contains(list) : "List is already contained: " + list;
|
||||
lists.add(0, list);
|
||||
final InternalListModificationListener listener = new InternalListModificationListener(list);
|
||||
@@ -72,7 +72,7 @@ public final class AggregatedObservableList<T> {
|
||||
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
|
||||
}
|
||||
|
||||
public void removeList(@NotNull ObservableList<T> list) {
|
||||
public void removeList(@NotNull ObservableList<? extends T> list) {
|
||||
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
|
||||
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
|
||||
final int index = lists.indexOf(list);
|
||||
@@ -98,7 +98,7 @@ public final class AggregatedObservableList<T> {
|
||||
* @param list the list in question
|
||||
* @return the start index of this list in the aggregated List
|
||||
*/
|
||||
private int getStartIndex(@NotNull ObservableList<T> list) {
|
||||
private int getStartIndex(@NotNull ObservableList<? extends T> list) {
|
||||
int startIndex = 0;
|
||||
//System.out.println("=== searching startIndex of " + list);
|
||||
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
|
||||
@@ -120,7 +120,7 @@ public final class AggregatedObservableList<T> {
|
||||
* @param startIndex the start of the list (retrieve with {@link #getStartIndex(ObservableList)}
|
||||
* @return the end index of this list in the aggregated List
|
||||
*/
|
||||
private int getEndIndex(@NotNull ObservableList<T> list, int startIndex) {
|
||||
private int getEndIndex(@NotNull ObservableList<? extends T> list, int startIndex) {
|
||||
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
|
||||
final int index = lists.indexOf(list);
|
||||
return startIndex + sizes.get(index) - 1;
|
||||
@@ -129,9 +129,9 @@ public final class AggregatedObservableList<T> {
|
||||
private final class InternalListModificationListener implements ListChangeListener<T> {
|
||||
|
||||
@NotNull
|
||||
private final ObservableList<T> list;
|
||||
private final ObservableList<? extends T> list;
|
||||
|
||||
public InternalListModificationListener(@NotNull ObservableList<T> list) {
|
||||
public InternalListModificationListener(@NotNull ObservableList<? extends T> list) {
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
|
||||
@@ -387,7 +387,7 @@ public final class SelfDependencyPatcher {
|
||||
}
|
||||
}
|
||||
|
||||
String sha1 = Hex.encodeHex(digest.digest());
|
||||
String sha1 = HexFormat.of().formatHex(digest.digest());
|
||||
if (!dependency.sha1().equalsIgnoreCase(sha1))
|
||||
throw new ChecksumMismatchException("SHA-1", dependency.sha1(), sha1);
|
||||
}
|
||||
|
||||
@@ -34,45 +34,39 @@ public final class Locales {
|
||||
private Locales() {
|
||||
}
|
||||
|
||||
private static Locale getDefaultLocale() {
|
||||
String language = System.getenv("HMCL_LANGUAGE");
|
||||
if (StringUtils.isNotBlank(language))
|
||||
return Locale.forLanguageTag(language);
|
||||
else
|
||||
return LocaleUtils.SYSTEM_DEFAULT;
|
||||
}
|
||||
|
||||
public static final SupportedLocale DEFAULT = new SupportedLocale("def", getDefaultLocale()) {
|
||||
@Override
|
||||
public boolean isDefault() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
public static final SupportedLocale DEFAULT;
|
||||
|
||||
static {
|
||||
String language = System.getenv("HMCL_LANGUAGE");
|
||||
DEFAULT = new SupportedLocale(true, "def",
|
||||
StringUtils.isBlank(language) ? LocaleUtils.SYSTEM_DEFAULT : Locale.forLanguageTag(language));
|
||||
}
|
||||
|
||||
/**
|
||||
* English
|
||||
*/
|
||||
public static final SupportedLocale EN = new SupportedLocale("en", Locale.ENGLISH);
|
||||
public static final SupportedLocale EN = new SupportedLocale("en");
|
||||
|
||||
/**
|
||||
* Spanish
|
||||
*/
|
||||
public static final SupportedLocale ES = new SupportedLocale("es", Locale.forLanguageTag("es"));
|
||||
public static final SupportedLocale ES = new SupportedLocale("es");
|
||||
|
||||
/**
|
||||
* Russian
|
||||
*/
|
||||
public static final SupportedLocale RU = new SupportedLocale("ru", Locale.forLanguageTag("ru"));
|
||||
public static final SupportedLocale RU = new SupportedLocale("ru");
|
||||
|
||||
/**
|
||||
* Ukrainian
|
||||
*/
|
||||
public static final SupportedLocale UK = new SupportedLocale("uk", Locale.forLanguageTag("uk"));
|
||||
public static final SupportedLocale UK = new SupportedLocale("uk");
|
||||
|
||||
/**
|
||||
* Japanese
|
||||
*/
|
||||
public static final SupportedLocale JA = new SupportedLocale("ja", Locale.JAPANESE);
|
||||
public static final SupportedLocale JA = new SupportedLocale("ja");
|
||||
|
||||
/**
|
||||
* Chinese (Simplified)
|
||||
@@ -87,7 +81,7 @@ public final class Locales {
|
||||
/**
|
||||
* Wenyan (Classical Chinese)
|
||||
*/
|
||||
public static final SupportedLocale WENYAN = new SupportedLocale("lzh", Locale.forLanguageTag("lzh"));
|
||||
public static final SupportedLocale WENYAN = new SupportedLocale("lzh");
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = List.of(DEFAULT, EN, ES, JA, RU, UK, ZH_HANS, ZH_HANT, WENYAN);
|
||||
|
||||
@@ -103,20 +97,30 @@ public final class Locales {
|
||||
}
|
||||
|
||||
@JsonAdapter(SupportedLocale.TypeAdapter.class)
|
||||
public static class SupportedLocale {
|
||||
public static final class SupportedLocale {
|
||||
private final boolean isDefault;
|
||||
private final String name;
|
||||
private final Locale locale;
|
||||
private ResourceBundle resourceBundle;
|
||||
private DateTimeFormatter dateTimeFormatter;
|
||||
private List<Locale> candidateLocales;
|
||||
|
||||
SupportedLocale(String name, Locale locale) {
|
||||
SupportedLocale(boolean isDefault, String name, Locale locale) {
|
||||
this.isDefault = isDefault;
|
||||
this.name = name;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
SupportedLocale(String name) {
|
||||
this(false, name, Locale.forLanguageTag(name));
|
||||
}
|
||||
|
||||
SupportedLocale(String name, Locale locale) {
|
||||
this(false, name, locale);
|
||||
}
|
||||
|
||||
public boolean isDefault() {
|
||||
return false;
|
||||
return isDefault;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -128,9 +132,6 @@ public final class Locales {
|
||||
}
|
||||
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
if (inLocale.locale.getLanguage().equals("lzh"))
|
||||
inLocale = ZH_HANT;
|
||||
|
||||
if (isDefault()) {
|
||||
try {
|
||||
return inLocale.getResourceBundle().getString("lang.default");
|
||||
@@ -140,17 +141,32 @@ public final class Locales {
|
||||
}
|
||||
}
|
||||
|
||||
Locale inJavaLocale = inLocale.getLocale();
|
||||
if (LocaleUtils.isISO3Language(inJavaLocale.getLanguage())) {
|
||||
String iso1 = LocaleUtils.getISO1Language(inJavaLocale);
|
||||
if (LocaleUtils.isISO1Language(iso1)) {
|
||||
Locale.Builder builder = new Locale.Builder()
|
||||
.setLocale(inJavaLocale)
|
||||
.setLanguage(iso1);
|
||||
|
||||
if (inJavaLocale.getScript().isEmpty())
|
||||
builder.setScript(LocaleUtils.getScript(inJavaLocale));
|
||||
|
||||
inJavaLocale = builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.locale.getLanguage().equals("lzh")) {
|
||||
if (LocaleUtils.isChinese(inLocale.locale))
|
||||
if (inJavaLocale.getLanguage().equals("zh"))
|
||||
return "文言";
|
||||
|
||||
String name = locale.getDisplayName(inLocale.getLocale());
|
||||
String name = locale.getDisplayName(inJavaLocale);
|
||||
return name.equals("lzh") || name.equals("Literary Chinese")
|
||||
? "Chinese (Classical)"
|
||||
: name;
|
||||
}
|
||||
|
||||
return locale.getDisplayName(inLocale.getLocale());
|
||||
return locale.getDisplayName(inJavaLocale);
|
||||
}
|
||||
|
||||
public ResourceBundle getResourceBundle() {
|
||||
@@ -237,8 +253,8 @@ public final class Locales {
|
||||
}
|
||||
|
||||
public boolean isSameLanguage(SupportedLocale other) {
|
||||
return (this.getLocale().getLanguage().equals(other.getLocale().getLanguage()))
|
||||
|| (LocaleUtils.isChinese(this.getLocale()) && LocaleUtils.isChinese(other.getLocale()));
|
||||
return LocaleUtils.getISO1Language(this.getLocale())
|
||||
.equals(LocaleUtils.getISO1Language(other.getLocale()));
|
||||
}
|
||||
|
||||
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
|
||||
|
||||
@@ -74,6 +74,11 @@ public final class WenyanUtils {
|
||||
builder.append(DI_ZHI[mod(yearOffset, DI_ZHI.length)]);
|
||||
}
|
||||
|
||||
static void appendHour(StringBuilder builder, int hour) {
|
||||
builder.append(DI_ZHI[((hour + 1) % 24) / 2]);
|
||||
builder.append(hour % 2 == 0 ? '正' : '初');
|
||||
}
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
LocalDateTime localDateTime;
|
||||
if (time instanceof Instant)
|
||||
@@ -92,8 +97,7 @@ public final class WenyanUtils {
|
||||
|
||||
builder.append(' ');
|
||||
|
||||
builder.append(numberToString(localDateTime.getHour()));
|
||||
builder.append('时');
|
||||
appendHour(builder, localDateTime.getHour());
|
||||
builder.append(numberToString(localDateTime.getMinute()));
|
||||
builder.append('分');
|
||||
builder.append(numberToString(localDateTime.getSecond()));
|
||||
|
||||
@@ -389,6 +389,9 @@ fatal.config_loading_failure=Cannot load configuration files.\n\
|
||||
For macOS, try putting HMCL somewhere with permissions other than "Desktop", "Downloads", and "Documents" and try again.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher could not load the configuration file because it was created by user "%1$s".\n\
|
||||
Please open HMCL as root user (not recommended), or execute the following command in the terminal to change the ownership of the configuration file to the current user:\n%2$s
|
||||
fatal.config_unsupported_version=The current configuration file was created by a newer version of Hello Minecraft! Launcher, and this version of HMCL cannot load it properly.\n\
|
||||
Please update and restart HMCL.\n\
|
||||
Before updating the launcher, any settings you modify will not be saved.
|
||||
fatal.mac_app_translocation=Hello Minecraft! Launcher is isolated to a temporary directory by the OS due to macOS security mechanisms.\n\
|
||||
Please move HMCL to a different directory before attempting to open. Otherwise, your settings and game data may be lost after restarting.\n\
|
||||
Do you still want to continue?
|
||||
|
||||
@@ -378,6 +378,7 @@ fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft
|
||||
fatal.config_in_temp_dir=你正在暫存目錄中開啟 Hello Minecraft! Launcher,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
|
||||
fatal.config_unsupported_version=當前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被保存。
|
||||
fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Launcher 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
|
||||
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
|
||||
|
||||
@@ -387,6 +387,7 @@ fatal.config_change_owner_root=你正在使用 root 账户启动 Hello Minecraft
|
||||
fatal.config_in_temp_dir=你正在临时文件夹中启动 Hello Minecraft! Launcher,你的设置和游戏数据可能会丢失。建议将 HMCL 移动至其他位置再启动。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 HMCL 对“%s”文件夹及该文件夹下的文件拥有读写权限。\n对于 macOS,尝试将 HMCL 放在除“桌面”“下载”“文稿”之外的有权限的地方再试。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 无法加载配置文件,因为配置文件是由用户“%1$s”创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$s\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_unsupported_version=当前配置文件是由更高版本的 Hello Minecraft! Launcher 创建的,当前版本的 HMCL 无法正常加载。请更新并重启 HMCL。\n在更新启动器之前,你修改的所有设置都不会被保存。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.mac_app_translocation=由于 macOS 的安全机制,Hello Minecraft! Launcher 被系统隔离至临时文件夹中。\n请将 HMCL 移动到其他文件夹后再尝试启动,否则你的设置和游戏数据可能会在重启后丢失。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -170,7 +170,7 @@ all-the-mods-6;164;;All the Mods 6;;ATM6
|
||||
garden-of-glass-questbook-edition;165;;Garden of Glass (Questbook Edition);;
|
||||
;166;;圣剑传奇;Sword Legendary;
|
||||
;167;;FTB Presents Direwolf20 1.16;;
|
||||
team-rustic;168;;Rusty Sink 1.20;;
|
||||
team-rustic;168;;Rusty Sink 2;;
|
||||
enigmatica2expertskyblock;169;;Enigmatica 2: Expert Skyblock;;E2ES
|
||||
;170;;休闲-生存-2021版;Leisure Survival-2021;ls21
|
||||
hexxit-updated;171;;Hexxit Updated;;
|
||||
@@ -195,7 +195,7 @@ mineshafts-monsters;189;;矿井与怪物;Mineshafts & Monsters;
|
||||
crazy-craft-remastered;190;;Crazy Craft Remastered;;
|
||||
engineers-life-2;191;;Engineer's Life 2;;EL2
|
||||
hr-new-beginnings-2;192;;HR: New Beginnings 2;;
|
||||
minecolonies-testpack;193;;模拟殖民地;MineColonies Official;
|
||||
minecolonies-official;193;;模拟殖民地;MineColonies Official;
|
||||
tnp-limitless;194;;TNP Limitless;;
|
||||
tnp-limitless-2;195;;TNP Limitless 2;;
|
||||
tnp-limitless-3;196;;TNP Limitless 3;;
|
||||
@@ -251,9 +251,9 @@ fabulously-optimized;248;;难以置信的优化;Fabulously Optimized;FO
|
||||
better-minecraft-otv-edition;249;;Better Minecraft (OfflineTV Edition);;
|
||||
evolution-reset;250;;进化:重启;Evolution: Reset;ER
|
||||
the-nuclear-wasteland;251;;核荒原;The Nuclear Wasteland;
|
||||
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BOC
|
||||
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BoC
|
||||
;253;;边界生存;Border Survival;BSV
|
||||
medieval-mc-fabric;255;;Medieval Minecraft;;MMC
|
||||
medieval-mc-forge-mmc1;255;;Medieval Minecraft;;MMC
|
||||
heavens-of-sorcery;256;;巫术天堂;Heavens of Sorcery;
|
||||
herodotus;257;;希罗多德;Herodotus-Authentic Edition:Official;HDS
|
||||
;258;;重生世界;Rebirth world;RW
|
||||
@@ -282,7 +282,7 @@ world-of-dragons-ii;282;;World of Dragons II;;
|
||||
tensura-unleashed;283;;Tensura Unleashed;;
|
||||
skyopolis-4;284;;Skyopolis 4;;
|
||||
ftb-oceanblock;286;;FTB OceanBlock;;
|
||||
tolkiencraft-iii-return-to-middle-earth;287;;TolkienCraft III - Return to Middle-earth;;
|
||||
tolkiencraft-iii-return-to-middle-earth;287;;托尔金工艺3 - 重返中洲;TolkienCraft III - Return to Middle-earth;TC3
|
||||
delivery-inc;288;;Delivery Inc.;;
|
||||
poetica;289;;Poetica;;
|
||||
divine-journey-2;290;;神圣之旅2;Divine Journey 2;DJ2
|
||||
@@ -354,7 +354,7 @@ all-the-mods-expert-remastered;355;;All The Mods Expert: Remastered;;
|
||||
all-the-mods-0;356;;All the Mods 0;;ATM0
|
||||
all-the-mods-fabric;357;;All the Mods Fabric;;ATMF
|
||||
the-pixelmon-modpack;358;;The Pixelmon Modpack;;
|
||||
better-minecraft-skyblock;359;;更好的我的世界:空岛;Better Minecraft Skyblock;
|
||||
better-mc-skyblock;359;;更好的我的世界空岛;Better MC Skyblock;BMCS
|
||||
age-of-fate;360;;命运时代;Age of Fate;AOF
|
||||
minecraft-kingdoms;361;;MC Kingdoms;;
|
||||
;362;;幻想旅程;Imaginary Journey;
|
||||
@@ -413,7 +413,6 @@ epoch-runner-formerly-nuclear-beyond-modpack;417;;时代领跑人;Epoch Runner;
|
||||
;418;;星海征途;The Journey of Star Sea;TJoSS
|
||||
roguelike-adventures-and-dungeons-2;419;;冒险与地牢2;Roguelike Adventures and Dungeons 2;RAD2
|
||||
multiskyblock;420;;模块化空岛;MultiSkyBlock;MSB
|
||||
;421;;人性化;Humanized;H
|
||||
create-live-3;422;;Create Live 3;;
|
||||
magnificia;423;;Magnificia;;
|
||||
;424;;魔法金属-新手向导;ManaMetalMod - WithNewbHelper;
|
||||
@@ -484,7 +483,7 @@ dimensionhopper;490;;Dimension Hopper:The Fall;;
|
||||
technology-empty-island;491;;科技空岛;RxanGrben;
|
||||
greed-the-resource-4-infinity;492;;Greed the Resource 4 - Infinity;;
|
||||
caveopolis;493;;Caveopolis;;
|
||||
;494;;硬核生存;HardCraft;HC
|
||||
;494;;坚毅征途;HardCraft;HC
|
||||
;495;;怀末韵云;Institutions mz;MZ1
|
||||
feed-the-factory;496;;Feed the Factory;;FTF
|
||||
fantasy-realm;497;;幻想领域;Fantasy Realm;
|
||||
@@ -567,7 +566,6 @@ expert-of-magic-art-world;573;;魔艺世界;Expert of Magic Art World;EOMAW
|
||||
nuclear-radiation-2;576;;核污染二;nuclear radiation 2;NR2
|
||||
;577;;白狐铃的奇思妙想;Arctic Fox Ling Fantasy;AFLF
|
||||
;578;;MineSpace;;MS
|
||||
;579;;史诗战斗:兵团战争;EPIC Fight Corps War;EFC
|
||||
;580;;月见清兰的个人优化;TskimiSeiran's Idea;TkSrI
|
||||
;581;;兔头:真视世界;;
|
||||
;582;;灾厄战争;;
|
||||
@@ -584,7 +582,7 @@ the-winter-frontier;591;;冬境边域;The Winter Frontier;TWF
|
||||
;593;;太阳机械厂;Create Sun;CS
|
||||
dawn-craft;594;;破晓之界;DawnCraft: Echoes of Legends;DC
|
||||
aof-presents-skylore;595;;AOF: Skylore;;
|
||||
keletus-gregpack;596;;可乐兔的格雷整合包;Keletu's GregPack;
|
||||
keletus-gregpack;596;;可乐兔的格雷整合包;Keletu's GregPack;GtMagic
|
||||
ftb-skies;597;;FTB Skies;;
|
||||
tnp-limitless-5;598;;TNP Limitless 5;;
|
||||
create-back-on-track;599;;Create: Back on Track;;
|
||||
@@ -614,8 +612,8 @@ eternal-night-modpacks;622;;永夜;Eternal Night;ETN
|
||||
all-the-mods-9;623;;All the Mods 9;;ATM9
|
||||
spoornpack;624;;Spoornpack;;
|
||||
;625;;起源RPG;;ORPG
|
||||
;626;;钓月织梦;Tsuri No Gatsu;TSNG
|
||||
;627;;冰河物语;Ice Age Fairy;IAF
|
||||
;626;;织彩钓月;Oriun Tagayasu;OTSU
|
||||
;627;;冰河物语;;
|
||||
hommage;628;;Hommage;;
|
||||
days-to-craft;629;;Seven Days To Craft;;
|
||||
emcworld;630;;质能世界;EMCWorld;
|
||||
@@ -668,7 +666,7 @@ journey-watch-ytz;676;;征途 守望;Journey Watch;JW
|
||||
;677;;Arcomua Lite 原版整合;Arcomua Lite;AL
|
||||
incremental-industries;678;;Incremental Industries;;
|
||||
;679;;海王星计划:重构;NeptunePlan:Reboot;NPR
|
||||
;680;;中世纪生存日记;Medieval Survival Diary;MSD
|
||||
;680;;求生日记:荒野;Survival Diary: Wilderness;SWD
|
||||
a-new-technological-era;681;;A New Technological Era;;ANTE
|
||||
;682;;超能力之旅;Meet Your Superpower;MYS
|
||||
musketeer;683;;Musketeer - One for All;;
|
||||
@@ -692,7 +690,7 @@ enigmatica9;699;;Enigmatica 9;;E9
|
||||
create-arcane-engineering;701;;机械动力:奥术工程;Create: Arcane Engineering;CAE
|
||||
chroma-endless-2;702;;Chroma Endless 2;;
|
||||
techrevolution;703;;TechRevolution;;
|
||||
cabin;704;;CABIN: Create Above & Beyond In Newer;;CABIN
|
||||
cabin;704;;Create Above & Beyond In Newer;;CABIN
|
||||
aoc;705;;All of Create;;
|
||||
;706;;交错维度3;Among Dimensions 3;AD
|
||||
gregfactory-sky;707;;GregFactory Sky;;GFS
|
||||
@@ -735,8 +733,8 @@ life-in-the-village-3;740;;Life in the village 3;;LitV3
|
||||
;745;;FTB Genesis;;
|
||||
cook-in-skyblock;746;;天空厨房;Cook in skyland;CiS
|
||||
;747;;被恐惧支配 1;Dominated by Fear 1;DoF
|
||||
new-vanilla-modpack;748;;新的原版;New Vanilla;NVA
|
||||
;749;;群峦救援非官方版;Terrafirma Rescue Unofficial;TFRU
|
||||
new-vanilla-newhome;748;;新的原版;New Vanilla;NVA
|
||||
;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
|
||||
stoneopolis;750;;Stoneopolis;;
|
||||
ftb-skies-expert;751;;FTB Skies Expert;;
|
||||
wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
|
||||
@@ -744,7 +742,7 @@ wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
|
||||
;754;;随心之旅;spontaneous journey;
|
||||
oxmodpack;755;;OxMODPACK 8 +Online;;
|
||||
gregtech-community-pack-modern;756;;GregTech Community Pack Modern;;GCPM
|
||||
;757;;锻造大师3;Master Blacksmith3;MB3
|
||||
;757;;锻造大师4;Master Blacksmith4;MB4
|
||||
;758;;科技用于探索世界;Technology Exploration World;
|
||||
;759;;幻梦闲笔;Aetherial Voyage;
|
||||
fear-the-mist;760;;雾中的恐惧;Fear The Mist;
|
||||
@@ -764,7 +762,7 @@ infernal-rpg;771;;Infernal Origins;;
|
||||
;774;;AIR for JAVA;;AFJ
|
||||
;775;;Create Extra;;
|
||||
frs-optimisation-for-survival;776;;Fr的生存优化;Fr's Optimisation for Survival;FROS
|
||||
;777;;Exile Magic Constructor;;EMC
|
||||
exile-magic-constructor;777;;Exile Magic Constructor;;EMC
|
||||
;778;;Landscapes Reimagined Genesis;;
|
||||
al-nl;779;;冒险生活 - 新生活;Adventure Life - New Life;AL-NL
|
||||
harvista;780;;拾穗;Harvista;
|
||||
@@ -819,19 +817,19 @@ all-the-mods-9-to-the-sky;828;;All The Mod 9 - To The Sky;;ATM9S
|
||||
fallenpetals;830;;落英;FallenPetals;FP
|
||||
dungeon-heroes;831;;Dungeon Heroes (RPG Series);;
|
||||
play-as-dragon-gothic-edition;832;;Play as Dragon: Gothic Edition;;
|
||||
skyblock-burgeria;833;;空岛汉堡店;;
|
||||
skyblock-burgeria;833;;空岛汉堡店;Skyblock Burgeria;
|
||||
crafting-craft-2;834;;合成工艺2;Crafting Craft 2;
|
||||
;835;;僵尸入侵100天;Zombie Invade 100 Days;ZI100D
|
||||
compact-world-expert;836;;Compact World Expert;;CWE
|
||||
a-fragment-of-peace;837;;宁然一隅;A Fragment of Peace;AFoP
|
||||
;837;;宁然一隅;A Fragment of Peace;AFoP
|
||||
easy-create-modpack;838;;Easy Create;;
|
||||
;839;;流线鱼概念;Fish Spi Rally;FSRMP
|
||||
meatballcraft;840;;肉丸工艺,次元提升;MeatballCraft, Dimensional Ascension;
|
||||
meatballcraft;840;;肉丸工艺,次元飞升;MeatballCraft, Dimensional Ascension;MBC
|
||||
seaopolis-submerged;841;;海之城: 潜没水中;Seaopolis: Submerged;
|
||||
;842;;空中厕所;PoopSky;
|
||||
compact-sky;843;;收缩空岛;Compact Sky;
|
||||
back-to-back;844;;背靠背;Back to Back;BTB
|
||||
adventure-exploration-part2;845;;冒险与探索;Adventure & Exploration;AnE
|
||||
ane2;845;;冒险与探索;Adventure & Exploration;AnE
|
||||
;846;;复兴;Renaissance;
|
||||
live-like-a-castaway;847;;木筏求生2;Raftcraft2;RC2
|
||||
mermaid-legend;848;;人鱼传说;Mermaid legend;
|
||||
@@ -847,7 +845,6 @@ create-prepare-to-dye;856;;Create Prepare to Dye;;
|
||||
create-leisurely-delight;858;;机械动力:悠然乐事;Create Delight;CD
|
||||
;859;;蔚蓝悠悠;Azure Leisurely;AL
|
||||
infinityevolved-reloaded;860;;Infinity Evolved: Reloaded;;
|
||||
;861;;空中厕所:生产线;Poopsky:Assembly Line;PAL
|
||||
;862;;佛罗伦萨迷思;Fantasy in Florence;FIF
|
||||
craftoria;863;;Craftoria;;
|
||||
mechanic-craft;864;;MechanicCraft;;
|
||||
@@ -878,7 +875,7 @@ create-stellar;888;;Create Stellar;;
|
||||
monifactory;889;;Monifactory;;
|
||||
create-factory-builder;890;;Create: Factory Builder;;
|
||||
;891;;群峦传说:宝宝快乐版;TerraFirma: Baby Happy Edition;TFBH
|
||||
;892;;阶段: 残骸;Phase: the Debris;
|
||||
;892;;阶段: 残骸;Phase: the Debris;PTD
|
||||
;893;;AE管道;AE Pipes;APE
|
||||
skyt-2;894;;SkyT 2;;
|
||||
stagecraft;895;;StageCraft;;
|
||||
@@ -909,7 +906,7 @@ strictly-medieval-3-forge;917;;Strictly Medival 3;;
|
||||
easy-to-ascension;920;;易如飞升;Easy to Ascension;
|
||||
;921;;幽匿末日;The Last Day of Sculk;TLDS
|
||||
;922;;交错空岛;Crossisland;CrI
|
||||
;923;;机械动力:魔法乐事;Create Magic's Delight;CRMD
|
||||
;923;;机动工程:刀剑与魔法;Mechanical Engineering: SlashBlade and Magic;CRMD
|
||||
minetech-evolution;924;;科技进化;Minetech Evolution;MTE
|
||||
gregified-sky-edition;925;;Gregified: Sky Edition;;
|
||||
;926;;原版扩展;Additonal Vanilla;AV
|
||||
@@ -917,14 +914,14 @@ create-on-a-potato;927;;Create on a Potato PC: Ultimate;;CPU
|
||||
;928;;现实主义;realism;RLM
|
||||
;929;;晋升之路;path of advance;POA
|
||||
;930;;永无止境:超越常新;Above and Beyond : Refresh;CABR
|
||||
cosmic-frontiers;931;;Cosmic Frontiers;;
|
||||
cosmic-frontiers;931;;星河边疆;Cosmic Frontiers;CF
|
||||
integrated-minecraft;932;;Integrated MC;;
|
||||
;933;;Rising Legends;;
|
||||
;934;;城乡之旅;Travel Township;TT
|
||||
;934;;城乡之旅;Harvest's Journey;HJ
|
||||
;935;;龙鸣牢大;FarmerKobeBryant;FKB
|
||||
setgamedifficulty-reika;936;;SetGameDifficulty Reika;;
|
||||
epic-journey-gtceu;937;;Epic Journey GTCEu;;
|
||||
;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
|
||||
terrafirmacraft-leap-the-top;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
|
||||
gregtech-skybound;939;;Gregtech: Skybound;;
|
||||
simple-tfc;940;;简单群峦;SimpleTFC;
|
||||
ftb-presents-direwolf20-s14;941;;FTB Presents Direwolf20 1.21;;
|
||||
@@ -948,10 +945,10 @@ gregtania6-skyblock;959;;Gregtania6 Skyblock;;
|
||||
harmonious-engineering;960;;Harmonious Engineering;;
|
||||
valley-of-farming-and-ranching;961;;农牧物语;Valley of Farming and Ranching;
|
||||
;962;;机械动力:火车;Create Train;
|
||||
;963;;理想国冒险之旅;;AOIC
|
||||
the-old-era-of-the-republic-fot-yifei;963;;理想国冒险之旅;;AOIC
|
||||
;964;;生欲:求生2;Vivere The Zombie Apocalypse-II;VZA2
|
||||
;965;;起源-世界;OriginCraft Pulses;
|
||||
;966;;Legacies;;
|
||||
legacies-mc;966;;Legacies;;
|
||||
the-legend-of-2900;967;;海上机械师;The Legend Of 2900;TLOU
|
||||
mirasites-100-days;968;;畸变:蛊之奇境;Mirasites-BrokenMemories;MBM
|
||||
;969;;末日余晖:独自一人;;MRYH
|
||||
@@ -970,7 +967,7 @@ venoms-craft;981;;Venom's Craft;;
|
||||
;982;;慢生活冒险;;
|
||||
;983;;多元工艺:绘镇曲;Painted Town Melody;PTM
|
||||
;984;;遗忘之海;Leisurely Life;
|
||||
;985;;幻想国度;F_Land;
|
||||
;985;;幻想国度;Fancy_Land;
|
||||
;986;;群峦传说:重制;TerraFirmaCraft Remake;TFCRe
|
||||
;987;;未来;;
|
||||
;988;;起源生物—崛起;;
|
||||
@@ -988,12 +985,12 @@ industrial-machines;1000;;Industrial Machines;;
|
||||
auto-terrafirmacraft-tfc-create;1001;;Auto-TerraFirmaCraft (TFC + Create);;ATFC
|
||||
;1002;;时间之书:星的日记;The Book of Time: A Star's Journal;TASL
|
||||
;1003;;特摄世界;Tokusatsu World;TW
|
||||
journey-of-reincarnation;1004;;转世之旅;Journey of Reincarnation;
|
||||
journey-of-reincarnation;1004;;转世之旅;Tensura: Journey of Reincarnation;
|
||||
;1005;;冒险与休闲;Adventure and Leisure;AL
|
||||
enigmatica10;1006;;Enigmatica 10;;E10
|
||||
mc-chocolate-edition;1007;;The Chocolate Edition;;
|
||||
mc-chocolate-edition;1007;;Chocolate Edition Minecraft;;
|
||||
civilization-l;1008;;Civilization-L;;
|
||||
;1009;;群峦:降临;TerraFirma:Advent;TFA
|
||||
terrafirma-advent;1009;;群峦:降临;TerraFirma:Advent;TFA
|
||||
valhelsia-7;1010;;Valhelsia 7;;
|
||||
mechanical-mastery-plus;1011;;Mechanical Mastery Plus;;
|
||||
ashes-to-ashes;1012;;尘归尘;Ashes To Ashes;ATA
|
||||
@@ -1001,23 +998,23 @@ ashes-to-ashes;1012;;尘归尘;Ashes To Ashes;ATA
|
||||
create-silence-and-prosperity;1014;;机械动力:寂静与繁荣;Create Silence and Prosperity;CSP
|
||||
mif;1015;;MI:Foundation;;MIF
|
||||
c2mse2;1016;;Create 2 Mekanism: Sky Edition 2;;C2MSE2
|
||||
;1017;;Tsuri No Gatsu 5 - FDCraft Version;;TSNG5-FDC
|
||||
;1017;;耕云钓月;Kumo O Tagayasu;KOTS
|
||||
;1018;;无限战争4;Infinity War4;INF4
|
||||
;1019;;群峦传说:工业长路;;
|
||||
;1020;;自然冒险 初生;Nature Adventure Newborn;NAB
|
||||
;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
|
||||
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
|
||||
hmbo;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
|
||||
create-star-valley;1022;;Create: Gears of History;;
|
||||
;1023;;阿尔法计划;AlphaPlan;AP
|
||||
spell-dimension;1024;;咒次元;Spell Dimension;SD
|
||||
skyfactory-5;1025;;天空工厂 5;SkyFactory 5;SF5
|
||||
;1026;;破劫之刃;Break Disaster;
|
||||
reminiscent-create;1027;;Reminiscent Create;;
|
||||
techsert;1028;;Techsert;;TH
|
||||
techsert;1028;;匿踪之城;Techsert;TH
|
||||
prehistoric-world-dinosaurs-adventure;1029;;Prehistoric World - Dinosaurs & Adventure;;
|
||||
terminal;1030;;终末;Terminal;
|
||||
;1031;;异次元水晶 3;Isolated Crystal 3;
|
||||
isolated-crystal-3;1031;;异次元水晶 3;Isolated Crystal 3;
|
||||
;1032;;机械动力:无限总是小于零;Create:Infinity Always Smaller Than Zero;CIASZ
|
||||
;1033;;踪灭I-开端;;
|
||||
;1033;;踪灭I-起源;;
|
||||
;1034;;踪灭II;;
|
||||
rabbits-dawn-craft;1035;;某兔子的破晓之界;A Certain Rabbit's DawnCraft - Dawn of the Deep Soul;RDC
|
||||
;1036;;铁砧天空;;ASKY
|
||||
@@ -1027,7 +1024,7 @@ the-adventure-of-valkyriens;1038;;瓦尔基里大冒险;The Adventure of Valkyri
|
||||
;1040;;群峦传说:天才版;;
|
||||
compression;1041;;Compression;;
|
||||
compact-sky-easy;1042;;Compact Sky: Easy;;CSE
|
||||
;1043;;丰收与探索;;
|
||||
;1043;;丰收与探索;Harvest and Exploration;
|
||||
;1044;;脆骨症:黯光;No Flesh Within Chest-DIM;NFWC-DIM
|
||||
;1045;;小雷的优化冒险;;
|
||||
cobblemon-eternal-edition;1046;;Cobblemon Eternal;;
|
||||
@@ -1035,3 +1032,150 @@ cobblemon-eternal-edition;1046;;Cobblemon Eternal;;
|
||||
gregtech-expert;1048;;GregTech Expert;;
|
||||
;1049;;Beta Horizons;;
|
||||
minecraft-legendary-edition;1050;;Legendary Edition;;
|
||||
;1051;;Origenceal LPackage;;OLP
|
||||
;1052;;我的世界:未闻的故事;;mtus
|
||||
;1053;;Versatile;;
|
||||
;1054;;勇者之章 Ⅲ;Chapter of Yuusha Ⅲ;CY3
|
||||
forge-frontier;1055;;Create - Forge Frontier;;
|
||||
create-workshop-uwu;1056;;Create: Workshop UwU Edition;;
|
||||
;1057;;古钠重铸;Oldium Optimized;OO
|
||||
;1058;;越寰;Leaping across the world;LTW
|
||||
ftb-oceanblock-2;1059;;FTB OceanBlock 2;;
|
||||
;1060;;伊甸空天2;Eden Aerospace 2;EAS2
|
||||
create-infinity-mechanism;1061;;机械动力:无限构件;Create:Infinity Mechanism;CIM
|
||||
beyond-depth;1062;;Beyond Depth;;
|
||||
;1063;;枪械交界地;TACZ Amid Crucible Zone;
|
||||
;1064;;真幻世界;Realfantacy World;R.F.
|
||||
minenufactory;1065;;创世工厂;Minenufactory;MF
|
||||
craft-to-exile-2;1066;;放逐之路2;Craft to Exile 2 (VR Support);CTE2
|
||||
;1067;;龙境III:菌土重来;Dragon Odyssey:Return of the Fungal Earth;DO3
|
||||
the-culinary-journey;1068;;美食的旅途;The Culinary Journey;TCJ
|
||||
;1069;;缥缈方舟;;PMFZ
|
||||
;1070;;东方匠魂传;Touhou Tinker Fantasy;TTF
|
||||
;1071;;亵渎;;MYBL
|
||||
techopolis-3;1072;;Techopolis 3;;
|
||||
sky1234;1074;;sky1234;;
|
||||
star-technology;1075;;Star Technology;;
|
||||
;1076;;Hyper World;;HW
|
||||
;1077;;妖月的魔法金属整合包;yaoyaoDream's ManaMetalMod Modpack;YMM
|
||||
techcraft-severwinter;1078;;科技工艺:严冬;TechCraft: SevereWinter;TCSW
|
||||
randblock2-winterize;1079;;随机空岛 2:银装素裹;RandBlock2:Winterize;
|
||||
create-the-brass-concerto;1080;;机械动力:黄铜协奏曲;Create: The Brass Concerto;CBC
|
||||
explorers-eve;1081;;Explorer's Eve;;
|
||||
all-the-magic-arcana;1082;;All the Magic :Arcana;;ATMAr
|
||||
;1083;;创想新世界;Inspired Creations: New World;ICNW
|
||||
farming-crossing-5-made-to-order;1084;;农业森友会 5;Farming Crossing 5: Made to Order!;
|
||||
;1085;;Ben10 Craft;;BEN10
|
||||
;1086;;渺小的世界;Small world;
|
||||
mierno;1087;;迷厄路;Mierno;
|
||||
;1088;;最后那些天;Those last day;
|
||||
;1089;;泠月狐基础整合;LunarFox;
|
||||
roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures and Dungeons 3;RAD3
|
||||
;1091;;羽舲界;Hanekmio Instance;HMI
|
||||
;1092;;玄理2:洗剪版;EnigTech 2 - Dishwasher's Cut;ET2DC
|
||||
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
|
||||
egged-up;1094;;Egged Up;;
|
||||
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;
|
||||
;1096;;愚者;The Fool;TF
|
||||
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
|
||||
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
|
||||
;1099;;血月低语:暗夜惊魂;Bloodmoon's Whisper;BW
|
||||
;1100;;MC饭;Delicious in Minecraft;DIM
|
||||
;1101;;美味的甜点总是有毒;;
|
||||
delight-desert-chef;1102;;乐事:沙漠厨师;Delight: Desert Chef;DDC
|
||||
;1103;;异界随行诗;Otherworld Traveling Poems;
|
||||
;1104;;无边荒漠;;
|
||||
;1105;;空中厕所2;PoopSky2;
|
||||
fear-nightfall;1106;;Fear Nightfall;;
|
||||
homestead-cozy;1107;;Homestead - A Cozy Survival Experience;;HD
|
||||
madness-of-the-skies;1108;;Madness of the skies;;
|
||||
;1109;;魔之逆鳞;Nemesis of Demons;NOD
|
||||
emc-and-fe-plus2;1110;;EMC & FE ++;;E&F
|
||||
;1111;;云之谷;CloudValley;
|
||||
bm-exosphere;1112;;BM: Exosphere;;BME
|
||||
;1113;;锻造大师:侠之器;Master Blacksmith:Chivalrous blade;MB:CB
|
||||
;1114;;自然;Nature;NA
|
||||
;1115;;农场物语;FarmingTales;FT
|
||||
optimaxcraft;1116;;OptiMaxCraft;;OMC
|
||||
;1117;;决战黎明;Decisive Battle at Dawn;DBAD
|
||||
floaty-creatoria;1118;;天工浮生;Floaty Creatoria;
|
||||
;1119;;简约生电优化;Simplicity Survival Optimization;SSO
|
||||
;1120;;星辰之下;Under the Stars;UtS
|
||||
madness-of-the-skies-2;1121;;Madness of the Skies 2;;
|
||||
raspberry-flavoured;1122;;Raspberry Flavoured;;RF
|
||||
skillet-man;1123;;平底锅侠;Skillet Man;
|
||||
;1124;;格雷科技:新征程;GregTech Odyssey;GTO
|
||||
;1125;;无中生无尽;Ex Nihilo Ad Avaritia;ENAA
|
||||
;1126;;Undertale;;
|
||||
create-creative-and-beyond;1127;;机械动力:创造与超越;Create Creative and Beyond;CCB
|
||||
create-delight-remake;1128;;机械动力:齿轮盛宴;Create:Delight Remake;CDR
|
||||
ftb-skies-2;1129;;FTB Skies 2;;
|
||||
;1130;;星辰时代:科技魔法;Stellar Era: Technomagic;SETM
|
||||
aircrashed-engineer;1131;;坠机工程师;Aircrashed Engineer;ACE
|
||||
project-tfcreate;1132;;Project: TFCreate;;
|
||||
;1133;;落幕曲;;
|
||||
;1134;;超大陆:勇气;Supercontinent:Pluck;SUPL
|
||||
create-chronicles-the-endventure;1135;;Create Chronicles: The Endventure;;
|
||||
life-in-the-village-4;1136;;Life in the village 4;;LITV4
|
||||
real-island-2-modular-craft;1137;;真实空岛2:模块化之旅;Real Island 2 - Modular Craft;RI2
|
||||
;1138;;奥术咖啡馆;;
|
||||
unyielding;1139;;Unyielding;;
|
||||
;1140;;剑与王国;;SNK
|
||||
the-fungal-infection-coop;1141;;The Fungal Infection | Co-Op Base Defense;;
|
||||
mc-eternal-2;1142;;永恒的MC 2;MC Eternal 2;MCE2
|
||||
solo-leveling-reawakening;1143;;Solo Leveling - Reawakening;;
|
||||
create-ultimate-selection;1144;;Create: Ultimate Selection - QUESTS NOW!;;
|
||||
;1145;;元素觉醒;Elemental Awakening;
|
||||
endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE Horror Experience;;
|
||||
;1147;;锻造之旅;;
|
||||
picky;1148;;Picky;;
|
||||
resource-factory;1149;;资源工厂;Resource Factory;
|
||||
stacia-2-expert;1150;;Stacia 2 Expert;;S2E
|
||||
;1151;;田园乐事;;
|
||||
;1152;;世界之旅;Journey of the world;JOTW
|
||||
;1153;;清新优化;Fresh optimization;FRO
|
||||
;1154;;遗世之匠;Touhou:The Forgotten Swordsmith;
|
||||
;1155;;千禧:蝉鸣之时;;
|
||||
skyrealm-descent;1157;;Skyrealm: Descent;;
|
||||
;1158;;远征:以太利亚;Expedition: Ætheria;
|
||||
;1159;;快速优化;Rapid Optimization;RO
|
||||
liminal-industries;1160;;阈限工业;Liminal Industries;
|
||||
;1161;;史诗冒险;;EA
|
||||
;1162;;根自日常;ROOTINE;
|
||||
blue-archive-x-mineraft;1163;;我的世界x蔚蓝档案;BlueArchive x Minecraft;BACraft
|
||||
;1164;;Fresh & Smooth;;
|
||||
;1165;;机动重启工程:风之旅;The Caravan of Winds;TCW
|
||||
;1166;;Furry Fandom Club Boss Rush Official Offline;;FFCBR
|
||||
immersive-ultra;1167;;极致沉浸;Immersive Ultra;
|
||||
;1168;;Polymania;;
|
||||
survivearchive;1169;;SurviveArchive - A Blue Archive modpack;;
|
||||
beloong;1170;;化龍;BeLoong;
|
||||
;1171;;畅游自然;;SiF
|
||||
;1172;;退休工艺;Retirement Craft;RmC
|
||||
;1173;;随机枪械乱斗;NimShade's Random Gunfight;NRG
|
||||
;1174;;雪狐的基础整合;SF Integrated Modpack;SFI
|
||||
;1175;;硬核惊变;Hardcore Surprise;HS
|
||||
society-sunlit-valley;1176;;Society: Sunlit Valley;;
|
||||
ciscos-fantasy-medieval-rpg-dragonfyre;1177;;Cisco's Fantasy Medieval RPG [Dragonfyre];;
|
||||
all-the-mods-10-sky;1178;;All the Mods 10: To the Sky;;ATM10S
|
||||
;1179;;谐伴匠途;;
|
||||
the-cursed-wanderer-reborn;1180;;咒游诸界:重生;The Cursed Wanderer: Reborn;TCWR
|
||||
;1181;;Turbo Vulkan;;TuVk
|
||||
uls-zsurvival-craft;1182;;Z-Liminex;;
|
||||
;1183;;MITE 重生 3;MITE Reborn 3;MR
|
||||
reclamation-reclaim-the-world;1184;;Reclamation - Reclaim the World!;;
|
||||
;1185;;寰宇地球;Earthworld;EW
|
||||
pokehaan-craft-2;1186;;Pokehaan Craft 2;;
|
||||
project-nope-zone;1187;;Project Nope Zone;;PNZ
|
||||
all-the-simple-zero;1188;;一切简易;All the Simple;ATS
|
||||
;1189;;匠战妖域;Tinkers' Warfare Demon Realm;twdr
|
||||
;1190;;空想生存;;
|
||||
;1191;;东方起源鉴;Touhou Origins Annals;TOA
|
||||
realm-of-destiny;1192;;生命之境;Realm of Destiny;ROD
|
||||
;1193;;奇妙的世界;Special World;
|
||||
;1194;;毁灭;DESTRUCTION;DES
|
||||
pvzw;1195;;植物大战僵尸:世界;Plants VS Zombies : World;PVZW
|
||||
;1196;;农场物语Forge版;FarmingTales Forge;FT-WT
|
||||
new-century-adventure;1197;;新世纪之险;New Century Adventure;NCA
|
||||
repalmon;1198;;Re帕鲁梦;;RPLM
|
||||
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
@@ -39,4 +41,18 @@ public final class WenyanUtilsTest {
|
||||
assertYearToString("甲子", -2996);
|
||||
assertYearToString("庚子", 1000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHourToString() {
|
||||
|
||||
List<String> list = List.of(
|
||||
"子正", "丑初", "丑正", "寅初", "寅正", "卯初", "卯正", "辰初", "辰正", "巳初", "巳正", "午初",
|
||||
"午正", "未初", "未正", "申初", "申正", "酉初", "酉正", "戌初", "戌正", "亥初", "亥正", "子初"
|
||||
);
|
||||
for (int hour = 0; hour < list.size(); hour++) {
|
||||
StringBuilder builder = new StringBuilder(2);
|
||||
WenyanUtils.appendHour(builder, hour);
|
||||
assertEquals(list.get(hour), builder.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.util.SwingUtils;
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class Main {
|
||||
private static final int MINIMUM_JAVA_VERSION = 11;
|
||||
private static final int MINIMUM_JAVA_VERSION = 17;
|
||||
|
||||
private Main() {
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
boot.unsupported_java_version=HMCL requires Java 11 or later to run, but still supports launching games with Java 8. Please install the latest version of Java and try opening HMCL again.\nYou can keep your old version of Java. HMCL can detect and manage multiple Java installations, and will automatically select the appropriate Java version for your game.
|
||||
boot.unsupported_java_version=HMCL requires Java 17 or later to run, but still supports launching games with Java 6~16. Please install the latest version of Java and try opening HMCL again.\nYou can keep your old version of Java. HMCL can detect and manage multiple Java installations, and will automatically select the appropriate Java version for your game.
|
||||
@@ -16,4 +16,4 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
boot.unsupported_java_version=HMCL 需要 Java 11 或更高版本才能執行,但依然支援使用 Java 8 啟動遊戲。請安裝最新版本的 Java 再嘗試開啟 HMCL。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
|
||||
boot.unsupported_java_version=HMCL 需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。請安裝最新版本的 Java 再嘗試開啟 HMCL。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
|
||||
@@ -16,4 +16,4 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
boot.unsupported_java_version=HMCL 需要 Java 11 或更高版本才能运行,但依然支持使用 Java 8 启动游戏。请安装最新版本的 Java 再尝试启动 HMCL。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
boot.unsupported_java_version=HMCL 需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。请安装最新版本的 Java 再尝试启动 HMCL。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
@@ -3,8 +3,8 @@ plugins {
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
sourceCompatibility = "11"
|
||||
targetCompatibility = "11"
|
||||
sourceCompatibility = "17"
|
||||
targetCompatibility = "17"
|
||||
}
|
||||
|
||||
tasks.compileJava {
|
||||
|
||||
@@ -19,13 +19,13 @@ package org.jackhuang.hmcl.auth.offline;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelReader;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
@@ -90,7 +90,7 @@ public final class Texture {
|
||||
digest.update(buf, 0, pos);
|
||||
}
|
||||
|
||||
return Hex.encodeHex(digest.digest());
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
|
||||
private static void putInt(byte[] array, int offset, int x) {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -33,11 +32,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -250,7 +245,7 @@ public class FileDownloadTask extends FetchTask<Void> {
|
||||
|
||||
// Integrity check
|
||||
if (checksum != null) {
|
||||
String actualChecksum = Hex.encodeHex(digest.digest());
|
||||
String actualChecksum = HexFormat.of().formatHex(digest.digest());
|
||||
if (!checksum.equalsIgnoreCase(actualChecksum)) {
|
||||
throw new ChecksumMismatchException(algorithm, checksum, actualChecksum);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -74,15 +75,15 @@ public final class DigestUtils {
|
||||
}
|
||||
|
||||
public static String digestToString(String algorithm, byte[] data) throws IOException {
|
||||
return Hex.encodeHex(digest(algorithm, data));
|
||||
return HexFormat.of().formatHex(digest(algorithm, data));
|
||||
}
|
||||
|
||||
public static String digestToString(String algorithm, Path path) throws IOException {
|
||||
return Hex.encodeHex(digest(algorithm, path));
|
||||
return HexFormat.of().formatHex(digest(algorithm, path));
|
||||
}
|
||||
|
||||
public static String digestToString(String algorithm, InputStream data) throws IOException {
|
||||
return Hex.encodeHex(digest(algorithm, data));
|
||||
return HexFormat.of().formatHex(digest(algorithm, data));
|
||||
}
|
||||
|
||||
private static final ThreadLocal<byte[]> threadLocalBuffer = ThreadLocal.withInitial(() -> new byte[STREAM_BUFFER_LENGTH]);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class Hex {
|
||||
|
||||
private static final byte[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
|
||||
private static int toDigit(char ch) throws IOException {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return ch - '0';
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'f') {
|
||||
return ch - 'a' + 10;
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'F') {
|
||||
return ch - 'A' + 10;
|
||||
}
|
||||
|
||||
throw new IOException("Illegal hexadecimal character " + ch);
|
||||
}
|
||||
|
||||
public static byte[] decodeHex(String str) throws IOException {
|
||||
if ((str.length() & 0x1) != 0)
|
||||
throw new IOException("Odd number of characters.");
|
||||
|
||||
int len = str.length() >> 1;
|
||||
byte[] out = new byte[len];
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
int j = i << 1;
|
||||
int f = (toDigit(str.charAt(j)) << 4) | toDigit(str.charAt(j + 1));
|
||||
|
||||
out[i] = (byte) f;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static String encodeHex(byte[] data) {
|
||||
int l = data.length;
|
||||
byte[] out = new byte[l << 1];
|
||||
|
||||
for (int i = 0; i < l; i++) {
|
||||
byte b = data[i];
|
||||
|
||||
int j = i << 1;
|
||||
out[j] = DIGITS_LOWER[((0xF0 & b) >>> 4)];
|
||||
out[j + 1] = DIGITS_LOWER[(0xF & b)];
|
||||
}
|
||||
return new String(out, 0, 0, out.length);
|
||||
}
|
||||
|
||||
private Hex() {
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,10 @@ public final class Lang {
|
||||
|
||||
/**
|
||||
* Construct a mutable map by given key-value pairs.
|
||||
*
|
||||
* @param pairs entries in the new map
|
||||
* @param <K> the type of keys
|
||||
* @param <V> the type of values
|
||||
* @param <K> the type of keys
|
||||
* @param <V> the type of values
|
||||
* @return the map which contains data in {@code pairs}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
@@ -60,9 +61,10 @@ public final class Lang {
|
||||
|
||||
/**
|
||||
* Construct a mutable map by given key-value pairs.
|
||||
*
|
||||
* @param pairs entries in the new map
|
||||
* @param <K> the type of keys
|
||||
* @param <V> the type of values
|
||||
* @param <K> the type of keys
|
||||
* @param <V> the type of values
|
||||
* @return the map which contains data in {@code pairs}.
|
||||
*/
|
||||
public static <K, V> Map<K, V> mapOf(Iterable<Pair<K, V>> pairs) {
|
||||
@@ -122,9 +124,10 @@ public final class Lang {
|
||||
|
||||
/**
|
||||
* Cast {@code obj} to V dynamically.
|
||||
* @param obj the object reference to be cast.
|
||||
*
|
||||
* @param obj the object reference to be cast.
|
||||
* @param clazz the class reference of {@code V}.
|
||||
* @param <V> the type that {@code obj} is being cast to.
|
||||
* @param <V> the type that {@code obj} is being cast to.
|
||||
* @return {@code obj} in the type of {@code V}.
|
||||
*/
|
||||
public static <V> Optional<V> tryCast(Object obj, Class<V> clazz) {
|
||||
@@ -154,8 +157,8 @@ public final class Lang {
|
||||
/**
|
||||
* Join two collections into one list.
|
||||
*
|
||||
* @param a one collection, to be joined.
|
||||
* @param b another collection to be joined.
|
||||
* @param a one collection, to be joined.
|
||||
* @param b another collection to be joined.
|
||||
* @param <T> the super type of elements in {@code a} and {@code b}
|
||||
* @return the joint collection
|
||||
*/
|
||||
@@ -172,6 +175,16 @@ public final class Lang {
|
||||
return list == null ? null : list.isEmpty() ? null : new ArrayList<>(list);
|
||||
}
|
||||
|
||||
public static <T> int indexWhere(List<T> list, Predicate<T> predicate) {
|
||||
int idx = 0;
|
||||
for (T value : list) {
|
||||
if (predicate.test(value))
|
||||
return idx;
|
||||
idx++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void executeDelayed(Runnable runnable, TimeUnit timeUnit, long timeout, boolean isDaemon) {
|
||||
thread(() -> {
|
||||
try {
|
||||
@@ -185,6 +198,7 @@ public final class Lang {
|
||||
|
||||
/**
|
||||
* Start a thread invoking {@code runnable} immediately.
|
||||
*
|
||||
* @param runnable code to run.
|
||||
* @return the reference of the started thread
|
||||
*/
|
||||
@@ -194,8 +208,9 @@ public final class Lang {
|
||||
|
||||
/**
|
||||
* Start a thread invoking {@code runnable} immediately.
|
||||
*
|
||||
* @param runnable code to run
|
||||
* @param name the name of thread
|
||||
* @param name the name of thread
|
||||
* @return the reference of the started thread
|
||||
*/
|
||||
public static Thread thread(Runnable runnable, String name) {
|
||||
@@ -204,8 +219,9 @@ public final class Lang {
|
||||
|
||||
/**
|
||||
* Start a thread invoking {@code runnable} immediately.
|
||||
*
|
||||
* @param runnable code to run
|
||||
* @param name the name of thread
|
||||
* @param name the name of thread
|
||||
* @param isDaemon true if thread will be terminated when only daemon threads are running.
|
||||
* @return the reference of the started thread
|
||||
*/
|
||||
@@ -258,7 +274,8 @@ public final class Lang {
|
||||
|
||||
/**
|
||||
* Find the first non-null reference in given list.
|
||||
* @param t nullable references list.
|
||||
*
|
||||
* @param t nullable references list.
|
||||
* @param <T> the type of nullable references
|
||||
* @return the first non-null reference.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.gson;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/// Marks a class as being serialized/deserialized by Gson.
|
||||
/// This annotation is not retained at runtime and only serves as a reminder to developers not to change field names arbitrarily.
|
||||
///
|
||||
/// TODO: Add this annotation to all classes in the HMCL project that need to be serialized/deserialized.
|
||||
///
|
||||
/// @author Glavo
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface JsonSerializable {
|
||||
}
|
||||
@@ -40,7 +40,10 @@ public final class PaintAdapter extends TypeAdapter<Paint> {
|
||||
int green = (int) Math.round(color.getGreen() * 255.);
|
||||
int blue = (int) Math.round(color.getBlue() * 255.);
|
||||
int opacity = (int) Math.round(color.getOpacity() * 255.);
|
||||
out.value(String.format("#%02x%02x%02x%02x", red, green, blue, opacity));
|
||||
if (opacity < 255)
|
||||
out.value(String.format("#%02x%02x%02x%02x", red, green, blue, opacity));
|
||||
else
|
||||
out.value(String.format("#%02x%02x%02x", red, green, blue));
|
||||
} else if (value instanceof LinearGradient
|
||||
|| value instanceof RadialGradient) {
|
||||
out.value(value.toString());
|
||||
|
||||
+36
-32
@@ -31,8 +31,8 @@ import java.util.ResourceBundle;
|
||||
/// - For all Chinese locales, `zh-CN` is always added to the candidate list. If `zh-Hans` already exists in the candidate list,
|
||||
/// `zh-CN` is inserted before `zh`; otherwise, it is inserted after `zh`.
|
||||
/// - For all Traditional Chinese locales, `zh-TW` is always added to the candidate list (before `zh`).
|
||||
/// - For all Chinese variants (such as `lzh`, `cmn`, `yue`, etc.), a candidate list with the language code replaced by `zh`
|
||||
/// is added to the end of the candidate list.
|
||||
/// - For all [supported][LocaleUtils#toISO1Language(String)] ISO 639-3 language code (such as `eng`, `zho`, `lzh`, etc.),
|
||||
/// a candidate list with the language code replaced by the ISO 639-1 (Macro)language code is added to the end of the candidate list.
|
||||
///
|
||||
/// @author Glavo
|
||||
public class DefaultResourceBundleControl extends ResourceBundle.Control {
|
||||
@@ -52,53 +52,57 @@ public class DefaultResourceBundleControl extends ResourceBundle.Control {
|
||||
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
|
||||
if (locale.getLanguage().isEmpty())
|
||||
return getCandidateLocales(baseName, Locale.ENGLISH);
|
||||
|
||||
if (LocaleUtils.isChinese(locale)) {
|
||||
String language = locale.getLanguage();
|
||||
else if (LocaleUtils.isChinese(locale)) {
|
||||
String script = locale.getScript();
|
||||
|
||||
if (script.isEmpty()) {
|
||||
script = LocaleUtils.getScript(locale);
|
||||
locale = new Locale.Builder()
|
||||
.setLocale(locale)
|
||||
.setScript(script)
|
||||
.build();
|
||||
if (!script.isEmpty())
|
||||
return getCandidateLocales(baseName, new Locale.Builder()
|
||||
.setLocale(locale)
|
||||
.setScript(script)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
List<Locale> locales = super.getCandidateLocales("", locale);
|
||||
String language = locale.getLanguage();
|
||||
|
||||
if (language.equals("zh")) {
|
||||
if (locales.contains(LocaleUtils.LOCALE_ZH_HANT) && !locales.contains(Locale.TRADITIONAL_CHINESE)) {
|
||||
locales = ensureEditable(locales);
|
||||
int chineseIdx = locales.indexOf(Locale.CHINESE);
|
||||
if (chineseIdx >= 0)
|
||||
locales.add(chineseIdx, Locale.TRADITIONAL_CHINESE);
|
||||
}
|
||||
List<Locale> locales = super.getCandidateLocales(baseName, locale);
|
||||
|
||||
if (!locales.contains(Locale.SIMPLIFIED_CHINESE)) {
|
||||
int chineseIdx = locales.indexOf(Locale.CHINESE);
|
||||
// Is ISO 639-3 language tag
|
||||
if (language.length() == 3) {
|
||||
String iso1 = LocaleUtils.toISO1Language(locale.getLanguage());
|
||||
|
||||
if (chineseIdx >= 0) {
|
||||
locales = ensureEditable(locales);
|
||||
if (locales.contains(LocaleUtils.LOCALE_ZH_HANS))
|
||||
locales.add(chineseIdx, Locale.SIMPLIFIED_CHINESE);
|
||||
else
|
||||
locales.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (iso1.length() == 2) {
|
||||
locales = ensureEditable(locales);
|
||||
locales.removeIf(it -> !it.getLanguage().equals(language));
|
||||
|
||||
locales.addAll(getCandidateLocales("", new Locale.Builder()
|
||||
locales.addAll(getCandidateLocales(baseName, new Locale.Builder()
|
||||
.setLocale(locale)
|
||||
.setLanguage("zh")
|
||||
.setLanguage(iso1)
|
||||
.build()));
|
||||
}
|
||||
} else if (language.equals("zh")) {
|
||||
if (locales.contains(LocaleUtils.LOCALE_ZH_HANT) && !locales.contains(Locale.TRADITIONAL_CHINESE)) {
|
||||
locales = ensureEditable(locales);
|
||||
int chineseIdx = locales.indexOf(Locale.CHINESE);
|
||||
if (chineseIdx >= 0)
|
||||
locales.add(chineseIdx, Locale.TRADITIONAL_CHINESE);
|
||||
}
|
||||
|
||||
return locales;
|
||||
if (!locales.contains(Locale.SIMPLIFIED_CHINESE)) {
|
||||
int chineseIdx = locales.indexOf(Locale.CHINESE);
|
||||
|
||||
if (chineseIdx >= 0) {
|
||||
locales = ensureEditable(locales);
|
||||
if (locales.contains(LocaleUtils.LOCALE_ZH_HANS))
|
||||
locales.add(chineseIdx, Locale.SIMPLIFIED_CHINESE);
|
||||
else
|
||||
locales.add(chineseIdx + 1, Locale.SIMPLIFIED_CHINESE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.getCandidateLocales(baseName, locale);
|
||||
return locales;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,20 @@ public final class LocaleUtils {
|
||||
: locale.stripExtensions().toLanguageTag();
|
||||
}
|
||||
|
||||
public static boolean isISO1Language(String language) {
|
||||
return language.length() == 2;
|
||||
}
|
||||
|
||||
public static boolean isISO3Language(String language) {
|
||||
return language.length() == 3;
|
||||
}
|
||||
|
||||
public static @NotNull String getISO1Language(Locale locale) {
|
||||
String language = locale.getLanguage();
|
||||
if (language.isEmpty()) return "en";
|
||||
return isISO3Language(language) ? toISO1Language(language) : language;
|
||||
}
|
||||
|
||||
/// Get the script of the locale. If the script is empty and the language is Chinese,
|
||||
/// the script will be inferred based on the language, the region and the variant.
|
||||
public static @NotNull String getScript(Locale locale) {
|
||||
@@ -164,20 +178,29 @@ public final class LocaleUtils {
|
||||
|
||||
// ---
|
||||
|
||||
/// Try to convert ISO 639-3 language codes to ISO 639-1 language codes.
|
||||
public static String toISO1Language(String languageTag) {
|
||||
return switch (languageTag) {
|
||||
case "eng" -> "en";
|
||||
case "spa" -> "es";
|
||||
case "jpa" -> "ja";
|
||||
case "rus" -> "ru";
|
||||
case "ukr" -> "uk";
|
||||
case "zho", "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
|
||||
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue" -> "zh";
|
||||
default -> languageTag;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isEnglish(Locale locale) {
|
||||
return locale.getLanguage().equals("en") || locale.getLanguage().isEmpty();
|
||||
return "en".equals(getISO1Language(locale));
|
||||
}
|
||||
|
||||
public static final Set<String> CHINESE_TRADITIONAL_REGIONS = Set.of("TW", "HK", "MO");
|
||||
public static final Set<String> CHINESE_LATN_VARIANTS = Set.of("pinyin", "wadegile", "tongyong");
|
||||
public static final Set<String> CHINESE_LANGUAGES = Set.of(
|
||||
"zh",
|
||||
"zho", "cmn", "lzh", "cdo", "cjy", "cpx", "czh",
|
||||
"gan", "hak", "hsn", "mnp", "nan", "wuu", "yue"
|
||||
);
|
||||
|
||||
public static boolean isChinese(Locale locale) {
|
||||
return CHINESE_LANGUAGES.contains(locale.getLanguage());
|
||||
return "zh".equals(getISO1Language(locale));
|
||||
}
|
||||
|
||||
private LocaleUtils() {
|
||||
|
||||
+4
-8
@@ -20,13 +20,11 @@ package org.jackhuang.hmcl.util.platform.hardware;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class HardwareDetector {
|
||||
private static final boolean USE_FAST_FETCH = "true".equalsIgnoreCase(System.getProperty("hmcl.hardware.fastfetch", "true"));
|
||||
|
||||
@@ -40,9 +38,8 @@ public class HardwareDetector {
|
||||
|
||||
public long getTotalMemorySize() {
|
||||
try {
|
||||
OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean();
|
||||
if (bean instanceof com.sun.management.OperatingSystemMXBean) {
|
||||
return ((com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean()).getTotalPhysicalMemorySize();
|
||||
if (ManagementFactory.getOperatingSystemMXBean() instanceof com.sun.management.OperatingSystemMXBean bean) {
|
||||
return bean.getTotalMemorySize();
|
||||
}
|
||||
} catch (NoClassDefFoundError ignored) {
|
||||
}
|
||||
@@ -52,9 +49,8 @@ public class HardwareDetector {
|
||||
|
||||
public long getFreeMemorySize() {
|
||||
try {
|
||||
OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean();
|
||||
if (bean instanceof com.sun.management.OperatingSystemMXBean) {
|
||||
return ((com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean()).getFreePhysicalMemorySize();
|
||||
if (ManagementFactory.getOperatingSystemMXBean() instanceof com.sun.management.OperatingSystemMXBean bean) {
|
||||
return bean.getFreeMemorySize();
|
||||
}
|
||||
} catch (NoClassDefFoundError ignored) {
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.regex.Pattern;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public abstract class GameVersionNumber implements Comparable<GameVersionNumber> {
|
||||
public abstract sealed class GameVersionNumber implements Comparable<GameVersionNumber> {
|
||||
|
||||
public static String[] getDefaultGameVersions() {
|
||||
return Versions.DEFAULT_GAME_VERSIONS;
|
||||
@@ -103,8 +103,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
if (this instanceof Special)
|
||||
return true;
|
||||
|
||||
if (this instanceof Snapshot) {
|
||||
Snapshot snapshot = (Snapshot) this;
|
||||
if (this instanceof Snapshot snapshot) {
|
||||
return snapshot.intValue == Snapshot.toInt(15, 14, 'a');
|
||||
}
|
||||
|
||||
@@ -131,6 +130,39 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
return compareToImpl(other);
|
||||
}
|
||||
|
||||
/// @see #isAtLeast(String, String, boolean)
|
||||
public boolean isAtLeast(@NotNull String releaseVersion, @NotNull String snapshotVersion) {
|
||||
return isAtLeast(releaseVersion, snapshotVersion, false);
|
||||
}
|
||||
|
||||
/// When comparing between Release Version and Snapshot Version, it is necessary to load `/assets/game/versions.txt` and perform a lookup, which is less efficient.
|
||||
/// Therefore, when checking whether a version contains a certain feature, you should use this method and provide both the first release version and the exact snapshot version that introduced the feature,
|
||||
/// so that the comparison can be performed quickly without a lookup.
|
||||
///
|
||||
/// For example, the datapack feature was introduced in Minecraft 1.13, and more specifically in snapshot `17w43a`.
|
||||
/// So you can test whether a game version supports datapacks like this:
|
||||
///
|
||||
/// ```java
|
||||
/// GameVersionNumber.asVersion("...").isAtLeast("1.13", "17w43a");
|
||||
/// ```
|
||||
///
|
||||
/// @param strictReleaseVersion When `strictReleaseVersion` is `false`, `releaseVersion` is considered less than
|
||||
/// its corresponding pre/rc versions.
|
||||
public boolean isAtLeast(@NotNull String releaseVersion, @NotNull String snapshotVersion, boolean strictReleaseVersion) {
|
||||
if (this instanceof Release self) {
|
||||
Release other;
|
||||
if (strictReleaseVersion) {
|
||||
other = Release.parse(releaseVersion);
|
||||
} else {
|
||||
other = Release.parseSimple(releaseVersion);
|
||||
}
|
||||
|
||||
return self.compareToRelease(other) >= 0;
|
||||
} else {
|
||||
return this.compareTo(Snapshot.parse(snapshotVersion)) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
@@ -201,9 +233,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Old)) return false;
|
||||
Old other = (Old) o;
|
||||
return type == other.type && this.versionNumber.compareTo(other.versionNumber) == 0;
|
||||
return o instanceof Old other && type == other.type && this.versionNumber.compareTo(other.versionNumber) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -256,6 +286,52 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
return new Release(value, 1, minor, patch, eaType, eaVersion);
|
||||
}
|
||||
|
||||
private static int getNumberLength(String value, int offset) {
|
||||
int current = offset;
|
||||
while (current < value.length()) {
|
||||
char ch = value.charAt(current);
|
||||
if (ch < '0' || ch > '9')
|
||||
break;
|
||||
|
||||
current++;
|
||||
}
|
||||
|
||||
return current - offset;
|
||||
}
|
||||
|
||||
/// Quickly parses a simple format (`1\.[0-9]+(\.[0-9]+)?`) release version.
|
||||
/// The returned [#eaType] will be set to [#TYPE_UNKNOWN], meaning it will be less than all pre/rc and official versions of this version.
|
||||
///
|
||||
/// @see GameVersionNumber#isAtLeast(String, String)
|
||||
static Release parseSimple(String value) {
|
||||
if (!value.startsWith("1."))
|
||||
throw new IllegalArgumentException(value);
|
||||
|
||||
final int minorOffset = 2;
|
||||
|
||||
int minorLength = getNumberLength(value, minorOffset);
|
||||
if (minorLength == 0)
|
||||
throw new IllegalArgumentException(value);
|
||||
|
||||
try {
|
||||
int minor = Integer.parseInt(value.substring(minorOffset, minorOffset + minorLength));
|
||||
int patch = 0;
|
||||
|
||||
if (minorOffset + minorLength < value.length()) {
|
||||
int patchOffset = minorOffset + minorLength + 1;
|
||||
|
||||
if (patchOffset >= value.length() || value.charAt(patchOffset - 1) != '.')
|
||||
throw new IllegalArgumentException(value);
|
||||
|
||||
patch = Integer.parseInt(value.substring(patchOffset));
|
||||
}
|
||||
|
||||
return new Release(value, 1, minor, patch, TYPE_UNKNOWN, VersionNumber.ZERO);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException(value);
|
||||
}
|
||||
}
|
||||
|
||||
private final int major;
|
||||
private final int minor;
|
||||
private final int patch;
|
||||
@@ -280,24 +356,20 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
|
||||
int compareToRelease(Release other) {
|
||||
int c = Integer.compare(this.major, other.major);
|
||||
if (c != 0) {
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
c = Integer.compare(this.minor, other.minor);
|
||||
if (c != 0) {
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
c = Integer.compare(this.patch, other.patch);
|
||||
if (c != 0) {
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
c = Integer.compare(this.eaType, other.eaType);
|
||||
if (c != 0) {
|
||||
if (c != 0)
|
||||
return c;
|
||||
}
|
||||
|
||||
return this.eaVersion.compareTo(other.eaVersion);
|
||||
}
|
||||
@@ -356,9 +428,12 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Release other = (Release) o;
|
||||
return major == other.major && minor == other.minor && patch == other.patch && eaType == other.eaType && eaVersion.equals(other.eaVersion);
|
||||
return o instanceof Release other
|
||||
&& major == other.major
|
||||
&& minor == other.minor
|
||||
&& patch == other.patch
|
||||
&& eaType == other.eaType
|
||||
&& eaVersion.equals(other.eaVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,9 +503,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Snapshot other = (Snapshot) o;
|
||||
return this.intValue == other.intValue;
|
||||
return o instanceof Snapshot other && this.intValue == other.intValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -534,9 +607,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Special other = (Special) o;
|
||||
return Objects.equals(this.value, other.value);
|
||||
return o instanceof Special other && this.value.equals(other.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,8 +637,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
if (currentRelease == null)
|
||||
currentRelease = (Release) version;
|
||||
|
||||
if (version instanceof Snapshot) {
|
||||
Snapshot snapshot = (Snapshot) version;
|
||||
if (version instanceof Snapshot snapshot) {
|
||||
snapshots.add(snapshot);
|
||||
snapshotPrev.add(currentRelease);
|
||||
} else if (version instanceof Release) {
|
||||
@@ -576,8 +646,7 @@ public abstract class GameVersionNumber implements Comparable<GameVersionNumber>
|
||||
if (currentRelease.eaType == Release.TYPE_GA) {
|
||||
defaultGameVersions.addFirst(currentRelease.value);
|
||||
}
|
||||
} else if (version instanceof Special) {
|
||||
Special special = (Special) version;
|
||||
} else if (version instanceof Special special) {
|
||||
special.prev = prev;
|
||||
SPECIALS.put(special.value, special);
|
||||
} else
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.ByteArray.*;
|
||||
@@ -123,8 +123,8 @@ public final class ByteArrayTest {
|
||||
|
||||
private static byte[] byteArray(String string) {
|
||||
try {
|
||||
return Hex.decodeHex(string);
|
||||
} catch (IOException e) {
|
||||
return HexFormat.of().parseHex(string);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2023 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class HexTest {
|
||||
|
||||
@Test
|
||||
public void testDecodeHex() throws IOException {
|
||||
assertArrayEquals(new byte[0], Hex.decodeHex(""));
|
||||
assertArrayEquals(new byte[1], Hex.decodeHex("00"));
|
||||
assertArrayEquals(new byte[4], Hex.decodeHex("00000000"));
|
||||
|
||||
assertArrayEquals(
|
||||
new byte[]{0x10, (byte) 0xa3, (byte) 0xd1, (byte) 0xff},
|
||||
Hex.decodeHex("10a3D1Ff")
|
||||
);
|
||||
|
||||
assertThrows(IOException.class, () -> Hex.decodeHex("1"));
|
||||
assertThrows(IOException.class, () -> Hex.decodeHex("1a1"));
|
||||
assertThrows(IOException.class, () -> Hex.decodeHex("1g"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncodeHex() {
|
||||
assertEquals("", Hex.encodeHex(new byte[0]));
|
||||
assertEquals("00", Hex.encodeHex(new byte[1]));
|
||||
assertEquals("00000000", Hex.encodeHex(new byte[4]));
|
||||
assertEquals("10a3d1ff", Hex.encodeHex(new byte[]{0x10, (byte) 0xa3, (byte) 0xd1, (byte) 0xff}));
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,23 @@ public final class LocaleUtilsTest {
|
||||
|
||||
assertCandidateLocales("ja", List.of("ja", "und"));
|
||||
assertCandidateLocales("ja-JP", List.of("ja-JP", "ja", "und"));
|
||||
assertCandidateLocales("jpa", List.of("jpa", "ja", "und"));
|
||||
assertCandidateLocales("jpa-JP", List.of("jpa-JP", "jpa", "ja-JP", "ja", "und"));
|
||||
|
||||
assertCandidateLocales("en", List.of("en", "und"));
|
||||
assertCandidateLocales("en-US", List.of("en-US", "en", "und"));
|
||||
assertCandidateLocales("eng", List.of("eng", "en", "und"));
|
||||
assertCandidateLocales("eng-US", List.of("eng-US", "eng", "en-US", "en", "und"));
|
||||
|
||||
assertCandidateLocales("es", List.of("es", "und"));
|
||||
assertCandidateLocales("spa", List.of("spa", "es", "und"));
|
||||
|
||||
assertCandidateLocales("ru", List.of("ru", "und"));
|
||||
assertCandidateLocales("rus", List.of("rus", "ru", "und"));
|
||||
|
||||
assertCandidateLocales("uk", List.of("uk", "und"));
|
||||
assertCandidateLocales("ukr", List.of("ukr", "uk", "und"));
|
||||
|
||||
assertCandidateLocales("und", List.of("en", "und"));
|
||||
}
|
||||
|
||||
|
||||
+57
-7
@@ -26,6 +26,7 @@ import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.versioning.GameVersionNumber.asGameVersion;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
@@ -66,8 +67,8 @@ public final class GameVersionNumberTest {
|
||||
}
|
||||
|
||||
private static void assertGameVersionEquals(String version1, String version2) {
|
||||
assertEquals(0, GameVersionNumber.asGameVersion(version1).compareTo(version2), errorMessage(version1, version2));
|
||||
assertEquals(GameVersionNumber.asGameVersion(version1), GameVersionNumber.asGameVersion(version2), errorMessage(version1, version2));
|
||||
assertEquals(0, asGameVersion(version1).compareTo(version2), errorMessage(version1, version2));
|
||||
assertEquals(asGameVersion(version1), asGameVersion(version2), errorMessage(version1, version2));
|
||||
}
|
||||
|
||||
private static String toString(GameVersionNumber gameVersionNumber) {
|
||||
@@ -76,12 +77,12 @@ public final class GameVersionNumberTest {
|
||||
|
||||
private static void assertOrder(String... versions) {
|
||||
for (int i = 0; i < versions.length - 1; i++) {
|
||||
GameVersionNumber version1 = GameVersionNumber.asGameVersion(versions[i]);
|
||||
GameVersionNumber version1 = asGameVersion(versions[i]);
|
||||
|
||||
assertGameVersionEquals(versions[i]);
|
||||
|
||||
for (int j = i + 1; j < versions.length; j++) {
|
||||
GameVersionNumber version2 = GameVersionNumber.asGameVersion(versions[j]);
|
||||
GameVersionNumber version2 = asGameVersion(versions[j]);
|
||||
|
||||
assertEquals(-1, version1.compareTo(version2), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2)));
|
||||
assertEquals(1, version2.compareTo(version1), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2)));
|
||||
@@ -92,7 +93,7 @@ public final class GameVersionNumberTest {
|
||||
}
|
||||
|
||||
private void assertOldVersion(String oldVersion, GameVersionNumber.Type type, String versionNumber) {
|
||||
GameVersionNumber version = GameVersionNumber.asGameVersion(oldVersion);
|
||||
GameVersionNumber version = asGameVersion(oldVersion);
|
||||
assertInstanceOf(GameVersionNumber.Old.class, version);
|
||||
GameVersionNumber.Old old = (GameVersionNumber.Old) version;
|
||||
assertSame(type, old.type);
|
||||
@@ -100,7 +101,7 @@ public final class GameVersionNumberTest {
|
||||
}
|
||||
|
||||
private static boolean isAprilFools(String version) {
|
||||
return GameVersionNumber.asGameVersion(version).isAprilFools();
|
||||
return asGameVersion(version).isAprilFools();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,10 +143,36 @@ public final class GameVersionNumberTest {
|
||||
public void testParseNew() {
|
||||
List<String> versions = readVersions();
|
||||
for (String version : versions) {
|
||||
assertFalse(GameVersionNumber.asGameVersion(version) instanceof GameVersionNumber.Old, "version=" + version);
|
||||
assertFalse(asGameVersion(version) instanceof GameVersionNumber.Old, "version=" + version);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertSimpleReleaseVersion(String simpleReleaseVersion, int minor, int patch) {
|
||||
GameVersionNumber.Release release = GameVersionNumber.Release.parseSimple(simpleReleaseVersion);
|
||||
assertAll("Assert Simple Release Version " + simpleReleaseVersion,
|
||||
() -> assertEquals(1, release.getMajor()),
|
||||
() -> assertEquals(minor, release.getMinor()),
|
||||
() -> assertEquals(patch, release.getPatch()),
|
||||
() -> assertEquals(GameVersionNumber.Release.TYPE_UNKNOWN, release.getEaType()),
|
||||
() -> assertEquals(VersionNumber.ZERO, release.getEaVersion())
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseSimpleRelease() {
|
||||
assertSimpleReleaseVersion("1.0", 0, 0);
|
||||
assertSimpleReleaseVersion("1.13", 13, 0);
|
||||
assertSimpleReleaseVersion("1.21.8", 21, 8);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("2.0"));
|
||||
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1..0"));
|
||||
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0."));
|
||||
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.a"));
|
||||
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.1a"));
|
||||
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0a"));
|
||||
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0.0.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareRelease() {
|
||||
assertGameVersionEquals("0.0");
|
||||
@@ -281,4 +308,27 @@ public final class GameVersionNumberTest {
|
||||
"1.1"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isAtLeast() {
|
||||
assertTrue(asGameVersion("1.13").isAtLeast("1.13", "17w43a"));
|
||||
assertTrue(asGameVersion("1.13.1").isAtLeast("1.13", "17w43a"));
|
||||
assertTrue(asGameVersion("1.14").isAtLeast("1.13", "17w43a"));
|
||||
assertTrue(asGameVersion("1.13-rc1").isAtLeast("1.13", "17w43a"));
|
||||
assertTrue(asGameVersion("1.13-pre1").isAtLeast("1.13", "17w43a"));
|
||||
assertTrue(asGameVersion("17w43a").isAtLeast("1.13", "17w43a"));
|
||||
assertTrue(asGameVersion("17w43b").isAtLeast("1.13", "17w43a"));
|
||||
assertTrue(asGameVersion("17w45a").isAtLeast("1.13", "17w43a"));
|
||||
|
||||
assertFalse(asGameVersion("17w31a").isAtLeast("1.13", "17w43a"));
|
||||
assertFalse(asGameVersion("1.12").isAtLeast("1.13", "17w43a"));
|
||||
assertFalse(asGameVersion("1.12.2").isAtLeast("1.13", "17w43a"));
|
||||
assertFalse(asGameVersion("1.12.2-pre1").isAtLeast("1.13", "17w43a"));
|
||||
assertFalse(asGameVersion("rd-132211").isAtLeast("1.13", "17w43a"));
|
||||
assertFalse(asGameVersion("a1.0.6").isAtLeast("1.13", "17w43a"));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> asGameVersion("1.13").isAtLeast("17w43a", "17w43a"));
|
||||
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "1.13"));
|
||||
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime"));
|
||||
}
|
||||
}
|
||||
@@ -97,38 +97,46 @@ public record Document(DocumentFileTree directory,
|
||||
|
||||
String macroName = matcher.group("name");
|
||||
String endLine = "<!-- #END " + macroName + " -->";
|
||||
var properties = new LinkedHashMap<String, List<String>>();
|
||||
var lines = new ArrayList<String>();
|
||||
while (true) {
|
||||
line = reader.readLine();
|
||||
|
||||
if (line == null)
|
||||
throw new IOException("Missing end line for macro: " + macroName);
|
||||
else if (line.startsWith("<!-- #END")) {
|
||||
if (line.equals(endLine)) {
|
||||
break;
|
||||
} else {
|
||||
throw new IOException("Invalid macro end line: " + line);
|
||||
}
|
||||
} else {
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
var properties = new LinkedHashMap<String, List<String>>();
|
||||
int propertiesCount = 0;
|
||||
|
||||
// Handle properties
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!line.startsWith("<!-- #") || line.equals(endLine))
|
||||
for (String macroBodyLine : lines) {
|
||||
if (!macroBodyLine.startsWith("<!-- #"))
|
||||
break;
|
||||
|
||||
Matcher propertyMatcher = MACRO_PROPERTY_LINE.matcher(line);
|
||||
Matcher propertyMatcher = MACRO_PROPERTY_LINE.matcher(macroBodyLine);
|
||||
if (propertyMatcher.matches()) {
|
||||
String propertyName = propertyMatcher.group("name");
|
||||
String propertyValue = parsePropertyValue(propertyMatcher.group("value"));
|
||||
|
||||
properties.computeIfAbsent(propertyName, k -> new ArrayList<>(1))
|
||||
.add(propertyValue);
|
||||
propertiesCount++;
|
||||
} else {
|
||||
throw new IOException("Invalid macro property line: " + line);
|
||||
throw new IOException("Invalid macro property line: " + macroBodyLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle lines
|
||||
if (line != null && !line.equals(endLine)) {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("<!-- #"))
|
||||
break;
|
||||
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (line == null || !line.equals(endLine))
|
||||
throw new IOException("Invalid macro end line: " + line);
|
||||
if (propertiesCount > 0)
|
||||
lines.subList(0, propertiesCount).clear();
|
||||
|
||||
items.add(new MacroBlock(macroName,
|
||||
Collections.unmodifiableMap(properties),
|
||||
|
||||
@@ -17,14 +17,64 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.docs;
|
||||
|
||||
import javax.print.Doc;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/// Macro processor for automatically updating documentation.
|
||||
///
|
||||
/// Users can use the macro processor in `.md` documents within the `docs` folder and its subfolders.
|
||||
/// The parts to be processed should be wrapped with `<!-- #BEGIN MACRO_NAME -->` and `<!-- #END MACRO_NAME -->` lines.
|
||||
///
|
||||
/// For example, if you create a document `FOO.md` and translate it into Simplified Chinese, Traditional Chinese, and Japanese,
|
||||
/// you can add the following content in these files to create links to other language versions:
|
||||
///
|
||||
/// ```markdown
|
||||
/// <!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
/// <!-- #END LANGUAGE_SWITCHER -->
|
||||
/// ```
|
||||
///
|
||||
/// After running `./gradlew updateDocuments`, the macro processor will automatically update the content between these two lines:
|
||||
///
|
||||
/// ```
|
||||
/// <!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
/// **English** | 中文 ([简体](FOO_zh.md), [繁體](FOO_zh_Hant.md)) | [日本語](FOO_ja.md)
|
||||
/// <!-- #END LANGUAGE_SWITCHER -->
|
||||
/// ```
|
||||
///
|
||||
/// @author Glavo
|
||||
public enum MacroProcessor {
|
||||
/// Does not process the content in any way.
|
||||
///
|
||||
/// Supported properties:
|
||||
///
|
||||
/// - `NAME`: The name of this block (used by other macros).
|
||||
/// - `PROCESS_LINK`: If set to `FALSE`, document links in the content will not be automatically updated.
|
||||
BLOCK {
|
||||
@Override
|
||||
public void apply(Document document, Document.MacroBlock macroBlock, StringBuilder outputBuilder) throws IOException {
|
||||
var mutableProperties = new LinkedHashMap<>(macroBlock.properties());
|
||||
MacroProcessor.removeSingleProperty(mutableProperties, "NAME");
|
||||
boolean processLink = !"FALSE".equalsIgnoreCase(MacroProcessor.removeSingleProperty(mutableProperties, "PROCESS_LINK"));
|
||||
|
||||
if (!mutableProperties.isEmpty())
|
||||
throw new IllegalArgumentException("Unsupported properties: " + mutableProperties.keySet());
|
||||
|
||||
MacroProcessor.writeBegin(outputBuilder, macroBlock);
|
||||
MacroProcessor.writeProperties(outputBuilder, macroBlock);
|
||||
for (String line : macroBlock.contentLines()) {
|
||||
if (processLink)
|
||||
MacroProcessor.processLine(outputBuilder, line, document);
|
||||
else
|
||||
outputBuilder.append(line).append('\n');
|
||||
}
|
||||
MacroProcessor.writeEnd(outputBuilder, macroBlock);
|
||||
}
|
||||
},
|
||||
|
||||
/// Used to automatically generate links to other language versions of the current document.
|
||||
///
|
||||
/// Does not support any properties.
|
||||
LANGUAGE_SWITCHER {
|
||||
private static <T> boolean containsIdentity(List<T> list, T element) {
|
||||
for (T t : list) {
|
||||
@@ -104,17 +154,187 @@ public enum MacroProcessor {
|
||||
MacroProcessor.writeEnd(outputBuilder, macroBlock);
|
||||
}
|
||||
},
|
||||
BLOCK {
|
||||
|
||||
/// Copy the block with the specified name from the English version of the current document.
|
||||
///
|
||||
/// Supported properties:
|
||||
///
|
||||
/// - `NAME` (required): Specifies the block to be copied.
|
||||
/// - `REPLACE` (repeatable): Used to replace specified text. Accepts a list containing two strings. The first string is a regular expression for matching content; the second string is the replacement target.
|
||||
/// - `PROCESS_LINK`: If set to `FALSE`, document links in the content will not be automatically updated.
|
||||
COPY {
|
||||
private record Replace(Pattern pattern, String replacement) {
|
||||
}
|
||||
|
||||
private static IllegalArgumentException illegalReplace(String value) {
|
||||
return new IllegalArgumentException("Illegal replacement pattern: " + value);
|
||||
}
|
||||
|
||||
private static Replace parseReplace(String value) {
|
||||
List<String> list = MacroProcessor.parseStringList(value);
|
||||
if (list.size() != 2)
|
||||
throw illegalReplace(value);
|
||||
|
||||
return new Replace(Pattern.compile(list.get(0)), list.get(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(Document document, Document.MacroBlock macroBlock, StringBuilder outputBuilder) throws IOException {
|
||||
var mutableProperties = new LinkedHashMap<>(macroBlock.properties());
|
||||
String blockName = MacroProcessor.removeSingleProperty(mutableProperties, "NAME");
|
||||
if (blockName == null)
|
||||
throw new IllegalArgumentException("Missing property: NAME");
|
||||
|
||||
List<Replace> replaces = Objects.requireNonNullElse(mutableProperties.remove("REPLACE"), List.<String>of())
|
||||
.stream()
|
||||
.map(it -> parseReplace(it))
|
||||
.toList();
|
||||
|
||||
boolean processLink = !"FALSE".equalsIgnoreCase(MacroProcessor.removeSingleProperty(mutableProperties, "PROCESS_LINK"));
|
||||
|
||||
if (!mutableProperties.isEmpty())
|
||||
throw new IllegalArgumentException("Unsupported properties: " + mutableProperties.keySet());
|
||||
|
||||
LocalizedDocument localizedDocument = document.directory().getFiles().get(document.name());
|
||||
Document fromDocument;
|
||||
if (localizedDocument == null || (fromDocument = localizedDocument.getDocuments().get(DocumentLocale.ENGLISH)) == null)
|
||||
throw new IOException("Document " + document.name() + " for english does not exist");
|
||||
|
||||
List<String> nameList = List.of(blockName);
|
||||
|
||||
var fromBlock = (Document.MacroBlock) fromDocument.items().stream()
|
||||
.filter(it -> it instanceof Document.MacroBlock macro
|
||||
&& macro.name().equals(BLOCK.name())
|
||||
&& nameList.equals(macro.properties().get("NAME"))
|
||||
)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IOException("Cannot find the block \"" + blockName + "\" in " + fromDocument.file()));
|
||||
|
||||
MacroProcessor.writeBegin(outputBuilder, macroBlock);
|
||||
MacroProcessor.writeProperties(outputBuilder, macroBlock);
|
||||
for (String line : macroBlock.contentLines()) {
|
||||
outputBuilder.append(line).append('\n');
|
||||
for (String line : fromBlock.contentLines()) {
|
||||
for (Replace replace : replaces) {
|
||||
line = replace.pattern.matcher(line).replaceAll(replace.replacement());
|
||||
}
|
||||
if (processLink)
|
||||
processLine(outputBuilder, line, document);
|
||||
else
|
||||
outputBuilder.append(line).append('\n');
|
||||
}
|
||||
MacroProcessor.writeEnd(outputBuilder, macroBlock);
|
||||
}
|
||||
};
|
||||
},
|
||||
;
|
||||
|
||||
private static String removeSingleProperty(Map<String, List<String>> properties, String name) {
|
||||
List<String> values = properties.remove(name);
|
||||
if (values == null || values.isEmpty())
|
||||
return null;
|
||||
|
||||
if (values.size() != 1)
|
||||
throw new IllegalArgumentException("Unexpected number of property " + name + ": " + values.size());
|
||||
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
private static List<String> parseStringList(String str) {
|
||||
if (str.isBlank()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// Split the string with ' and space cleverly.
|
||||
ArrayList<String> parts = new ArrayList<>(2);
|
||||
|
||||
boolean hasValue = false;
|
||||
StringBuilder current = new StringBuilder(str.length());
|
||||
for (int i = 0; i < str.length(); ) {
|
||||
char c = str.charAt(i);
|
||||
if (c == '\'' || c == '"') {
|
||||
hasValue = true;
|
||||
int end = str.indexOf(c, i + 1);
|
||||
if (end < 0) {
|
||||
end = str.length();
|
||||
}
|
||||
current.append(str, i + 1, end);
|
||||
i = end + 1;
|
||||
|
||||
} else if (c == ' ' || c == '\t') {
|
||||
if (hasValue) {
|
||||
parts.add(current.toString());
|
||||
current.setLength(0);
|
||||
hasValue = false;
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
hasValue = true;
|
||||
current.append(c);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (hasValue)
|
||||
parts.add(current.toString());
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static final Pattern LINK_PATTERN = Pattern.compile(
|
||||
"(?<=]\\()[a-zA-Z0-9_\\-./]+\\.md(?=\\))"
|
||||
);
|
||||
|
||||
static void processLine(StringBuilder outputBuilder, String line, Document document) {
|
||||
outputBuilder.append(LINK_PATTERN.matcher(line).replaceAll(matchResult -> {
|
||||
String rawLink = matchResult.group();
|
||||
String[] splitPath = rawLink.split("/");
|
||||
|
||||
if (splitPath.length == 0)
|
||||
return rawLink;
|
||||
|
||||
String fileName = splitPath[splitPath.length - 1];
|
||||
if (!fileName.endsWith(".md"))
|
||||
return rawLink;
|
||||
|
||||
DocumentFileTree current = document.directory();
|
||||
for (int i = 0; i < splitPath.length - 1; i++) {
|
||||
String name = splitPath[i];
|
||||
switch (name) {
|
||||
case "" -> {
|
||||
return rawLink;
|
||||
}
|
||||
case "." -> {
|
||||
continue;
|
||||
}
|
||||
case ".." -> {
|
||||
current = current.getParent();
|
||||
if (current == null)
|
||||
return rawLink;
|
||||
}
|
||||
default -> {
|
||||
current = current.getChildren().get(name);
|
||||
if (current == null)
|
||||
return rawLink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DocumentLocale.LocaleAndName currentLocaleAndName = DocumentLocale.parseFileName(fileName.substring(0, fileName.length() - ".md".length()));
|
||||
LocalizedDocument localizedDocument = current.getFiles().get(currentLocaleAndName.name());
|
||||
if (localizedDocument != null) {
|
||||
List<DocumentLocale> candidateLocales = document.locale().getCandidates();
|
||||
for (DocumentLocale candidateLocale : candidateLocales) {
|
||||
if (candidateLocale == currentLocaleAndName.locale())
|
||||
return rawLink;
|
||||
|
||||
Document targetDoc = localizedDocument.getDocuments().get(candidateLocale);
|
||||
if (targetDoc != null) {
|
||||
splitPath[splitPath.length - 1] = targetDoc.file().getFileName().toString();
|
||||
return String.join("/", splitPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rawLink;
|
||||
})).append('\n');
|
||||
}
|
||||
|
||||
private static void writeBegin(StringBuilder builder, Document.MacroBlock macroBlock) throws IOException {
|
||||
builder.append("<!-- #BEGIN ");
|
||||
|
||||
@@ -25,8 +25,6 @@ import org.gradle.api.tasks.TaskAction;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class UpdateDocuments extends DefaultTask {
|
||||
@@ -36,71 +34,12 @@ public abstract class UpdateDocuments extends DefaultTask {
|
||||
|
||||
// ---
|
||||
|
||||
private static final Pattern LINK_PATTERN = Pattern.compile(
|
||||
"(?<=]\\()[a-zA-Z0-9_\\-./]+\\.md(?=\\))"
|
||||
);
|
||||
|
||||
private void processLine(StringBuilder outputBuilder, String line, Document document) {
|
||||
outputBuilder.append(LINK_PATTERN.matcher(line).replaceAll(matchResult -> {
|
||||
String rawLink = matchResult.group();
|
||||
String[] splitPath = rawLink.split("/");
|
||||
|
||||
if (splitPath.length == 0)
|
||||
return rawLink;
|
||||
|
||||
String fileName = splitPath[splitPath.length - 1];
|
||||
if (!fileName.endsWith(".md"))
|
||||
return rawLink;
|
||||
|
||||
DocumentFileTree current = document.directory();
|
||||
for (int i = 0; i < splitPath.length - 1; i++) {
|
||||
String name = splitPath[i];
|
||||
switch (name) {
|
||||
case "" -> {
|
||||
return rawLink;
|
||||
}
|
||||
case "." -> {
|
||||
continue;
|
||||
}
|
||||
case ".." -> {
|
||||
current = current.getParent();
|
||||
if (current == null)
|
||||
return rawLink;
|
||||
}
|
||||
default -> {
|
||||
current = current.getChildren().get(name);
|
||||
if (current == null)
|
||||
return rawLink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DocumentLocale.LocaleAndName currentLocaleAndName = DocumentLocale.parseFileName(fileName.substring(0, fileName.length() - ".md".length()));
|
||||
LocalizedDocument localizedDocument = current.getFiles().get(currentLocaleAndName.name());
|
||||
if (localizedDocument != null) {
|
||||
List<DocumentLocale> candidateLocales = document.locale().getCandidates();
|
||||
for (DocumentLocale candidateLocale : candidateLocales) {
|
||||
if (candidateLocale == currentLocaleAndName.locale())
|
||||
return rawLink;
|
||||
|
||||
Document targetDoc = localizedDocument.getDocuments().get(candidateLocale);
|
||||
if (targetDoc != null) {
|
||||
splitPath[splitPath.length - 1] = targetDoc.file().getFileName().toString();
|
||||
return String.join("/", splitPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rawLink;
|
||||
})).append('\n');
|
||||
}
|
||||
|
||||
private void updateDocument(Document document) throws IOException {
|
||||
StringBuilder outputBuilder = new StringBuilder(8192);
|
||||
|
||||
for (Document.Item item : document.items()) {
|
||||
if (item instanceof Document.Line line) {
|
||||
processLine(outputBuilder, line.content(), document);
|
||||
MacroProcessor.processLine(outputBuilder, line.content(), document);
|
||||
} else if (item instanceof Document.MacroBlock macro) {
|
||||
var processor = MacroProcessor.valueOf(macro.name());
|
||||
processor.apply(document, macro, outputBuilder);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
**English** | 中文 ([简体](PLATFORM_zh.md), [繁體](PLATFORM_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
<!-- #BEGIN BLOCK -->
|
||||
<!-- #PROPERTY NAME=PLATFORM_TABLE -->
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
|
||||
@@ -16,6 +18,7 @@
|
||||
| LoongArch64 (Old World) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
<!-- #END BLOCK -->
|
||||
|
||||
Legend:
|
||||
|
||||
|
||||
+17
-12
@@ -4,18 +4,23 @@
|
||||
[English](PLATFORM.md) | **中文** (**简体**, [繁體](PLATFORM_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
|
||||
| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=PLATFORM_TABLE -->
|
||||
<!-- #PROPERTY REPLACE="\\(Old World\\)" "(旧世界)" -->
|
||||
<!-- #PROPERTY REPLACE="\\(use Rosetta 2\\)" "(使用 Rosetta 2)" -->
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
|
||||
| LoongArch64 (旧世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
<!-- #END COPY -->
|
||||
|
||||
图例:
|
||||
|
||||
|
||||
+17
-12
@@ -4,18 +4,23 @@
|
||||
[English](PLATFORM.md) | **中文** ([简体](PLATFORM_zh.md), **繁體**)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌(Minecraft 1.13~1.21.8) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
|
||||
| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=PLATFORM_TABLE -->
|
||||
<!-- #PROPERTY REPLACE="\\(Old World\\)" "(舊世界)" -->
|
||||
<!-- #PROPERTY REPLACE="\\(use Rosetta 2\\)" "(使用 Rosetta 2)" -->
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
|
||||
| x86-64 | ✅️ | ✅️ | ✅️ | 👌 (Minecraft 1.13~1.21.8) |
|
||||
| x86 | ✅️ (~1.20.4) | ✅️ (~1.20.4) | / | / |
|
||||
| ARM64 | 👌 (Minecraft 1.8~1.18.2)<br/>✅ (Minecraft 1.19+) | 👌 (Minecraft 1.8~1.21.8) | 👌 (Minecraft 1.6~1.18.2)<br/>✅ (Minecraft 1.19+)<br/>✅ (使用 Rosetta 2) | ❔ |
|
||||
| ARM32 | /️ | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| MIPS64el | / | 👌 (Minecraft 1.8~1.20.1) | / | / |
|
||||
| RISC-V 64 | / | 👌 (Minecraft 1.13~1.21.8) | / | / |
|
||||
| LoongArch64 | / | 👌 (Minecraft 1.6~1.21.8) | / | / |
|
||||
| LoongArch64 (舊世界) | / | 👌 (Minecraft 1.6~1.20.1) | / | / |
|
||||
| PowerPC-64 (Little-Endian) | / | ❔ | / | / |
|
||||
| S390x | / | ❔ | / | / |
|
||||
<!-- #END COPY -->
|
||||
|
||||
圖例:
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN BLOCK -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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_ru.md) | [українська](README_uk.md)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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) | **українська**
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Hello Minecraft! Launcher
|
||||
|
||||
<!-- #BEGIN COPY -->
|
||||
<!-- #PROPERTY NAME=BADGES -->
|
||||
[](https://ci.huangyuhui.net/job/HMCL)
|
||||

|
||||

|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](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)
|
||||
|
||||
Reference in new issue
Block a user