Compare commits

..
209 changed files with 2867 additions and 7431 deletions

No files matched your search

+2 -11
View File
@@ -1,6 +1,6 @@
name: Java CI
on: [push, pull_request]
on: [push]
jobs:
build:
@@ -8,20 +8,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v1
- 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
+2 -8
View File
@@ -11,6 +11,7 @@ hs_err_pid*
*.log
.mine*
NVIDIA
*.json
# gradle build
/build/
@@ -37,11 +38,4 @@ NVIDIA
# netbeans
.nb-gradle
*.exe
!/HMCL/src/main/resources/assets/HMCLauncher.exe
# macos
.DS_Store
# test
/hmcl.json
*.exe
+22 -56
View File
@@ -1,17 +1,12 @@
buildscript {
repositories {
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
}
}
plugins {
id 'com.github.johnrengelman.shadow' version '4.0.0'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
import java.nio.file.FileSystems
@@ -21,28 +16,25 @@ 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 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"
def buildnumber = System.getenv("BUILD_NUMBER") ?: "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.3"
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation rootProject.files("lib/JFoenix.jar")
compile project(":HMCLCore")
compile rootProject.files("lib/JFoenix.jar")
}
def digest(String algorithm, byte[] bytes) {
@@ -83,50 +75,19 @@ ext.packer = Pack200.newPacker()
packer.properties()["pack.effort"] = "9"
ext.unpacker = Pack200.newUnpacker()
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
// .class files remain the same.
def repack(File file) {
def packed = new ByteArrayOutputStream()
new JarFile(file).withCloseable { packer.pack(it, packed) }
new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) }
}
sourceSets {
java11 {
java {
srcDirs = ['src/main/java11']
}
}
}
compileJava11Java {
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
targetCompatibility = 11
}
jar {
enabled = false
dependsOn shadowJar
}
shadowJar {
classifier = null
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version,
'Class-Path': 'pack200.jar',
'Add-Opens': [
'java.base/java.lang',
'java.base/java.lang.reflect',
'javafx.graphics/javafx.css',
'javafx.base/com.sun.javafx.runtime',
@@ -138,7 +99,6 @@ shadowJar {
'javafx.graphics/com.sun.javafx.stage'
].join(" "),
'Add-Exports': [
'java.base/jdk.internal.loader',
'javafx.controls/com.sun.javafx.scene.control.behavior',
'javafx.controls/javafx.scene.control.skin',
'javafx.controls/com.sun.javafx.scene.control',
@@ -147,9 +107,22 @@ shadowJar {
'javafx.base/com.sun.javafx.event'
].join(" ")
}
finalizedBy shadowJar
}
shadowJar {
classifier = null
exclude 'META-INF/maven/**'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
dependencies {
exclude(dependency('org.jetbrains:annotations'))
}
doLast {
repack(jar.archivePath) // see repack()
repack(jar.archivePath)
attachSignature(jar.archivePath)
createChecksum(jar.archivePath)
}
@@ -169,9 +142,8 @@ 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()
javaexec {
main = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
}
}
}
@@ -179,11 +151,6 @@ processResources {
convertToBSS "assets/css/root.css"
convertToBSS "assets/css/blue.css"
into('META-INF/versions/11') {
from sourceSets.java11.output
}
dependsOn java11Classes
}
task makePack(dependsOn: jar) {
@@ -198,7 +165,6 @@ task makePack(dependsOn: jar) {
task makePackXz(dependsOn: makePack) doLast {
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
// Our CI server does not have enough memory space to compress file at highest level.
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packXz)
}
@@ -23,22 +23,19 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
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.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
@@ -50,14 +47,11 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
public static final CookieManager COOKIE_MANAGER = new CookieManager();
@Override
public void start(Stage primaryStage) {
Thread.currentThread().setUncaughtExceptionHandler(CRASH_REPORTER);
CookieHandler.setDefault(COOKIE_MANAGER);
try {
try {
ConfigHolder.init();
@@ -73,8 +67,6 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
initIcon();
UpdateChecker.init();
primaryStage.show();
@@ -90,17 +82,7 @@ public final class Launcher extends Application {
Controllers.onApplicationStop();
}
private void initIcon() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
AwtUtils.setAppleIcon(image);
}
public static void main(String[] args) {
if (UpdateHandler.processArguments(args)) {
return;
}
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
@@ -160,16 +142,6 @@ public final class Launcher extends Application {
} catch (URISyntaxException e) {
return null;
}
} else {
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;
@@ -17,8 +17,8 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import javax.net.ssl.*;
import javax.swing.*;
@@ -43,14 +43,12 @@ 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);
System.setProperty("javafx.autoproxy.disable", "true");
checkJavaFX();
checkDirectoryPath();
// This environment check will take ~300ms
@@ -61,10 +59,9 @@ public final class Main {
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
checkJavaFX();
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
if (UpdateHandler.processArguments(args)) {
return;
}
Launcher.main(args);
}
@@ -80,13 +77,9 @@ public final class Main {
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
Class.forName("javafx.application.Application");
} catch (ClassNotFoundException e) {
showErrorAndExit(i18n("fatal.missing_javafx"));
}
}
@@ -17,12 +17,11 @@
*/
package org.jackhuang.hmcl;
import java.nio.file.Path;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Stores metadata about this application.
*/
@@ -40,14 +39,5 @@ public final class Metadata {
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
private static Path getHMCLDirectory() {
String home = System.getProperty("user.home", ".");
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
// to fulfill XDG standard.
return Paths.get(home, ".cache", "hmcl");
}
return OperatingSystem.getWorkingDirectory("hmcl");
}
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
@@ -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);
}
@@ -19,24 +19,13 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.File;
import java.io.IOException;
@@ -44,7 +33,6 @@ import java.nio.file.Files;
import java.util.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class HMCLGameRepository extends DefaultGameRepository {
@@ -235,7 +223,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (LibraryAnalyzer.isModded(this, version))
else if (version.getMainClass() != null &&
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
@@ -282,56 +273,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
return FORBIDDEN.contains(id);
}
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
VersionSetting vs = getVersionSetting(version);
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.getMaxMemory())
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) {
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
config.getManifest().injectLaunchOptions(builder);
}
} catch (IOException | JsonParseException e) {
e.printStackTrace();
}
}
return builder.create();
}
@Override
public File getModpackConfiguration(String version) {
return new File(getVersionRoot(version), "modpack.cfg");
@@ -0,0 +1,74 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Export the game to a mod pack file.
*/
public class HMCLModpackExportTask extends Task<Void> {
private final DefaultGameRepository repository;
private final String version;
private final List<String> whitelist;
private final Modpack modpack;
private final File output;
/**
* @param output mod pack file.
* @param version to locate version.json
*/
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this.repository = repository;
this.version = version;
this.whitelist = whitelist;
this.modpack = modpack;
this.output = output;
onDone().register(event -> {
if (event.isFailed()) output.delete();
});
}
@Override
public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(version + ".jar");
blackList.add(version + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> Modpack.acceptFile(path, blackList, whitelist));
Version mv = repository.getResolvedPreservingPatchesVersion(version);
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
.orElseThrow(() -> new IOException("Cannot parse the version of " + version));
zip.putTextFile(JsonUtils.GSON.toJson(mv.setJar(gameVersion)), "minecraft/pack.json"); // Making "jar" to gameVersion is to be compatible with old HMCL.
zip.putTextFile(JsonUtils.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
}
}
@@ -31,17 +31,15 @@ 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.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid 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.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
import org.jackhuang.hmcl.launch.PermissionException;
import org.jackhuang.hmcl.launch.ProcessCreationException;
@@ -36,11 +35,7 @@ import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
@@ -55,7 +50,10 @@ import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
@@ -64,11 +62,9 @@ import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.*;
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;
@@ -144,12 +140,10 @@ public final class LauncherHelper {
}), Task.composeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
if ("Curse".equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
else if ("Server".equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
} catch (IOException e) {
@@ -175,7 +169,7 @@ public final class LauncherHelper {
repository,
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
authInfo,
repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()),
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(repository, selectedVersion, authInfo, launchingLatch, gameVersion.isPresent())
@@ -318,32 +312,6 @@ public final class LauncherHelper {
flag = true;
}
if (!flag && version.getJavaVersion() != null) {
if (java.getParsedVersion() < version.getJavaVersion().getMajorVersion()) {
Optional<JavaVersion> acceptableJava = JavaVersion.getJavas().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (acceptableJava.isPresent()) {
setting.setJavaVersion(acceptableJava.get());
} else {
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;
}
}
}
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJavas().stream()
@@ -366,8 +334,8 @@ public final class LauncherHelper {
}
// LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader.
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9_AND_LATER
&& version.getMainClass().equals(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN)
&& version.getLibraries().stream()
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
@@ -399,7 +367,7 @@ public final class LauncherHelper {
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) {
org.jackhuang.hmcl.util.platform.Platform.IS_64_BIT) {
final JavaVersion java32 = java;
// First find if same java version but whose platform is 64-bit installed.
@@ -474,36 +442,6 @@ public final class LauncherHelper {
onAccept.run();
}
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<Void> future = new CompletableFuture<>();
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
profile.getDependency().getDownloadProvider()).executor(false);
executor.addTaskListener(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
super.onStop(success, executor);
Platform.runLater(() -> {
if (!success) {
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
} else {
future.complete(null);
}
});
}
});
javaDownloadingPane.setExecutor(executor, true);
Controllers.dialog(javaDownloadingPane);
executor.start();
return future;
}
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
@@ -23,8 +23,6 @@ import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.curse.CurseManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
@@ -45,20 +43,12 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
public final class ModpackHelper {
private ModpackHelper() {}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
ignored.printStackTrace();
// ignore it, not a valid MCBBS modpack.
}
try {
return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) {
@@ -158,16 +148,12 @@ public final class ModpackHelper {
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
.thenComposeAsync(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, ((ServerModpackManifest) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
}
@@ -199,11 +185,6 @@ public final class ModpackHelper {
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
case McbbsModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
case ServerModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof ServerModpackManifest))
throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
@@ -251,18 +232,5 @@ public final class ModpackHelper {
}
}
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
});
}
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
if (manifest.getLaunchInfo().getMinMemory() > vs.getMaxMemory())
vs.setMaxMemory(manifest.getLaunchInfo().getMinMemory());
});
}
}
@@ -0,0 +1,46 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import java.util.Objects;
public final class MultiMCInstallVersionSettingTask extends Task<Void> {
private final Profile profile;
private final MultiMCInstanceConfiguration manifest;
private final String version;
public MultiMCInstallVersionSettingTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
this.profile = profile;
this.manifest = manifest;
this.version = version;
setExecutor(Schedulers.javafx());
}
@Override
public void execute() {
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
ModpackHelper.toVersionSetting(manifest, vs);
}
}
@@ -19,14 +19,16 @@ package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
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.*;
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.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
@@ -40,10 +42,7 @@ import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -135,7 +134,6 @@ public final class TexturesLoader {
// ==== Skins ====
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
static {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
@@ -183,30 +181,6 @@ public final class TexturesLoader {
}
}, uuidFallback);
}
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
return BindingMapping.of(account.getTextures())
.map(textures -> textures
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}
// ====
// ==== Avatar ====
@@ -231,34 +205,16 @@ public final class TexturesLoader {
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(TexturesLoader::toFXImage);
.map(img -> SwingFXUtils.toFXImage(img, null));
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(TexturesLoader::toFXImage);
if (account instanceof YggdrasilAccount) {
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
} else {
return Bindings.createObjectBinding(
() -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
}
}
// ====
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
private static WritableImage toFXImage(BufferedImage image) {
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
PixelWriter pw = wr.getPixelWriter();
final int iw = image.getWidth();
final int ih = image.getHeight();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
}
}
return wr;
}
}
@@ -34,20 +34,15 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
@@ -56,7 +51,6 @@ import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -84,8 +78,6 @@ public final class Accounts {
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MicrosoftAccountLoginStage.INSTANCE));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_AUTHLIB_INJECTOR, FACTORY_MICROSOFT);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -94,7 +86,6 @@ public final class Accounts {
type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("yggdrasil", FACTORY_MOJANG);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
type2factory.put("microsoft", FACTORY_MICROSOFT);
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
}
@@ -117,8 +108,6 @@ public final class Accounts {
return FACTORY_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return FACTORY_MOJANG;
else if (account instanceof MicrosoftAccount)
return FACTORY_MICROSOFT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
@@ -320,8 +309,7 @@ public final class Accounts {
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
@@ -23,11 +23,11 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Settings {
import java.util.Locale;
public class Settings {
private static Settings instance;
@@ -21,14 +21,16 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
@@ -37,6 +39,8 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
*
* @author huangyuhui
@@ -44,6 +48,8 @@ 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() {
@@ -65,7 +71,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() {
@@ -118,39 +124,6 @@ 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() {
@@ -368,7 +341,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() {
@@ -405,7 +378,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.
@@ -427,7 +400,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.
@@ -558,8 +531,36 @@ public final class VersionSetting implements Cloneable {
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
nativesDirProperty.addListener(listener);
nativesDirTypeProperty.addListener(listener);
}
public LaunchOptions toLaunchOptions(File gameDir, boolean checkJava) throws InterruptedException {
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
.setMaxMemory(getMaxMemory())
.setMinMemory(getMinMemory())
.setMetaspace(Lang.toIntOrNull(getPermSize()))
.setWidth(getWidth())
.setHeight(getHeight())
.setFullscreen(isFullscreen())
.setServerIp(getServerIp())
.setWrapper(getWrapper())
.setPrecalledCommand(getPreLaunchCommand())
.setNoGeneratedJVMArgs(isNoJVMArgs());
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
}
}
return builder.create();
}
@Override
@@ -587,7 +588,6 @@ public final class VersionSetting implements Cloneable {
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setLauncherVisibility(getLauncherVisibility());
versionSetting.setNativesDir(getNativesDir());
return versionSetting;
}
@@ -619,8 +619,6 @@ 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;
}
@@ -650,7 +648,6 @@ 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));
@@ -659,7 +656,6 @@ 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;
}
@@ -1,20 +0,0 @@
package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public final class AwtUtils {
private AwtUtils() {
}
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
Object taskBar = getTaskBarMethod.invoke(null);
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
setIconImageMethod.invoke(taskBar, image);
} catch (Throwable ignore) {
}
}
}
@@ -27,12 +27,10 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -66,9 +64,6 @@ public final class Controllers {
private static RootPage rootPage;
private static DecoratorController decorator;
private Controllers() {
}
public static Scene getScene() {
return scene;
}
@@ -112,7 +107,6 @@ public final class Controllers {
Logging.LOG.info("Start initializing application");
Controllers.stage = stage;
MicrosoftAccountLoginStage.INSTANCE.initOwner(stage);
stage.setHeight(config().getHeight());
stageHeight.bind(stage.heightProperty());
@@ -129,7 +123,7 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
Task.runAsync(JavaVersion::initialize).start();
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
@@ -31,8 +31,6 @@ 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) {
@@ -40,8 +38,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,7 +430,6 @@ 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 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 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 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};
}
}
@@ -198,7 +198,7 @@ public final class LogWindow extends Stage {
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
} catch (IOException ignored) {
}
}
});
@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
@@ -30,6 +29,8 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class UpgradeDialog extends JFXDialogLayout {
private final WebView webView = new WebView();
public UpgradeDialog(Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
@@ -37,7 +38,6 @@ public class UpgradeDialog extends JFXDialogLayout {
{
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
@@ -17,53 +17,20 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class WebStage extends Stage {
protected final StackPane pane = new StackPane();
protected final JFXProgressBar progressBar = new JFXProgressBar();
protected final WebView webView = new WebView();
protected final WebEngine webEngine = webView.getEngine();
private final WebView webView = new WebView();
public WebStage() {
this(800, 480);
}
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
setScene(new Scene(webView, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getIcons().add(newImage("/assets/img/icon.png"));
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
switch (webView.getEngine().getLoadWorker().getState()) {
case SUCCEEDED:
case FAILED:
case CANCELLED:
return false;
default:
return true;
}
}, webEngine.getLoadWorker().stateProperty()));
BorderPane borderPane = new BorderPane();
borderPane.setPickOnBounds(false);
borderPane.setTop(progressBar);
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
pane.getChildren().setAll(webView, borderPane);
}
public WebView getWebView() {
@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
@@ -32,8 +30,6 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
@@ -41,23 +37,13 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import static java.util.Collections.emptySet;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -87,9 +73,7 @@ public class AccountListItem extends RadioButton {
if (account instanceof OfflineAccount) {
title.bind(characterName);
} else {
title.bind(
account.getUsername().isEmpty() ? characterName :
Bindings.concat(account.getUsername(), " - ", characterName));
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
}
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
@@ -100,7 +84,7 @@ public class AccountListItem extends RadioButton {
return new AccountListItemSkin(this);
}
public Task<?> refreshAsync() {
private Task<?> refreshAsync() {
return Task.runAsync(() -> {
account.clearCache();
try {
@@ -121,32 +105,17 @@ public class AccountListItem extends RadioButton {
});
}
public ObservableBooleanValue canUploadSkin() {
if (account instanceof YggdrasilAccount) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount aiAccount = (AuthlibInjectorAccount) account;
ObjectBinding<Optional<CompleteGameProfile>> profile = aiAccount.getYggdrasilService().getProfileRepository().binding(aiAccount.getUUID());
return createBooleanBinding(() -> {
Set<TextureType> uploadableTextures = profile.get()
.map(AuthlibInjectorAccount::getUploadableTextures)
.orElse(emptySet());
return uploadableTextures.contains(TextureType.SKIN);
}, profile);
} else {
return createBooleanBinding(() -> true);
}
} else {
return createBooleanBinding(() -> false);
}
public void refresh() {
refreshAsync().whenComplete(e -> {}).start();
}
/**
* @return the skin upload task, null if no file is selected
*/
@Nullable
public Task<?> uploadSkin() {
public boolean canUploadSkin() {
return account instanceof YggdrasilAccount && !(account instanceof AuthlibInjectorAccount);
}
public void uploadSkin() {
if (!(account instanceof YggdrasilAccount)) {
return null;
return;
}
FileChooser chooser = new FileChooser();
@@ -154,31 +123,23 @@ public class AccountListItem extends RadioButton {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) {
return null;
return;
}
return refreshAsync()
.thenRunAsync(() -> {
BufferedImage skinImg;
try {
skinImg = ImageIO.read(selectedFile);
} catch (IOException e) {
throw new InvalidSkinException("Failed to read skin image", e);
}
if (skinImg == null) {
throw new InvalidSkinException("Failed to read skin image");
}
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
})
.thenComposeAsync(refreshAsync())
.whenComplete(Schedulers.javafx(), e -> {
if (e != null) {
Controllers.dialog(AddAccountPane.accountException(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
}
});
Controllers.prompt(new PromptDialogPane.Builder(i18n("account.skin.upload"), (questions, resolve, reject) -> {
PromptDialogPane.Builder.CandidatesQuestion q = (PromptDialogPane.Builder.CandidatesQuestion) questions.get(0);
String model = q.getValue() == 0 ? "" : "slim";
refreshAsync()
.thenRunAsync(() -> ((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath()))
.thenComposeAsync(this::refreshAsync)
.thenRunAsync(Schedulers.javafx(), resolve::run)
.whenComplete(Schedulers.javafx(), e -> {
if (e != null) {
reject.accept(AddAccountPane.accountException(e));
}
}).start();
}).addQuestion(new PromptDialogPane.Builder.CandidatesQuestion(i18n("account.skin.model"),
i18n("account.skin.model.default"), i18n("account.skin.model.slim"))));
}
public void remove() {
@@ -31,11 +31,8 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -89,38 +86,20 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
btnRefresh.setOnMouseClicked(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
.whenComplete(ex -> spinnerRefresh.hideSpinner())
.start();
});
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
spinnerRefresh.setContent(btnRefresh);
right.getChildren().add(spinnerRefresh);
right.getChildren().add(btnRefresh);
JFXButton btnUpload = new JFXButton();
SpinnerPane spinnerUpload = new SpinnerPane();
btnUpload.setOnMouseClicked(e -> {
Task<?> uploadTask = skinnable.uploadSkin();
if (uploadTask != null) {
spinnerUpload.showSpinner();
uploadTask
.whenComplete(Schedulers.javafx(), ex -> spinnerUpload.hideSpinner())
.start();
}
});
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
spinnerUpload.setContent(btnUpload);
spinnerUpload.getStyleClass().add("small-spinner-pane");
right.getChildren().add(spinnerUpload);
if (skinnable.canUploadSkin()) {
JFXButton btnUpload = new JFXButton();
btnUpload.setOnMouseClicked(e -> skinnable.uploadSkin());
btnUpload.getStyleClass().add("toggle-icon4");
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
right.getChildren().add(btnUpload);
}
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <[email protected]> and contributors
* Copyright (C) 2020 huangyuhui <[email protected]> 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
@@ -49,7 +49,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import java.util.ArrayList;
import java.util.List;
@@ -69,7 +68,6 @@ public class AddAccountPane extends StackPane {
@FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning;
@FXML private Label lblPassword;
@FXML private Label lblUsername;
@FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer;
@@ -87,10 +85,10 @@ 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::resetServerSelection));
resetServerSelection();
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
cboType.getItems().setAll(Accounts.FACTORIES);
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
// try selecting the preferred login type
cboType.getSelectionModel().select(
@@ -110,26 +108,24 @@ public class AddAccountPane extends StackPane {
// remember the last used login type
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
txtUsername.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresUsername, loginType));
lblUsername.visibleProperty().bind(txtUsername.visibleProperty());
txtPassword.visibleProperty().bind(Bindings.createBooleanBinding(() -> loginType.get().getLoginType().requiresPassword, loginType));
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
lblPassword.visibleProperty().bind(txtPassword.visibleProperty());
cboServers.visibleProperty().bind(loginType.isEqualTo(Accounts.FACTORY_AUTHLIB_INJECTOR));
lblInjectorServer.visibleProperty().bind(cboServers.visibleProperty());
txtUsername.getValidators().add(new Validator(i18n("input.email"), this::validateUsername));
txtUsername.getValidators().add(new Validator(i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
() -> !( // consider the opposite situation: input is valid
(!txtUsername.isVisible() || txtUsername.validate()) &&
txtUsername.validate() &&
// invisible means the field is not needed, neither should it be validated
(!txtPassword.isVisible() || txtPassword.validate()) &&
(!cboServers.isVisible() || cboServers.getSelectionModel().getSelectedItem() != null)
),
txtUsername.textProperty(), txtPassword.textProperty(),
loginType, cboServers.getSelectionModel().selectedItemProperty(),
txtPassword.visibleProperty(), cboServers.visibleProperty()));
txtUsername.textProperty(),
txtPassword.textProperty(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
// authlib-injector links
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
@@ -139,20 +135,6 @@ public class AddAccountPane extends StackPane {
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
}
private boolean validateUsername(String username) {
AccountFactory<?> loginType = cboType.getSelectionModel().getSelectedItem();
if (loginType == Accounts.FACTORY_OFFLINE) {
return true;
} else if (loginType == Accounts.FACTORY_AUTHLIB_INJECTOR) {
AuthlibInjectorServer server = cboServers.getSelectionModel().getSelectedItem();
if (server != null && server.isNonEmailLogin()) {
return true;
}
}
return username.contains("@");
}
private static final String[] ALLOWED_LINKS = { "register" };
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
@@ -174,13 +156,12 @@ public class AddAccountPane extends StackPane {
return unmodifiableList(result);
}
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();
});
/**
* Selects the first server if no server is selected.
*/
private void selectDefaultServer() {
if (!cboServers.getItems().isEmpty() && cboServers.getSelectionModel().isEmpty()) {
cboServers.getSelectionModel().select(0);
}
}
@@ -260,7 +241,7 @@ public class AddAccountPane extends StackPane {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
private static class Selector extends BorderPane implements CharacterSelector {
private class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();
@@ -344,8 +325,6 @@ public class AddAccountPane extends StackPane {
return i18n("account.failed.injector_download_failure");
} else if (exception instanceof CharacterDeletedException) {
return i18n("account.failed.character_deleted");
} else if (exception instanceof InvalidSkinException) {
return i18n("account.skin.invalid_skin");
} else if (exception.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage();
} else {
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <[email protected]> and contributors
* Copyright (C) 2020 huangyuhui <[email protected]> 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(0, serverBeingAdded);
config().getAuthlibInjectorServers().add(serverBeingAdded);
}
fireEvent(new DialogCloseEvent());
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <[email protected]> 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.ui.account;
import javafx.application.Platform;
import javafx.stage.Modality;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.ui.WebStage;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import static org.jackhuang.hmcl.Launcher.COOKIE_MANAGER;
public class MicrosoftAccountLoginStage extends WebStage implements MicrosoftService.WebViewCallback {
public static final MicrosoftAccountLoginStage INSTANCE = new MicrosoftAccountLoginStage();
CompletableFuture<String> future;
Predicate<String> urlTester;
public MicrosoftAccountLoginStage() {
super(600, 600);
initModality(Modality.APPLICATION_MODAL);
titleProperty().bind(webEngine.titleProperty());
webEngine.locationProperty().addListener((observable, oldValue, newValue) -> {
if (urlTester != null && urlTester.test(newValue)) {
future.complete(newValue);
hide();
}
});
showingProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
if (future != null) {
future.completeExceptionally(new InterruptedException());
}
future = null;
urlTester = null;
}
});
}
@Override
public CompletableFuture<String> show(MicrosoftService service, Predicate<String> urlTester, String initialURL) {
Platform.runLater(() -> {
COOKIE_MANAGER.getCookieStore().removeAll();
webEngine.load(initialURL);
show();
});
this.future = new CompletableFuture<>();
this.urlTester = urlTester;
return future;
}
}
@@ -23,10 +23,7 @@ import javafx.util.Duration;
public interface AnimationHandler {
Duration getDuration();
Pane getCurrentRoot();
Node getPreviousNode();
Node getCurrentNode();
}
@@ -99,10 +99,9 @@ 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()));
@@ -1,29 +0,0 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils;
public class RequiredValidator extends ValidatorBase {
public RequiredValidator() {
}
public RequiredValidator(@NamedArg("message") String message) {
super(message);
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
hasErrors.set(StringUtils.isBlank(textField.getText()));
}
}
@@ -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 {
}
}
class Tab {
public static 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");
@@ -24,6 +24,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.DownloadManager;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
@@ -37,7 +38,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Consumer<Region> onCancel;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
private final Consumer<DownloadManager.SpeedEvent> speedEventHandler;
@FXML
private Label lblTitle;
@@ -75,7 +76,7 @@ public class TaskExecutorDialogPane extends StackPane {
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
});
};
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
DownloadManager.speedEvent.channel(DownloadManager.SpeedEvent.class).registerWeak(speedEventHandler);
}
public void setExecutor(TaskExecutor executor) {
@@ -34,16 +34,14 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -130,7 +128,7 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
} else if (task instanceof HMCLModpackExportTask) {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
@@ -276,7 +274,7 @@ public final class TaskListPane extends StackPane {
public void setThrowable(Throwable throwable) {
unbind();
state.setText(throwable.getLocalizedMessage());
state.setText(throwable == null ? "" : throwable.getLocalizedMessage());
bar.setProgress(0);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <[email protected]> and contributors
* Copyright (C) 2020 huangyuhui <[email protected]> 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,18 +18,26 @@
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;
@@ -47,22 +55,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;
@@ -81,6 +89,27 @@ 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();
@@ -90,6 +119,17 @@ 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() {
@@ -134,10 +174,10 @@ public class DecoratorController {
List<Path> candidates;
try (Stream<Path> stream = Files.list(imageDir)) {
candidates = stream
.filter(Files::isReadable)
.filter(Files::isRegularFile)
.filter(it -> {
String ext = getExtension(it).toLowerCase();
return ext.equals("png") || ext.equals("jpg");
String filename = it.getFileName().toString();
return filename.endsWith(".png") || filename.endsWith(".jpg");
})
.collect(toList());
} catch (IOException e) {
@@ -159,23 +199,13 @@ public class DecoratorController {
}
private Optional<Image> tryLoadImage(Path path) {
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();
if (Files.isRegularFile(path)) {
try {
return Optional.of(new Image(path.toAbsolutePath().toUri().toString()));
} catch (IllegalArgumentException ignored) {
}
}
if (img.getException() != null) {
LOG.log(WARNING, "Couldn't load background image", img.getException());
return Optional.empty();
}
return Optional.of(img);
return Optional.empty();
}
// ==== Navigation ====
@@ -25,6 +25,7 @@ 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 {
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
@@ -91,17 +90,8 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
if (name.isPresent()) {
txtModpackName.setText(name.get());
txtModpackName.setDisable(true);
} else {
txtModpackName.getValidators().addAll(
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
}
btnInstall.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtModpackName.validate(),
txtModpackName.textProperty()));
Optional<File> filePath = tryCast(controller.getSettings().get(MODPACK_FILE), File.class);
if (filePath.isPresent()) {
selectedFile = filePath.get();
@@ -132,6 +122,11 @@ public final class LocalModpackPage extends StackPane implements WizardPage {
if (!name.isPresent()) {
txtModpackName.setText(manifest.getName());
txtModpackName.getValidators().addAll(
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().hasVersion(str) && StringUtils.isNotBlank(str)),
new Validator(i18n("version.forbidden_name"), str -> !profile.getRepository().forbidsVersion(str))
);
txtModpackName.textProperty().addListener(e -> btnInstall.setDisable(!txtModpackName.validate()));
}
}, e -> {
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageType.ERROR);
@@ -114,7 +114,7 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
resolve.run();
Controllers.taskDialog(
new FileDownloadTask(url, modpack.toFile(), null)
new FileDownloadTask(url, modpack.toFile())
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
resolve.run();
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.game.HMCLModpackExportTask;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Collections;
@@ -62,97 +63,72 @@ public final class ExportWizardProvider implements WizardProvider {
@SuppressWarnings("unchecked")
List<String> whitelist = (List<String>) settings.get(ModpackFileSelectionPage.MODPACK_FILE_SELECTION);
File modpackFile = (File) settings.get(ModpackInfoPage.MODPACK_FILE);
ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
exportInfo.setWhitelist(whitelist);
String modpackName = (String) settings.get(ModpackInfoPage.MODPACK_NAME);
String modpackAuthor = (String) settings.get(ModpackInfoPage.MODPACK_AUTHOR);
String modpackFileApi = (String) settings.get(ModpackInfoPage.MODPACK_FILE_API);
String modpackVersion = (String) settings.get(ModpackInfoPage.MODPACK_VERSION);
String modpackDescription = (String) settings.get(ModpackInfoPage.MODPACK_DESCRIPTION);
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER);
return exportWithLauncher(modpackType, exportInfo, modpackFile);
switch (modpackType) {
case ModpackTypeSelectionPage.MODPACK_TYPE_HMCL:
return exportAsHMCL(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, includeLauncher);
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
return exportAsMultiMC(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription);
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
return exportAsServer(whitelist, modpackFile, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi);
default:
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
}
}
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportAsHMCL(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, boolean includeLauncherRaw) {
List<File> launcherJar = Launcher.getCurrentJarFiles();
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
return new Task<Object>() {
File tempModpack;
Task<?> exportTask;
boolean includeLauncher = includeLauncherRaw && launcherJar != null;
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
File dest;
if (packWithLauncher) {
dest = tempModpack = Files.createTempFile("hmcl", ".zip").toFile();
} else {
dest = modpackFile;
}
switch (modpackType) {
case ModpackTypeSelectionPage.MODPACK_TYPE_MCBBS:
exportTask = exportAsMcbbs(exportInfo, dest);
break;
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
exportTask = exportAsMultiMC(exportInfo, dest);
break;
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
exportTask = exportAsServer(exportInfo, dest);
break;
default:
throw new IllegalStateException("Unrecognized modpack type " + modpackType);
}
}
@Override
public Collection<Task<?>> getDependents() {
return Collections.singleton(exportTask);
}
@Override
public void execute() throws Exception {
if (!packWithLauncher) return;
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.setPreferredLoginType(config().getPreferredLoginType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg.toPath(), "bg");
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
zip.putFile(background_png, "background.png");
File background_jpg = new File("background.jpg").getAbsoluteFile();
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
}
}
};
}
private Task<?> exportAsMcbbs(ModpackExportInfo exportInfo, File modpackFile) {
return new Task<Void>() {
Task<?> dependency = null;
@Override
public void execute() {
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
public void execute() throws Exception {
File tempModpack = includeLauncher ? Files.createTempFile("hmcl", ".zip").toFile() : modpackFile;
dependency = new HMCLModpackExportTask(profile.getRepository(), version, whitelist,
new Modpack(modpackName, modpackAuthor, modpackVersion, null, modpackDescription, StandardCharsets.UTF_8, null), tempModpack);
if (includeLauncher) {
dependency = dependency.thenRunAsync(() -> {
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.setPreferredLoginType(config().getPreferredLoginType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg.toPath(), "bg");
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
zip.putFile(background_png, "background.png");
File background_jpg = new File("background.jpg").getAbsoluteFile();
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
}
});
}
}
@Override
@@ -162,30 +138,30 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportAsMultiMC(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription) {
return new Task<Void>() {
Task<?> dependency;
@Override
public void execute() {
VersionSetting vs = profile.getVersionSetting(version);
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, exportInfo.getWhitelist(),
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, whitelist,
new MultiMCInstanceConfiguration(
"OneSix",
exportInfo.getName() + "-" + exportInfo.getVersion(),
modpackName + "-" + modpackVersion,
null,
Lang.toIntOrNull(vs.getPermSize()),
vs.getWrapper(),
vs.getPreLaunchCommand(),
null,
exportInfo.getDescription(),
modpackDescription,
null,
exportInfo.getJavaArguments(),
vs.getJavaArgs(),
vs.isFullscreen(),
vs.getWidth(),
vs.getHeight(),
vs.getMaxMemory(),
exportInfo.getMinMemory(),
vs.getMinMemory(),
vs.isShowLogs(),
/* showConsoleOnError */ true,
/* autoCloseConsole */ false,
@@ -205,13 +181,13 @@ public final class ExportWizardProvider implements WizardProvider {
};
}
private Task<?> exportAsServer(ModpackExportInfo exportInfo, File modpackFile) {
private Task<?> exportAsServer(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi) {
return new Task<Void>() {
Task<?> dependency;
@Override
public void execute() {
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
dependency = new ServerModpackExportTask(profile.getRepository(), version, whitelist, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi, modpackFile);
}
@Override
@@ -227,7 +203,7 @@ public final class ExportWizardProvider implements WizardProvider {
case 0:
return new ModpackTypeSelectionPage(controller);
case 1:
return new ModpackInfoPage(controller, profile.getRepository(), version);
return new ModpackInfoPage(controller, version);
case 2:
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
default:
@@ -87,24 +87,21 @@ 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();
@@ -129,13 +126,12 @@ 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() || node.isIndeterminate()) {
if (node.isSelected()) {
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);
}
}
}
}
@@ -17,96 +17,76 @@
*/
package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextArea;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.controls.JFXToggleButton;
import com.jfoenix.validation.RequiredFieldValidator;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.ObservableList;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.MODPACK_TYPE_SERVER;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackInfoPage extends Control implements WizardPage {
private final WizardController controller;
private final HMCLGameRepository gameRepository;
private final ModpackExportInfo.Options options;
private final String versionName;
private final boolean canIncludeLauncher;
private final boolean showFileApi;
private final ModpackExportInfo exportInfo = new ModpackExportInfo();
private SimpleStringProperty versionName = new SimpleStringProperty();
private SimpleStringProperty modpackName = new SimpleStringProperty();
private SimpleStringProperty modpackFileApi = new SimpleStringProperty();
private SimpleStringProperty modpackAuthor = new SimpleStringProperty();
private SimpleStringProperty modpackVersion = new SimpleStringProperty("1.0");
private SimpleStringProperty modpackDescription = new SimpleStringProperty();
private SimpleBooleanProperty includingLauncher = new SimpleBooleanProperty();
private ObjectProperty<EventHandler<? super MouseEvent>> next = new SimpleObjectProperty<>();
private final SimpleStringProperty name = new SimpleStringProperty("");
private final SimpleStringProperty author = new SimpleStringProperty("");
private final SimpleStringProperty version = new SimpleStringProperty("1.0");
private final SimpleStringProperty description = new SimpleStringProperty("");
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 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("");
public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) {
public ModpackInfoPage(WizardController controller, String version) {
this.controller = controller;
this.gameRepository = gameRepository;
this.options = tryCast(controller.getSettings().get(MODPACK_INFO_OPTION), ModpackExportInfo.Options.class)
.orElseThrow(() -> new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"));
this.versionName = version;
name.set(version);
author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
VersionSetting versionSetting = gameRepository.getVersionSetting(versionName);
minMemory.set(Optional.ofNullable(versionSetting.getMinMemory()).orElse(0));
launchArguments.set(versionSetting.getMinecraftArgs());
javaArguments.set(versionSetting.getJavaArgs());
modpackName.set(version);
modpackAuthor.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
versionName.set(version);
List<File> launcherJar = Launcher.getCurrentJarFiles();
canIncludeLauncher = launcherJar != null;
showFileApi = controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER;
next.set(e -> onNext());
}
@FXML
private void onNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("modpack.wizard.step.initialization.save"));
@@ -116,35 +96,24 @@ public final class ModpackInfoPage extends Control implements WizardPage {
controller.onEnd();
return;
}
exportInfo.setName(name.get());
exportInfo.setFileApi(fileApi.get());
exportInfo.setVersion(version.get());
exportInfo.setAuthor(author.get());
exportInfo.setDescription(description.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setUrl(url.get());
exportInfo.setForceUpdate(forceUpdate.get());
exportInfo.setPackWithLauncher(packWithLauncher.get());
exportInfo.setMinMemory(minMemory.get());
exportInfo.setLaunchArguments(launchArguments.get());
exportInfo.setJavaArguments(javaArguments.get());
exportInfo.setAuthlibInjectorServer(authlibInjectorServer.get());
if (StringUtils.isNotBlank(mcbbsThreadId.get())) {
exportInfo.setOrigins(Collections.singletonList(new McbbsModpackManifest.Origin(
"mcbbs", Integer.parseInt(mcbbsThreadId.get())
)));
}
controller.getSettings().put(MODPACK_INFO, exportInfo);
controller.getSettings().put(MODPACK_NAME, modpackName.get());
controller.getSettings().put(MODPACK_FILE_API, modpackFileApi.get());
controller.getSettings().put(MODPACK_VERSION, modpackVersion.get());
controller.getSettings().put(MODPACK_AUTHOR, modpackAuthor.get());
controller.getSettings().put(MODPACK_FILE, file);
controller.getSettings().put(MODPACK_DESCRIPTION, modpackDescription.get());
controller.getSettings().put(MODPACK_INCLUDE_LAUNCHER, includingLauncher.get());
controller.onNext();
}
@Override
public void cleanup(Map<String, Object> settings) {
controller.getSettings().remove(MODPACK_INFO);
controller.getSettings().remove(MODPACK_NAME);
controller.getSettings().remove(MODPACK_VERSION);
controller.getSettings().remove(MODPACK_AUTHOR);
controller.getSettings().remove(MODPACK_DESCRIPTION);
controller.getSettings().remove(MODPACK_INCLUDE_LAUNCHER);
controller.getSettings().remove(MODPACK_FILE);
}
@Override
@@ -157,12 +126,19 @@ public final class ModpackInfoPage extends Control implements WizardPage {
return new ModpackInfoPageSkin(this);
}
public static final String MODPACK_INFO = "modpack.info";
public static final String MODPACK_NAME = "modpack.name";
public static final String MODPACK_FILE_API = "modpack.file_api";
public static final String MODPACK_VERSION = "modpack.version";
public static final String MODPACK_AUTHOR = "archive.author";
public static final String MODPACK_DESCRIPTION = "modpack.description";
public static final String MODPACK_INCLUDE_LAUNCHER = "modpack.include_launcher";
public static final String MODPACK_FILE = "modpack.file";
public static final String MODPACK_INFO_OPTION = "modpack.info.option";
public static class ModpackInfoPageSkin extends SkinBase<ModpackInfoPage> {
private ObservableList<Node> originList;
private final JFXTextField txtModpackName;
private final JFXTextField txtModpackFileApi;
private final JFXTextField txtModpackAuthor;
private final JFXTextField txtModpackVersion;
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
super(skinnable);
@@ -175,8 +151,6 @@ public final class ModpackInfoPage extends Control implements WizardPage {
scroll.setFitToHeight(true);
getChildren().setAll(scroll);
List<JFXTextField> validatingFields = new ArrayList<>();
{
BorderPane borderPane = new BorderPane();
borderPane.setStyle("-fx-padding: 16;");
@@ -205,37 +179,31 @@ public final class ModpackInfoPage extends Control implements WizardPage {
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
Label versionNameLabel = new Label();
versionNameLabel.setText(skinnable.versionName);
versionNameLabel.textProperty().bind(skinnable.versionName);
borderPane1.setRight(versionNameLabel);
list.getContent().add(borderPane1);
}
{
JFXTextField txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.name);
txtModpackName = new JFXTextField();
txtModpackName.textProperty().bindBidirectional(skinnable.modpackName);
txtModpackName.setLabelFloat(true);
txtModpackName.setPromptText(i18n("modpack.name"));
txtModpackName.getValidators().add(new RequiredValidator(i18n("modpack.not_a_valid_name")));
RequiredFieldValidator validator = new RequiredFieldValidator();
validator.setMessage(i18n("modpack.not_a_valid_name"));
txtModpackName.getValidators().add(validator);
StackPane.setMargin(txtModpackName, insets);
list.getContent().add(txtModpackName);
validatingFields.add(txtModpackName);
}
if (skinnable.options.isRequireFileApi()) {
JFXTextField txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
if (skinnable.showFileApi) {
txtModpackFileApi = new JFXTextField();
txtModpackFileApi.textProperty().bindBidirectional(skinnable.modpackFileApi);
txtModpackFileApi.setLabelFloat(true);
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
if (skinnable.options.isValidateFileApi()) {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackFileApi.getValidators().add(validator);
txtModpackFileApi.getValidators().add(new Validator(s -> {
if (s.isEmpty()) {
return true;
}
try {
new URL(s).toURI();
return true;
@@ -245,37 +213,35 @@ public final class ModpackInfoPage extends Control implements WizardPage {
}));
StackPane.setMargin(txtModpackFileApi, insets);
list.getContent().add(txtModpackFileApi);
validatingFields.add(txtModpackFileApi);
} else {
txtModpackFileApi = null;
}
{
JFXTextField txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
txtModpackAuthor = new JFXTextField();
txtModpackAuthor.textProperty().bindBidirectional(skinnable.modpackAuthor);
txtModpackAuthor.setLabelFloat(true);
txtModpackAuthor.setPromptText(i18n("archive.author"));
txtModpackAuthor.getValidators().add(new RequiredValidator());
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackAuthor.getValidators().add(validator);
StackPane.setMargin(txtModpackAuthor, insets);
list.getContent().add(txtModpackAuthor);
validatingFields.add(txtModpackAuthor);
}
{
JFXTextField txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
txtModpackVersion = new JFXTextField();
txtModpackVersion.textProperty().bindBidirectional(skinnable.modpackVersion);
txtModpackVersion.setLabelFloat(true);
txtModpackVersion.setPromptText(i18n("archive.version"));
txtModpackVersion.getValidators().add(new RequiredValidator());
RequiredFieldValidator validator = new RequiredFieldValidator();
txtModpackVersion.getValidators().add(validator);
StackPane.setMargin(txtModpackVersion, insets);
list.getContent().add(txtModpackVersion);
validatingFields.add(txtModpackVersion);
}
{
JFXTextArea area = new JFXTextArea();
area.textProperty().bindBidirectional(skinnable.description);
area.textProperty().bindBidirectional(skinnable.modpackDescription);
area.setLabelFloat(true);
area.setPromptText(i18n("modpack.desc"));
area.setMinHeight(400);
@@ -283,123 +249,18 @@ public final class ModpackInfoPage extends Control implements WizardPage {
list.getContent().add(area);
}
if (skinnable.options.isRequireForceUpdate()) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.force_update")));
list.getContent().add(pane);
JFXToggleButton button = new JFXToggleButton();
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
}
if (skinnable.options.isRequireAuthlibInjectorServer()) {
JFXComboBox<AuthlibInjectorServer> cboServers = new JFXComboBox<>();
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
skinnable.authlibInjectorServer.bind(Bindings.createStringBinding(() ->
Optional.ofNullable(cboServers.getSelectionModel().getSelectedItem())
.map(AuthlibInjectorServer::getUrl)
.orElse(null)));
BorderPane pane = new BorderPane();
Label left = new Label(i18n("account.injector.server"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
pane.setLeft(left);
pane.setRight(cboServers);
list.getContent().add(pane);
}
if (skinnable.options.isRequireMinMemory()) {
JFXTextField txtMinMemory = new JFXTextField();
FXUtils.bindInt(txtMinMemory, skinnable.minMemory);
txtMinMemory.getValidators().add(new NumberValidator(i18n("input.number"), false));
FXUtils.setLimitWidth(txtMinMemory, 300);
validatingFields.add(txtMinMemory);
BorderPane pane = new BorderPane();
Label label = new Label(i18n("settings.min_memory"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
pane.setLeft(label);
BorderPane.setAlignment(txtMinMemory, Pos.CENTER_RIGHT);
pane.setRight(txtMinMemory);
list.getContent().add(pane);
}
if (skinnable.options.isRequireLaunchArguments()) {
JFXTextField txtLaunchArguments = new JFXTextField();
txtLaunchArguments.textProperty().bindBidirectional(skinnable.launchArguments);
txtLaunchArguments.setLabelFloat(true);
txtLaunchArguments.setPromptText(i18n("settings.advanced.minecraft_arguments"));
StackPane.setMargin(txtLaunchArguments, insets);
list.getContent().add(txtLaunchArguments);
}
if (skinnable.options.isRequireJavaArguments()) {
JFXTextField txtJavaArguments = new JFXTextField();
txtJavaArguments.textProperty().bindBidirectional(skinnable.javaArguments);
txtJavaArguments.setLabelFloat(true);
txtJavaArguments.setPromptText(i18n("settings.advanced.jvm_args"));
StackPane.setMargin(txtJavaArguments, insets);
list.getContent().add(txtJavaArguments);
}
if (skinnable.options.isRequireOrigins() || skinnable.options.isRequireUrl()) {
BorderPane originPane = new BorderPane();
Label title = new Label(i18n("modpack.origin"));
originPane.setTop(title);
VBox container = new VBox();
BorderPane.setMargin(container, new Insets(0, 0, 0, 16));
originPane.setCenter(container);
list.getContent().add(originPane);
if (skinnable.options.isRequireUrl()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.url"));
pane.setLeft(left);
JFXTextField txtModpackUrl = new JFXTextField();
txtModpackUrl.textProperty().bindBidirectional(skinnable.url);
pane.setRight(txtModpackUrl);
container.getChildren().add(pane);
}
if (skinnable.options.isRequireOrigins()) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8, 0, 8, 0));
Label left = new Label(i18n("modpack.origin.mcbbs"));
pane.setLeft(left);
JFXTextField txtMcbbs = new JFXTextField();
FXUtils.setValidateWhileTextChanged(txtMcbbs, true);
txtMcbbs.getValidators().add(new NumberValidator(i18n("input.number"), true));
txtMcbbs.textProperty().bindBidirectional(skinnable.mcbbsThreadId);
pane.setRight(txtMcbbs);
container.getChildren().add(pane);
validatingFields.add(txtMcbbs);
}
}
{
BorderPane pane = new BorderPane();
pane.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
list.getContent().add(pane);
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_HMCL) {
BorderPane borderPane1 = new BorderPane();
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.include_launcher")));
list.getContent().add(borderPane1);
JFXToggleButton button = new JFXToggleButton();
button.setDisable(!skinnable.canIncludeLauncher);
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
button.selectedProperty().bindBidirectional(skinnable.includingLauncher);
button.setSize(8);
button.setMinHeight(16);
button.setMaxHeight(16);
pane.setRight(button);
borderPane1.setRight(button);
}
}
@@ -415,13 +276,15 @@ public final class ModpackInfoPage extends Control implements WizardPage {
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
nextButton.setText(i18n("wizard.next"));
nextButton.getStyleClass().add("jfx-button-raised");
nextButton.disableProperty().bind(
// Disable nextButton if any text of JFXTextFields in validatingFields does not fulfill
// our requirement.
Bindings.createBooleanBinding(() -> validatingFields.stream()
.map(field -> !field.validate())
.reduce(false, (left, right) -> left || right),
validatingFields.stream().map(JFXTextField::textProperty).toArray(StringProperty[]::new)));
if (skinnable.showFileApi) {
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate() || !txtModpackFileApi.validate(),
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty(), txtModpackFileApi.textProperty()));
} else {
nextButton.disableProperty().bind(Bindings.createBooleanBinding(() ->
!txtModpackName.validate() || !txtModpackVersion.validate() || !txtModpackAuthor.validate(),
txtModpackName.textProperty(), txtModpackAuthor.textProperty(), txtModpackVersion.textProperty()));
}
hbox.getChildren().add(nextButton);
}
}
@@ -20,23 +20,18 @@ package org.jackhuang.hmcl.ui.export;
import com.jfoenix.controls.JFXButton;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import java.util.Map;
import static org.jackhuang.hmcl.ui.export.ModpackInfoPage.MODPACK_INFO_OPTION;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackTypeSelectionPage extends StackPane implements WizardPage {
private final WizardController controller;
@FXML
private JFXButton btnMCBBS;
private JFXButton btnHMCL;
@FXML
private JFXButton btnMultiMC;
@FXML
@@ -46,15 +41,12 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
this.controller = controller;
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
JFXButton[] buttons = new JFXButton[]{btnMCBBS, btnMultiMC, btnServer};
String[] types = new String[]{MODPACK_TYPE_MCBBS, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
ModpackExportInfo.Options[] options = new ModpackExportInfo.Options[]{McbbsModpackExportTask.OPTION, MultiMCModpackExportTask.OPTION, ServerModpackExportTask.OPTION};
JFXButton[] buttons = new JFXButton[]{btnHMCL, btnMultiMC, btnServer};
String[] types = new String[]{MODPACK_TYPE_HMCL, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
for (int i = 0; i < types.length; ++i) {
String type = types[i];
ModpackExportInfo.Options option = options[i];
buttons[i].setOnMouseClicked(e -> {
controller.getSettings().put(MODPACK_TYPE, type);
controller.getSettings().put(MODPACK_INFO_OPTION, option);
controller.onNext();
});
}
@@ -71,7 +63,7 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
public static final String MODPACK_TYPE = "modpack.type";
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
public static final String MODPACK_TYPE_HMCL = "hmcl";
public static final String MODPACK_TYPE_MULTIMC = "multimc";
public static final String MODPACK_TYPE_SERVER = "server";
}
@@ -535,12 +535,8 @@ 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,7 +68,6 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
{
setMessage(i18n("profile.already_exists"));
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
@@ -132,7 +132,7 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
String getSubtitle() {
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
return StringUtils.parseColorEscapes(packInfo.getDescription());
}
Datapack.Pack getPackInfo() {
@@ -82,7 +82,7 @@ public class GameItem extends Control {
CompletableFuture.runAsync(() -> {
try {
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
ModpackConfiguration<Void> config = profile.getRepository().readModpackConfiguration(version);
if (config == null) return;
tag.set(config.getVersion());
} catch (IOException e) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.versions;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
@@ -39,8 +40,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -35,11 +35,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameDirectoryType;
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.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -80,7 +76,6 @@ 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;
@@ -93,13 +88,11 @@ 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;
@@ -145,11 +138,6 @@ 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;
@@ -191,7 +179,6 @@ 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());
@@ -211,9 +198,6 @@ 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
@@ -225,7 +209,6 @@ 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());
@@ -257,10 +240,6 @@ 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,9 +46,7 @@ import java.util.logging.Level;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Versions {
private Versions() {
}
public class Versions {
public static void deleteVersion(Profile profile, String version) {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER;
@@ -158,7 +156,7 @@ public final 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,6 +50,7 @@ public class WorldExportPage extends WizardSinglePage {
return new WorldExportPageSkin(this);
}
public StringProperty pathProperty() {
return path;
}
@@ -94,4 +94,5 @@ public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
getChildren().setAll(container);
}
}
@@ -22,17 +22,11 @@ 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,13 +23,9 @@ 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,7 +18,7 @@
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
@@ -26,24 +26,31 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
class HMCLDownloadTask extends FileDownloadTask {
class HMCLDownloadTask extends Task<Void> {
private RemoteVersion.Type archiveFormat;
private final FileDownloadTask task;
private final RemoteVersion.Type archiveFormat;
private final Path target;
public HMCLDownloadTask(RemoteVersion version, Path target) {
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
archiveFormat = version.getType();
this.task = new FileDownloadTask(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
this.archiveFormat = version.getType();
this.target = target;
}
@Override
public Collection<Task<?>> getDependents() {
return Collections.singletonList(this);
}
@Override
public void execute() throws Exception {
super.execute();
try {
Path target = getFile().toPath();
switch (archiveFormat) {
case JAR:
break;
@@ -52,7 +59,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))) {
Pack200Utils.unpack(in, out);
Pack200.newUnpacker().unpack(in, out);
}
break;
@@ -60,7 +67,7 @@ class HMCLDownloadTask extends FileDownloadTask {
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
} catch (Throwable e) {
getFile().delete();
Files.deleteIfExists(target);
throw e;
}
}
@@ -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 (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
if (SystemUtils.JRE_CAPABILITY_PACK200 && 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));
@@ -199,7 +199,7 @@ public final class UpdateHandler {
StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
for (int i = 0; i < stacktrace.length; i++) {
StackTraceElement element = stacktrace[i];
if (Main.class.getName().equals(element.getClassName()) && "main".equals(element.getMethodName())) {
if (Main.class.getName().equals(element.getClassName())) {
// we've reached the main method
return i + 1 != stacktrace.length;
}
@@ -1,35 +0,0 @@
/*
* 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;
import java.util.Set;
/**
* Utility for Adding JavaFX to module path.
*
* @author ZekerZhayard
*/
public final class JavaFXPatcher {
private JavaFXPatcher() {
}
public static void patch(Set<String> modules, Path... jarPaths) {
// Nothing to do with Java 8
}
}
@@ -1,340 +0,0 @@
/*
* 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 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;
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;
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() {
}
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.
*/
public static void patch() throws PatchException, IncompatibleVersionException {
// Do nothing if JavaFX is detected
try {
try {
forName("javafx.application.Application");
return;
} catch (Exception ignored) {
}
} catch (UnsupportedClassVersionError error) {
// Loading the JavaFX class was unsupported.
// We are probably on 8 and its on 11
throw new IncompatibleVersionException();
}
// So the problem with Java 8 is that some distributions DO NOT BUNDLE JAVAFX
// Why is this a problem? OpenJFX does not come in public bundles prior to Java 11
// So you're out of luck unless you change your JDK or update Java.
if (CURRENT_JAVA.getParsedVersion() < 11) {
throw new IncompatibleVersionException();
}
// We can only self-patch JavaFX on x86-64 platform.
// For ARM support, user's manual patch is required.
if (Architecture.CURRENT != Architecture.X86_64) {
throw new IncompatibleVersionException();
}
// Otherwise we're free to download in Java 11+
LOG.info("Missing JavaFX dependencies, attempting to patch in missing classes");
// Download missing dependencies
List<DependencyDescriptor> missingDependencies = checkMissingDependencies();
if (!missingDependencies.isEmpty()) {
try {
fetchDependencies(missingDependencies);
} catch (IOException e) {
throw new PatchException("Failed to download dependencies", e);
}
}
// Add the dependencies
try {
loadFromCache();
} catch (IOException ex) {
throw new PatchException("Failed to load JavaFX cache", ex);
} catch (ReflectiveOperationException | NoClassDefFoundError ex) {
throw new PatchException("Failed to add dependencies to classpath!", ex);
}
LOG.info(" - Done!");
}
/**
* 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.
*/
private static void loadFromCache() throws IOException, ReflectiveOperationException {
LOG.info(" - Loading dependencies...");
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);
}
/**
* Download dependencies.
*
* @throws IOException When the files cannot be fetched or saved.
*/
private static void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
ProgressFrame dialog = new ProgressFrame(i18n("download.javafx"));
dialog.setVisible(true);
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();
}
private static List<DependencyDescriptor> checkMissingDependencies() {
List<DependencyDescriptor> missing = new ArrayList<>();
for (DependencyDescriptor dependency : JFX_DEPENDENCIES) {
if (!Files.exists(dependency.localPath())) {
missing.add(dependency);
continue;
}
try {
verifyChecksum(dependency);
} catch (ChecksumMismatchException e) {
LOG.warning("Corrupted dependency " + dependency.filename() + ": " + e.getMessage());
missing.add(dependency);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return missing;
}
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);
}
}
public static class PatchException extends Exception {
PatchException(String message, Throwable cause) {
super(message, cause);
}
}
public static class IncompatibleVersionException extends Exception {
}
public static class ProgressFrame extends JDialog {
private final JProgressBar progressBar;
private final JLabel progressText;
public ProgressFrame(String title) {
super((Dialog) null);
JPanel panel = new JPanel();
setResizable(false);
setTitle(title);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setBounds(100, 100, 600, 150);
setContentPane(panel);
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 };
panel.setLayout(gridBagLayout);
progressText = new JLabel("");
GridBagConstraints gbc_lblProgressText = new GridBagConstraints();
gbc_lblProgressText.insets = new Insets(10, 0, 5, 0);
gbc_lblProgressText.gridx = 0;
gbc_lblProgressText.gridy = 0;
panel.add(progressText, gbc_lblProgressText);
progressBar = new JProgressBar();
GridBagConstraints gbc_progressBar = new GridBagConstraints();
gbc_progressBar.insets = new Insets(0, 25, 5, 25);
gbc_progressBar.fill = GridBagConstraints.HORIZONTAL;
gbc_progressBar.gridx = 0;
gbc_progressBar.gridy = 1;
panel.add(progressBar, gbc_progressBar);
JButton btnCancel = new JButton(i18n("button.cancel"));
btnCancel.addActionListener(e -> {
System.exit(-1);
});
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
gbc_btnCancel.insets = new Insets(0, 25, 5, 25);
gbc_btnCancel.fill = GridBagConstraints.HORIZONTAL;
gbc_btnCancel.gridx = 0;
gbc_btnCancel.gridy = 2;
panel.add(btnCancel, gbc_btnCancel);
}
public void setStatus(String status) {
progressText.setText(status);
}
public void setProgress(int current, int total) {
progressBar.setValue(current);
progressBar.setMaximum(total);
}
}
}
@@ -28,8 +28,6 @@ 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");
@@ -63,18 +61,12 @@ 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,104 +0,0 @@
/*
* 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.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.nio.file.Path;
import java.util.List;
import java.util.Set;
import jdk.internal.loader.BuiltinClassLoader;
/**
* Utility for Adding JavaFX to module path.
*
* @author ZekerZhayard
*/
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 jarPaths All jar paths
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
*/
public static void patch(Set<String> modules, Path... jarPaths) throws ReflectiveOperationException {
// Find all modules
ModuleFinder finder = ModuleFinder.of(jarPaths);
// Load all modules as unnamed module
for (ModuleReference mref : finder.findAll()) {
((BuiltinClassLoader) ClassLoader.getSystemClassLoader()).loadModule(mref);
}
// Define all modules
Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, modules);
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
// Add-Exports and Add-Opens
try {
// Some hacks
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
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) {
String[] name = target.split("/", 2); // <module>/<package>
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
}
}
Binary file not shown.
@@ -4,10 +4,8 @@
<?import com.jfoenix.validation.RequiredFieldValidator?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.shape.SVGPath?>
<?import org.jackhuang.hmcl.ui.construct.RequiredValidator?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
type="StackPane">
@@ -47,13 +45,13 @@
</JFXButton>
</HBox>
<Label fx:id="lblUsername" text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<JFXTextField fx:id="txtUsername" GridPane.columnIndex="1" GridPane.rowIndex="2" GridPane.columnSpan="2"
FXUtils.validateWhileTextChanged="true" onAction="#onCreationAccept">
<validators>
<RequiredValidator message="%input.not_empty">
</RequiredValidator>
<RequiredFieldValidator message="%input.not_empty">
</RequiredFieldValidator>
</validators>
</JFXTextField>
@@ -13,11 +13,11 @@
</fx:define>
<VBox fx:id="list" styleClass="jfx-list-view" maxHeight="150" maxWidth="300">
<Label padding="${insets}" text="%modpack.export.as" />
<JFXButton fx:id="btnMCBBS" prefWidth="${list.width}">
<JFXButton fx:id="btnHMCL" prefWidth="${list.width}">
<graphic>
<BorderPane mouseTransparent="true">
<left>
<TwoLineListItem title="%modpack.type.mcbbs" subtitle="%modpack.type.mcbbs.export" />
<TwoLineListItem title="%modpack.type.hmcl" subtitle="%modpack.type.hmcl.export" />
</left>
<right>
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
@@ -53,7 +53,7 @@
</JFXTextField>
</right>
</BorderPane>
<BorderPane> <!-- Launcher Visibility -->
<left>
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
@@ -128,9 +128,6 @@
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.

After

Width:  |  Height:  |  Size: 292 KiB

@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2021 huangyuhui <[email protected]> and contributors
# Copyright (C) 2020 huangyuhui <[email protected]> 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) 2021 huangyuhui.
about.copyright.statement=Copyright (c) 2020 huangyuhui.
about.author=Author
about.author.statement=huanghongxun ([email protected])
about.thanks_to=Thanks to
@@ -57,16 +57,16 @@ account.injector.server_name=Server Name
account.manage=Account List
account.methods=Login Type
account.methods.authlib_injector=authlib-injector
account.methods.microsoft=Microsoft Account
account.methods.offline=Offline
account.methods.yggdrasil=Mojang
account.missing=No Account
account.missing.add=Click here to add
account.password=Password
account.skin.file=Skin file
account.skin.model=Character model
account.skin.model.default=Steve
account.skin.model.slim=Alex
account.skin.upload=Upload skin
account.skin.upload.failed=Failed to upload skin
account.skin.invalid_skin=Unrecognized skin file
account.username=Name
archive.author=Authors
@@ -106,15 +106,13 @@ download.failed.refresh=Unable to download version list. Click here to retry.
download.provider.mcbbs=MCBBS (https://www.mcbbs.net/)
download.provider.bmclapi=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.provider.mojang=Mojang (OptiFine download service is provided by BMCLAPI)
download.javafx=Downloading the JavaFX runtime components
extension.bat=Windows Bat file
extension.mod=Mod file
extension.png=Image file
extension.sh=Bash shell
fatal.javafx.incompatible=Application cannot patch JavaFX on current Java environment below 11.\nPlease run this app using JDK 11 or higher or a JDK with JavaFX bundled.
fatal.javafx.missing=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Oracle JRE 8, or install BellSoft Liberica Full JRE.\nIf you are using other OpenJDK builds, please ensure OpenJFX is included.
fatal.missing_javafx=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Java 8 or 10.\nIf you are using OpenJDK, please ensure OpenJFX is included.
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use Hello Minecraft! Launcher, but it will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause the launcher not to function properly.\nPlease upgrade your Java Runtime to 8u101 or later to resolve the problem.
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure Hello Minecraft! Launcher has read and write access to "%s" and the files in it.
fatal.migration_requires_manual_reboot=The update is complete. Please reopen Hello Minecraft! Launcher.
@@ -183,7 +181,6 @@ launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later versions of Java.
launch.advice.newer_java=Java 8 is recommended to make the game faster. For many Minecraft 1.12 of higher, and most mods, Java 8 is required.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game may crash. Shall we continue launching?
launch.advice.require_newer_java_version=Minecraft %1$s requires Java %2$s or later, are you willing to download one now?
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. Shall we continue launching?
launch.failed=Unable to launch
launch.failed.cannot_create_jvm=Java virtual machine could not be created. Java arguments may cause issues. Please restart without JVM arguments.
@@ -196,7 +193,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 Game
launch.state.waiting_launching=Launching modpack
launch.wrong_javadir=Invalid Java directory, default Java path will be applied.
launcher=Launcher
@@ -266,10 +263,6 @@ modpack.invalid=Invalid modpack file.
modpack.mismatched_type=Inappropriate modpack type, your current game is a %s modpack, but your update file is a %s modpack.
modpack.name=Modpack Name
modpack.not_a_valid_name=Invalid modpack name
modpack.origin=Origin
modpack.origin.url=Official Website
modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=Thread id
modpack.scan=Scanning this modpack
modpack.task.install=Import Modpack
modpack.task.install.error=This modpack file cannot be recognized. Only Curse and MultiMC modpacks are supported.
@@ -279,8 +272,8 @@ modpack.type.curse.completion=Install files related to Curse modpack
modpack.type.curse.tolerable_error=We cannot complete the download of all files of this Curse modpack. You can retry the download when starting corresponding game version. You may retry for a couple of times due to network problems.
modpack.type.curse.error=Unable to install this Curse modpack. Please retry.
modpack.type.curse.not_found=Some of required resources are missing and thus could not be downloaded. Please consider the latest version or other modpacks.
modpack.type.mcbbs=MCBBS Standard
modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher
modpack.type.hmcl=Hello Minecraft! Launcher
modpack.type.hmcl.export=Can be imported by Hello Minecraft! Launcher
modpack.type.multimc=MultiMC
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
modpack.type.server=Server Auto-Update Modpack
@@ -296,7 +289,6 @@ modpack.wizard.step.2.title=Add files to the modpack.
modpack.wizard.step.3=Modpack Type
modpack.wizard.step.3.title=Choose the format of the modpack.
modpack.wizard.step.initialization.exported_version=Exported game version
modpack.wizard.step.initialization.force_update=Force updating modpack if possible
modpack.wizard.step.initialization.include_launcher=Include the launcher
modpack.wizard.step.initialization.save=Export to...
modpack.wizard.step.initialization.warning=Before creating a modpack, you should ensure that the game can launch successfully,\nand that your Minecraft is a release version.\nDo NOT add mods which cannot be redistributed.
@@ -364,16 +356,13 @@ 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 (overriding defaults)
settings.advanced.jvm_args=Java VM Arguments
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
@@ -415,7 +404,6 @@ settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=Account
settings.launcher.theme=Theme
settings.min_memory=Min Memory/MB
settings.max_memory=Max Memory/MB
settings.physical_memory=Physical Memory Size
settings.show_log=Show Logs
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2021 huangyuhui <[email protected]> and contributors
# Copyright (C) 2020 huangyuhui <[email protected]> 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) 2021 huangyuhui.
about.copyright.statement=Copyright (c) 2020 huangyuhui.
about.author=Autor
about.author.statement=huanghongxun ([email protected])
about.thanks_to=Gracias a
@@ -57,7 +57,6 @@ account.injector.server_name=Nombre de servidor
account.manage=Lista de cuentas
account.methods=Tipo de inición
account.methods.authlib_injector=authlib-injector
account.methods.microsoft=Microsoft
account.methods.offline=Offline
account.methods.yggdrasil=Mojang
account.missing=No hay cuenta
@@ -335,16 +334,13 @@ 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 (overriding defaults)
settings.advanced.jvm_args=Java VM Arguments
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) 2021 huangyuhui <[email protected]> and contributors
# Copyright (C) 2020 huangyuhui <[email protected]> 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) 2021 huangyuhui.
about.copyright.statement=(C) 2020 huangyuhui.
about.author=Автор
about.author.statement=huanghongxun ([email protected])
about.thanks_to=Отдельное спасибо
@@ -57,7 +57,6 @@ account.injector.server_name=Имя сервера
account.manage=Список уч. записей
account.methods=Тип входа
account.methods.authlib_injector=authlib-injector
account.methods.microsoft=Microsoft
account.methods.offline=Вход без пароля
account.methods.yggdrasil=Уч. запись Mojang
account.missing=Без уч. записи
@@ -359,9 +358,6 @@ 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) 2021 huangyuhui <[email protected]> and contributors
# Copyright (C) 2020 huangyuhui <[email protected]> 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) 2021 huangyuhui。
about.copyright.statement=著作權所有 (c) 2020 huangyuhui。
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.thanks_to=鳴謝
@@ -57,16 +57,16 @@ account.injector.server_name=伺服器名稱
account.manage=帳戶列表
account.methods=登入方式
account.methods.authlib_injector=authlib-injector 登入
account.methods.microsoft=微軟帳戶
account.methods.offline=離線模式
account.methods.yggdrasil=正版登入
account.missing=沒有遊戲帳戶
account.missing.add=按一下此處加入帳戶
account.password=密碼
account.skin.file=皮膚圖片檔案
account.skin.model=人物模型
account.skin.model.default=Steve
account.skin.model.slim=Alex
account.skin.upload=上傳皮膚
account.skin.upload=皮膚上傳失敗
account.skin.invalid_skin=無法識別的皮膚文件
account.username=使用者名稱
archive.author=作者
@@ -106,15 +106,13 @@ download.failed.refresh=載入版本列表失敗,按一下此處重試。
download.provider.mcbbs=我的世界中文論壇 (MCBBS, https://www.mcbbs.net/)
download.provider.bmclapi=BMCLAPI (bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方伺服器 (OptiFine 自動安裝的下載來源是 BMCLAPI)
download.javafx=正在下載 JavaFX 運行時組件
extension.bat=Windows 指令碼
extension.mod=模組檔案
extension.png=圖片檔案
extension.sh=Bash 指令碼
fatal.javafx.incompatible=缺少 JavaFX 運行環境。\nHMCL 無法在低於 Java 11 且缺少 JavaFX 的 Java 環境上自行補全 JavaFX 運行環境。\n請更換如 Oracle Java 8 等包含 JavaFX 的 Java 運行環境,或者更新到 Java 11 或更高版本。
fatal.javafx.missing=找不到 JavaFX。\n如果您使用的是 Java 11 或更高版本,請降級到 Oracle JRE 8,或者安裝 BellSoft Liberica Full JRE。\n如果您使用的是其他 OpenJDK 构建,請確保其包含 OpenJFX。
fatal.missing_javafx=找不到 JavaFX。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=目前的 Java 平台缺少 DST Root CA X3 憑證。\n您依然可以使用 Hello Minecraft! Launcher,但會無法連線到部分網站 (如使用 Lets Encrypt 憑證的站台),這可能會使 Hello Minecraft! Launcher 無法正常運作。\n請將您的 Java 升級到 8u101 以上來解決此問題。
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 Hello Minecraft! Launcher 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 Hello Minecraft! Launcher。
@@ -358,16 +356,13 @@ 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) 2021 huangyuhui <[email protected]> and contributors
# Copyright (C) 2020 huangyuhui <[email protected]> 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) 2021 huangyuhui.
about.copyright.statement=版权所有 (c) 2020 huangyuhui.
about.author=作者
about.author.statement=huanghongxun ([email protected])
about.thanks_to=鸣谢
@@ -57,16 +57,16 @@ account.injector.server_name=服务器名称
account.manage=账户列表
account.methods=登录方式
account.methods.authlib_injector=外置登录 (authlib-injector)
account.methods.microsoft=微软登录
account.methods.offline=离线模式
account.methods.yggdrasil=Mojang 账号
account.methods.yggdrasil=正版登录
account.missing=没有游戏账户
account.missing.add=点击此处添加账户
account.password=密码
account.skin.file=皮肤图片文件
account.skin.model=人物模型
account.skin.model.default=Steve
account.skin.model.slim=Alex
account.skin.upload=上传皮肤
account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件
account.username=用户名
archive.author=作者
@@ -106,15 +106,13 @@ download.failed.refresh=加载版本列表失败,点击此处重试。
download.provider.mcbbs=我的世界中文论坛 (MCBBS, https://www.mcbbs.net/)
download.provider.bmclapi=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.provider.mojang=官方(OptiFine 自动安装使用 BMCLAPI 下载源)
download.javafx=正在下载 JavaFX 运行时组件
extension.bat=Windows 脚本
extension.mod=模组文件
extension.png=图片文件
extension.sh=Bash 脚本
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHMCL 无法在低于 Java 11 且缺少 JavaFX 的 Java 环境上自行补全 JavaFX 运行环境。\n请更换如 Oracle Java 8 等包含 JavaFX 的 Java 运行环境,或者更新到 Java 11 或更高版本。
fatal.javafx.missing=缺少 JavaFX 运行环境。\n如果您使用的是 Java 11 或更高版本,请降级到 Oracle JRE 8,或者安装 BellSoft Liberica Full JRE。\n如果您使用的是其他 OpenJDK 构建,請確保其包含 OpenJFX。
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 Hello Minecraft! Launcher,但将无法连接到部分站点(如使用 Lets Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 Hello Minecraft! Launcher 对 "%s" 目录及该目录下的文件拥有读写权限。
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 Hello Minecraft! Launcher。
@@ -178,12 +176,11 @@ launch.advice.uncorrected=如果您确实希望使用您自定义的 Java 虚拟
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2773 或更新的版本。是否继续启动?
launch.advice.forge28_2_2_optifine=Forge 28.2.2 或更高版本与 OptiFine 不兼容。请更换 Forge 至 28.2.1 或更低版本。是否继续启动?
launch.advice.java8_1_13=Minecraft 1.13 及以上版本只能运行在 Java 8 或更高版本上,请使用 Java 8 或最新版本。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装 Java 8。
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅,而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要 Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏。是否继续启动?
launch.advice.require_newer_java_version=Minecraft %1$s 仅能运行在 Java %2$s 或更高版本上,是否下载?
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小。是否继续启动?
launch.failed=启动失败
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
@@ -240,11 +237,11 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
modpack.choose.remote=从互联网下载整合包
modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图)。
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 Markdown(图片请用网络图)。
modpack.description=整合包描述
modpack.enter_name=给游戏起个你喜欢的名字
modpack.export=导出整合包
modpack.export.as=请选择整合包类型 (若无法决定,请选择我的世界中文论坛整合包标准)
modpack.export.as=请选择整合包类型 (若无法决定,请选择 HMCL 类型)
modpack.file_api=整合包下载链接前缀
modpack.files.blueprints=BuildCraft 蓝图
modpack.files.config=Mod 配置文件
@@ -266,10 +263,6 @@ modpack.invalid=无效的整合包升级文件,可能是下载时出现问题
modpack.mismatched_type=整合包类型不匹配,当前游戏是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.name=整合包名称
modpack.not_a_valid_name=不是一个有效的整合包名称
modpack.origin=来源
modpack.origin.url=官方网站
modpack.origin.mcbbs=MCBBS
modpack.origin.mcbbs.prompt=贴子 id
modpack.scan=解析整合包
modpack.task.install=导入整合包
modpack.task.install.error=无法识别该整合包,目前仅支持导入 Curse、MultiMC、HMCL 整合包。
@@ -279,8 +272,8 @@ modpack.type.curse.completion=下载 Curse 整合包相关文件
modpack.type.curse.tolerable_error=但未能完成 Curse 整合包文件的下载,您可以在启动该游戏版本时继续 Curse 整合包文件的下载。由于网络问题,您可能需要重试多次。
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载,请尝试该整合包的最新版本或者安装其他整合包。
modpack.type.mcbbs=我的世界中文论坛整合包标准
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 导入
modpack.type.hmcl=HMCL
modpack.type.hmcl.export=可以被 Hello Minecraft! Launcher (HMCL) 导入
modpack.type.multimc=MultiMC
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入
modpack.type.server=服务器自动更新整合包
@@ -296,7 +289,6 @@ modpack.wizard.step.2.title=选中你想加到整合包中的文件或文件夹
modpack.wizard.step.3=整合包类型
modpack.wizard.step.3.title=选择整合包导出类型
modpack.wizard.step.initialization.exported_version=要导出的游戏版本
modpack.wizard.step.initialization.force_update=强制升级整合包至最新版本(需要自建服务器)
modpack.wizard.step.initialization.include_launcher=包含启动器
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
modpack.wizard.step.initialization.warning=在制作整合包前,请您确认您选择的版本可以正常启动,\n并保证您的 Minecraft 是正式版而非快照版,\n而且不应当将不允许非官方途径传播的 Mod、材质包等纳入整合包。\n整合包会保存您目前的下载源设置
@@ -364,16 +356,13 @@ 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 地址(不必填写,启动游戏后直接进入对应服务器)
@@ -415,7 +404,6 @@ settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=账户
settings.launcher.theme=主题
settings.min_memory=最小内存(MB)
settings.max_memory=最大内存(MB)
settings.physical_memory=物理内存大小
settings.show_log=查看日志
@@ -1,68 +0,0 @@
[
{
"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"
}
}
]
-24
View File
@@ -1,24 +0,0 @@
plugins {
id 'java-library'
}
dependencies {
api group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
api group: 'org.tukaani', name: 'xz', version: '1.8'
api(group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0') {
exclude group: 'org.jetbrains', module: 'annotations'
}
api group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
api group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
api group: 'com.nqzero', name: 'permit-reflect', version: '0.3'
compileOnlyApi group: 'org.jetbrains', name: 'annotations', version: '16.0.3'
// compileOnlyApi group: 'org.openjfx', name: 'javafx-base', version: '15', classifier: 'win'
// compileOnlyApi group: 'org.openjfx', name: 'javafx-controls', version: '15', classifier: 'win'
// compileOnlyApi group: 'org.openjfx', name: 'javafx-fxml', version: '15', classifier: 'win'
// compileOnlyApi group: 'org.openjfx', name: 'javafx-graphics', version: '15', classifier: 'win'
// compileOnlyApi group: 'org.openjfx', name: 'javafx-media', version: '15', classifier: 'win'
// compileOnlyApi group: 'org.openjfx', name: 'javafx-swing', version: '15', classifier: 'win'
// compileOnlyApi group: 'org.openjfx', name: 'javafx-web', version: '15', classifier: 'win'
}
@@ -21,10 +21,6 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
@@ -96,10 +92,6 @@ public abstract class Account implements Observable {
Platform.runLater(helper::invalidate);
}
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
return Bindings.createObjectBinding(Optional::empty);
}
@Override
public String toString() {
return new ToStringBuilder(this)
@@ -25,52 +25,7 @@ import java.util.Map;
*/
public abstract class AccountFactory<T extends Account> {
public enum AccountLoginType {
/**
* Either username or password should not be provided.
* AccountFactory will take its own way to check credentials.
*/
NONE(false, false),
/**
* AccountFactory only needs username.
*/
USERNAME(true, false),
/**
* AccountFactory needs both username and password for credential verification.
*/
USERNAME_PASSWORD(true, true);
public final boolean requiresUsername, requiresPassword;
AccountLoginType(boolean requiresUsername, boolean requiresPassword) {
this.requiresUsername = requiresUsername;
this.requiresPassword = requiresPassword;
}
}
/**
* Informs how this account factory verifies user's credential.
* @see AccountLoginType
*/
public abstract AccountLoginType getLoginType();
/**
* Create a new(to be verified via network) account, and log in.
* @param selector for character selection if multiple characters belong to single account. Pick out which character to act as.
* @param username username of the account if needed.
* @param password password of the account if needed.
* @param additionalData extra data for specific account factory.
* @return logged-in account.
* @throws AuthenticationException if an error occurs when logging in.
*/
public abstract T create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException;
/**
* Create a existing(stored in local files) account.
* @param storage serialized account data.
* @return account stored in local storage. Credentials may expired, and you should refresh account state later.
*/
public abstract T fromStorage(Map<Object, Object> storage);
}
@@ -24,7 +24,7 @@ import java.util.List;
/**
* This interface is for your application to open a GUI for user to choose the character
* when a having-multi-character yggdrasil account is being logging in.
* when a having-multi-character yggdrasil account is being logging in..
*/
public interface CharacterSelector {
@@ -21,8 +21,6 @@ import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments;
@@ -35,8 +33,6 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet;
public class AuthlibInjectorAccount extends YggdrasilAccount {
private final AuthlibInjectorServer server;
@@ -160,22 +156,4 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
.append("server", getServer().getUrl())
.toString();
}
public static Set<TextureType> getUploadableTextures(CompleteGameProfile profile) {
String prop = profile.getProperties().get("uploadableTextures");
if (prop == null)
return emptySet();
Set<TextureType> result = EnumSet.noneOf(TextureType.class);
for (String val : prop.split(",")) {
val = val.toUpperCase();
TextureType parsed;
try {
parsed = TextureType.valueOf(val);
} catch (IllegalArgumentException e) {
continue;
}
result.add(parsed);
}
return unmodifiableSet(result);
}
}
@@ -44,11 +44,6 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
this.serverLookup = serverLookup;
}
@Override
public AccountLoginType getLoginType() {
return AccountLoginType.USERNAME_PASSWORD;
}
@Override
public AuthlibInjectorAccount create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
@@ -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 <a href="https://github.com/yushijinhun/authlib-injector/wiki/%E5%90%AF%E5%8A%A8%E5%99%A8%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83#dnd-%E6%96%B9%E5%BC%8F%E6%B7%BB%E5%8A%A0-yggdrasil-%E6%9C%8D%E5%8A%A1%E7%AB%AF">Launcher Technical Specification for Authlib-Injector</a>
* @see 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
*/
public final class AuthlibInjectorDnD {
@@ -17,12 +17,11 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.io.NetworkUtils.toURL;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID;
@@ -56,7 +55,7 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
@Override
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
return toURL(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
throw new UnsupportedOperationException();
}
@Override
@@ -68,4 +67,12 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
public String toString() {
return apiRoot;
}
private URL toURL(String url) throws AuthenticationException {
try {
return new URL(url);
} catch (MalformedURLException e) {
throw new AuthenticationException(e);
}
}
}
@@ -110,7 +110,6 @@ public class AuthlibInjectorServer implements Observable {
@Nullable
private transient String name;
private transient Map<String, String> links = emptyMap();
private transient boolean nonEmailLogin;
private transient boolean metadataRefreshed;
private final transient ObservableHelper helper = new ObservableHelper(this);
@@ -146,10 +145,6 @@ public class AuthlibInjectorServer implements Observable {
return links;
}
public boolean isNonEmailLogin() {
return nonEmailLogin;
}
public String fetchMetadataResponse() throws IOException {
if (metadataResponse == null || !metadataRefreshed) {
refreshMetadata();
@@ -199,9 +194,6 @@ public class AuthlibInjectorServer implements Observable {
return converted;
})
.orElse(emptyMap());
this.nonEmailLogin = metaObject.flatMap(meta -> tryCast(meta.get("feature.non_email_login"), JsonPrimitive.class))
.map(it -> it.getAsBoolean())
.orElse(false);
}
}
@@ -1,145 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.microsoft;
import javafx.beans.binding.ObjectBinding;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import static java.util.Objects.requireNonNull;
public class MicrosoftAccount extends Account {
protected final MicrosoftService service;
protected UUID characterUUID;
private boolean authenticated = false;
private MicrosoftSession session;
protected MicrosoftAccount(MicrosoftService service, MicrosoftSession session) {
this.service = requireNonNull(service);
this.session = requireNonNull(session);
this.characterUUID = requireNonNull(session.getProfile().getId());
}
protected MicrosoftAccount(MicrosoftService service, CharacterSelector characterSelector) throws AuthenticationException {
this.service = requireNonNull(service);
MicrosoftSession acquiredSession = service.authenticate();
if (acquiredSession.getProfile() == null) {
session = service.refresh(acquiredSession);
} else {
session = acquiredSession;
}
characterUUID = session.getProfile().getId();
authenticated = true;
}
@Override
public String getUsername() {
// TODO: email of Microsoft account is blocked by oauth.
return "";
}
@Override
public String getCharacter() {
return session.getProfile().getName();
}
@Override
public UUID getUUID() {
return session.getProfile().getId();
}
@Override
public AuthInfo logIn() throws AuthenticationException {
if (!authenticated) {
if (service.validate(session.getTokenType(), session.getAccessToken())) {
authenticated = true;
} else {
MicrosoftSession acquiredSession = service.authenticate();
if (acquiredSession.getProfile() == null) {
session = service.refresh(acquiredSession);
} else {
session = acquiredSession;
}
characterUUID = session.getProfile().getId();
authenticated = true;
}
}
return session.toAuthInfo();
}
@Override
public AuthInfo logInWithPassword(String password) throws AuthenticationException {
throw new UnsupportedOperationException();
}
@Override
public Optional<AuthInfo> playOffline() {
return Optional.of(session.toAuthInfo());
}
@Override
public Map<Object, Object> toStorage() {
return session.toStorage();
}
public MicrosoftService getService() {
return service;
}
@Override
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
return BindingMapping.of(service.getProfileRepository().binding(session.getAuthorization()))
.map(profile -> profile.flatMap(MicrosoftService::getTextures));
}
@Override
public void clearCache() {
authenticated = false;
}
@Override
public String toString() {
return "MicrosoftAccount[uuid=" + characterUUID + ", name=" + getCharacter() + "]";
}
@Override
public int hashCode() {
return characterUUID.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MicrosoftAccount that = (MicrosoftAccount) o;
return characterUUID.equals(that.characterUUID);
}
}
@@ -1,53 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.microsoft;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector;
import java.util.Map;
import java.util.Objects;
public class MicrosoftAccountFactory extends AccountFactory<MicrosoftAccount> {
private final MicrosoftService service;
public MicrosoftAccountFactory(MicrosoftService service) {
this.service = service;
}
@Override
public AccountLoginType getLoginType() {
return AccountLoginType.NONE;
}
@Override
public MicrosoftAccount create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
return new MicrosoftAccount(service, selector);
}
@Override
public MicrosoftAccount fromStorage(Map<Object, Object> storage) {
Objects.requireNonNull(storage);
MicrosoftSession session = MicrosoftSession.fromStorage(storage);
return new MicrosoftAccount(service, session);
}
}
@@ -1,348 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.microsoft;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public class MicrosoftService {
private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10, TimeUnit.SECONDS);
private static final Pattern OAUTH_URL_PATTERN = Pattern.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$");
private final WebViewCallback callback;
private final ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> profileRepository;
public MicrosoftService(WebViewCallback callback) {
this.callback = callback;
this.profileRepository = new ObservableOptionalCache<>(
authorization -> {
LOG.info("Fetching properties");
return getCompleteProfile(authorization);
},
(uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid, e),
POOL);
}
public ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> getProfileRepository() {
return profileRepository;
}
public MicrosoftSession authenticate() throws AuthenticationException {
requireNonNull(callback);
try {
// Microsoft OAuth Flow
String code = callback.show(this, urlToBeTested -> OAUTH_URL_PATTERN.matcher(urlToBeTested).find(), "https://login.live.com/oauth20_authorize.srf" +
"?client_id=00000000402b5328" +
"&response_type=code" +
"&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL" +
"&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf")
.thenApply(url -> {
Matcher matcher = OAUTH_URL_PATTERN.matcher(url);
matcher.find();
return matcher.group(1);
})
.get();
// Authorization Code -> Token
String responseText = HttpRequest.POST("https://login.live.com/oauth20_token.srf").form(mapOf(
pair("client_id", "00000000402b5328"),
pair("code", code),
pair("grant_type", "authorization_code"),
pair("redirect_uri", "https://login.live.com/oauth20_desktop.srf"),
pair("scope", "service::user.auth.xboxlive.com::MBI_SSL"))).getString();
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText, LiveAuthorizationResponse.class);
// Authenticate with XBox Live
XBoxLiveAuthenticationResponse xboxResponse = HttpRequest.POST("https://user.auth.xboxlive.com/user/authenticate")
.json(mapOf(
pair("Properties", mapOf(
pair("AuthMethod", "RPS"),
pair("SiteName", "user.auth.xboxlive.com"),
pair("RpsTicket", response.accessToken)
)),
pair("RelyingParty", "http://auth.xboxlive.com"),
pair("TokenType", "JWT")))
.getJson(XBoxLiveAuthenticationResponse.class);
String uhs = (String) xboxResponse.displayClaims.xui.get(0).get("uhs");
// Authenticate with XSTS
XBoxLiveAuthenticationResponse xstsResponse = HttpRequest.POST("https://xsts.auth.xboxlive.com/xsts/authorize")
.json(mapOf(
pair("Properties", mapOf(
pair("SandboxId", "RETAIL"),
pair("UserTokens", Collections.singletonList(xboxResponse.token))
)),
pair("RelyingParty", "rp://api.minecraftservices.com/"),
pair("TokenType", "JWT")))
.getJson(XBoxLiveAuthenticationResponse.class);
// Authenticate with Minecraft
MinecraftLoginWithXBoxResponse minecraftResponse = HttpRequest.POST("https://api.minecraftservices.com/authentication/login_with_xbox")
.json(mapOf(pair("identityToken", "XBL3.0 x=" + uhs + ";" + xstsResponse.token)))
.getJson(MinecraftLoginWithXBoxResponse.class);
// Checking Game Ownership
MinecraftStoreResponse storeResponse = HttpRequest.GET("https://api.minecraftservices.com/entitlements/mcstore")
.authorization(String.format("%s %s", minecraftResponse.tokenType, minecraftResponse.accessToken))
.getJson(MinecraftStoreResponse.class);
handleErrorResponse(storeResponse);
if (storeResponse.items.isEmpty()) {
throw new NoCharacterException();
}
return new MicrosoftSession(minecraftResponse.tokenType, minecraftResponse.accessToken, new MicrosoftSession.User(minecraftResponse.username), null);
} catch (IOException e) {
throw new ServerDisconnectException(e);
} catch (InterruptedException e) {
throw new NoSelectedCharacterException();
} catch (ExecutionException e) {
if (e.getCause() instanceof InterruptedException) {
throw new NoSelectedCharacterException();
} else {
throw new ServerDisconnectException(e);
}
} catch (JsonParseException e) {
throw new ServerResponseMalformedException(e);
}
}
public MicrosoftSession refresh(MicrosoftSession oldSession) throws AuthenticationException {
try {
// Get the profile
MinecraftProfileResponse profileResponse = HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
.authorization(String.format("%s %s", oldSession.getTokenType(), oldSession.getAccessToken()))
.getJson(MinecraftProfileResponse.class);
handleErrorResponse(profileResponse);
return new MicrosoftSession(oldSession.getTokenType(), oldSession.getAccessToken(), oldSession.getUser(), new MicrosoftSession.GameProfile(profileResponse.id, profileResponse.name));
} catch (IOException e) {
throw new ServerDisconnectException(e);
} catch (JsonParseException e) {
throw new ServerResponseMalformedException(e);
}
}
public Optional<MinecraftProfileResponse> getCompleteProfile(String authorization) throws AuthenticationException {
try {
return Optional.ofNullable(HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
.authorization(authorization)
.getJson(MinecraftProfileResponse.class));
} catch (IOException e) {
throw new ServerDisconnectException(e);
} catch (JsonParseException e) {
throw new ServerResponseMalformedException(e);
}
}
public boolean validate(String tokenType, String accessToken) throws AuthenticationException {
requireNonNull(tokenType);
requireNonNull(accessToken);
try {
HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
.authorization(String.format("%s %s", tokenType, accessToken))
.filter((url, responseCode) -> {
if (responseCode / 100 == 4) {
throw new ResponseCodeException(url, responseCode);
}
})
.getString();
return true;
} catch (ResponseCodeException e) {
return false;
} catch (IOException e) {
throw new ServerDisconnectException(e);
}
}
private static void handleErrorResponse(MinecraftErrorResponse response) throws AuthenticationException {
if (response.error != null) {
throw new RemoteAuthenticationException(response.error, response.errorMessage, response.developerMessage);
}
}
public static Optional<Map<TextureType, Texture>> getTextures(MinecraftProfileResponse profile) {
Objects.requireNonNull(profile);
Map<TextureType, Texture> textures = new EnumMap<>(TextureType.class);
if (!profile.skins.isEmpty()) {
textures.put(TextureType.SKIN, new Texture(profile.skins.get(0).url, null));
}
// if (!profile.capes.isEmpty()) {
// textures.put(TextureType.CAPE, new Texture(profile.capes.get(0).url, null);
// }
return Optional.of(textures);
}
private static class LiveAuthorizationResponse {
@SerializedName("token_type")
String tokenType;
@SerializedName("expires_in")
int expiresIn;
@SerializedName("scope")
String scope;
@SerializedName("access_token")
String accessToken;
@SerializedName("refresh_token")
String refreshToken;
@SerializedName("user_id")
String userId;
@SerializedName("foci")
String foci;
}
private static class XBoxLiveAuthenticationResponseDisplayClaims {
List<Map<Object, Object>> xui;
}
private static class XBoxLiveAuthenticationResponse {
@SerializedName("IssueInstant")
String issueInstant;
@SerializedName("NotAfter")
String notAfter;
@SerializedName("Token")
String token;
@SerializedName("DisplayClaims")
XBoxLiveAuthenticationResponseDisplayClaims displayClaims;
}
private static class MinecraftLoginWithXBoxResponse {
@SerializedName("username")
String username;
@SerializedName("roles")
List<String> roles;
@SerializedName("access_token")
String accessToken;
@SerializedName("token_type")
String tokenType;
@SerializedName("expires_in")
int expiresIn;
}
private static class MinecraftStoreResponseItem {
@SerializedName("name")
String name;
@SerializedName("signature")
String signature;
}
private static class MinecraftStoreResponse extends MinecraftErrorResponse {
@SerializedName("items")
List<MinecraftStoreResponseItem> items;
@SerializedName("signature")
String signature;
@SerializedName("keyId")
String keyId;
}
public static class MinecraftProfileResponseSkin implements Validation {
public String id;
public String state;
public String url;
public String variant; // CLASSIC, SLIM
public String alias;
@Override
public void validate() throws JsonParseException, TolerableValidationException {
Validation.requireNonNull(id, "id cannot be null");
Validation.requireNonNull(state, "state cannot be null");
Validation.requireNonNull(url, "url cannot be null");
Validation.requireNonNull(variant, "variant cannot be null");
}
}
public static class MinecraftProfileResponseCape {
}
public static class MinecraftProfileResponse extends MinecraftErrorResponse implements Validation {
@SerializedName("id")
UUID id;
@SerializedName("name")
String name;
@SerializedName("skins")
List<MinecraftProfileResponseSkin> skins;
@SerializedName("capes")
List<MinecraftProfileResponseCape> capes;
@Override
public void validate() throws JsonParseException, TolerableValidationException {
Validation.requireNonNull(id, "id cannot be null");
Validation.requireNonNull(name, "name cannot be null");
Validation.requireNonNull(skins, "skins cannot be null");
Validation.requireNonNull(capes, "capes cannot be null");
}
}
private static class MinecraftErrorResponse {
public String path;
public String errorType;
public String error;
public String errorMessage;
public String developerMessage;
}
public interface WebViewCallback {
CompletableFuture<String> show(MicrosoftService service, Predicate<String> urlTester, String initialURL);
}
}
@@ -1,121 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.auth.microsoft;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.Map;
import java.util.UUID;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair;
public class MicrosoftSession {
private final String tokenType;
private final String accessToken;
private final User user;
private final GameProfile profile;
public MicrosoftSession(String tokenType, String accessToken, User user, GameProfile profile) {
this.tokenType = tokenType;
this.accessToken = accessToken;
this.user = user;
this.profile = profile;
}
public String getTokenType() {
return tokenType;
}
public String getAccessToken() {
return accessToken;
}
public String getAuthorization() {
return String.format("%s %s", getTokenType(), getAccessToken());
}
public User getUser() {
return user;
}
public GameProfile getProfile() {
return profile;
}
public static MicrosoftSession fromStorage(Map<?, ?> storage) {
UUID uuid = tryCast(storage.get("uuid"), String.class).map(UUIDTypeAdapter::fromString).orElseThrow(() -> new IllegalArgumentException("uuid is missing"));
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
String tokenType = tryCast(storage.get("tokenType"), String.class).orElseThrow(() -> new IllegalArgumentException("tokenType is missing"));
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
return new MicrosoftSession(tokenType, accessToken, new User(userId), new GameProfile(uuid, name));
}
public Map<Object, Object> toStorage() {
requireNonNull(profile);
requireNonNull(user);
return mapOf(
pair("tokenType", tokenType),
pair("accessToken", accessToken),
pair("uuid", UUIDTypeAdapter.fromUUID(profile.getId())),
pair("displayName", profile.getName()),
pair("userid", user.id)
);
}
public AuthInfo toAuthInfo() {
requireNonNull(profile);
return new AuthInfo(profile.getName(), profile.getId(), accessToken, "{}");
}
public static class User {
private final String id;
public User(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
public static class GameProfile {
private final UUID id;
private final String name;
public GameProfile(UUID id, String name) {
this.id = id;
this.name = name;
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
}
}
@@ -31,17 +31,12 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
*
* @author huangyuhui
*/
public final class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
public static final OfflineAccountFactory INSTANCE = new OfflineAccountFactory();
private OfflineAccountFactory() {
}
@Override
public AccountLoginType getLoginType() {
return AccountLoginType.USERNAME;
}
public OfflineAccount create(String username, UUID uuid) {
return new OfflineAccount(username, uuid);
}
@@ -17,8 +17,9 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import static java.util.Objects.requireNonNull;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.jackhuang.hmcl.util.Immutable;
@@ -37,7 +38,7 @@ public class CompleteGameProfile extends GameProfile {
public CompleteGameProfile(UUID id, String name, Map<String, String> properties) {
super(id, name);
this.properties = Objects.requireNonNull(properties);
this.properties = requireNonNull(properties);
}
public CompleteGameProfile(GameProfile profile, Map<String, String> properties) {
@@ -17,7 +17,8 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import java.util.Objects;
import static java.util.Objects.requireNonNull;
import java.util.UUID;
import org.jackhuang.hmcl.util.Immutable;
@@ -39,8 +40,8 @@ public class GameProfile implements Validation {
private final String name;
public GameProfile(UUID id, String name) {
this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name);
this.id = requireNonNull(id);
this.name = requireNonNull(name);
}
public UUID getId() {
@@ -53,7 +54,9 @@ public class GameProfile implements Validation {
@Override
public void validate() throws JsonParseException {
Validation.requireNonNull(id, "Game profile id cannot be null");
Validation.requireNonNull(name, "Game profile name cannot be null");
if (id == null)
throw new JsonParseException("Game profile id cannot be null");
if (name == null)
throw new JsonParseException("Game profile name cannot be null");
}
}
@@ -17,13 +17,20 @@
*/
package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.*;
import static java.util.Collections.unmodifiableMap;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import static java.util.Collections.unmodifiableMap;
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;
public class PropertyMapSerializer implements JsonSerializer<Map<String, String>>, JsonDeserializer<Map<String, String>> {
@@ -50,4 +57,4 @@ public class PropertyMapSerializer implements JsonSerializer<Map<String, String>
});
return result;
}
}
}
@@ -27,14 +27,11 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.NoCharacterException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class YggdrasilAccount extends Account {
@@ -192,20 +189,6 @@ public class YggdrasilAccount extends Account {
service.getProfileRepository().invalidate(characterUUID);
}
@Override
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
return BindingMapping.of(service.getProfileRepository().binding(getUUID()))
.map(profile -> profile.flatMap(it -> {
try {
return YggdrasilService.getTextures(it);
} catch (ServerResponseMalformedException e) {
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
return Optional.empty();
}
}));
}
public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException {
service.uploadSkin(characterUUID, session.getAccessToken(), model, file);
}
@@ -226,7 +209,6 @@ public class YggdrasilAccount extends Account {
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || obj.getClass() != YggdrasilAccount.class)
return false;
YggdrasilAccount another = (YggdrasilAccount) obj;
@@ -36,17 +36,12 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
private final YggdrasilService service;
private YggdrasilService service;
public YggdrasilAccountFactory(YggdrasilService service) {
this.service = service;
}
@Override
public AccountLoginType getLoginType() {
return AccountLoginType.USERNAME_PASSWORD;
}
@Override
public YggdrasilAccount create(CharacterSelector selector, String username, String password, Object additionalData) throws AuthenticationException {
Objects.requireNonNull(selector);
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.HttpMultipartRequest;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
@@ -51,7 +52,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
public class YggdrasilService {
private static final ThreadPoolExecutor POOL = threadPool("YggdrasilProfileProperties", true, 2, 10, TimeUnit.SECONDS);
private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS);
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
@@ -163,7 +164,10 @@ public class YggdrasilService {
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
}
}
requireEmpty(NetworkUtils.readData(con));
String response = IOUtils.readFullyAsString(con.getInputStream());
if (response.startsWith("{")) {
handleErrorMessage(fromJson(response, ErrorResponse.class));
}
} catch (IOException e) {
throw new AuthenticationException(e);
}
@@ -214,14 +218,18 @@ public class YggdrasilService {
response.accessToken,
response.selectedProfile,
response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles),
response.user == null ? null : response.user.getProperties());
response.user);
}
private static void requireEmpty(String response) throws AuthenticationException {
if (StringUtils.isBlank(response))
return;
handleErrorMessage(fromJson(response, ErrorResponse.class));
try {
handleErrorMessage(fromJson(response, ErrorResponse.class));
} catch (JsonParseException e) {
throw new ServerResponseMalformedException(e);
}
}
private static void handleErrorMessage(ErrorResponse response) throws AuthenticationException {
@@ -245,7 +253,7 @@ public class YggdrasilService {
try {
return GSON.fromJson(text, typeOfT);
} catch (JsonParseException e) {
throw new ServerResponseMalformedException(text, e);
throw new ServerResponseMalformedException("Server response: " + text, e);
}
}
@@ -21,7 +21,6 @@ import com.google.gson.Gson;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
@@ -37,15 +36,14 @@ public class YggdrasilSession {
private final String accessToken;
private final GameProfile selectedProfile;
private final List<GameProfile> availableProfiles;
@Nullable
private final Map<String, String> userProperties;
private final User user;
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, Map<String, String> userProperties) {
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, User user) {
this.clientToken = clientToken;
this.accessToken = accessToken;
this.selectedProfile = selectedProfile;
this.availableProfiles = availableProfiles;
this.userProperties = userProperties;
this.user = user;
}
public String getClientToken() {
@@ -70,8 +68,8 @@ public class YggdrasilSession {
return availableProfiles;
}
public Map<String, String> getUserProperties() {
return userProperties;
public User getUser() {
return user;
}
public static YggdrasilSession fromStorage(Map<?, ?> storage) {
@@ -79,28 +77,34 @@ public class YggdrasilSession {
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, userProperties);
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
}
public Map<Object, Object> toStorage() {
if (selectedProfile == null)
throw new IllegalStateException("No character is selected");
if (user == null)
throw new IllegalStateException("No user is specified");
return mapOf(
pair("clientToken", clientToken),
pair("accessToken", accessToken),
pair("uuid", UUIDTypeAdapter.fromUUID(selectedProfile.getId())),
pair("displayName", selectedProfile.getName()),
pair("userProperties", userProperties));
pair("userid", user.getId()),
pair("userProperties", user.getProperties()));
}
public AuthInfo toAuthInfo() {
if (selectedProfile == null)
throw new IllegalStateException("No character is selected");
if (user == null)
throw new IllegalStateException("No user is specified");
return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken,
Optional.ofNullable(userProperties)
Optional.ofNullable(user.getProperties())
.map(properties -> properties.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> Collections.singleton(e.getValue()))))
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -38,7 +38,7 @@ public abstract class GameBuilder {
}
/**
* The new game version name, for .minecraft/&lt;version name&gt;.
* The new game version name, for .minecraft/<version name>.
*
* @param name the name of new game version.
*/
@@ -19,7 +19,6 @@ 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;
@@ -85,11 +84,6 @@ 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;
@@ -147,16 +141,10 @@ 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|fmlloader)")),
FORGE(true, "forge", Pattern.compile("net\\.minecraftforge"), Pattern.compile("forge")),
LITELOADER(true, "liteloader", Pattern.compile("com\\.mumfrey"), Pattern.compile("liteloader")),
OPTIFINE(false, "optifine", Pattern.compile("(net\\.)?optifine"), Pattern.compile("^(?!.*launchwrapper).*$"));
@@ -214,5 +202,4 @@ 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,26 +17,29 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.*;
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.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.*;
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;
public class MaintainTask extends Task<Version> {
private final GameRepository repository;
@@ -65,10 +68,7 @@ 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, maintainGameWithCpwModLauncher(repository, unique(version)), true);
} else if (mainClass != null && mainClass.equals(LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN)) {
// Forge 1.17
return maintainGameWithCpwBoostrapLauncher(repository, unique(version));
return maintainOptiFineLibrary(repository, maintainGameWithModLauncher(repository, unique(version)), true);
} 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 maintainGameWithCpwModLauncher(GameRepository repository, Version version) {
private static Version maintainGameWithModLauncher(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=${library_directory}/" + library.getPath());
builder.addJvmArgument("-Dhmcl.transformer.candidates=${libraries_directory}/" + library.getPath());
if (!libraryExisting) builder.addLibrary(hmclTransformerDiscoveryService);
Path libraryPath = repository.getLibraryFile(version, hmclTransformerDiscoveryService).toPath();
try {
@@ -149,59 +149,6 @@ 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());
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.download.VersionMismatchException;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.DownloadManager;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -72,8 +73,11 @@ public final class ForgeInstallTask extends Task<Version> {
installer = Files.createTempFile("forge-installer", ".jar");
dependent = new FileDownloadTask(
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
installer.toFile(), null);
new DownloadManager.DownloadTaskStateBuilder()
.setUrls(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()))
.setFile(installer.toFile())
.setInitialParts(3)
.build());
dependent.setCacheRepository(dependencyManager.getCacheRepository());
dependent.setCaching(true);
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
@@ -173,7 +177,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.getVersion()), installer);
return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getPath().getVersion().replaceAll("(?i)forge", "")), installer);
} else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) {
ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class);
if (!gameVersion.get().equals(profile.getInstall().getMinecraft()))
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff. Show more