Compare commits

...
58 Commits
Author SHA1 Message Date
xtex 0d69e944f6 fix: #930(Search new Java registry location first) 2021-07-25 17:29:44 +08:00
huanghongxun 1dc4e3f730 fix: 1.17 Forge auto installation 2021-07-25 17:29:00 +08:00
huanghongxun 14d1bccd14 fix: update condition check for whether the game is modded. 2021-07-25 15:58:22 +08:00
huanghongxun 5de150b06e fix: Version.jar should be resolved to id of self if null instead of the id of ancestor. 2021-07-25 01:21:08 +08:00
huanghongxun e34dd39b9a fix: malformed locale name. Closes #925. 2021-07-24 14:56:18 +08:00
Haowei Wen e7d5faeeac fix: DirectoryStream not closed
https://github.com/huanghongxun/HMCL/commit/dc3167472175d023878f9ae32230b93964588bc1#r53347194
2021-07-12 13:19:16 +08:00
huanghongxun 46a7269087 chore: ignore hmcl.json 2021-07-10 23:25:26 +08:00
huanghongxun dc31674721 fix: Do not throw exception when path to cached jvm installations does not exist. 2021-07-10 23:24:31 +08:00
Glavo afe6bc7456 用户 JVM 参数优先于默认参数 (#903)
* close #885: use the GC selected by user by default

* fix: default JVM args conflict with user args

* Options take precedence over JVM args

* compile huge methods by default

* update settings.advanced.jvm_args text

* Using JVM args suppression options

* update settings.advanced.jvm_args text
2021-07-10 23:10:04 +08:00
huanghongxun 378fd04463 feat: placeholders in user customized game arguments. Closes #920 2021-07-10 23:09:11 +08:00
huanghongxun 3e751ba5c6 chore: some comments on "libraries_directory" 2021-07-10 23:08:39 +08:00
Glavo 8fc1ec7bb8 替换对 JDK 内置 Pack200 的依赖,允许在 JDK14 及更高版本上构建项目 (#904)
* fix: Pack200 has been removed

* Automatically add pack200 to class path

* add license header to Pack200Utils.java
2021-07-10 22:44:13 +08:00
huanghongxun e5326a3c4d fix: switch to "library_directory" defined by official launcher 2021-07-10 21:22:57 +08:00
huanghongxun fe279234d8 feat: Support "invalid" hash code in download info 2021-07-10 21:16:34 +08:00
huanghongxun ef9b041310 feat: Support placeholders in version.json. Closes #923 2021-07-10 21:16:33 +08:00
Ghost_chu 9db576d4bc 修复在manifest中的因拼写问题造成的与文档不符的问题 (#921) 2021-07-09 13:02:17 +08:00
Haowei Wen a84971abfc update english translation 2021-06-24 21:05:30 +08:00
Haowei Wen a7a79db842 update copyright year in settings page 2021-06-24 21:00:38 +08:00
Haowei Wen d5da14fb1e remove welcome page (reduce ~300k size) 2021-06-24 20:43:29 +08:00
Haowei Wen 0d2e287f67 load welcome.png only when necessary
This saves ~200ms startup time.
2021-06-23 06:08:11 +08:00
Haowei Wen c329f1cafb log error when background can't be loaded 2021-06-23 04:57:01 +08:00
Haowei Wen ab1a2f6fcb fix: NPE in ModpackInfoPage 2021-06-15 00:34:50 +08:00
Haowei Wen 463dc56702 delete license-header.txt since it's not used 2021-06-14 19:12:38 +08:00
Haowei Wen 88e23cd0d2 feat: speed up openjfx dependencies checking
SHA-1 of dependencies are defined in openjfx-dependencies.json
to avoid network requests during startup.

openjfx-dependencies.json can be generated using
tools/generate-openjfx-dependencies.sh
2021-06-14 14:22:56 +08:00
Haowei Wen 9776aafab6 fix missing license in some files 2021-06-14 02:45:17 +08:00
Haowei Wen fbb5a03e5d feat: select authlib-injector server after added
* The most recently added server will be at the top.
* In Add Account dialog, after adding an authlib-injector server,
  it will be automatically selected.
2021-06-13 22:58:05 +08:00
huanghongxun 7c11ce53f3 fix: NPE. Closes #896. 2021-06-12 18:43:54 +08:00
huanghongxun 66f4c022d5 fix: ask user whether to download java16. 2021-06-12 18:43:53 +08:00
yaoxi-std 4b36450f9a changed language files, fixed #898 2021-06-12 16:53:24 +08:00
huanghongxun 36b58bd1bb feat: Support CurseForge modpack with Fabric as modloader. Closes #897. 2021-06-12 01:25:10 +08:00
Glavo 0296b6f73d fix: export modpack 2021-06-08 19:06:09 +08:00
huanghongxun 447afaa76f fix: typo 2021-05-30 03:39:29 +08:00
huanghongxun a0191aa7c2 fix: lint 2021-05-30 03:37:55 +08:00
huanghongxun e7c9fcf767 feat: checkstyle in GitHub Actions 2021-05-30 03:35:26 +08:00
huanghongxun fc017515c5 feat: check style & format 2021-05-30 03:34:44 +08:00
huanghongxun 6d75c4d34f chore: C++ google style 2021-05-30 02:42:39 +08:00
huanghongxun 258a6628e4 fix: Cannot start application when code page is 65001. Closes #814.
This commit may cause breaking change: changing working directory to the directory where the executable locates in.
2021-05-30 02:25:31 +08:00
huanghongxun 228695d511 fix: NPE 2021-05-29 17:22:06 +08:00
Glavo c2af530abb feat: lookup JRE installation in more places. 2021-05-29 17:18:56 +08:00
huanghongxun 69bc826e30 feat: BMCLAPI now supports downloading Java 16. 2021-05-29 17:17:00 +08:00
huanghongxun 5d0944ed19 feat: lookup JRE installation in more places. 2021-05-29 17:06:08 +08:00
寒兮 43f4da5dd3 fix: GITHUB_SHA build version NullPointException 2021-05-29 17:05:12 +08:00
寒兮 c6ab2cec35 fix: short commit version use lower case 2021-05-29 17:05:12 +08:00
寒兮 b0dc689ba7 fix: use GITHUB_SHA instead of the git command 2021-05-29 17:05:12 +08:00
寒兮 e11b1f37b9 chore: artifacts version use git short commit 2021-05-29 17:05:12 +08:00
huanghongxun 3e5fac4df6 feat: lookup JRE installation in more places. 2021-05-29 16:50:07 +08:00
huanghongxun e63fa40822 feat: run github actions on pull request 2021-05-29 16:49:24 +08:00
huanghongxun e01096bff0 fix: lint 2021-05-29 15:15:40 +08:00
yaoxi-std 2a0cfb1125 fixed bugs 2021-05-29 01:23:58 +08:00
yaoxi-std d4f0a04ccd change NativesDir into LaunchOptions 2021-05-29 01:23:58 +08:00
yaoxi-std 7cbe24b35b add: allow customize LWJGL path (runs on M1) 2021-05-29 01:23:58 +08:00
huanghongxun b7da21e5ce fix: not select downloaded Java automatically. Closes #886. 2021-05-27 00:17:33 +08:00
Glavo 0ae1411f0a fix #861: Unable open zipfs for signed exe file 2021-05-25 15:05:42 +08:00
huanghongxun 66889cac27 chore: version 3.3 2021-05-25 13:25:43 +08:00
huanghongxun 67bd2ac386 fix: typo 2021-05-25 13:23:09 +08:00
huanghongxun 1f278c2a49 feat: publish unsigned artifacts by GitHub Actions 2021-05-25 13:20:33 +08:00
huanghongxun 1674f1828f fix: hmclauncher 2021-05-25 13:14:43 +08:00
huanghongxun 472a0a039f fix: fuck lfs 2021-05-25 13:13:33 +08:00
123 changed files with 1742 additions and 902 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
*.exe filter=lfs diff=lfs merge=lfs -text
+11 -2
View File
@@ -1,6 +1,6 @@
name: Java CI
on: [push]
on: [push, pull_request]
jobs:
build:
@@ -8,11 +8,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
java-package: jdk+fx
- name: Check style main
run: ./gradlew checkstyleMain
- name: Check style test
run: ./gradlew checkstyleTest
- name: Build with Gradle
run: ./gradlew build
- name: Upload Artifacts
uses: actions/upload-artifact@v2
with:
name: HMCL
path: HMCL/build/libs
+8 -2
View File
@@ -11,7 +11,6 @@ hs_err_pid*
*.log
.mine*
NVIDIA
*.json
# gradle build
/build/
@@ -38,4 +37,11 @@ NVIDIA
# netbeans
.nb-gradle
*.exe
*.exe
!/HMCL/src/main/resources/assets/HMCLauncher.exe
# macos
.DS_Store
# test
/hmcl.json
+17 -8
View File
@@ -1,9 +1,11 @@
buildscript {
repositories {
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
}
}
@@ -19,18 +21,21 @@ import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipFile
import java.nio.file.Files
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
import org.glavo.pack200.Pack200
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
def dev = null
def shortcommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7) ?: null
if (shortcommit != null && !shortcommit.isEmpty()) dev = "dev-" + shortcommit
def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "SNAPSHOT"
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.3"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
@@ -96,8 +101,10 @@ sourceSets {
}
compileJava11Java {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
if(JavaVersion.current() < JavaVersion.VERSION_11) {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
}
}
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
sourceCompatibility = 11
@@ -117,6 +124,7 @@ shadowJar {
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version,
'Class-Path': 'pack200.jar',
'Add-Opens': [
'java.base/java.lang',
'java.base/java.lang.reflect',
@@ -161,8 +169,9 @@ processResources {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
javaexec {
main = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
}
@@ -207,4 +216,4 @@ task makeExecutables(dependsOn: jar) doLast {
build.dependsOn makePackXz
build.dependsOn makePackGz
build.dependsOn makeExecutables
build.dependsOn makeExecutables
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.*;
import java.nio.file.Paths;
@@ -160,10 +161,15 @@ public final class Launcher extends Application {
return null;
}
} else {
File jarFile = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath());
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
try {
File jarFile = new File(URLDecoder.decode(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
} catch (UnsupportedEncodingException e) {
LOG.log(Level.WARNING, "Failed to decode jar path", e);
return null;
}
}
if (result.isEmpty())
return null;
@@ -43,6 +43,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
private Main() {
}
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
@@ -64,7 +64,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
try {
// TODO: Dirty implementation here
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
FileUtils.writeText(optionsFile, "lang:zh_cn\nforceUnicodeFont:true\n");
FileUtils.writeText(optionsFile, "lang:zh_CN\nforceUnicodeFont:true\n");
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
}
@@ -235,10 +235,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (version.getMainClass() != null &&
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
else if (LibraryAnalyzer.isModded(this, version))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
@@ -306,7 +303,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
@@ -31,15 +31,17 @@ import java.nio.file.Path;
* @author huangyuhui
*/
public final class HMCLModpackManager {
private HMCLModpackManager() {
}
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
@@ -68,6 +68,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -325,9 +326,19 @@ public final class LauncherHelper {
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
} else {
Controllers.dialog(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), MessageType.WARNING, () -> {
downloadJava(version.getJavaVersion(), profile).thenAccept(x -> onAccept.run());
});
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), () -> {
downloadJava(version.getJavaVersion(), profile)
.thenAcceptAsync(x -> {
try {
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
newAcceptableJava.ifPresent(setting::setJavaVersion);
} catch (InterruptedException e) {
LOG.log(Level.SEVERE, "Cannot list javas", e);
}
}, Platform::runLater).thenAccept(x -> onAccept.run());
}, null);
flag = true;
}
}
@@ -477,7 +488,7 @@ public final class LauncherHelper {
super.onStop(success, executor);
Platform.runLater(() -> {
if (!success) {
future.completeExceptionally(executor.getException());
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
} else {
future.complete(null);
}
@@ -26,11 +26,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
@@ -39,13 +35,15 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -263,4 +261,4 @@ public final class TexturesLoader {
}
return wr;
}
}
}
@@ -23,11 +23,11 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.util.Locale;
public class Settings {
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Settings {
private static Settings instance;
@@ -22,6 +22,7 @@ import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
@@ -43,8 +44,6 @@ import java.util.stream.Collectors;
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
public transient String id;
private boolean global = false;
public boolean isGlobal() {
@@ -66,7 +65,7 @@ public final class VersionSetting implements Cloneable {
* 1. Global settings.
* 2. Version settings.
* If a version claims that it uses global settings, its version setting will be disabled.
*
* <p>
* Defaults false because if one version uses global first, custom version file will not be generated.
*/
public boolean isUsesGlobal() {
@@ -119,6 +118,39 @@ public final class VersionSetting implements Cloneable {
defaultJavaPathProperty.set(defaultJavaPath);
}
/**
* 0 - .minecraft/versions/&lt;version&gt;/natives/<br/>
*/
private final ObjectProperty<NativesDirectoryType> nativesDirTypeProperty = new SimpleObjectProperty<>(this, "nativesDirType", NativesDirectoryType.VERSION_FOLDER);
public ObjectProperty<NativesDirectoryType> nativesDirTypeProperty() {
return nativesDirTypeProperty;
}
public NativesDirectoryType getNativesDirType() {
return nativesDirTypeProperty.get();
}
public void setNativesDirType(NativesDirectoryType nativesDirType) {
nativesDirTypeProperty.set(nativesDirType);
}
// Path to lwjgl natives directory
private final StringProperty nativesDirProperty = new SimpleStringProperty(this, "nativesDirProperty", "");
public StringProperty nativesDirProperty() {
return nativesDirProperty;
}
public String getNativesDir() {
return nativesDirProperty.get();
}
public void setNativesDir(String nativesDir) {
nativesDirProperty.set(nativesDir);
}
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public StringProperty javaDirProperty() {
@@ -336,7 +368,7 @@ public final class VersionSetting implements Cloneable {
/**
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* <p>
* Format: ip:port or without port.
*/
public String getServerIp() {
@@ -373,7 +405,7 @@ public final class VersionSetting implements Cloneable {
/**
* The width of Minecraft window, defaults 800.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -395,7 +427,7 @@ public final class VersionSetting implements Cloneable {
/**
* The height of Minecraft window, defaults 480.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -526,6 +558,8 @@ public final class VersionSetting implements Cloneable {
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
}
@Override
@@ -553,6 +587,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
return versionSetting;
}
@@ -584,6 +619,8 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
return obj;
}
@@ -613,6 +650,7 @@ public final class VersionSetting implements Cloneable {
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
@@ -621,6 +659,7 @@ public final class VersionSetting implements Cloneable {
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
return vs;
}
@@ -3,7 +3,10 @@ package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public class AwtUtils {
public final class AwtUtils {
private AwtUtils() {
}
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
@@ -66,6 +66,9 @@ public final class Controllers {
private static RootPage rootPage;
private static DecoratorController decorator;
private Controllers() {
}
public static Scene getScene() {
return scene;
}
@@ -31,6 +31,8 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public final class DialogController {
private DialogController() {
}
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
@@ -38,8 +40,8 @@ public final class DialogController {
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
res.set(it);
latch.countDown();
}, latch::countDown);
Controllers.dialog(pane);
});
@@ -430,6 +430,7 @@ public final class FXUtils {
* You should <b>only and always</b> use {@code bindEnum} as well as {@code unbindEnum} at the same time.
* @param comboBox the combo box being bound with the property which can be inferred by {@code bindEnum}.
* @see #bindEnum(JFXComboBox, Property)
* @deprecated Use {@link ExtendedProperties#selectedItemPropertyFor(ComboBox)}
*/
@SuppressWarnings("unchecked")
@Deprecated
@@ -102,11 +102,11 @@ public class InstallerItem extends Control {
}
public static class InstallerItemGroup {
public InstallerItem game = new InstallerItem(MINECRAFT);
public InstallerItem fabric = new InstallerItem(FABRIC);
public InstallerItem forge = new InstallerItem(FORGE);
public InstallerItem liteLoader = new InstallerItem(LITELOADER);
public InstallerItem optiFine = new InstallerItem(OPTIFINE);
public final InstallerItem game = new InstallerItem(MINECRAFT);
public final InstallerItem fabric = new InstallerItem(FABRIC);
public final InstallerItem forge = new InstallerItem(FORGE);
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
public InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
@@ -133,7 +133,7 @@ public class InstallerItem extends Control {
}
public InstallerItem[] getLibraries() {
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -87,8 +87,8 @@ public class AddAccountPane extends StackPane {
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboServers.getItems().addListener(onInvalidating(this::resetServerSelection));
resetServerSelection();
cboType.getItems().setAll(Accounts.FACTORIES);
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
@@ -174,12 +174,13 @@ public class AddAccountPane extends StackPane {
return unmodifiableList(result);
}
/**
* Selects the first server if no server is selected.
*/
private void selectDefaultServer() {
if (!cboServers.getItems().isEmpty() && cboServers.getSelectionModel().isEmpty()) {
cboServers.getSelectionModel().select(0);
private void resetServerSelection() {
if (!cboServers.getItems().isEmpty()) {
Platform.runLater(() -> {
// the selection will not be updated as expected
// if we call it immediately
cboServers.getSelectionModel().selectFirst();
});
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -129,7 +129,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
@FXML
private void onAddFinish() {
if (!config().getAuthlibInjectorServers().contains(serverBeingAdded)) {
config().getAuthlibInjectorServers().add(serverBeingAdded);
config().getAuthlibInjectorServers().add(0, serverBeingAdded);
}
fireEvent(new DialogCloseEvent());
}
@@ -23,7 +23,10 @@ import javafx.util.Duration;
public interface AnimationHandler {
Duration getDuration();
Pane getCurrentRoot();
Node getPreviousNode();
Node getCurrentNode();
}
@@ -99,9 +99,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
// we're getting dragged without getting a mouse press
dragStart = thumb.localToParent(me.getX(), me.getY());
}
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY() : cur.getX() - dragStart.getX();
double position = preDragThumbPos + dragPos / (trackLength() - thumbLength());
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable()) getSkinnable().requestFocus();
if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable())
getSkinnable().requestFocus();
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
if (!Double.isNaN(newValue)) {
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
@@ -42,11 +42,11 @@ public class SpinnerPane extends Control {
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading");
public void showSpinner() {
setLoading(true);
setLoading(true);
}
public void hideSpinner() {
setLoading(false);
setLoading(false);
}
public Node getContent() {
@@ -62,15 +62,15 @@ public class SpinnerPane extends Control {
}
public boolean isLoading() {
return loading.get();
return loading.get();
}
public BooleanProperty loadingProperty() {
return loading;
return loading;
}
public void setLoading(boolean loading) {
this.loading.set(loading);
this.loading.set(loading);
}
@Override
@@ -174,7 +174,7 @@ public interface TabControl {
}
}
public static class Tab {
class Tab {
private final StringProperty id = new SimpleStringProperty(this, "id");
private final StringProperty text = new SimpleStringProperty(this, "text");
private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected");
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,26 +18,18 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.Config;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -55,22 +47,22 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.logging.Level;
import java.util.stream.Stream;
import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
private final Decorator decorator;
private final ImageView welcomeView;
private final Navigator navigator;
private JFXDialog dialog;
@@ -89,27 +81,6 @@ public class DecoratorController {
decorator.onBackNavButtonActionProperty().set(e -> back());
decorator.onRefreshNavButtonActionProperty().set(e -> refresh());
welcomeView = new ImageView();
welcomeView.setImage(newImage("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
FXUtils.limitSize(welcomeView, 796, 517);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
new KeyFrame(Duration.ZERO, new KeyValue(welcomeView.opacityProperty(), 1.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), new KeyValue(welcomeView.opacityProperty(), 0.0D, Interpolator.EASE_BOTH)),
new KeyFrame(new Duration(300), e2 -> decorator.getContainer().remove(welcomeView))
);
nowAnimation.play();
});
if (switchedToNewUI()) {
if (config().getLocalization().getLocale() == Locale.CHINA) {
// currently, user guide is only available in Chinese
decorator.getContainer().setAll(welcomeView);
}
}
setupBackground();
setupAuthlibInjectorDnD();
@@ -119,17 +90,6 @@ public class DecoratorController {
return decorator;
}
/**
* @return true if the user is seeing the current version of UI for the first time.
*/
private boolean switchedToNewUI() {
if (config().getUiVersion() < Config.CURRENT_UI_VERSION) {
config().setUiVersion(Config.CURRENT_UI_VERSION);
return true;
}
return false;
}
// ==== Background ====
private void setupBackground() {
@@ -174,10 +134,10 @@ public class DecoratorController {
List<Path> candidates;
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(Files::isRegularFile)
.filter(Files::isReadable)
.filter(it -> {
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
String ext = getExtension(it).toLowerCase();
return ext.equals("png") || ext.equals("jpg");
})
.collect(toList());
} catch (IOException e) {
@@ -199,13 +159,23 @@ public class DecoratorController {
}
private Optional<Image> tryLoadImage(Path path) {
if (Files.isRegularFile(path)) {
try {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
if (!Files.isReadable(path))
return Optional.empty();
Image img;
try {
img = new Image(path.toAbsolutePath().toUri().toString());
} catch (IllegalArgumentException e) {
LOG.log(WARNING, "Couldn't load background image", e);
return Optional.empty();
}
return Optional.empty();
if (img.getException() != null) {
LOG.log(WARNING, "Couldn't load background image", img.getException());
return Optional.empty();
}
return Optional.of(img);
}
// ==== Navigation ====
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.wizard.*;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements WizardDisplayer {
@@ -87,21 +87,24 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null)
if (files != null) {
for (File it : files) {
CheckBoxTreeItem<String> subNode = getTreeItem(it, basePath + "/" + it.getName());
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected())
node.setIndeterminate(true);
node.getChildren().add(subNode);
if (subNode != null) {
node.setSelected(subNode.isSelected() || node.isSelected());
if (!subNode.isSelected()) {
node.setIndeterminate(true);
}
node.getChildren().add(subNode);
}
}
}
if (!node.isSelected()) node.setIndeterminate(false);
// Empty folder need not to be displayed.
if (node.getChildren().isEmpty())
if (node.getChildren().isEmpty()) {
return null;
}
}
HBox graphic = new HBox();
@@ -126,12 +129,13 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
private void getFilesNeeded(CheckBoxTreeItem<String> node, String basePath, List<String> list) {
if (node == null) return;
if (node.isSelected()) {
if (node.isSelected() || node.isIndeterminate()) {
if (basePath.length() > "minecraft/".length())
list.add(StringUtils.substringAfter(basePath, "minecraft/"));
for (TreeItem<String> child : node.getChildren()) {
if (child instanceof CheckBoxTreeItem)
if (child instanceof CheckBoxTreeItem) {
getFilesNeeded(((CheckBoxTreeItem<String>) child), basePath + "/" + child.getValue(), list);
}
}
}
}
@@ -78,13 +78,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
private final SimpleStringProperty url = new SimpleStringProperty("");
private final SimpleBooleanProperty forceUpdate = new SimpleBooleanProperty();
private final SimpleBooleanProperty packWithLauncher = new SimpleBooleanProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty();
private final SimpleStringProperty fileApi = new SimpleStringProperty("");
private final SimpleIntegerProperty minMemory = new SimpleIntegerProperty(0);
private final SimpleStringProperty authlibInjectorServer = new SimpleStringProperty();
private final SimpleStringProperty launchArguments = new SimpleStringProperty("");
private final SimpleStringProperty javaArguments = new SimpleStringProperty("");
private final ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty();
private final SimpleStringProperty mcbbsThreadId = new SimpleStringProperty("");
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
this.controller = controller;
@@ -535,8 +535,12 @@ public abstract class SettingsView extends StackPane {
}
protected abstract void onUpdate();
protected abstract void onHelp();
protected abstract void onExportLogs();
protected abstract void onSponsor();
protected abstract void clearCacheDirectory();
}
@@ -68,6 +68,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
{
setMessage(i18n("profile.already_exists"));
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
@@ -35,7 +35,11 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -76,6 +80,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
@FXML private JFXTextField txtWidth;
@FXML private JFXTextField txtHeight;
@FXML private JFXTextField txtMaxMemory;
@FXML private JFXTextField txtNativesPath;
@FXML private JFXTextField txtJVMArgs;
@FXML private JFXTextField txtGameArgs;
@FXML private JFXTextField txtMetaspace;
@@ -88,11 +93,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
@FXML private JFXComboBox<LauncherVisibility> cboLauncherVisibility;
@FXML private JFXCheckBox chkFullscreen;
@FXML private Label lblPhysicalMemory;
@FXML private Label lblCustomizedNativesPath;
@FXML private JFXToggleButton chkNoJVMArgs;
@FXML private JFXToggleButton chkNoGameCheck;
@FXML private JFXToggleButton chkNoJVMCheck;
@FXML private MultiFileItem<JavaVersion> javaItem;
@FXML private MultiFileItem<GameDirectoryType> gameDirItem;
@FXML private MultiFileItem<NativesDirectoryType> nativesDirItem;
@FXML private JFXToggleButton chkShowLogs;
@FXML private ImagePickerItem iconPickerItem;
@FXML private JFXCheckBox chkEnableSpecificSettings;
@@ -138,6 +145,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.createChildren(i18n("settings.advanced.game_dir.independent"), GameDirectoryType.VERSION_FOLDER)
));
nativesDirItem.setCustomUserData(NativesDirectoryType.CUSTOM);
nativesDirItem.loadChildren(Arrays.asList(
nativesDirItem.createChildren(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER)
));
chkEnableSpecificSettings.selectedProperty().addListener((a, b, newValue) -> {
if (versionId == null) return;
@@ -179,6 +191,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.unbindInt(txtMaxMemory, lastVersionSetting.maxMemoryProperty());
FXUtils.unbindString(javaItem.getTxtCustom(), lastVersionSetting.javaDirProperty());
FXUtils.unbindString(gameDirItem.getTxtCustom(), lastVersionSetting.gameDirProperty());
FXUtils.unbindString(nativesDirItem.getTxtCustom(), lastVersionSetting.nativesDirProperty());
FXUtils.unbindString(txtJVMArgs, lastVersionSetting.javaArgsProperty());
FXUtils.unbindString(txtGameArgs, lastVersionSetting.minecraftArgsProperty());
FXUtils.unbindString(txtMetaspace, lastVersionSetting.permSizeProperty());
@@ -198,6 +211,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind();
nativesDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.nativesDirTypeProperty());
nativesDirItem.subtitleProperty().unbind();
}
// unbind data fields
@@ -209,6 +225,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.bindInt(txtMaxMemory, versionSetting.maxMemoryProperty());
FXUtils.bindString(javaItem.getTxtCustom(), versionSetting.javaDirProperty());
FXUtils.bindString(gameDirItem.getTxtCustom(), versionSetting.gameDirProperty());
FXUtils.bindString(nativesDirItem.getTxtCustom(), versionSetting.nativesDirProperty());
FXUtils.bindString(txtJVMArgs, versionSetting.javaArgsProperty());
FXUtils.bindString(txtGameArgs, versionSetting.minecraftArgsProperty());
FXUtils.bindString(txtMetaspace, versionSetting.permSizeProperty());
@@ -240,6 +257,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
nativesDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
lastVersionSetting = versionSetting;
@@ -46,7 +46,9 @@ import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class Versions {
public final class Versions {
private Versions() {
}
public static void deleteVersion(Profile profile, String version) {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER;
@@ -156,7 +158,7 @@ public class Versions {
Controllers.getRootPage().checkAccount();
else if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id))
Controllers.dialog(i18n("version.empty.launch"), i18n("launch.failed"), MessageDialogPane.MessageType.ERROR, () -> {
Controllers.getRootPage().getSelectionModel().select(Controllers.getRootPage().getGameTab());
Controllers.getRootPage().getSelectionModel().select(Controllers.getRootPage().getGameTab());
});
else
return true;
@@ -50,7 +50,6 @@ public class WorldExportPage extends WizardSinglePage {
return new WorldExportPageSkin(this);
}
public StringProperty pathProperty() {
return path;
}
@@ -94,5 +94,4 @@ public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
getChildren().setAll(container);
}
}
@@ -22,11 +22,17 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
public interface Navigation {
void onStart();
void onNext();
void onPrev(boolean cleanUp);
boolean canPrev();
void onFinish();
void onEnd();
void onCancel();
enum NavigationDirection {
@@ -23,9 +23,13 @@ import java.util.Map;
public interface WizardProvider {
void start(Map<String, Object> settings);
Object finish(Map<String, Object> settings);
Node createPage(WizardController controller, int step, Map<String, Object> settings);
boolean cancel();
default boolean cancelIfCannotGoBack() {
return false;
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
@@ -26,7 +27,6 @@ import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
class HMCLDownloadTask extends FileDownloadTask {
@@ -52,7 +52,7 @@ class HMCLDownloadTask extends FileDownloadTask {
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200.newUnpacker().unpack(in, out);
Pack200Utils.unpack(in, out);
}
break;
@@ -21,9 +21,9 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
import java.util.Optional;
@@ -38,7 +38,7 @@ public class RemoteVersion {
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
if (SystemUtils.JRE_CAPABILITY_PACK200 && packXZUrl != null && packXZHash != null) {
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
return new RemoteVersion(version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash));
} else if (jarUrl != null && jarHash != null) {
return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash));
@@ -1,3 +1,20 @@
/*
* 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.nio.file.Path;
@@ -8,7 +25,10 @@ import java.util.Set;
*
* @author ZekerZhayard
*/
public class JavaFXPatcher {
public final class JavaFXPatcher {
private JavaFXPatcher() {
}
public static void patch(Set<String> modules, Path... jarPaths) {
// Nothing to do with Java 8
}
@@ -1,55 +1,144 @@
/*
* 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/>.
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2017-2021 Matthew Coley
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.Collectors;
import static java.lang.Class.forName;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA;
/**
* Utility for patching self when missing dependencies.
* Copy from https://github.com/Col-E/Recaf/blob/master/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java
*
* @author Matt
*/
public class SelfDependencyPatcher {
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
private static final String DEFAULT_JFX_VERSION = "16";
private static final Map<String, String> JFX_DEPENDENCIES = new HashMap<>();
import java.awt.Dialog;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
static {
addJfxDependency("base");
addJfxDependency("controls");
addJfxDependency("fxml");
addJfxDependency("graphics");
addJfxDependency("media");
// Fix #874: Remove the dependency on javafx.swing
// addJfxDependency("swing");
addJfxDependency("web");
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
// From: https://github.com/Col-E/Recaf/blob/7378b397cee664ae81b7963b0355ef8ff013c3a7/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java
public final class SelfDependencyPatcher {
private SelfDependencyPatcher() {
}
private static void addJfxDependency(String name) {
JFX_DEPENDENCIES.put("javafx." + name, jfxUrl(name));
static class DependencyDescriptor {
private static final String REPOSITORY_URL = "https://maven.aliyun.com/repository/central/";
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
private static String currentArchClassifier() {
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return "linux";
case OSX:
return "mac";
default:
return "win";
}
}
public String module;
public String groupId;
public String artifactId;
public String version;
public Map<String, String> sha1;
public String filename() {
return artifactId + "-" + version + "-" + currentArchClassifier() + ".jar";
}
public String sha1() {
return sha1.get(currentArchClassifier());
}
public String url() {
return REPOSITORY_URL + groupId.replace('.', '/') + "/" + artifactId + "/" + version + "/" + filename();
}
public Path localPath() {
return DEPENDENCIES_DIR_PATH.resolve(filename());
}
}
private static final String DEPENDENCIES_LIST_FILE = "/assets/openjfx-dependencies.json";
private static List<DependencyDescriptor> readDependencies() {
String content;
try (InputStream in = SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE)) {
content = IOUtils.readFullyAsString(in, UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return new Gson().fromJson(content, TypeToken.getParameterized(List.class, DependencyDescriptor.class).getType());
}
private static final List<DependencyDescriptor> JFX_DEPENDENCIES = readDependencies();
/**
* Patch in any missing dependencies, if any.
*/
@@ -81,17 +170,17 @@ public class SelfDependencyPatcher {
// Otherwise we're free to download in Java 11+
LOG.info("Missing JavaFX dependencies, attempting to patch in missing classes");
// Check if dependencies need to be downloaded
if (!hasCachedDependencies()) {
LOG.info(" - No local cache, downloading dependencies...");
// Download missing dependencies
List<DependencyDescriptor> missingDependencies = checkMissingDependencies();
if (!missingDependencies.isEmpty()) {
try {
fetchDependencies();
} catch (Exception ex) {
throw new PatchException("Failed to download dependencies", ex);
fetchDependencies(missingDependencies);
} catch (IOException e) {
throw new PatchException("Failed to download dependencies", e);
}
} else {
LOG.info(" - Local cache found!");
}
// Add the dependencies
try {
loadFromCache();
@@ -106,17 +195,21 @@ public class SelfDependencyPatcher {
/**
* Inject them into the current classpath.
*
* @throws IOException
* When the locally cached dependency urls cannot be resolved.
* @throws ReflectiveOperationException
* When the call to add these urls to the system classpath failed.
* @throws IOException When the locally cached dependency urls cannot be resolved.
* @throws ReflectiveOperationException When the call to add these urls to the system classpath failed.
*/
private static void loadFromCache() throws IOException, ReflectiveOperationException {
LOG.info(" - Loading dependencies...");
List<Path> jarPaths = new ArrayList<>();
List<String> jfxDepFile = JFX_DEPENDENCIES.values().stream().map(SelfDependencyPatcher::getFileName).collect(Collectors.toList());
Files.walk(DEPENDENCIES_DIR_PATH).filter(p -> jfxDepFile.contains(p.getFileName().toString())).forEach(jarPaths::add);
JavaFXPatcher.patch(JFX_DEPENDENCIES.keySet(), jarPaths.toArray(new Path[0]));
Set<String> modules = JFX_DEPENDENCIES.stream()
.map(it -> it.module)
.collect(toSet());
Path[] jars = JFX_DEPENDENCIES.stream()
.map(it -> it.localPath())
.toArray(Path[]::new);
JavaFXPatcher.patch(modules, jars);
}
/**
@@ -124,106 +217,57 @@ public class SelfDependencyPatcher {
*
* @throws IOException When the files cannot be fetched or saved.
*/
private static void fetchDependencies() throws Exception {
// Get dir to store dependencies in
Path dependenciesDir = DEPENDENCIES_DIR_PATH;
if (!Files.isDirectory(dependenciesDir)) {
Files.createDirectories(dependenciesDir);
}
private static void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
ProgressFrame dialog = new ProgressFrame(i18n("download.javafx"));
ForkJoinTask<Void> task = ForkJoinPool.commonPool().submit(() -> {
// Download each dependency
Collection<String> dependencies = JFX_DEPENDENCIES.values();
int i = 1;
for (String dependencyUrlPath : dependencies) {
URL depURL = new URL(dependencyUrlPath);
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(dependencyUrlPath));
int finalI = i;
SwingUtilities.invokeLater(() -> {
dialog.setStatus(dependencyUrlPath);
dialog.setProgress(finalI, dependencies.size());
});
Files.copy(depURL.openStream(), dependencyFilePath, StandardCopyOption.REPLACE_EXISTING);
checksum(dependencyFilePath, dependencyUrlPath);
i++;
}
dialog.dispose();
return null;
});
dialog.setVisible(true);
task.get();
int progress = 0;
for (DependencyDescriptor dependency : dependencies) {
int currentProgress = ++progress;
SwingUtilities.invokeLater(() -> {
dialog.setStatus(dependency.url());
dialog.setProgress(currentProgress, dependencies.size());
});
LOG.info("Downloading " + dependency.url());
Files.createDirectories(dependency.localPath().getParent());
Files.copy(new URL(dependency.url()).openStream(), dependency.localPath(), StandardCopyOption.REPLACE_EXISTING);
verifyChecksum(dependency);
}
dialog.dispose();
}
/**
* @return {@code true} when the dependencies directory has files in it.
*/
private static boolean hasCachedDependencies() {
if (!Files.isDirectory(DEPENDENCIES_DIR_PATH))
return false;
private static List<DependencyDescriptor> checkMissingDependencies() {
List<DependencyDescriptor> missing = new ArrayList<>();
for (String url : JFX_DEPENDENCIES.values()) {
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(url));
if (!Files.exists(dependencyFilePath))
return false;
for (DependencyDescriptor dependency : JFX_DEPENDENCIES) {
if (!Files.exists(dependency.localPath())) {
missing.add(dependency);
continue;
}
try {
checksum(dependencyFilePath, url);
verifyChecksum(dependency);
} catch (ChecksumMismatchException e) {
return false;
} catch (IOException ignored) {
// Ignore other situations
LOG.warning("Corrupted dependency " + dependency.filename() + ": " + e.getMessage());
missing.add(dependency);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return true;
return missing;
}
private static void checksum(Path dependencyFilePath, String dependencyUrlPath) throws IOException {
String expectedHash = NetworkUtils.doGet(NetworkUtils.toURL(dependencyUrlPath + ".sha1"));
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependencyFilePath));
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
String expectedHash = dependency.sha1();
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependency.localPath()));
if (!expectedHash.equalsIgnoreCase(actualHash)) {
throw new ChecksumMismatchException("SHA-1", expectedHash, actualHash);
}
}
/**
* @param url Full url path.
* @return Name of file at url.
*/
private static String getFileName(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
/**
* @param component Name of the component.
* @return Formed URL for the component.
*/
private static String jfxUrl(String component) {
return jfxUrl(component, DEFAULT_JFX_VERSION);
}
private static String jfxUrl(String component, String version) {
// https://repo1.maven.org/maven2/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar
return String.format("https://maven.aliyun.com/repository/central/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
component, version, component, version, getMvnName());
// return String.format("https://bmclapi.bangbang93.com/maven/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
// component, version, component, version, getMvnName());
}
private static String getMvnName() {
switch (OperatingSystem.CURRENT_OS) {
case LINUX:
return "linux";
case OSX:
return "mac";
default:
return "win";
}
}
public static class PatchException extends Exception {
PatchException(String message, Throwable cause) {
super(message, cause);
@@ -234,8 +278,6 @@ public class SelfDependencyPatcher {
}
public static class ProgressFrame extends JDialog {
private int totalTasks = 0;
private int finishedTasks = 0;
private final JProgressBar progressBar;
private final JLabel progressText;
@@ -253,10 +295,10 @@ public class SelfDependencyPatcher {
setLocationRelativeTo(null);
GridBagLayout gridBagLayout = new GridBagLayout();
gridBagLayout.columnWidths = new int[]{600, 0};
gridBagLayout.rowHeights = new int[]{0, 0, 0, 200};
gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE};
gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 1.0};
gridBagLayout.columnWidths = new int[] { 600, 0 };
gridBagLayout.rowHeights = new int[] { 0, 0, 0, 200 };
gridBagLayout.columnWeights = new double[] { 1.0, Double.MIN_VALUE };
gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 1.0 };
panel.setLayout(gridBagLayout);
progressText = new JLabel("");
@@ -28,6 +28,8 @@ import java.util.Locale;
import java.util.ResourceBundle;
public final class Locales {
private Locales() {
}
public static final SupportedLocale DEFAULT = new SupportedLocale(Locale.getDefault(), "lang.default");
@@ -61,12 +63,18 @@ public final class Locales {
public static SupportedLocale getLocaleByName(String name) {
if (name == null) return DEFAULT;
switch (name.toLowerCase()) {
case "en": return EN;
case "zh": return ZH;
case "zh_cn": return ZH_CN;
case "es": return ES;
case "ru": return RU;
default: return DEFAULT;
case "en":
return EN;
case "zh":
return ZH;
case "zh_cn":
return ZH_CN;
case "es":
return ES;
case "ru":
return RU;
default:
return DEFAULT;
}
}
@@ -1,22 +1,31 @@
/*
* 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.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import jdk.internal.loader.BuiltinClassLoader;
@@ -25,11 +34,34 @@ import jdk.internal.loader.BuiltinClassLoader;
*
* @author ZekerZhayard
*/
public class JavaFXPatcher {
public final class JavaFXPatcher {
private JavaFXPatcher() {
}
private static final String[] addOpens = {
"javafx.base/com.sun.javafx.runtime",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"javafx.controls/com.sun.javafx.scene.control"
};
private static final String[] addExports = {
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.graphics/com.sun.javafx.stage",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"javafx.controls/com.sun.javafx.scene.control"
};
/**
* Add JavaFX to module path at runtime.
*
* @param modules All module names
* @param modules All module names
* @param jarPaths All jar paths
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
*/
@@ -47,32 +79,26 @@ public class JavaFXPatcher {
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
// Add-Exports and Add-Opens
try (
FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + JavaFXPatcher.class.getProtectionDomain().getCodeSource().getLocation().toURI()), Map.of("create", "true"));
InputStream stream = Files.newInputStream(fs.getPath("/META-INF/MANIFEST.MF"))
) {
try {
// Some hacks
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
Attributes attributes = new Manifest(stream).getMainAttributes();
addExportsOrOpens(attributes.getValue("Add-Exports"), layer, lookup.findVirtual(Module.class, "implAddExportsToAllUnnamed", MethodType.methodType(void.class, String.class)));
addExportsOrOpens(attributes.getValue("Add-Opens"), layer, lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class)));
addExportsOrOpens(addExports, layer, lookup.findVirtual(Module.class, "implAddExportsToAllUnnamed", MethodType.methodType(void.class, String.class)));
addExportsOrOpens(addOpens, layer, lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class)));
} catch (Throwable t) {
throw new ReflectiveOperationException(t);
}
}
private static void addExportsOrOpens(String targets, ModuleLayer layer, MethodHandle handle) {
for (String target : targets.split("\\s+")) {
private static void addExportsOrOpens(String[] targets, ModuleLayer layer, MethodHandle handle) {
for (String target : targets) {
String[] name = target.split("/", 2); // <module>/<package>
if (name[0].startsWith("javafx.")) {
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
}
}
Binary file not shown.
@@ -53,7 +53,7 @@
</JFXTextField>
</right>
</BorderPane>
<BorderPane> <!-- Launcher Visibility -->
<left>
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
@@ -128,6 +128,9 @@
fx:id="txtPrecallingCommand" StackPane.margin="$insets"/>
<JFXTextField labelFloat="true" promptText="%settings.advanced.server_ip" styleClass="fit-width"
fx:id="txtServerIP" StackPane.margin="$insets"/>
<MultiFileItem fx:id="nativesDirItem" title="%settings.advanced.natives_directory" chooserTitle="%settings.advanced.natives_directory.choose"
hasSubtitle="true" customText="%settings.custom" directory="true" />
<BorderPane>
<left>
<Label BorderPane.alignment="CENTER_LEFT" text="%settings.advanced.no_jvm_args"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Contributors: dxNeil, machinesmith42
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2020 huangyuhui.
about.copyright.statement=Copyright (c) 2021 huangyuhui.
about.author=Author
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Thanks to
@@ -196,7 +196,7 @@ launch.state.dependencies=Dependencies
launch.state.done=Done
launch.state.logging_in=Logging In
launch.state.modpack=Loading modpack
launch.state.waiting_launching=Launching modpack
launch.state.waiting_launching=Launching Game
launch.wrong_javadir=Invalid Java directory, default Java path will be applied.
launcher=Launcher
@@ -364,13 +364,16 @@ settings.advanced.dont_check_jvm_validity=Don't check whether JVM can launch the
settings.advanced.game_dir.default=Standard (.minecraft/)
settings.advanced.game_dir.independent=Independent (.minecraft/versions/<version name>/, except for assets and libraries)
settings.advanced.java_permanent_generation_space=PermGen Space/MB
settings.advanced.jvm_args=Java VM Arguments
settings.advanced.jvm_args=Java VM Arguments (overriding defaults)
settings.advanced.launcher_visibility.close=Close the launcher when the game launched.
settings.advanced.launcher_visibility.hide=Hide the launcher when the game launched.
settings.advanced.launcher_visibility.hide_and_reopen=Hide the launcher and re-open when game closes.
settings.advanced.launcher_visibility.keep=Keep the launcher visible.
settings.advanced.launcher_visible=Launcher Visibility
settings.advanced.minecraft_arguments=Minecraft Arguments
settings.advanced.natives_directory=Local Library Path (LWJGL)
settings.advanced.natives_directory.choose=Choose Local Library Path
settings.advanced.natives_directory.default=Standerd (.minecraft/versions/<version name>/natives/)
settings.advanced.no_jvm_args=No Default JVM Args
settings.advanced.precall_command=Pre-Launch command (will be executed before game starts)
settings.advanced.server_ip=Server Address
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Contributors: poopingpenis
about.copyright=Copyright
about.copyright.statement=Copyright (c) 2020 huangyuhui.
about.copyright.statement=Copyright (c) 2021 huangyuhui.
about.author=Autor
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Gracias a
@@ -335,13 +335,16 @@ settings.advanced.dont_check_jvm_validity=No verificar si JVM puede lanzar el ju
settings.advanced.game_dir.default=Por defecto (.minecraft/)
settings.advanced.game_dir.independent=Independiente (.minecraft/versions/<version name>/, excepto assets,libraries)
settings.advanced.java_permanent_generation_space=PermGen Space/MB
settings.advanced.jvm_args=Java VM Arguments
settings.advanced.jvm_args=Java VM Arguments (overriding defaults)
settings.advanced.launcher_visibility.close=Cerrar launcher cuando juego es iniciado.
settings.advanced.launcher_visibility.hide=Esconder launcher cuando juego is iniciado.
settings.advanced.launcher_visibility.hide_and_reopen=Esconder launcher y reabrir cuando juego cierre.
settings.advanced.launcher_visibility.keep=Mantener launcher visible.
settings.advanced.launcher_visible=Visibilidad de launcher
settings.advanced.minecraft_arguments=Minecraft Arguments
settings.advanced.natives_directory=Loka biblioteko- vojo (LWJGL)
settings.advanced.natives_directory.choose=Elektu lokan bibliotekvojn
settings.advanced.natives_directory.default=Por defecto (.minecraft/versions/<version name>/natives/)
settings.advanced.no_jvm_args=No JVM Args por defecto
settings.advanced.precall_command=Comando pre-inicio (será ejecutado antes de inicio del juego)
settings.advanced.server_ip=Host de servidor
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Содействующие: Botylyov Yury, The_BadUser
about.copyright=Авторские права
about.copyright.statement=(C) 2020 huangyuhui.
about.copyright.statement=(C) 2021 huangyuhui.
about.author=Автор
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=Отдельное спасибо
@@ -359,6 +359,9 @@ settings.advanced.launcher_visibility.hide_and_reopen=Сворачивать л
settings.advanced.launcher_visibility.keep=Оставлять лаунчер видимым.
settings.advanced.launcher_visible=Видимость лаунчера
settings.advanced.minecraft_arguments=Параметры Minecraft
settings.advanced.natives_directory=путь к локальной библиотеке (LWJGL)
settings.advanced.natives_directory.choose=Выберите путь к локальной библиотеке
settings.advanced.natives_directory.default=По умолчанию (.minecraft/versions/<версия>/natives/)
settings.advanced.no_jvm_args=По умолчанию параметры JVM отсутствуют
settings.advanced.precall_command=Команда предзапуска (будет выполнено до запуска игры)
settings.advanced.server_ip=Адрес сервера
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Contributors: pan93412
about.copyright=著作權
about.copyright.statement=著作權所有 (c) 2020 huangyuhui。
about.copyright.statement=著作權所有 (c) 2021 huangyuhui。
about.author=作者
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鳴謝
@@ -358,13 +358,16 @@ settings.advanced.dont_check_jvm_validity=不檢查 JVM 與遊戲的相容性
settings.advanced.game_dir.default=預設(.minecraft/)
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(不必填寫,格式: MB)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫,覆蓋默認設置)
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟
settings.advanced.launcher_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
settings.advanced.natives_directory=本地庫路徑(LWJGL)
settings.advanced.natives_directory.choose=選擇本地庫路徑
settings.advanced.natives_directory.default=預設(.minecraft/versions/<版本名>/natives/)
settings.advanced.no_jvm_args=不使用預設的 JVM 參數
settings.advanced.precall_command=啟動前執行指令(不必填寫,將在遊戲啟動前呼叫使用)
settings.advanced.server_ip=直接進入伺服器 IP 位址(不必填寫,啟動遊戲後直接進入對應伺服器)
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
# 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
@@ -18,7 +18,7 @@
# Contributors: huangyuhui
about.copyright=版权
about.copyright.statement=版权所有 (c) 2020 huangyuhui.
about.copyright.statement=版权所有 (c) 2021 huangyuhui.
about.author=作者
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鸣谢
@@ -364,13 +364,16 @@ settings.advanced.dont_check_jvm_validity=不检查 JVM 与游戏的兼容性
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,单位 MB)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写,覆盖默认设置)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
settings.advanced.launcher_visibility.hide_and_reopen=隐藏启动器并在游戏结束后重新打开
settings.advanced.launcher_visibility.keep=保持启动器可见
settings.advanced.launcher_visible=启动器可见性
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
settings.advanced.natives_directory=本地库路径(LWJGL)
settings.advanced.natives_directory.choose=选择本地库路径
settings.advanced.natives_directory.default=预设(.minecraft/versions/<版本名>/natives/)
settings.advanced.no_jvm_args=不添加默认的 JVM 参数
settings.advanced.precall_command=启动前执行命令(不必填写,将在游戏启动前调用)
settings.advanced.server_ip=直入服务器 IP 地址(不必填写,启动游戏后直接进入对应服务器)
@@ -0,0 +1,68 @@
[
{
"module": "javafx.base",
"groupId": "org.openjfx",
"artifactId": "javafx-base",
"version": "16",
"sha1": {
"linux": "7d6b85ec89e99ea40c2a63bb1f23d43e05cd2557",
"mac": "ea97d8a5ab95d070df86de1215f29f08378c98c8",
"win": "e0564fea3f27dd3a10fa62e5005612333fc244bc"
}
},
{
"module": "javafx.controls",
"groupId": "org.openjfx",
"artifactId": "javafx-controls",
"version": "16",
"sha1": {
"linux": "116b127e512d23ddb84c62017c256e7f67f5b9eb",
"mac": "8df933b095bdd8203dc937e39e3a9f199654e47e",
"win": "65cdeae29c67d25932dcc66ca4f7d269923631ba"
}
},
{
"module": "javafx.fxml",
"groupId": "org.openjfx",
"artifactId": "javafx-fxml",
"version": "16",
"sha1": {
"linux": "c7147c450773c3d4fd038ac9d1a6fdebbc3c11e0",
"mac": "7e4f07a331991485560673a398d6988cc805b67f",
"win": "369f646fc29d2223cc98cfc3c059f2409b3e8fcb"
}
},
{
"module": "javafx.graphics",
"groupId": "org.openjfx",
"artifactId": "javafx-graphics",
"version": "16",
"sha1": {
"linux": "01bf2fd9f083daa2f492c4c58c4f7d5acb6f4b7d",
"mac": "a45de3d40e217f7ba22cecf54abc281d13a172f6",
"win": "0cd18f8477818b40cfdbcfec7e2fc15f632f0a2f"
}
},
{
"module": "javafx.media",
"groupId": "org.openjfx",
"artifactId": "javafx-media",
"version": "16",
"sha1": {
"linux": "10edb5fd8eb64b3312b31225c396e1850b9d178b",
"mac": "9e0248083267cdc22b2b698f0b0b62b7399ad32d",
"win": "6d776658906fc25051734bc6b03f214cc72c808e"
}
},
{
"module": "javafx.web",
"groupId": "org.openjfx",
"artifactId": "javafx-web",
"version": "16",
"sha1": {
"linux": "522f626e1798a7d589d9b187ca5c74c5c2f8a0ee",
"mac": "32b0e13b649ea828e7d185187f178680cc04e564",
"win": "d2f80089a4ae1629a5ec14637abf4f6d70b96dab"
}
}
]
@@ -17,19 +17,19 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.io.NetworkUtils.decodeURL;
import java.util.Optional;
import java.util.function.Consumer;
import javafx.event.EventHandler;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.io.NetworkUtils.decodeURL;
/**
* @author yushijinhun
* @see https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#dnd-%E6%96%B9%E5%BC%8F%E6%B7%BB%E5%8A%A0-yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF
* @see <a href="https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#dnd-%E6%96%B9%E5%BC%8F%E6%B7%BB%E5%8A%A0-yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF">Launcher Technical Specification for Authlib-Injector</a>
*/
public final class AuthlibInjectorDnD {
@@ -31,7 +31,7 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
*
* @author huangyuhui
*/
public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
public final class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
public static final OfflineAccountFactory INSTANCE = new OfflineAccountFactory();
private OfflineAccountFactory() {
@@ -17,20 +17,13 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import static java.util.Collections.unmodifiableMap;
import com.google.gson.*;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import static java.util.Collections.unmodifiableMap;
public class PropertyMapSerializer implements JsonSerializer<Map<String, String>>, JsonDeserializer<Map<String, String>> {
@@ -57,4 +50,4 @@ public class PropertyMapSerializer implements JsonSerializer<Map<String, String>
});
return result;
}
}
}
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -78,7 +78,6 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
@Override
public String injectURL(String baseURL) {
if (baseURL.contains("v1/products/java-runtime")) return baseURL;
return baseURL
.replace("https://bmclapi2.bangbang93.com", apiRoot)
.replace("https://launchermeta.mojang.com", apiRoot)
@@ -38,7 +38,7 @@ public abstract class GameBuilder {
}
/**
* The new game version name, for .minecraft/<version name>.
* The new game version name, for .minecraft/&lt;version name&gt;.
*
* @param name the name of new game version.
*/
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.VersionProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -84,6 +85,11 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
return modLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> modLauncher.equals(patch.getMainClass()));
}
public boolean hasBootstrapLauncher() {
final String bootstrapLauncher = "cpw.mods.bootstraplauncher.BootstrapLauncher";
return bootstrapLauncher.equals(version.getMainClass()) || version.getPatches().stream().anyMatch(patch -> bootstrapLauncher.equals(patch.getMainClass()));
}
private Version removingMatchedLibrary(Version version, String libraryId) {
LibraryType type = LibraryType.fromPatchId(libraryId);
if (type == null) return version;
@@ -141,10 +147,16 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
return new LibraryAnalyzer(version, libraries);
}
public static boolean isModded(VersionProvider provider, Version version) {
Version resolvedVersion = version.resolve(provider);
String mainClass = resolvedVersion.getMainClass();
return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass) || mainClass.startsWith("net.fabricmc") || mainClass.startsWith("cpw.mods"));
}
public enum LibraryType {
MINECRAFT(true, "game", Pattern.compile("^$"), Pattern.compile("^$")),
FABRIC(true, "fabric", Pattern.compile("net\\.fabricmc"), Pattern.compile("fabric-loader")),
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("forge")),
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("(forge|fmlloader)")),
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader")),
OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$"));
@@ -202,4 +214,5 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
public static final String VANILLA_MAIN = "net.minecraft.client.main.Main";
public static final String LAUNCH_WRAPPER_MAIN = "net.minecraft.launchwrapper.Launch";
public static final String MOD_LAUNCHER_MAIN = "cpw.mods.modlauncher.Launcher";
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
}
@@ -17,29 +17,26 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.CompatibilityRule;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.VersionLibraryBuilder;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.FORGE;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.LITELOADER;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.OPTIFINE;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
public class MaintainTask extends Task<Version> {
private final GameRepository repository;
@@ -68,7 +65,10 @@ public class MaintainTask extends Task<Version> {
return maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(unique(version), true), false);
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.MOD_LAUNCHER_MAIN)) {
// Forge 1.13 and OptiFine
return maintainOptiFineLibrary(repository, maintainGameWithModLauncher(repository, unique(version)), true);
return maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true);
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN)) {
// Forge 1.17
return maintainGameWithCpwBoostrapLauncher(repository, unique(version));
} else {
// Vanilla Minecraft does not need maintain
// Fabric does not need maintain, nothing compatible with fabric now.
@@ -122,7 +122,7 @@ public class MaintainTask extends Task<Version> {
return mainClass == null ? ret : ret.setMainClass(mainClass);
}
private static Version maintainGameWithModLauncher(GameRepository repository, Version version) {
private static Version maintainGameWithCpwModLauncher(GameRepository repository, Version version) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
@@ -133,7 +133,7 @@ public class MaintainTask extends Task<Version> {
Optional<Library> optiFine = version.getLibraries().stream().filter(library -> library.is("optifine", "OptiFine")).findAny();
boolean libraryExisting = version.getLibraries().stream().anyMatch(library -> library.is("org.jackhuang.hmcl", "transformer-discovery-service"));
optiFine.ifPresent(library -> {
builder.addJvmArgument("-Dhmcl.transformer.candidates=${libraries_directory}/" + library.getPath());
builder.addJvmArgument("-Dhmcl.transformer.candidates=${library_directory}/" + library.getPath());
if (!libraryExisting) builder.addLibrary(hmclTransformerDiscoveryService);
Path libraryPath = repository.getLibraryFile(version, hmclTransformerDiscoveryService).toPath();
try {
@@ -149,6 +149,59 @@ public class MaintainTask extends Task<Version> {
return builder.build();
}
private static String updateIgnoreList(GameRepository repository, Version version, String ignoreList) {
String[] ignores = ignoreList.split(",");
List<String> newIgnoreList = new ArrayList<>();
// To resolve the the problem that name of primary jar may conflict with the module naming convention,
// we need to manually ignore ${primary_jar}.
newIgnoreList.add("${primary_jar}");
Path libraryDirectory = repository.getLibrariesDirectory(version).toPath().toAbsolutePath();
// The default ignoreList is too loose and may cause some problems, we replace them with the absolute version.
// For example, if "client-extra" is in ignoreList, and game directory contains "client-extra" component, all
// libraries will be ignored, which is not expected.
for (String classpathName : repository.getClasspath(version)) {
Path classpathFile = Paths.get(classpathName).toAbsolutePath();
String fileName = classpathFile.getFileName().toString();
if (Stream.of(ignores).anyMatch(fileName::contains)) {
// This library should be ignored for Jigsaw module finding by Forge.
String absolutePath;
if (classpathFile.startsWith(libraryDirectory)) {
// Note: It's assumed using "/" instead of File.separator in classpath
absolutePath = "${library_directory}${file_separator}" + libraryDirectory.relativize(classpathFile).toString().replace(File.separator, "${file_separator}");
} else {
absolutePath = classpathFile.toString();
}
newIgnoreList.add(StringUtils.substringBefore(absolutePath, ","));
}
}
return String.join(",", newIgnoreList);
}
// Fix wrong configurations when launching 1.17+ with Forge.
private static Version maintainGameWithCpwBoostrapLauncher(GameRepository repository, Version version) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
if (!libraryAnalyzer.has(FORGE)) return version;
// The default ignoreList set by Forge installer does not fulfill our requirements
List<Argument> jvm = builder.getMutableJvmArguments();
for (int i = 0; i < jvm.size(); i++) {
Argument jvmArg = jvm.get(i);
if (jvmArg instanceof StringArgument) {
String jvmArgStr = jvmArg.toString();
if (jvmArgStr.startsWith("-DignoreList=")) {
jvm.set(i, new StringArgument("-DignoreList=" + updateIgnoreList(repository, version, jvmArgStr.substring("-DignoreList=".length()))));
}
}
}
return builder.build();
}
private static Version maintainOptiFineLibrary(GameRepository repository, Version version, boolean remove) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
List<Library> libraries = new ArrayList<>(version.getLibraries());
@@ -173,7 +173,7 @@ public final class ForgeInstallTask extends Task<Version> {
ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class);
if (!gameVersion.get().equals(profile.getMinecraft()))
throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get());
return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getPath().getVersion().replaceAll("(?i)forge", "")), installer);
return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getVersion()), installer);
} else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) {
ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class);
if (!gameVersion.get().equals(profile.getInstall().getMinecraft()))
@@ -25,10 +25,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
@Immutable
@@ -37,15 +34,17 @@ public class ForgeNewInstallProfile implements Validation {
private final int spec;
private final String minecraft;
private final String json;
private final String version;
private final Artifact path;
private final List<Library> libraries;
private final List<Processor> processors;
private final Map<String, Datum> data;
public ForgeNewInstallProfile(int spec, String minecraft, String json, Artifact path, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
public ForgeNewInstallProfile(int spec, String minecraft, String json, String version, Artifact path, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
this.spec = spec;
this.minecraft = minecraft;
this.json = json;
this.version = version;
this.path = path;
this.libraries = libraries;
this.processors = processors;
@@ -74,12 +73,20 @@ public class ForgeNewInstallProfile implements Validation {
return json;
}
/**
*
* @return forge version.
*/
public String getVersion() {
return version;
}
/**
* Maven artifact path for the main jar to install.
* @return artifact path of the main jar.
*/
public Artifact getPath() {
return path;
public Optional<Artifact> getPath() {
return Optional.ofNullable(path);
}
/**
@@ -112,7 +119,7 @@ public class ForgeNewInstallProfile implements Validation {
@Override
public void validate() throws JsonParseException, TolerableValidationException {
if (minecraft == null || json == null || path == null)
if (minecraft == null || json == null || version == null)
throw new JsonParseException("ForgeNewInstallProfile is malformed");
}
@@ -76,6 +76,50 @@ public class ForgeNewInstallTask extends Task<Version> {
setSignificance(TaskSignificance.MINOR);
}
private static String replaceTokens(Map<String, String> tokens, String value) {
StringBuilder buf = new StringBuilder();
for (int x = 0; x < value.length(); x++) {
char c = value.charAt(x);
if (c == '\\') {
if (x == value.length() - 1)
throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value);
buf.append(value.charAt(++x));
} else if (c == '{' || c == '\'') {
StringBuilder key = new StringBuilder();
for (int y = x + 1; y <= value.length(); y++) {
if (y == value.length())
throw new IllegalArgumentException("Illegal pattern (Unclosed " + c + "): " + value);
char d = value.charAt(y);
if (d == '\\') {
if (y == value.length() - 1)
throw new IllegalArgumentException("Illegal pattern (Bad escape): " + value);
key.append(value.charAt(++y));
} else {
if (c == '{' && d == '}') {
x = y;
break;
}
if (c == '\'' && d == '\'') {
x = y;
break;
}
key.append(d);
}
}
if (c == '\'') {
buf.append(key);
} else {
if (!tokens.containsKey(key.toString()))
throw new IllegalArgumentException("Illegal pattern: " + value + " Missing Key: " + key);
buf.append(tokens.get(key.toString()));
}
} else {
buf.append(c);
}
}
return buf.toString();
}
private <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> plainConverter) throws E {
if (StringUtils.isSurrounded(literal, "{", "}"))
return var.get(StringUtils.removeSurrounding(literal, "{", "}"));
@@ -84,7 +128,7 @@ public class ForgeNewInstallTask extends Task<Version> {
else if (StringUtils.isSurrounded(literal, "[", "]"))
return gameRepository.getArtifactFile(version, Artifact.fromDescriptor(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
else
return plainConverter.apply(literal);
return plainConverter.apply(replaceTokens(var, literal));
}
@Override
@@ -116,10 +160,10 @@ public class ForgeNewInstallTask extends Task<Version> {
}
}
{
Path mainJar = profile.getPath().getPath(fs.getPath("maven"));
if (profile.getPath().isPresent()) {
Path mainJar = profile.getPath().get().getPath(fs.getPath("maven"));
if (Files.exists(mainJar)) {
Path dest = gameRepository.getArtifactFile(version, profile.getPath());
Path dest = gameRepository.getArtifactFile(version, profile.getPath().get());
FileUtils.copyFile(mainJar, dest);
}
}
@@ -130,7 +174,7 @@ public class ForgeNewInstallTask extends Task<Version> {
@Override
public void execute() throws Exception {
Path temp = Files.createTempDirectory("forge_installer");
Path temp = Files.createTempDirectory("forge_installer");
int finished = 0;
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
@@ -153,6 +197,10 @@ public class ForgeNewInstallTask extends Task<Version> {
data.put("SIDE", "client");
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
data.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath());
data.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath());
data.put("INSTALLER", installer.toAbsolutePath().toString());
data.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath());
for (ForgeNewInstallProfile.Processor processor : processors) {
Map<String, String> outputs = new HashMap<>();
@@ -105,7 +105,6 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
dependencies.add(task);
}
public static class GameAssetIndexMalformedException extends IOException {
}
}
@@ -19,12 +19,8 @@ package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -33,11 +29,9 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
/**
* Remove class digital verification file in game jar
@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.tukaani.xz.XZInputStream;
import java.io.*;
@@ -38,7 +38,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.jar.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
@@ -129,7 +131,7 @@ public class LibraryDownloadTask extends Task<Void> {
}
}
if (SystemUtils.JRE_CAPABILITY_PACK200 && testURLExistence(url)) {
if (Pack200Utils.isSupported() && testURLExistence(url)) {
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url + ".pack.xz");
task = new FileDownloadTask(urls, xzFile, null);
task.setCacheRepository(cacheRepository);
@@ -264,7 +266,7 @@ public class LibraryDownloadTask extends Task<Void> {
}
try (FileOutputStream jarBytes = new FileOutputStream(dest); JarOutputStream jos = new JarOutputStream(jarBytes)) {
Pack200.newUnpacker().unpack(temp.toFile(), jos);
Pack200Utils.unpack(temp.toFile(), jos);
JarEntry checksumsFile = new JarEntry("checksums.sha1");
checksumsFile.setTime(0L);
@@ -51,8 +51,8 @@ public class JavaDownloadTask extends Task<Void> {
public JavaDownloadTask(GameJavaVersion javaVersion, Path rootDir, DownloadProvider downloadProvider) {
this.javaVersion = javaVersion;
this.rootDir = rootDir;
this.javaDownloadsTask = new GetTask(NetworkUtils.toURL(
"https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"))
this.javaDownloadsTask = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(
"https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json")))
.thenComposeAsync(javaDownloadsJson -> {
JavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, JavaDownloads.class);
if (!allDownloads.getDownloads().containsKey(platform)) throw new UnsupportedPlatformException();
@@ -9,6 +9,7 @@ import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
@@ -17,6 +18,8 @@ import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class JavaRepository {
private JavaRepository() {
}
public static Task<?> downloadJava(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
return new JavaDownloadTask(javaVersion, getJavaStoragePath(), downloadProvider)
@@ -41,12 +44,17 @@ public final class JavaRepository {
Optional<String> platformOptional = getCurrentJavaPlatform();
if (platformOptional.isPresent()) {
String platform = platformOptional.get();
for (Path component : Files.newDirectoryStream(getJavaStoragePath())) {
Path javaHome = component.resolve(platform).resolve(component.getFileName());
try {
addJava(javaHome);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to determine Java at " + javaHome, e);
Path javaStoragePath = getJavaStoragePath();
if (Files.isDirectory(javaStoragePath)) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(javaStoragePath)) {
for (Path component : dirStream) {
Path javaHome = component.resolve(platform).resolve(component.getFileName());
try {
addJava(javaHome);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to determine Java at " + javaHome, e);
}
}
}
}
}
@@ -199,10 +199,10 @@ public final class OptiFineInstallTask extends Task<Version> {
* Install OptiFine library from existing local file.
*
* @param dependencyManager game repository
* @param version version.json
* @param installer the OptiFine installer
* @param version version.json
* @param installer the OptiFine installer
* @return the task to install library
* @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want.
* @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want.
* @throws VersionMismatchException if required game version of installer does not match the actual one.
*/
public static Task<Version> install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException {
@@ -227,7 +227,7 @@ public final class OptiFineInstallTask extends Task<Version> {
throw new VersionMismatchException(mcVersion, gameVersion.get());
return new OptiFineInstallTask(dependencyManager, version,
new OptiFineRemoteVersion(mcVersion, ofEdition + "_" + ofRelease, Collections.singletonList(""), false), installer);
new OptiFineRemoteVersion(mcVersion, ofEdition + "_" + ofRelease, Collections.singletonList(""), false), installer);
}
}
}
@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.util.platform.ManagedProcess;
/**
* This event gets fired when a JavaProcess exited abnormally and the exit code is not zero.
* <br></br>
*
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @author huangyuhui
@@ -34,7 +34,7 @@ public final class ProcessExitedAbnormallyEvent extends Event {
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
* @param process The process that exited abnormally.
*/
public ProcessExitedAbnormallyEvent(Object source, ManagedProcess process) {
@@ -29,7 +29,7 @@ public final class RefreshedVersionsEvent extends Event {
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.game.GameRepository]
* @param source {@link org.jackhuang.hmcl.game.GameRepository}
*/
public RefreshedVersionsEvent(Object source) {
super(source);
@@ -63,7 +63,7 @@ public class DownloadInfo implements Validation {
}
public String getSha1() {
return sha1;
return "invalid".equals(sha1) ? null : sha1;
}
public int getSize() {
@@ -35,4 +35,4 @@ public enum GameDirectoryType {
* user customized directory.
*/
CUSTOM
}
}
@@ -21,7 +21,9 @@ import org.jackhuang.hmcl.task.Task;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Supports operations on versioning.
@@ -88,7 +90,7 @@ public interface GameRepository extends VersionProvider {
/**
* Gets the root folder of specific version.
* The root folders the versions must be unique.
* For example, .minecraft/versions/<version name>/.
* For example, .minecraft/versions/&lt;version name&gt;/.
*/
File getVersionRoot(String id);
@@ -220,4 +222,14 @@ public interface GameRepository extends VersionProvider {
*/
File getLoggingObject(String version, String assetId, LoggingInfo loggingInfo);
default List<String> getClasspath(Version version) {
List<String> classpath = new ArrayList<>();
for (Library library : version.getLibraries())
if (library.appliesToCurrentEnvironment() && !library.isNative()) {
File f = getLibraryFile(version, library);
if (f.exists() && f.isFile())
classpath.add(f.getAbsolutePath());
}
return classpath;
}
}
@@ -45,6 +45,9 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
* @author huangyuhui
*/
public final class GameVersion {
private GameVersion() {
}
private static Optional<String> getVersionFromJson(Path versionJson) {
try {
Map<?, ?> version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), Map.class);
@@ -53,6 +53,8 @@ public class LaunchOptions implements Serializable {
private String proxyPass;
private boolean noGeneratedJVMArgs;
private String preLaunchCommand;
private NativesDirectoryType nativesDirType;
private String nativesDir;
/**
* The game directory
@@ -200,6 +202,21 @@ public class LaunchOptions implements Serializable {
return preLaunchCommand;
}
/**
* 0 - ./minecraft/versions/&lt;version&gt;/natives
* 1 - custom natives directory
*/
public NativesDirectoryType getNativesDirType() {
return nativesDirType;
}
/**
* Path to the natives directory, optional
*/
public String getNativesDir() {
return nativesDir;
}
public static class Builder {
private final LaunchOptions options = new LaunchOptions();
@@ -352,6 +369,14 @@ public class LaunchOptions implements Serializable {
return options.preLaunchCommand;
}
public NativesDirectoryType getNativesDirType() {
return options.nativesDirType;
}
public String getNativesDir() {
return options.nativesDir;
}
public Builder setGameDir(File gameDir) {
options.gameDir = gameDir;
return this;
@@ -454,5 +479,15 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setNativesDirType(NativesDirectoryType nativesDirType) {
options.nativesDirType = nativesDirType;
return this;
}
public Builder setNativesDir(String nativesDir) {
options.nativesDir = nativesDir;
return this;
}
}
}
@@ -0,0 +1,12 @@
package org.jackhuang.hmcl.game;
public enum NativesDirectoryType {
/**
* .minecraft/versions/&lt;version&gt;/natives
*/
VERSION_FOLDER,
/**
* user customized directory.
*/
CUSTOM
}
@@ -163,7 +163,7 @@ public class Version implements Comparable<Version>, Validation {
}
public String getJar() {
return jar;
return jar == null ? id : jar;
}
public String getInheritsFrom() {
@@ -275,14 +275,14 @@ public class Version implements Comparable<Version>, Validation {
if (inheritsFrom == null) {
if (isRoot())
thisVersion = new Version(id).setPatches(patches).setJar(id);
thisVersion = new Version(id).setPatches(patches);
else
thisVersion = this.jar == null ? this.setJar(id) : this;
thisVersion = this;
} else {
// To maximize the compatibility.
if (!resolvedSoFar.add(id)) {
Logging.LOG.log(Level.WARNING, "Found circular dependency versions: " + resolvedSoFar);
thisVersion = this.jar == null ? this.setJar(id) : this;
thisVersion = this;
} else {
// It is supposed to auto install an version in getVersion.
thisVersion = merge(provider.getVersion(inheritsFrom).resolve(provider, resolvedSoFar), false);
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
/**
*
@@ -145,6 +146,11 @@ public final class VersionLibraryBuilder {
}
}
public List<Argument> getMutableJvmArguments() {
jvmChanged = true;
return jvm;
}
public void addGameArgument(String... args) {
for (String arg : args)
game.add(new StringArgument(arg));
@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
@@ -44,19 +45,13 @@ import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
*
* @author huangyuhui
*/
public class DefaultLauncher extends Launcher {
@@ -88,53 +83,63 @@ public class DefaultLauncher extends Launcher {
if (!options.isNoGeneratedJVMArgs()) {
appendJvmArgs(res);
res.add("-Dminecraft.client.jar=" + repository.getVersionJar(version));
res.addDefault("-Dminecraft.client.jar=", repository.getVersionJar(version).toString());
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
res.add("-Xdock:name=Minecraft " + version.getId());
res.add("-Xdock:icon=" + repository.getAssetObject(version.getId(), version.getAssetIndex().getId(), "icons/minecraft.icns").getAbsolutePath());
res.addDefault("-Xdock:name=", "Minecraft " + version.getId());
res.addDefault("-Xdock:icon=", repository.getAssetObject(version.getId(), version.getAssetIndex().getId(), "icons/minecraft.icns").getAbsolutePath());
}
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
res.add("-Duser.home=" + options.getGameDir().getParent());
res.addDefault("-Duser.home=", options.getGameDir().getParent());
// Force using G1GC with its settings
// Using G1GC with its settings by default
if (options.getJava().getParsedVersion() >= JavaVersion.JAVA_8) {
res.add("-XX:+UnlockExperimentalVMOptions");
res.add("-XX:+UseG1GC");
res.add("-XX:G1NewSizePercent=20");
res.add("-XX:G1ReservePercent=20");
res.add("-XX:MaxGCPauseMillis=50");
res.add("-XX:G1HeapRegionSize=16M");
boolean addG1Args = true;
for (String javaArg : options.getJavaArguments()) {
if ("-XX:-UseG1GC".equals(javaArg) || (javaArg.startsWith("-XX:+Use") && javaArg.endsWith("GC"))) {
addG1Args = false;
break;
}
}
if (addG1Args) {
res.addUnstableDefault("UnlockExperimentalVMOptions", true);
res.addUnstableDefault("UseG1GC", true);
res.addUnstableDefault("G1NewSizePercent", "20");
res.addUnstableDefault("G1ReservePercent", "20");
res.addUnstableDefault("MaxGCPauseMillis", "50");
res.addUnstableDefault("G1HeapRegionSize", "16m");
}
}
if (options.getMetaspace() != null && options.getMetaspace() > 0)
if (options.getJava().getParsedVersion() < JavaVersion.JAVA_8)
res.add("-XX:PermSize= " + options.getMetaspace() + "m");
res.addDefault("-XX:PermSize=", options.getMetaspace() + "m");
else
res.add("-XX:MetaspaceSize=" + options.getMetaspace() + "m");
res.addDefault("-XX:MetaspaceSize=", options.getMetaspace() + "m");
res.add("-XX:-UseAdaptiveSizePolicy");
res.add("-XX:-OmitStackTraceInFastThrow");
res.add("-Xmn128m");
res.addUnstableDefault("UseAdaptiveSizePolicy", false);
res.addUnstableDefault("OmitStackTraceInFastThrow", false);
res.addUnstableDefault("DontCompileHugeMethods", false);
res.addDefault("-Xmn", "128m");
// As 32-bit JVM allocate 320KB for stack by default rather than 64-bit version allocating 1MB,
// causing Minecraft 1.13 crashed accounting for java.lang.StackOverflowError.
if (options.getJava().getPlatform() == Platform.BIT_32) {
res.add("-Xss1M");
res.addDefault("-Xss", "1m");
}
if (options.getMaxMemory() != null && options.getMaxMemory() > 0)
res.add("-Xmx" + options.getMaxMemory() + "m");
res.addDefault("-Xmx", options.getMaxMemory() + "m");
if (options.getMinMemory() != null && options.getMinMemory() > 0)
res.add("-Xms" + options.getMinMemory() + "m");
res.addDefault("-Xms", options.getMinMemory() + "m");
if (options.getJava().getParsedVersion() == JavaVersion.JAVA_16)
res.add("--illegal-access=permit");
res.addDefault("--illegal-access=", "permit");
res.add("-Dfml.ignoreInvalidMinecraftCertificates=true");
res.add("-Dfml.ignorePatchDiscrepancies=true");
res.addDefault("-Dfml.ignoreInvalidMinecraftCertificates=", "true");
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
}
Proxy proxy = options.getProxy();
@@ -144,24 +149,18 @@ public class DefaultLauncher extends Launcher {
String host = address.getHostString();
int port = address.getPort();
if (proxy.type() == Proxy.Type.HTTP) {
res.add("-Dhttp.proxyHost=" + host);
res.add("-Dhttp.proxyPort=" + port);
res.add("-Dhttps.proxyHost=" + host);
res.add("-Dhttps.proxyPort=" + port);
res.addDefault("-Dhttp.proxyHost=", host);
res.addDefault("-Dhttp.proxyPort=", String.valueOf(port));
res.addDefault("-Dhttps.proxyHost=", host);
res.addDefault("-Dhttps.proxyPort=", String.valueOf(port));
} else if (proxy.type() == Proxy.Type.SOCKS) {
res.add("-DsocksProxyHost=" + host);
res.add("-DsocksProxyPort=" + port);
res.addDefault("-DsocksProxyHost=", host);
res.addDefault("-DsocksProxyPort=", String.valueOf(port));
}
}
}
LinkedList<String> classpath = new LinkedList<>();
for (Library library : version.getLibraries())
if (library.appliesToCurrentEnvironment() && !library.isNative()) {
File f = repository.getLibraryFile(version, library);
if (f.exists() && f.isFile())
classpath.add(f.getAbsolutePath());
}
List<String> classpath = repository.getClasspath(version);
File jar = repository.getVersionJar(version);
if (!jar.exists() || !jar.isFile())
@@ -175,7 +174,6 @@ public class DefaultLauncher extends Launcher {
configuration.put("${natives_directory}", nativeFolder.getAbsolutePath());
configuration.put("${game_assets}", gameAssets.getAbsolutePath());
configuration.put("${assets_root}", gameAssets.getAbsolutePath());
configuration.put("${libraries_directory}", repository.getLibrariesDirectory(version).getAbsolutePath());
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
if (authInfo.getArguments() != null && authInfo.getArguments().getJvm() != null && !authInfo.getArguments().getJvm().isEmpty())
@@ -220,7 +218,7 @@ public class DefaultLauncher extends Launcher {
}
}
res.addAllWithoutParsing(options.getGameArguments());
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
return res;
@@ -281,6 +279,7 @@ public class DefaultLauncher extends Launcher {
protected Map<String, String> getConfigurations() {
return mapOf(
// defined by Minecraft official launcher
pair("${auth_player_name}", authInfo.getUsername()),
pair("${auth_session}", authInfo.getAccessToken()),
pair("${auth_access_token}", authInfo.getAccessToken()),
@@ -293,13 +292,30 @@ public class DefaultLauncher extends Launcher {
pair("${assets_index_name}", version.getAssetIndex().getId()),
pair("${user_properties}", authInfo.getUserProperties()),
pair("${resolution_width}", options.getWidth().toString()),
pair("${resolution_height}", options.getHeight().toString())
pair("${resolution_height}", options.getHeight().toString()),
pair("${library_directory}", repository.getLibrariesDirectory(version).getAbsolutePath()),
pair("${classpath_separator}", OperatingSystem.PATH_SEPARATOR),
pair("${primary_jar}", repository.getVersionJar(version).getAbsolutePath()),
pair("${language}", Locale.getDefault().toString()),
// defined by HMCL
// libraries_directory stands for historical reasons here. We don't know the official launcher
// had already defined "library_directory" as the placeholder for path to ".minecraft/libraries"
// when we propose this placeholder.
pair("${libraries_directory}", repository.getLibrariesDirectory(version).getAbsolutePath()),
// file_separator is used in -DignoreList
pair("${file_separator}", OperatingSystem.FILE_SEPARATOR)
);
}
@Override
public ManagedProcess launch() throws IOException, InterruptedException {
File nativeFolder = repository.getNativeDirectory(version.getId());
File nativeFolder = null;
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
nativeFolder = repository.getNativeDirectory(version.getId());
} else {
nativeFolder = new File(options.getNativesDir());
}
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = generateCommandLine(nativeFolder).asList();
@@ -308,7 +324,9 @@ public class DefaultLauncher extends Launcher {
throw new IllegalStateException("Illegal command line " + rawCommandLine);
}
decompressNatives(nativeFolder);
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
decompressNatives(nativeFolder);
}
File runDirectory = repository.getRunDirectory(version.getId());
@@ -348,8 +366,16 @@ public class DefaultLauncher extends Launcher {
public void makeLaunchScript(File scriptFile) throws IOException {
boolean isWindows = OperatingSystem.WINDOWS == OperatingSystem.CURRENT_OS;
File nativeFolder = repository.getNativeDirectory(version.getId());
decompressNatives(nativeFolder);
File nativeFolder = null;
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
nativeFolder = repository.getNativeDirectory(version.getId());
} else {
nativeFolder = new File(options.getNativesDir());
}
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
decompressNatives(nativeFolder);
}
if (isWindows && !FileUtils.getExtension(scriptFile).equals("bat"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' in Windows");
@@ -68,7 +68,7 @@ public final class FabricModMetadata {
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
return new ModInfo(modManager, modFile, metadata.name, new ModInfo.Description(metadata.description),
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "");
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "");
}
}
@@ -67,9 +67,9 @@ public final class ModInfo implements Comparable<ModInfo> {
try {
if (get())
ModInfo.this.file = modManager.enableMod(path);
ModInfo.this.file = modManager.enableMod(path);
else
ModInfo.this.file = modManager.disableMod(path);
ModInfo.this.file = modManager.disableMod(path);
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to invert state of mod file " + path, e);
}
@@ -36,7 +36,7 @@ public final class ModpackConfiguration<T> implements Validation {
private final List<FileInformation> overrides;
public ModpackConfiguration() {
this(null, null, "", null, Collections.emptyList());
this(null, null, "", null, Collections.emptyList());
}
public ModpackConfiguration(T manifest, String type, String name, String version, List<FileInformation> overrides) {
@@ -110,6 +110,7 @@ public final class ModpackConfiguration<T> implements Validation {
/**
* The relative path to Minecraft run directory
*
* @return the relative path to Minecraft run directory.
*/
public String getPath() {
@@ -83,6 +83,8 @@ public final class CurseInstallTask extends Task<Void> {
for (CurseManifestModLoader modLoader : manifest.getMinecraft().getModLoaders())
if (modLoader.getId().startsWith("forge-"))
builder.version("forge", modLoader.getId().substring("forge-".length()));
else if (modLoader.getId().startsWith("fabric-"))
builder.version("fabric", modLoader.getId().substring("fabric-".length()));
dependents.add(builder.buildAsync());
onDone().register(event -> {
@@ -23,7 +23,7 @@ import org.jackhuang.hmcl.util.Immutable;
/**
* CurseMetaMod is JSON structure for
* https://cursemeta.dries007.net/&lt;projectID&gt;/&lt;fileID&gt;.json
* https://addons-ecs.forgesvc.net/api/v2/addon/&lt;projectID&gt;/file/<fileID&gt;
* https://addons-ecs.forgesvc.net/api/v2/addon/&lt;projectID&gt;/file/&lt;fileID&gt;
*/
@Immutable
public final class CurseMetaMod {
@@ -202,7 +202,7 @@ public class McbbsModpackManifest implements Validation {
}
public static final class Settings {
@SerializedName("install_modes")
@SerializedName("install_mods")
private final boolean installMods;
@SerializedName("install_resourcepack")
@@ -79,8 +79,6 @@ public final class MultiMCManifest {
@SerializedName("suggests")
private final String suggests;
public MultiMCManifestCachedRequires(String equalsVersion, String uid, String suggests) {
this.equalsVersion = equalsVersion;
this.uid = uid;
@@ -56,7 +56,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
private final DefaultGameRepository repository;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, String name) {
this.zipFile = zipFile;
this.modpack = modpack;
@@ -96,7 +96,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
repository.removeVersionFromDisk(name);
});
}
@Override
public List<Task<?>> getDependencies() {
return dependencies;
@@ -138,7 +138,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
public List<Task<?>> getDependents() {
return dependents;
}
@Override
public void execute() throws Exception {
Version version = repository.readVersionJson(name);
@@ -169,11 +169,11 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
Path libraries = root.resolve("libraries");
if (Files.exists(libraries))
FileUtils.copyDirectory(libraries, repository.getVersionRoot(name).toPath().resolve("libraries"));
FileUtils.copyDirectory(libraries, repository.getVersionRoot(name).toPath().resolve("libraries"));
Path jarmods = root.resolve("jarmods");
if (Files.exists(jarmods))
FileUtils.copyDirectory(jarmods, repository.getVersionRoot(name).toPath().resolve("jarmods"));
FileUtils.copyDirectory(jarmods, repository.getVersionRoot(name).toPath().resolve("jarmods"));
}
dependencies.add(repository.saveAsync(version));
@@ -280,7 +280,6 @@ public abstract class Task<T> {
*/
public void postExecute() throws Exception {}
/**
* The collection of sub-tasks that should execute **before** this task running.
*/
@@ -54,6 +54,7 @@ public enum Log4jLevel {
public boolean lessOrEqual(Log4jLevel level) {
return this.level <= level.level;
}
public static final Pattern MINECRAFT_LOGGER = Pattern.compile("\\[(?<timestamp>[0-9:]+)] \\[[^/]+/(?<level>[^]]+)]");
public static final Pattern MINECRAFT_LOGGER_CATEGORY = Pattern.compile("\\[(?<timestamp>[0-9:]+)] \\[[^/]+/(?<level>[^]]+)] \\[(?<category>[^]]+)]");
public static final String JAVA_SYMBOL = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*";
@@ -31,6 +31,8 @@ import java.util.logging.*;
* @author huangyuhui
*/
public final class Logging {
private Logging() {
}
public static final Logger LOG = Logger.getLogger("HMCL");
private static ByteArrayOutputStream storedLogs = new ByteArrayOutputStream();
@@ -0,0 +1,123 @@
/*
* 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.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.jar.JarOutputStream;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* @author Glavo
*/
public final class Pack200Utils {
private Pack200Utils() {
}
private static final String[] IMPL_NAMES = {
"java.util.jar.Pack200",
"org.glavo.pack200.Pack200",
"io.pack200.Pack200"
};
private static final MethodHandle newUnpackerHandle;
private static final MethodHandle unpackHandle;
private static final MethodHandle unpackFileHandle;
static {
Class<?> pack200Class = null;
Class<?> unpackerClass = null;
for (String implName : IMPL_NAMES) {
try {
pack200Class = Class.forName(implName);
unpackerClass = Class.forName(implName + "$Unpacker");
break;
} catch (ClassNotFoundException ignored) {
}
}
if (pack200Class == null) {
LOG.warning("Pack200 not found");
newUnpackerHandle = null;
unpackHandle = null;
unpackFileHandle = null;
} else {
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle newUnpacker = null;
MethodHandle unpack = null;
MethodHandle unpackFile = null;
try {
newUnpacker = lookup.findStatic(pack200Class, "newUnpacker", MethodType.methodType(unpackerClass));
unpack = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, InputStream.class, JarOutputStream.class));
unpackFile = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, File.class, JarOutputStream.class));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to find pack200 methods", e);
}
if (newUnpacker != null) {
newUnpackerHandle = newUnpacker;
unpackHandle = unpack;
unpackFileHandle = unpackFile;
} else {
newUnpackerHandle = null;
unpackHandle = null;
unpackFileHandle = null;
}
}
}
public static boolean isSupported() {
return newUnpackerHandle != null;
}
public static void unpack(InputStream in, JarOutputStream out) throws IOException {
if (newUnpackerHandle == null) {
throw new UnsupportedOperationException("Pack200");
}
try {
unpackHandle.invoke(newUnpackerHandle.invoke(), in, out);
} catch (IOException | RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public static void unpack(File in, JarOutputStream out) throws IOException {
if (newUnpackerHandle == null) {
throw new UnsupportedOperationException("Pack200");
}
try {
unpackFileHandle.invoke(newUnpackerHandle.invoke(), in, out);
} catch (IOException | RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
@@ -24,7 +24,7 @@ import java.util.Objects;
*
* @author huangyuhui
*/
public class Pair<K, V> implements Map.Entry<K, V> {
public final class Pair<K, V> implements Map.Entry<K, V> {
public static <K, V> Pair<K, V> pair(K key, V value) {
return new Pair<>(key, value);
@@ -27,13 +27,16 @@ import java.util.function.Predicate;
* @author huangyuhui
*/
public final class ReflectionHelper {
private ReflectionHelper() {
}
private static Method accessible0;
static {
try {
accessible0 = AccessibleObject.class.getDeclaredMethod("setAccessible0", boolean.class);
accessible0.setAccessible(true);
} catch (Throwable ex) {
} catch (Throwable ignored) {
}
}
@@ -43,6 +46,7 @@ public final class ReflectionHelper {
/**
* Get caller, this method is caller sensitive.
*
* @param packageFilter returns false if we consider the given package is internal calls, not the caller
* @return the caller, method name, source file, line number
*/
@@ -26,4 +26,4 @@ public interface ExceptionalBiFunction<T, U, R, E extends Exception> {
requireNonNull(after);
return (t, u) -> after.apply(apply(t, u));
}
}
}
@@ -67,7 +67,6 @@ public class JsonTypeAdapterFactory implements TypeAdapterFactory {
};
}
private <T> TypeAdapter<T> createForJsonSubtype(Gson gson, TypeToken<T> type) {
Class<? super T> rawType = type.getRawType();
if (rawType.getSuperclass() == null) return null;
@@ -29,7 +29,7 @@ public interface Validation {
/**
* 1. Check some non-null fields and;
* 2. Check strings and;
* 3. Check generic type of lists <T> and maps <K, V> are correct.
* 3. Check generic type of lists &lt;T&gt; and maps &lt;K, V&gt; are correct.
*
* Will be called immediately after initialization.
* Throw an exception when values are malformed.
@@ -21,9 +21,9 @@ import java.io.IOException;
public class ChecksumMismatchException extends IOException {
private String algorithm;
private String expectedChecksum;
private String actualChecksum;
private final String algorithm;
private final String expectedChecksum;
private final String actualChecksum;
public ChecksumMismatchException(String algorithm, String expectedChecksum, String actualChecksum) {
super("Incorrect checksum (" + algorithm + "), expected: " + expectedChecksum + ", actual: " + actualChecksum);
@@ -124,10 +124,11 @@ public final class FileUtils {
/**
* Write plain text to file. Characters are encoded into bytes using UTF-8.
*
* <p>
* We don't care about platform difference of line separator. Because readText accept all possibilities of line separator.
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
* All characters in text will be written into the file in binary format. Existing data will be erased.
*
* @param file the path to the file
* @param text the text being written to file
* @throws IOException if an I/O error occurs
@@ -138,12 +139,13 @@ public final class FileUtils {
/**
* Write plain text to file.
*
* <p>
* We don't care about platform difference of line separator. Because readText accept all possibilities of line separator.
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
* All characters in text will be written into the file in binary format. Existing data will be erased.
* @param file the path to the file
* @param text the text being written to file
*
* @param file the path to the file
* @param text the text being written to file
* @param charset the charset to use for encoding
* @throws IOException if an I/O error occurs
*/
@@ -155,7 +157,8 @@ public final class FileUtils {
* Write byte array to file.
* It will create the file if it does not exist, or truncate the existing file to empty for rewriting.
* All bytes in byte array will be written into the file in binary format. Existing data will be erased.
* @param file the path to the file
*
* @param file the path to the file
* @param array the data being written to file
* @throws IOException if an I/O error occurs
*/
@@ -192,7 +195,7 @@ public final class FileUtils {
* Copy directory.
* Paths of all files relative to source directory will be the same as the ones relative to destination directory.
*
* @param src the source directory.
* @param src the source directory.
* @param dest the destination directory, which will be created if not existing.
* @throws IOException if an I/O error occurs.
*/
@@ -201,7 +204,7 @@ public final class FileUtils {
}
public static void copyDirectory(Path src, Path dest, Predicate<String> filePredicate) throws IOException {
Files.walkFileTree(src, new SimpleFileVisitor<Path>(){
Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (!filePredicate.test(src.relativize(file).toString())) {
@@ -228,7 +231,7 @@ public final class FileUtils {
/**
* Move file to trash.
*
* <p>
* This method is only implemented in Java 9. Please check we are using Java 9 by invoking isMovingToTrashSupported.
* Example:
* <pre>{@code
@@ -236,9 +239,10 @@ public final class FileUtils {
* FileUtils.moveToTrash(file);
* }
* }</pre>
*
* @param file the file being moved to trash.
* @see FileUtils#isMovingToTrashSupported()
* @return false if moveToTrash does not exist, or platform does not support Desktop.Action.MOVE_TO_TRASH
* @see FileUtils#isMovingToTrashSupported()
*/
public static boolean moveToTrash(File file) {
try {
@@ -253,6 +257,7 @@ public final class FileUtils {
/**
* Check if {@code java.awt.Desktop.moveToTrash} exists.
*
* @return true if the method exists.
*/
public static boolean isMovingToTrashSupported() {
@@ -31,6 +31,8 @@ import java.util.jar.JarFile;
import java.util.jar.Manifest;
public final class JarUtils {
private JarUtils() {
}
public static Optional<Path> thisJar() {
CodeSource codeSource = JarUtils.class.getProtectionDomain().getCodeSource();
@@ -86,6 +86,7 @@ public final class NetworkUtils {
break;
case '?':
left = false;
// fallthrough
default:
if (ch >= 0x80)
sb.append(encodeURL(Character.toString(ch)));
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more