mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-18 16:26:05 -04:00
feat: WIP: auto java.
This commit is contained in:
parent
59df08cd93
commit
04e6897a0d
@ -0,0 +1,93 @@
|
|||||||
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
|
import org.jackhuang.hmcl.util.Range;
|
||||||
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LAUNCH_WRAPPER_MAIN;
|
||||||
|
|
||||||
|
public enum HMCLJavaVersion {
|
||||||
|
|
||||||
|
// Minecraft>=1.17 requires Java 16
|
||||||
|
VANILLA_JAVA_16(HMCLJavaVersion.RULE_MANDATORY, versionRange("1.17", HMCLJavaVersion.MAX), versionRange("16", HMCLJavaVersion.MAX)),
|
||||||
|
// Minecraft>=1.13 requires Java 8
|
||||||
|
VANILLA_JAVA_8(HMCLJavaVersion.RULE_MANDATORY, versionRange("1.13", HMCLJavaVersion.MAX), versionRange("8", HMCLJavaVersion.MAX)),
|
||||||
|
// Minecraft>=1.7.10+Forge accepts Java 8
|
||||||
|
SUGGEST_JAVA_8(HMCLJavaVersion.RULE_SUGGESTED, versionRange("1.7.10", HMCLJavaVersion.MAX), versionRange("8", HMCLJavaVersion.MAX)),
|
||||||
|
// LaunchWrapper<=1.12 will crash because of assuming the system class loader is an instance of URLClassLoader (Java 8)
|
||||||
|
LAUNCH_WRAPPER(HMCLJavaVersion.RULE_MANDATORY, versionRange("0", "1.12"), versionRange("0", "8")) {
|
||||||
|
@Override
|
||||||
|
public boolean test(Version version) {
|
||||||
|
return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) &&
|
||||||
|
version.getLibraries().stream()
|
||||||
|
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
|
||||||
|
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Minecraft>=1.13 may crash when generating world on Java [1.8,1.8.0_51)
|
||||||
|
VANILLA_JAVA_8_51(HMCLJavaVersion.RULE_SUGGESTED, versionRange("1.13", HMCLJavaVersion.MAX), versionRange("1.8.0_51", HMCLJavaVersion.MAX)),
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
private final int type;
|
||||||
|
private final Range<VersionNumber> gameVersion;
|
||||||
|
private final Range<VersionNumber> javaVersion;
|
||||||
|
|
||||||
|
HMCLJavaVersion(int type, Range<VersionNumber> gameVersion, Range<VersionNumber> javaVersion) {
|
||||||
|
this.type = type;
|
||||||
|
this.gameVersion = gameVersion;
|
||||||
|
this.javaVersion = javaVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean test(Version version) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static JavaVersion findSuitableJavaVersion(VersionNumber gameVersion, Version version) throws InterruptedException {
|
||||||
|
Range<VersionNumber> mandatoryJavaRange = versionRange(MIN, MAX);
|
||||||
|
Range<VersionNumber> suggestedJavaRange = versionRange(MIN, MAX);
|
||||||
|
for (HMCLJavaVersion java : values()) {
|
||||||
|
if (java.gameVersion.contains(gameVersion) && java.test(version)) {
|
||||||
|
if (java.type == RULE_MANDATORY) {
|
||||||
|
mandatoryJavaRange = mandatoryJavaRange.intersectionWith(java.javaVersion);
|
||||||
|
suggestedJavaRange = suggestedJavaRange.intersectionWith(java.javaVersion);
|
||||||
|
} else if (java.type == RULE_SUGGESTED) {
|
||||||
|
suggestedJavaRange = suggestedJavaRange.intersectionWith(java.javaVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JavaVersion mandatory = null;
|
||||||
|
JavaVersion suggested = null;
|
||||||
|
for (JavaVersion javaVersion : JavaVersion.getJavas()) {
|
||||||
|
// select the latest java version that this version accepts.
|
||||||
|
if (mandatoryJavaRange.contains(javaVersion.getVersionNumber())) {
|
||||||
|
if (mandatory == null) mandatory = javaVersion;
|
||||||
|
else if (javaVersion.getVersionNumber().compareTo(mandatory.getVersionNumber()) > 0) {
|
||||||
|
mandatory = javaVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (suggestedJavaRange.contains(javaVersion.getVersionNumber())) {
|
||||||
|
if (suggested == null) suggested = javaVersion;
|
||||||
|
else if (javaVersion.getVersionNumber().compareTo(suggested.getVersionNumber()) > 0) {
|
||||||
|
suggested = javaVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggested != null) return suggested;
|
||||||
|
else return mandatory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int RULE_MANDATORY = 1;
|
||||||
|
public static final int RULE_SUGGESTED = 2;
|
||||||
|
|
||||||
|
public static final String MIN = "0";
|
||||||
|
public static final String MAX = "10000";
|
||||||
|
|
||||||
|
private static Range<VersionNumber> versionRange(String fromInclusive, String toExclusive) {
|
||||||
|
return Range.between(VersionNumber.asVersion(fromInclusive), VersionNumber.asVersion(toExclusive));
|
||||||
|
}
|
||||||
|
}
|
@ -21,14 +21,13 @@ import com.google.gson.*;
|
|||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import org.jackhuang.hmcl.game.GameDirectoryType;
|
import org.jackhuang.hmcl.game.*;
|
||||||
import org.jackhuang.hmcl.game.NativesDirectoryType;
|
|
||||||
import org.jackhuang.hmcl.game.ProcessPriority;
|
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
import org.jackhuang.hmcl.util.StringUtils;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
import org.jackhuang.hmcl.util.platform.Platform;
|
import org.jackhuang.hmcl.util.platform.Platform;
|
||||||
|
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
@ -36,6 +35,8 @@ import java.nio.file.InvalidPathException;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,6 +106,15 @@ public final class VersionSetting implements Cloneable {
|
|||||||
setDefaultJavaPath(null);
|
setDefaultJavaPath(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isJavaAutoSelected() {
|
||||||
|
return "Auto".equals(getJava());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJavaAutoSelected() {
|
||||||
|
setJava("Auto");
|
||||||
|
setDefaultJavaPath(null);
|
||||||
|
}
|
||||||
|
|
||||||
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
|
private final StringProperty defaultJavaPathProperty = new SimpleStringProperty(this, "defaultJavaPath", "");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -571,16 +581,20 @@ public final class VersionSetting implements Cloneable {
|
|||||||
launcherVisibilityProperty.set(launcherVisibility);
|
launcherVisibilityProperty.set(launcherVisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JavaVersion getJavaVersion() throws InterruptedException {
|
public CompletableFuture<JavaVersion> getJavaVersion(String gameVersion, Version version) {
|
||||||
return getJavaVersion(true);
|
return getJavaVersion(gameVersion, version, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException {
|
public CompletableFuture<JavaVersion> getJavaVersion(String gameVersion, Version version, boolean checkJava) {
|
||||||
// TODO: lazy initialization may result in UI suspension.
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
if (StringUtils.isBlank(getJava()))
|
if (StringUtils.isBlank(getJava()))
|
||||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
||||||
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
|
if ("Default".equals(getJava())) {
|
||||||
else if (isUsesCustomJavaDir()) {
|
return JavaVersion.fromCurrentEnvironment();
|
||||||
|
} else if (isJavaAutoSelected()) {
|
||||||
|
return HMCLJavaVersion.findSuitableJavaVersion(VersionNumber.asVersion(gameVersion), version);
|
||||||
|
} else if (isUsesCustomJavaDir()) {
|
||||||
try {
|
try {
|
||||||
if (checkJava)
|
if (checkJava)
|
||||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||||
@ -603,6 +617,10 @@ public final class VersionSetting implements Cloneable {
|
|||||||
.orElse(matchedJava.get(0));
|
.orElse(matchedJava.get(0));
|
||||||
}
|
}
|
||||||
} else throw new Error();
|
} else throw new Error();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new CancellationException();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setJavaVersion(JavaVersion java) {
|
public void setJavaVersion(JavaVersion java) {
|
||||||
|
@ -115,6 +115,7 @@ public class MultiFileItem<T> extends VBox {
|
|||||||
protected final String title;
|
protected final String title;
|
||||||
protected String subtitle;
|
protected String subtitle;
|
||||||
protected final T data;
|
protected final T data;
|
||||||
|
protected final BooleanProperty selected = new SimpleBooleanProperty();
|
||||||
|
|
||||||
public Option(String title, T data) {
|
public Option(String title, T data) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
@ -138,6 +139,18 @@ public class MultiFileItem<T> extends VBox {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSelected() {
|
||||||
|
return selected.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty selectedProperty() {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelected(boolean selected) {
|
||||||
|
this.selected.set(selected);
|
||||||
|
}
|
||||||
|
|
||||||
protected Node createItem(ToggleGroup group) {
|
protected Node createItem(ToggleGroup group) {
|
||||||
BorderPane pane = new BorderPane();
|
BorderPane pane = new BorderPane();
|
||||||
pane.setPadding(new Insets(3));
|
pane.setPadding(new Insets(3));
|
||||||
@ -147,6 +160,7 @@ public class MultiFileItem<T> extends VBox {
|
|||||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||||
left.setToggleGroup(group);
|
left.setToggleGroup(group);
|
||||||
left.setUserData(data);
|
left.setUserData(data);
|
||||||
|
selected.bind(left.selectedProperty());
|
||||||
pane.setLeft(left);
|
pane.setLeft(left);
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(subtitle)) {
|
if (StringUtils.isNotBlank(subtitle)) {
|
||||||
@ -208,6 +222,7 @@ public class MultiFileItem<T> extends VBox {
|
|||||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||||
left.setToggleGroup(group);
|
left.setToggleGroup(group);
|
||||||
left.setUserData(data);
|
left.setUserData(data);
|
||||||
|
selected.bind(left.selectedProperty());
|
||||||
pane.setLeft(left);
|
pane.setLeft(left);
|
||||||
|
|
||||||
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
|
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
|
||||||
@ -266,6 +281,7 @@ public class MultiFileItem<T> extends VBox {
|
|||||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||||
left.setToggleGroup(group);
|
left.setToggleGroup(group);
|
||||||
left.setUserData(data);
|
left.setUserData(data);
|
||||||
|
selected.bind(left.selectedProperty());
|
||||||
pane.setLeft(left);
|
pane.setLeft(left);
|
||||||
|
|
||||||
selector.disableProperty().bind(left.selectedProperty().not());
|
selector.disableProperty().bind(left.selectedProperty().not());
|
||||||
|
@ -101,6 +101,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
private final OptionToggleButton useNativeOpenALPane;
|
private final OptionToggleButton useNativeOpenALPane;
|
||||||
private final ComponentSublist javaSublist;
|
private final ComponentSublist javaSublist;
|
||||||
private final MultiFileItem<JavaVersion> javaItem;
|
private final MultiFileItem<JavaVersion> javaItem;
|
||||||
|
private final MultiFileItem.Option<JavaVersion> javaAutoDeterminedOption;
|
||||||
private final MultiFileItem.FileOption<JavaVersion> javaCustomOption;
|
private final MultiFileItem.FileOption<JavaVersion> javaCustomOption;
|
||||||
private final ComponentSublist gameDirSublist;
|
private final ComponentSublist gameDirSublist;
|
||||||
private final MultiFileItem<GameDirectoryType> gameDirItem;
|
private final MultiFileItem<GameDirectoryType> gameDirItem;
|
||||||
@ -199,6 +200,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
javaSublist.getContent().add(javaItem);
|
javaSublist.getContent().add(javaItem);
|
||||||
javaSublist.setTitle(i18n("settings.game.java_directory"));
|
javaSublist.setTitle(i18n("settings.game.java_directory"));
|
||||||
javaSublist.setHasSubtitle(true);
|
javaSublist.setHasSubtitle(true);
|
||||||
|
javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), null);
|
||||||
javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null)
|
javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null)
|
||||||
.setChooserTitle(i18n("settings.game.java_directory.choose"));
|
.setChooserTitle(i18n("settings.game.java_directory.choose"));
|
||||||
|
|
||||||
@ -541,6 +543,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
javaVersion.getPlatform().getBit()), javaVersion)
|
javaVersion.getPlatform().getBit()), javaVersion)
|
||||||
.setSubtitle(javaVersion.getBinary().toString()))
|
.setSubtitle(javaVersion.getBinary().toString()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
options.add(0, javaAutoDeterminedOption);
|
||||||
options.add(javaCustomOption);
|
options.add(javaCustomOption);
|
||||||
javaItem.loadChildren(options);
|
javaItem.loadChildren(options);
|
||||||
javaItemsLoaded = true;
|
javaItemsLoaded = true;
|
||||||
@ -661,8 +664,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
enableSpecificSettings.set(!versionSetting.isUsesGlobal());
|
enableSpecificSettings.set(!versionSetting.isUsesGlobal());
|
||||||
|
|
||||||
javaItem.setToggleSelectedListener(newValue -> {
|
javaItem.setToggleSelectedListener(newValue -> {
|
||||||
if (newValue.getUserData() == null) {
|
if (javaCustomOption.isSelected()) {
|
||||||
versionSetting.setUsesCustomJavaDir();
|
versionSetting.setUsesCustomJavaDir();
|
||||||
|
} else if (javaAutoDeterminedOption.isSelected()) {
|
||||||
|
versionSetting.setJavaAutoSelected();
|
||||||
} else {
|
} else {
|
||||||
versionSetting.setJavaVersion((JavaVersion) newValue.getUserData());
|
versionSetting.setJavaVersion((JavaVersion) newValue.getUserData());
|
||||||
}
|
}
|
||||||
@ -711,7 +716,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
|||||||
VersionSetting versionSetting = lastVersionSetting;
|
VersionSetting versionSetting = lastVersionSetting;
|
||||||
if (versionSetting == null)
|
if (versionSetting == null)
|
||||||
return;
|
return;
|
||||||
Task.supplyAsync(versionSetting::getJavaVersion)
|
Task.fromCompletableFuture(versionSetting.getJavaVersion())
|
||||||
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
|
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
|
||||||
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
|
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
|
||||||
.start();
|
.start();
|
||||||
|
@ -424,6 +424,7 @@ launch.failed.decompressing_natives=Unable to decompress native libraries.
|
|||||||
launch.failed.download_library=Unable to download library %s.
|
launch.failed.download_library=Unable to download library %s.
|
||||||
launch.failed.executable_permission=Unable to add permission to the launch script
|
launch.failed.executable_permission=Unable to add permission to the launch script
|
||||||
launch.failed.exited_abnormally=Game exited abnormally, please check the log, or ask someone for help.
|
launch.failed.exited_abnormally=Game exited abnormally, please check the log, or ask someone for help.
|
||||||
|
launch.failed.no_accepted_java=Cannot find the Java installation suitable for current game. If you think you have installed a suitable Java VM, you can manually select it in game settings.
|
||||||
launch.state.dependencies=Dependencies
|
launch.state.dependencies=Dependencies
|
||||||
launch.state.done=Done
|
launch.state.done=Done
|
||||||
launch.state.logging_in=Logging In
|
launch.state.logging_in=Logging In
|
||||||
@ -765,6 +766,7 @@ settings.game.dimension=Game Window Dimension
|
|||||||
settings.game.exploration=Explore
|
settings.game.exploration=Explore
|
||||||
settings.game.fullscreen=Fullscreen
|
settings.game.fullscreen=Fullscreen
|
||||||
settings.game.java_directory=Java Directory
|
settings.game.java_directory=Java Directory
|
||||||
|
settings.game.java_directory.auto=Automatically selected
|
||||||
settings.game.java_directory.bit=, %s-Bit
|
settings.game.java_directory.bit=, %s-Bit
|
||||||
settings.game.java_directory.choose=Choose Java Directory.
|
settings.game.java_directory.choose=Choose Java Directory.
|
||||||
settings.game.management=Manage
|
settings.game.management=Manage
|
||||||
|
@ -424,6 +424,7 @@ launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。
|
|||||||
launch.failed.download_library=無法下載遊戲相依元件 %s。
|
launch.failed.download_library=無法下載遊戲相依元件 %s。
|
||||||
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
|
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
|
||||||
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
|
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
|
||||||
|
launch.failed.no_accepted_java=找不到適合當前遊戲使用的 Java。如果您認為實際存在合適的 Java,您可以在遊戲設置中手動設置 Java。
|
||||||
launch.state.dependencies=處理遊戲相依元件
|
launch.state.dependencies=處理遊戲相依元件
|
||||||
launch.state.done=啟動完成
|
launch.state.done=啟動完成
|
||||||
launch.state.logging_in=登入
|
launch.state.logging_in=登入
|
||||||
@ -764,6 +765,7 @@ settings.game.dimension=遊戲介面解析度大小
|
|||||||
settings.game.exploration=瀏覽
|
settings.game.exploration=瀏覽
|
||||||
settings.game.fullscreen=全螢幕
|
settings.game.fullscreen=全螢幕
|
||||||
settings.game.java_directory=Java 路徑
|
settings.game.java_directory=Java 路徑
|
||||||
|
settings.game.java_directory.auto=自動選擇合適的 Java
|
||||||
settings.game.java_directory.bit=,%s 位
|
settings.game.java_directory.bit=,%s 位
|
||||||
settings.game.java_directory.choose=選擇 Java 路徑
|
settings.game.java_directory.choose=選擇 Java 路徑
|
||||||
settings.game.management=管理
|
settings.game.management=管理
|
||||||
|
@ -424,6 +424,7 @@ launch.failed.decompressing_natives=未能解压游戏本地库。
|
|||||||
launch.failed.download_library=未能下载游戏依赖 %s.
|
launch.failed.download_library=未能下载游戏依赖 %s.
|
||||||
launch.failed.executable_permission=未能为启动文件添加执行权限。
|
launch.failed.executable_permission=未能为启动文件添加执行权限。
|
||||||
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
|
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
|
||||||
|
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。如果您认为实际存在合适的 Java,您可以在游戏设置中手动设置 Java。
|
||||||
launch.state.dependencies=处理游戏依赖
|
launch.state.dependencies=处理游戏依赖
|
||||||
launch.state.done=启动完成
|
launch.state.done=启动完成
|
||||||
launch.state.logging_in=登录
|
launch.state.logging_in=登录
|
||||||
@ -764,6 +765,7 @@ settings.game.dimension=游戏窗口分辨率
|
|||||||
settings.game.exploration=浏览
|
settings.game.exploration=浏览
|
||||||
settings.game.fullscreen=全屏
|
settings.game.fullscreen=全屏
|
||||||
settings.game.java_directory=Java 路径
|
settings.game.java_directory=Java 路径
|
||||||
|
settings.game.java_directory.auto=自动选择合适的 Java
|
||||||
settings.game.java_directory.bit=,%s 位
|
settings.game.java_directory.bit=,%s 位
|
||||||
settings.game.java_directory.choose=选择 Java 路径
|
settings.game.java_directory.choose=选择 Java 路径
|
||||||
settings.game.management=管理
|
settings.game.management=管理
|
||||||
|
510
HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java
Normal file
510
HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
/*
|
||||||
|
* Hello Minecraft! Launcher
|
||||||
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.jackhuang.hmcl.util;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class Range<T> {
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private enum ComparableComparator implements Comparator {
|
||||||
|
INSTANCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comparable based compare implementation.
|
||||||
|
*
|
||||||
|
* @param obj1 left hand side of comparison
|
||||||
|
* @param obj2 right hand side of comparison
|
||||||
|
* @return negative, 0, positive comparison value
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compare(final Object obj1, final Object obj2) {
|
||||||
|
return ((Comparable) obj1).compareTo(obj2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialization version.
|
||||||
|
*
|
||||||
|
* @see java.io.Serializable
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Obtains a range with the specified minimum and maximum values (both inclusive).</p>
|
||||||
|
*
|
||||||
|
* <p>The range uses the natural ordering of the elements to determine where
|
||||||
|
* values lie in the range.</p>
|
||||||
|
*
|
||||||
|
* <p>The arguments may be passed in the order (min,max) or (max,min).
|
||||||
|
* The getMinimum and getMaximum methods will return the correct values.</p>
|
||||||
|
*
|
||||||
|
* @param <T> the type of the elements in this range
|
||||||
|
* @param fromInclusive the first value that defines the edge of the range, inclusive
|
||||||
|
* @param toInclusive the second value that defines the edge of the range, inclusive
|
||||||
|
* @return the range object, not null
|
||||||
|
* @throws IllegalArgumentException if either element is null
|
||||||
|
* @throws ClassCastException if the elements are not {@code Comparable}
|
||||||
|
*/
|
||||||
|
public static <T extends Comparable<T>> Range<T> between(final T fromInclusive, final T toInclusive) {
|
||||||
|
return between(fromInclusive, toInclusive, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Obtains a range with the specified minimum and maximum values (both inclusive).</p>
|
||||||
|
*
|
||||||
|
* <p>The range uses the specified {@code Comparator} to determine where
|
||||||
|
* values lie in the range.</p>
|
||||||
|
*
|
||||||
|
* <p>The arguments may be passed in the order (min,max) or (max,min).
|
||||||
|
* The getMinimum and getMaximum methods will return the correct values.</p>
|
||||||
|
*
|
||||||
|
* @param <T> the type of the elements in this range
|
||||||
|
* @param fromInclusive the first value that defines the edge of the range, inclusive
|
||||||
|
* @param toInclusive the second value that defines the edge of the range, inclusive
|
||||||
|
* @param comparator the comparator to be used, null for natural ordering
|
||||||
|
* @return the range object, not null
|
||||||
|
* @throws IllegalArgumentException if either element is null
|
||||||
|
* @throws ClassCastException if using natural ordering and the elements are not {@code Comparable}
|
||||||
|
*/
|
||||||
|
public static <T> Range<T> between(final T fromInclusive, final T toInclusive, final Comparator<T> comparator) {
|
||||||
|
return new Range<>(fromInclusive, toInclusive, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Obtains a range using the specified element as both the minimum
|
||||||
|
* and maximum in this range.</p>
|
||||||
|
*
|
||||||
|
* <p>The range uses the natural ordering of the elements to determine where
|
||||||
|
* values lie in the range.</p>
|
||||||
|
*
|
||||||
|
* @param <T> the type of the elements in this range
|
||||||
|
* @param element the value to use for this range, not null
|
||||||
|
* @return the range object, not null
|
||||||
|
* @throws IllegalArgumentException if the element is null
|
||||||
|
* @throws ClassCastException if the element is not {@code Comparable}
|
||||||
|
*/
|
||||||
|
public static <T extends Comparable<T>> Range<T> is(final T element) {
|
||||||
|
return between(element, element, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Obtains a range using the specified element as both the minimum
|
||||||
|
* and maximum in this range.</p>
|
||||||
|
*
|
||||||
|
* <p>The range uses the specified {@code Comparator} to determine where
|
||||||
|
* values lie in the range.</p>
|
||||||
|
*
|
||||||
|
* @param <T> the type of the elements in this range
|
||||||
|
* @param element the value to use for this range, must not be {@code null}
|
||||||
|
* @param comparator the comparator to be used, null for natural ordering
|
||||||
|
* @return the range object, not null
|
||||||
|
* @throws IllegalArgumentException if the element is null
|
||||||
|
* @throws ClassCastException if using natural ordering and the elements are not {@code Comparable}
|
||||||
|
*/
|
||||||
|
public static <T> Range<T> is(final T element, final Comparator<T> comparator) {
|
||||||
|
return between(element, element, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ordering scheme used in this range.
|
||||||
|
*/
|
||||||
|
private final Comparator<T> comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached output hashCode (class is immutable).
|
||||||
|
*/
|
||||||
|
private transient int hashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum value in this range (inclusive).
|
||||||
|
*/
|
||||||
|
private final T maximum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum value in this range (inclusive).
|
||||||
|
*/
|
||||||
|
private final T minimum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached output toString (class is immutable).
|
||||||
|
*/
|
||||||
|
private transient String toString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param element1 the first element, not null
|
||||||
|
* @param element2 the second element, not null
|
||||||
|
* @param comp the comparator to be used, null for natural ordering
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Range(final T element1, final T element2, final Comparator<T> comp) {
|
||||||
|
if (element1 == null || element2 == null) {
|
||||||
|
throw new IllegalArgumentException("Elements in a range must not be null: element1=" +
|
||||||
|
element1 + ", element2=" + element2);
|
||||||
|
}
|
||||||
|
if (comp == null) {
|
||||||
|
this.comparator = ComparableComparator.INSTANCE;
|
||||||
|
} else {
|
||||||
|
this.comparator = comp;
|
||||||
|
}
|
||||||
|
if (this.comparator.compare(element1, element2) < 1) {
|
||||||
|
this.minimum = element1;
|
||||||
|
this.maximum = element2;
|
||||||
|
} else {
|
||||||
|
this.minimum = element2;
|
||||||
|
this.maximum = element1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return comparator.compare(minimum, maximum) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether the specified element occurs within this range.</p>
|
||||||
|
*
|
||||||
|
* @param element the element to check for, null returns false
|
||||||
|
* @return true if the specified element occurs within this range
|
||||||
|
*/
|
||||||
|
public boolean contains(final T element) {
|
||||||
|
if (element == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return comparator.compare(element, minimum) > -1 && comparator.compare(element, maximum) < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range contains all the elements of the specified range.</p>
|
||||||
|
*
|
||||||
|
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||||
|
*
|
||||||
|
* @param otherRange the range to check, null returns false
|
||||||
|
* @return true if this range contains the specified range
|
||||||
|
* @throws RuntimeException if ranges cannot be compared
|
||||||
|
*/
|
||||||
|
public boolean containsRange(final Range<T> otherRange) {
|
||||||
|
if (otherRange == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return contains(otherRange.minimum)
|
||||||
|
&& contains(otherRange.maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks where the specified element occurs relative to this range.</p>
|
||||||
|
*
|
||||||
|
* <p>The API is reminiscent of the Comparable interface returning {@code -1} if
|
||||||
|
* the element is before the range, {@code 0} if contained within the range and
|
||||||
|
* {@code 1} if the element is after the range. </p>
|
||||||
|
*
|
||||||
|
* @param element the element to check for, not null
|
||||||
|
* @return -1, 0 or +1 depending on the element's location relative to the range
|
||||||
|
*/
|
||||||
|
public int elementCompareTo(final T element) {
|
||||||
|
// Comparable API says throw NPE on null
|
||||||
|
Objects.requireNonNull(element);
|
||||||
|
if (isAfter(element)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (isBefore(element)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element tests
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Compares this range to another object to test if they are equal.</p>.
|
||||||
|
*
|
||||||
|
* <p>To be equal, the minimum and maximum values must be equal, which
|
||||||
|
* ignores any differences in the comparator.</p>
|
||||||
|
*
|
||||||
|
* @param obj the reference object with which to compare
|
||||||
|
* @return true if this object is equal
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj) {
|
||||||
|
if (obj == this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || obj.getClass() != getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked") // OK because we checked the class above
|
||||||
|
final Range<T> range = (Range<T>) obj;
|
||||||
|
return minimum.equals(range.minimum) &&
|
||||||
|
maximum.equals(range.maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Gets the comparator being used to determine if objects are within the range.</p>
|
||||||
|
*
|
||||||
|
* <p>Natural ordering uses an internal comparator implementation, thus this
|
||||||
|
* method never returns null. See {@link #isNaturalOrdering()}.</p>
|
||||||
|
*
|
||||||
|
* @return the comparator being used, not null
|
||||||
|
*/
|
||||||
|
public Comparator<T> getComparator() {
|
||||||
|
return comparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Gets the maximum value in this range.</p>
|
||||||
|
*
|
||||||
|
* @return the maximum value in this range, not null
|
||||||
|
*/
|
||||||
|
public T getMaximum() {
|
||||||
|
return maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Gets the minimum value in this range.</p>
|
||||||
|
*
|
||||||
|
* @return the minimum value in this range, not null
|
||||||
|
*/
|
||||||
|
public T getMinimum() {
|
||||||
|
return minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Gets a suitable hash code for the range.</p>
|
||||||
|
*
|
||||||
|
* @return a hash code value for this object
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = hashCode;
|
||||||
|
if (hashCode == 0) {
|
||||||
|
result = 17;
|
||||||
|
result = 37 * result + getClass().hashCode();
|
||||||
|
result = 37 * result + minimum.hashCode();
|
||||||
|
result = 37 * result + maximum.hashCode();
|
||||||
|
hashCode = result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the intersection of {@code this} and an overlapping Range.
|
||||||
|
*
|
||||||
|
* @param other overlapping Range
|
||||||
|
* @return range representing the intersection of {@code this} and {@code other} ({@code this} if equal)
|
||||||
|
* @throws IllegalArgumentException if {@code other} does not overlap {@code this}
|
||||||
|
* @since 3.0.1
|
||||||
|
*/
|
||||||
|
public Range<T> intersectionWith(final Range<T> other) {
|
||||||
|
if (!this.isOverlappedBy(other)) {
|
||||||
|
throw new IllegalArgumentException(String.format(
|
||||||
|
"Cannot calculate intersection with non-overlapping range %s", other));
|
||||||
|
}
|
||||||
|
if (this.equals(other)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
final T min = getComparator().compare(minimum, other.minimum) < 0 ? other.minimum : minimum;
|
||||||
|
final T max = getComparator().compare(maximum, other.maximum) < 0 ? maximum : other.maximum;
|
||||||
|
return between(min, max, getComparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range is after the specified element.</p>
|
||||||
|
*
|
||||||
|
* @param element the element to check for, null returns false
|
||||||
|
* @return true if this range is entirely after the specified element
|
||||||
|
*/
|
||||||
|
public boolean isAfter(final T element) {
|
||||||
|
if (element == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return comparator.compare(element, minimum) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range is completely after the specified range.</p>
|
||||||
|
*
|
||||||
|
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||||
|
*
|
||||||
|
* @param otherRange the range to check, null returns false
|
||||||
|
* @return true if this range is completely after the specified range
|
||||||
|
* @throws RuntimeException if ranges cannot be compared
|
||||||
|
*/
|
||||||
|
public boolean isAfterRange(final Range<T> otherRange) {
|
||||||
|
if (otherRange == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isAfter(otherRange.maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range is before the specified element.</p>
|
||||||
|
*
|
||||||
|
* @param element the element to check for, null returns false
|
||||||
|
* @return true if this range is entirely before the specified element
|
||||||
|
*/
|
||||||
|
public boolean isBefore(final T element) {
|
||||||
|
if (element == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return comparator.compare(element, maximum) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range is completely before the specified range.</p>
|
||||||
|
*
|
||||||
|
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||||
|
*
|
||||||
|
* @param otherRange the range to check, null returns false
|
||||||
|
* @return true if this range is completely before the specified range
|
||||||
|
* @throws RuntimeException if ranges cannot be compared
|
||||||
|
*/
|
||||||
|
public boolean isBeforeRange(final Range<T> otherRange) {
|
||||||
|
if (otherRange == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isBefore(otherRange.minimum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range ends with the specified element.</p>
|
||||||
|
*
|
||||||
|
* @param element the element to check for, null returns false
|
||||||
|
* @return true if the specified element occurs within this range
|
||||||
|
*/
|
||||||
|
public boolean isEndedBy(final T element) {
|
||||||
|
if (element == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return comparator.compare(element, maximum) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Whether or not the Range is using the natural ordering of the elements.</p>
|
||||||
|
*
|
||||||
|
* <p>Natural ordering uses an internal comparator implementation, thus this
|
||||||
|
* method is the only way to check if a null comparator was specified.</p>
|
||||||
|
*
|
||||||
|
* @return true if using natural ordering
|
||||||
|
*/
|
||||||
|
public boolean isNaturalOrdering() {
|
||||||
|
return comparator == ComparableComparator.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range is overlapped by the specified range.</p>
|
||||||
|
*
|
||||||
|
* <p>Two ranges overlap if there is at least one element in common.</p>
|
||||||
|
*
|
||||||
|
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||||
|
*
|
||||||
|
* @param otherRange the range to test, null returns false
|
||||||
|
* @return true if the specified range overlaps with this
|
||||||
|
* range; otherwise, {@code false}
|
||||||
|
* @throws RuntimeException if ranges cannot be compared
|
||||||
|
*/
|
||||||
|
public boolean isOverlappedBy(final Range<T> otherRange) {
|
||||||
|
if (otherRange == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return otherRange.contains(minimum)
|
||||||
|
|| otherRange.contains(maximum)
|
||||||
|
|| contains(otherRange.minimum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Checks whether this range starts with the specified element.</p>
|
||||||
|
*
|
||||||
|
* @param element the element to check for, null returns false
|
||||||
|
* @return true if the specified element occurs within this range
|
||||||
|
*/
|
||||||
|
public boolean isStartedBy(final T element) {
|
||||||
|
if (element == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return comparator.compare(element, minimum) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Fits the given element into this range by returning the given element or, if out of bounds, the range minimum if
|
||||||
|
* below, or the range maximum if above.
|
||||||
|
* </p>
|
||||||
|
* <pre>
|
||||||
|
* Range<Integer> range = Range.between(16, 64);
|
||||||
|
* range.fit(-9) --> 16
|
||||||
|
* range.fit(0) --> 16
|
||||||
|
* range.fit(15) --> 16
|
||||||
|
* range.fit(16) --> 16
|
||||||
|
* range.fit(17) --> 17
|
||||||
|
* ...
|
||||||
|
* range.fit(63) --> 63
|
||||||
|
* range.fit(64) --> 64
|
||||||
|
* range.fit(99) --> 64
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param element the element to check for, not null
|
||||||
|
* @return the minimum, the element, or the maximum depending on the element's location relative to the range
|
||||||
|
* @since 3.10
|
||||||
|
*/
|
||||||
|
public T fit(final T element) {
|
||||||
|
Objects.requireNonNull(element);
|
||||||
|
if (isAfter(element)) {
|
||||||
|
return minimum;
|
||||||
|
}
|
||||||
|
if (isBefore(element)) {
|
||||||
|
return maximum;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Gets the range as a {@code String}.</p>
|
||||||
|
*
|
||||||
|
* <p>The format of the String is '[<i>min</i>..<i>max</i>]'.</p>
|
||||||
|
*
|
||||||
|
* @return the {@code String} representation of this range
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (toString == null) {
|
||||||
|
toString = "[" + minimum + ".." + maximum + "]";
|
||||||
|
}
|
||||||
|
return toString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Formats the receiver using the given format.</p>
|
||||||
|
*
|
||||||
|
* <p>This uses {@link java.util.Formattable} to perform the formatting. Three variables may
|
||||||
|
* be used to embed the minimum, maximum and comparator.
|
||||||
|
* Use {@code %1$s} for the minimum element, {@code %2$s} for the maximum element
|
||||||
|
* and {@code %3$s} for the comparator.
|
||||||
|
* The default format used by {@code toString()} is {@code [%1$s..%2$s]}.</p>
|
||||||
|
*
|
||||||
|
* @param format the format string, optionally containing {@code %1$s}, {@code %2$s} and {@code %3$s}, not null
|
||||||
|
* @return the formatted string, not null
|
||||||
|
*/
|
||||||
|
public String toString(final String format) {
|
||||||
|
return String.format(format, minimum, maximum, comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user