Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61597cb83d | ||
|
|
2f17552b2c | ||
|
|
f2d3d24338 | ||
|
|
e8a64f52f0 | ||
|
|
f55fe0fc45 | ||
|
|
1e3a5f9515 | ||
|
|
a9c7eb0954 | ||
|
|
289472d8ba | ||
|
|
f191e80100 | ||
|
|
607248e8b3 | ||
|
|
cecabe5bc4 | ||
|
|
947a2e3d91 | ||
|
|
382e0f3b1d | ||
|
|
6e440783dc | ||
|
|
fcd7d8ba83 | ||
|
|
a53d2a1370 | ||
|
|
20148d0b02 | ||
|
|
3114e33067 | ||
|
|
e137d72b76 | ||
|
|
cc087dee81 | ||
|
|
eed475cb28 | ||
|
|
d07c45f0b5 | ||
|
|
0aa0fa1b78 | ||
|
|
e91ec13dbd | ||
|
|
e36105d937 | ||
|
|
07d4df312d | ||
|
|
b521c5b76a | ||
|
|
c1596443f5 | ||
|
|
cadeb1f59c | ||
|
|
2d4223f7ed | ||
|
|
c453bbc7cd | ||
|
|
2184961e3c | ||
|
|
8afd8bde8b | ||
|
|
65952a5d81 | ||
|
|
ac1f2ec1ed | ||
|
|
454cc1e702 | ||
|
|
e3fa7428bf | ||
|
|
7ece35e28a | ||
|
|
d22dae9834 | ||
|
|
63bd707741 | ||
|
|
734c1e14f0 | ||
|
|
46869ad5f7 | ||
|
|
13fa713d58 | ||
|
|
94cd33af5c | ||
|
|
7efd9607e5 | ||
|
|
94539ad77b | ||
|
|
f2a86857bc | ||
|
|
3f2be63d08 | ||
|
|
2d7c1393d6 | ||
|
|
3d33230e25 | ||
|
|
dd4683e693 | ||
|
|
8bc5d2112f | ||
|
|
a828896989 | ||
|
|
5e8734aa95 | ||
|
|
1ac844f398 | ||
|
|
3f85ac208c | ||
|
|
c191186023 | ||
|
|
4715a95a54 | ||
|
|
8108f11c45 | ||
|
|
f4f1850271 | ||
|
|
5a81972f68 | ||
|
|
674dfca34d |
No files matched your search
@@ -0,0 +1 @@
|
||||
*.exe filter=lfs diff=lfs merge=lfs -text
|
||||
+43
-18
@@ -1,12 +1,15 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -33,8 +36,8 @@ version = versionroot + '.' + buildnumber
|
||||
mainClassName = 'org.jackhuang.hmcl.Main'
|
||||
|
||||
dependencies {
|
||||
compile project(":HMCLCore")
|
||||
compile rootProject.files("lib/JFoenix.jar")
|
||||
implementation project(":HMCLCore")
|
||||
implementation rootProject.files("lib/JFoenix.jar")
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
@@ -75,19 +78,47 @@ 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 {
|
||||
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,
|
||||
'Add-Opens': [
|
||||
'java.base/java.lang',
|
||||
'java.base/java.lang.reflect',
|
||||
'javafx.graphics/javafx.css',
|
||||
'javafx.base/com.sun.javafx.runtime',
|
||||
@@ -99,6 +130,7 @@ jar {
|
||||
'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',
|
||||
@@ -107,22 +139,9 @@ jar {
|
||||
'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)
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
@@ -151,6 +170,11 @@ 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) {
|
||||
@@ -165,6 +189,7 @@ 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)
|
||||
}
|
||||
@@ -182,4 +207,4 @@ task makeExecutables(dependsOn: jar) doLast {
|
||||
|
||||
build.dependsOn makePackXz
|
||||
build.dependsOn makePackGz
|
||||
build.dependsOn makeExecutables
|
||||
build.dependsOn makeExecutables
|
||||
@@ -23,19 +23,21 @@ 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.lang.management.ManagementFactory;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -47,11 +49,14 @@ 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();
|
||||
@@ -67,6 +72,8 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
@@ -82,7 +89,17 @@ 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));
|
||||
|
||||
@@ -142,6 +159,11 @@ public final class Launcher extends Application {
|
||||
} catch (URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
File jarFile = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath());
|
||||
String ext = FileUtils.getExtension(jarFile);
|
||||
if ("jar".equals(ext) || "exe".equals(ext))
|
||||
result.add(jarFile);
|
||||
}
|
||||
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.*;
|
||||
@@ -48,7 +48,6 @@ public final class Main {
|
||||
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
|
||||
checkJavaFX();
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
@@ -59,9 +58,10 @@ public final class Main {
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
}
|
||||
checkJavaFX();
|
||||
|
||||
// Fix title bar not displaying in GTK systems
|
||||
System.setProperty("jdk.gtk.version", "2");
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
@@ -77,9 +77,13 @@ public final class Main {
|
||||
|
||||
private static void checkJavaFX() {
|
||||
try {
|
||||
Class.forName("javafx.application.Application");
|
||||
} catch (ClassNotFoundException e) {
|
||||
showErrorAndExit(i18n("fatal.missing_javafx"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -39,5 +40,14 @@ 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 = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,24 @@ 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;
|
||||
@@ -33,6 +44,7 @@ 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 {
|
||||
@@ -273,6 +285,54 @@ 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());
|
||||
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");
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ 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;
|
||||
@@ -35,7 +36,11 @@ 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;
|
||||
@@ -50,10 +55,7 @@ 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.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.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
@@ -62,6 +64,7 @@ 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;
|
||||
@@ -140,10 +143,12 @@ public final class LauncherHelper {
|
||||
}), Task.composeAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
|
||||
if ("Curse".equals(configuration.getType()))
|
||||
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
|
||||
return new CurseCompletionTask(dependencyManager, selectedVersion);
|
||||
else if ("Server".equals(configuration.getType()))
|
||||
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.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) {
|
||||
@@ -169,7 +174,7 @@ public final class LauncherHelper {
|
||||
repository,
|
||||
version.getPatches().isEmpty() ? repository.getResolvedVersion(selectedVersion) : version,
|
||||
authInfo,
|
||||
setting.toLaunchOptions(profile.getGameDir(), !setting.isNotCheckJVM()),
|
||||
repository.getLaunchOptions(selectedVersion, 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())
|
||||
@@ -312,6 +317,22 @@ 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.dialog(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), version.getJavaVersion().getMajorVersion()), i18n("message.warning"), MessageType.WARNING, () -> {
|
||||
downloadJava(version.getJavaVersion(), profile).thenAccept(x -> onAccept.run());
|
||||
});
|
||||
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()
|
||||
@@ -334,8 +355,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_AND_LATER
|
||||
&& version.getMainClass().equals(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN)
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9
|
||||
&& LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|
||||
&& version.getLibraries().stream()
|
||||
.filter(library -> "launchwrapper".equals(library.getArtifactId()))
|
||||
.anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
|
||||
@@ -367,7 +388,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
org.jackhuang.hmcl.util.platform.Platform.IS_64_BIT) {
|
||||
Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) {
|
||||
final JavaVersion java32 = java;
|
||||
|
||||
// First find if same java version but whose platform is 64-bit installed.
|
||||
@@ -442,6 +463,36 @@ 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(executor.getException());
|
||||
} 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,6 +23,8 @@ 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;
|
||||
@@ -43,12 +45,20 @@ 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) {
|
||||
@@ -148,12 +158,16 @@ 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(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(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());
|
||||
}
|
||||
|
||||
@@ -185,6 +199,11 @@ 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()));
|
||||
@@ -232,5 +251,18 @@ 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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,46 +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.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,11 +19,13 @@ 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.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
@@ -37,6 +39,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
@@ -134,6 +137,7 @@ 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);
|
||||
@@ -181,6 +185,30 @@ 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 ====
|
||||
@@ -205,16 +233,34 @@ 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(img -> SwingFXUtils.toFXImage(img, null));
|
||||
.map(TexturesLoader::toFXImage);
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||
return BindingMapping.of(skinBinding(account))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(TexturesLoader::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
|
||||
() -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
}
|
||||
}
|
||||
// ====
|
||||
}
|
||||
|
||||
// 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,15 +34,20 @@ 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;
|
||||
@@ -51,6 +56,7 @@ 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;
|
||||
@@ -78,6 +84,8 @@ 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<>();
|
||||
@@ -86,6 +94,7 @@ 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));
|
||||
}
|
||||
@@ -108,6 +117,8 @@ 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);
|
||||
}
|
||||
@@ -309,7 +320,8 @@ 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_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
|
||||
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
|
||||
|
||||
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
||||
return i18n(Optional.ofNullable(unlocalizedLoginTypeNames.get(factory))
|
||||
|
||||
@@ -21,16 +21,13 @@ 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.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;
|
||||
@@ -39,8 +36,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -533,36 +528,6 @@ public final class VersionSetting implements Cloneable {
|
||||
defaultJavaPathProperty.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
|
||||
public VersionSetting clone() {
|
||||
VersionSetting versionSetting = new VersionSetting();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class 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,10 +27,12 @@ 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;
|
||||
@@ -107,6 +109,7 @@ 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());
|
||||
@@ -123,7 +126,7 @@ public final class Controllers {
|
||||
dialog(i18n("launcher.cache_directory.invalid"));
|
||||
}
|
||||
|
||||
Task.runAsync(JavaVersion::initialize).start();
|
||||
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
|
||||
|
||||
scene = new Scene(decorator.getDecorator());
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
|
||||
@@ -198,7 +198,7 @@ public final class LogWindow extends Stage {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException ignored) {
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ 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;
|
||||
@@ -29,8 +30,6 @@ 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")));
|
||||
@@ -38,6 +37,7 @@ 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,20 +17,53 @@
|
||||
*/
|
||||
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 {
|
||||
private final WebView webView = new WebView();
|
||||
protected final StackPane pane = new StackPane();
|
||||
protected final JFXProgressBar progressBar = new JFXProgressBar();
|
||||
protected final WebView webView = new WebView();
|
||||
protected final WebEngine webEngine = webView.getEngine();
|
||||
|
||||
public WebStage() {
|
||||
setScene(new Scene(webView, 800, 480));
|
||||
this(800, 480);
|
||||
}
|
||||
|
||||
public WebStage(int width, int height) {
|
||||
setScene(new Scene(pane, width, height));
|
||||
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,8 +18,10 @@
|
||||
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;
|
||||
@@ -30,6 +32,8 @@ 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;
|
||||
@@ -37,13 +41,23 @@ 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.PromptDialogPane;
|
||||
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 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 static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@@ -73,7 +87,9 @@ public class AccountListItem extends RadioButton {
|
||||
if (account instanceof OfflineAccount) {
|
||||
title.bind(characterName);
|
||||
} else {
|
||||
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
title.bind(
|
||||
account.getUsername().isEmpty() ? characterName :
|
||||
Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
@@ -84,7 +100,7 @@ public class AccountListItem extends RadioButton {
|
||||
return new AccountListItemSkin(this);
|
||||
}
|
||||
|
||||
private Task<?> refreshAsync() {
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
account.clearCache();
|
||||
try {
|
||||
@@ -105,17 +121,32 @@ public class AccountListItem extends RadioButton {
|
||||
});
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refreshAsync().whenComplete(e -> {}).start();
|
||||
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 boolean canUploadSkin() {
|
||||
return account instanceof YggdrasilAccount && !(account instanceof AuthlibInjectorAccount);
|
||||
}
|
||||
|
||||
public void uploadSkin() {
|
||||
/**
|
||||
* @return the skin upload task, null if no file is selected
|
||||
*/
|
||||
@Nullable
|
||||
public Task<?> uploadSkin() {
|
||||
if (!(account instanceof YggdrasilAccount)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
FileChooser chooser = new FileChooser();
|
||||
@@ -123,23 +154,31 @@ 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;
|
||||
return null;
|
||||
}
|
||||
|
||||
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"))));
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
|
||||
@@ -31,8 +31,11 @@ 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;
|
||||
@@ -86,20 +89,38 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton btnRefresh = new JFXButton();
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
SpinnerPane spinnerRefresh = new SpinnerPane();
|
||||
btnRefresh.setOnMouseClicked(e -> {
|
||||
spinnerRefresh.showSpinner();
|
||||
skinnable.refreshAsync()
|
||||
.whenComplete(ex -> spinnerRefresh.hideSpinner())
|
||||
.start();
|
||||
});
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
|
||||
right.getChildren().add(btnRefresh);
|
||||
spinnerRefresh.setContent(btnRefresh);
|
||||
right.getChildren().add(spinnerRefresh);
|
||||
|
||||
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 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);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.remove());
|
||||
|
||||
@@ -49,6 +49,7 @@ 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;
|
||||
@@ -68,6 +69,7 @@ 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;
|
||||
@@ -88,7 +90,7 @@ public class AddAccountPane extends StackPane {
|
||||
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
||||
selectDefaultServer();
|
||||
|
||||
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.getItems().setAll(Accounts.FACTORIES);
|
||||
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
|
||||
// try selecting the preferred login type
|
||||
cboType.getSelectionModel().select(
|
||||
@@ -108,24 +110,26 @@ public class AddAccountPane extends StackPane {
|
||||
// remember the last used login type
|
||||
loginType.addListener((observable, oldValue, newValue) -> config().setPreferredLoginType(Accounts.getLoginType(newValue)));
|
||||
|
||||
txtPassword.visibleProperty().bind(loginType.isNotEqualTo(Accounts.FACTORY_OFFLINE));
|
||||
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));
|
||||
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"), str -> !txtPassword.isVisible() || str.contains("@")));
|
||||
txtUsername.getValidators().add(new Validator(i18n("input.email"), this::validateUsername));
|
||||
|
||||
btnAccept.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !( // consider the opposite situation: input is valid
|
||||
txtUsername.validate() &&
|
||||
(!txtUsername.isVisible() || 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(), txtPassword.visibleProperty(),
|
||||
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
|
||||
txtUsername.textProperty(), txtPassword.textProperty(),
|
||||
loginType, cboServers.getSelectionModel().selectedItemProperty(),
|
||||
txtPassword.visibleProperty(), cboServers.visibleProperty()));
|
||||
|
||||
// authlib-injector links
|
||||
links.bind(BindingMapping.of(cboServers.getSelectionModel().selectedItemProperty())
|
||||
@@ -135,6 +139,20 @@ 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) {
|
||||
@@ -241,7 +259,7 @@ public class AddAccountPane extends StackPane {
|
||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
||||
}
|
||||
|
||||
private class Selector extends BorderPane implements CharacterSelector {
|
||||
private static class Selector extends BorderPane implements CharacterSelector {
|
||||
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXButton cancel = new JFXButton();
|
||||
@@ -325,6 +343,8 @@ 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 {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ 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;
|
||||
@@ -38,7 +37,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
public class TaskExecutorDialogPane extends StackPane {
|
||||
private TaskExecutor executor;
|
||||
private Consumer<Region> onCancel;
|
||||
private final Consumer<DownloadManager.SpeedEvent> speedEventHandler;
|
||||
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
@FXML
|
||||
private Label lblTitle;
|
||||
@@ -76,7 +75,7 @@ public class TaskExecutorDialogPane extends StackPane {
|
||||
lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit));
|
||||
});
|
||||
};
|
||||
DownloadManager.speedEvent.channel(DownloadManager.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
}
|
||||
|
||||
public void setExecutor(TaskExecutor executor) {
|
||||
|
||||
@@ -34,14 +34,16 @@ 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;
|
||||
@@ -128,7 +130,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 HMCLModpackExportTask) {
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
@@ -274,7 +276,7 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable == null ? "" : throwable.getLocalizedMessage());
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ 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;
|
||||
@@ -90,8 +91,17 @@ 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();
|
||||
@@ -122,11 +132,6 @@ 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())
|
||||
new FileDownloadTask(url, modpack.toFile(), null)
|
||||
.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.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
|
||||
@@ -36,7 +36,6 @@ 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;
|
||||
@@ -63,72 +62,97 @@ 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);
|
||||
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);
|
||||
ModpackExportInfo exportInfo = (ModpackExportInfo) settings.get(ModpackInfoPage.MODPACK_INFO);
|
||||
exportInfo.setWhitelist(whitelist);
|
||||
String modpackType = (String) settings.get(ModpackTypeSelectionPage.MODPACK_TYPE);
|
||||
boolean includeLauncher = (Boolean) settings.get(ModpackInfoPage.MODPACK_INCLUDE_LAUNCHER);
|
||||
|
||||
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);
|
||||
}
|
||||
return exportWithLauncher(modpackType, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
private Task<?> exportAsHMCL(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, boolean includeLauncherRaw) {
|
||||
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
|
||||
List<File> launcherJar = Launcher.getCurrentJarFiles();
|
||||
boolean includeLauncher = includeLauncherRaw && launcherJar != null;
|
||||
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
|
||||
return new Task<Object>() {
|
||||
File tempModpack;
|
||||
Task<?> exportTask;
|
||||
|
||||
@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() 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());
|
||||
}
|
||||
});
|
||||
}
|
||||
public void execute() {
|
||||
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -138,30 +162,30 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMultiMC(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription) {
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
VersionSetting vs = profile.getVersionSetting(version);
|
||||
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, whitelist,
|
||||
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, exportInfo.getWhitelist(),
|
||||
new MultiMCInstanceConfiguration(
|
||||
"OneSix",
|
||||
modpackName + "-" + modpackVersion,
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
null,
|
||||
Lang.toIntOrNull(vs.getPermSize()),
|
||||
vs.getWrapper(),
|
||||
vs.getPreLaunchCommand(),
|
||||
null,
|
||||
modpackDescription,
|
||||
exportInfo.getDescription(),
|
||||
null,
|
||||
vs.getJavaArgs(),
|
||||
exportInfo.getJavaArguments(),
|
||||
vs.isFullscreen(),
|
||||
vs.getWidth(),
|
||||
vs.getHeight(),
|
||||
vs.getMaxMemory(),
|
||||
vs.getMinMemory(),
|
||||
exportInfo.getMinMemory(),
|
||||
vs.isShowLogs(),
|
||||
/* showConsoleOnError */ true,
|
||||
/* autoCloseConsole */ false,
|
||||
@@ -181,13 +205,13 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsServer(List<String> whitelist, File modpackFile, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi) {
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, File modpackFile) {
|
||||
return new Task<Void>() {
|
||||
Task<?> dependency;
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, whitelist, modpackName, modpackAuthor, modpackVersion, modpackDescription, modpackFileApi, modpackFile);
|
||||
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,7 +227,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
case 0:
|
||||
return new ModpackTypeSelectionPage(controller);
|
||||
case 1:
|
||||
return new ModpackInfoPage(controller, version);
|
||||
return new ModpackInfoPage(controller, profile.getRepository(), version);
|
||||
case 2:
|
||||
return new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
|
||||
default:
|
||||
|
||||
@@ -17,76 +17,96 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
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 com.jfoenix.controls.*;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
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.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.export.ModpackTypeSelectionPage.*;
|
||||
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.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 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 ModpackExportInfo exportInfo = new ModpackExportInfo();
|
||||
|
||||
public ModpackInfoPage(WizardController controller, String version) {
|
||||
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) {
|
||||
this.controller = controller;
|
||||
modpackName.set(version);
|
||||
modpackAuthor.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getUsername).orElse(""));
|
||||
versionName.set(version);
|
||||
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());
|
||||
|
||||
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"));
|
||||
@@ -96,24 +116,35 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
controller.onEnd();
|
||||
return;
|
||||
}
|
||||
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());
|
||||
|
||||
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_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_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);
|
||||
controller.getSettings().remove(MODPACK_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,19 +157,12 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
return new ModpackInfoPageSkin(this);
|
||||
}
|
||||
|
||||
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_INFO = "modpack.info";
|
||||
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 final JFXTextField txtModpackName;
|
||||
private final JFXTextField txtModpackFileApi;
|
||||
private final JFXTextField txtModpackAuthor;
|
||||
private final JFXTextField txtModpackVersion;
|
||||
private ObservableList<Node> originList;
|
||||
|
||||
public ModpackInfoPageSkin(ModpackInfoPage skinnable) {
|
||||
super(skinnable);
|
||||
@@ -151,6 +175,8 @@ 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;");
|
||||
@@ -179,31 +205,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
borderPane1.setLeft(new Label(i18n("modpack.wizard.step.initialization.exported_version")));
|
||||
|
||||
Label versionNameLabel = new Label();
|
||||
versionNameLabel.textProperty().bind(skinnable.versionName);
|
||||
versionNameLabel.setText(skinnable.versionName);
|
||||
borderPane1.setRight(versionNameLabel);
|
||||
list.getContent().add(borderPane1);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.modpackName);
|
||||
JFXTextField txtModpackName = new JFXTextField();
|
||||
txtModpackName.textProperty().bindBidirectional(skinnable.name);
|
||||
txtModpackName.setLabelFloat(true);
|
||||
txtModpackName.setPromptText(i18n("modpack.name"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
validator.setMessage(i18n("modpack.not_a_valid_name"));
|
||||
txtModpackName.getValidators().add(validator);
|
||||
txtModpackName.getValidators().add(new RequiredValidator(i18n("modpack.not_a_valid_name")));
|
||||
StackPane.setMargin(txtModpackName, insets);
|
||||
list.getContent().add(txtModpackName);
|
||||
|
||||
validatingFields.add(txtModpackName);
|
||||
}
|
||||
|
||||
if (skinnable.showFileApi) {
|
||||
txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.modpackFileApi);
|
||||
if (skinnable.options.isRequireFileApi()) {
|
||||
JFXTextField txtModpackFileApi = new JFXTextField();
|
||||
txtModpackFileApi.textProperty().bindBidirectional(skinnable.fileApi);
|
||||
txtModpackFileApi.setLabelFloat(true);
|
||||
txtModpackFileApi.setPromptText(i18n("modpack.file_api"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackFileApi.getValidators().add(validator);
|
||||
|
||||
if (skinnable.options.isValidateFileApi()) {
|
||||
txtModpackFileApi.getValidators().add(new RequiredValidator());
|
||||
}
|
||||
|
||||
txtModpackFileApi.getValidators().add(new Validator(s -> {
|
||||
if (s.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
new URL(s).toURI();
|
||||
return true;
|
||||
@@ -213,35 +245,37 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
}));
|
||||
StackPane.setMargin(txtModpackFileApi, insets);
|
||||
list.getContent().add(txtModpackFileApi);
|
||||
} else {
|
||||
txtModpackFileApi = null;
|
||||
|
||||
validatingFields.add(txtModpackFileApi);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.modpackAuthor);
|
||||
JFXTextField txtModpackAuthor = new JFXTextField();
|
||||
txtModpackAuthor.textProperty().bindBidirectional(skinnable.author);
|
||||
txtModpackAuthor.setLabelFloat(true);
|
||||
txtModpackAuthor.setPromptText(i18n("archive.author"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackAuthor.getValidators().add(validator);
|
||||
txtModpackAuthor.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackAuthor, insets);
|
||||
list.getContent().add(txtModpackAuthor);
|
||||
|
||||
validatingFields.add(txtModpackAuthor);
|
||||
}
|
||||
|
||||
{
|
||||
txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.modpackVersion);
|
||||
JFXTextField txtModpackVersion = new JFXTextField();
|
||||
txtModpackVersion.textProperty().bindBidirectional(skinnable.version);
|
||||
txtModpackVersion.setLabelFloat(true);
|
||||
txtModpackVersion.setPromptText(i18n("archive.version"));
|
||||
RequiredFieldValidator validator = new RequiredFieldValidator();
|
||||
txtModpackVersion.getValidators().add(validator);
|
||||
txtModpackVersion.getValidators().add(new RequiredValidator());
|
||||
StackPane.setMargin(txtModpackVersion, insets);
|
||||
list.getContent().add(txtModpackVersion);
|
||||
|
||||
validatingFields.add(txtModpackVersion);
|
||||
}
|
||||
|
||||
{
|
||||
JFXTextArea area = new JFXTextArea();
|
||||
area.textProperty().bindBidirectional(skinnable.modpackDescription);
|
||||
area.textProperty().bindBidirectional(skinnable.description);
|
||||
area.setLabelFloat(true);
|
||||
area.setPromptText(i18n("modpack.desc"));
|
||||
area.setMinHeight(400);
|
||||
@@ -249,18 +283,123 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
list.getContent().add(area);
|
||||
}
|
||||
|
||||
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);
|
||||
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.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.includingLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.forceUpdate);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
borderPane1.setRight(button);
|
||||
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);
|
||||
|
||||
JFXToggleButton button = new JFXToggleButton();
|
||||
button.setDisable(!skinnable.canIncludeLauncher);
|
||||
button.selectedProperty().bindBidirectional(skinnable.packWithLauncher);
|
||||
button.setSize(8);
|
||||
button.setMinHeight(16);
|
||||
button.setMaxHeight(16);
|
||||
pane.setRight(button);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,15 +415,13 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
nextButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
nextButton.setText(i18n("wizard.next"));
|
||||
nextButton.getStyleClass().add("jfx-button-raised");
|
||||
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()));
|
||||
}
|
||||
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)));
|
||||
hbox.getChildren().add(nextButton);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,18 +20,23 @@ 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 btnHMCL;
|
||||
private JFXButton btnMCBBS;
|
||||
@FXML
|
||||
private JFXButton btnMultiMC;
|
||||
@FXML
|
||||
@@ -41,12 +46,15 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
|
||||
this.controller = controller;
|
||||
FXUtils.loadFXML(this, "/assets/fxml/modpack/type.fxml");
|
||||
|
||||
JFXButton[] buttons = new JFXButton[]{btnHMCL, btnMultiMC, btnServer};
|
||||
String[] types = new String[]{MODPACK_TYPE_HMCL, MODPACK_TYPE_MULTIMC, MODPACK_TYPE_SERVER};
|
||||
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};
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -63,7 +71,7 @@ public final class ModpackTypeSelectionPage extends StackPane implements WizardP
|
||||
|
||||
public static final String MODPACK_TYPE = "modpack.type";
|
||||
|
||||
public static final String MODPACK_TYPE_HMCL = "hmcl";
|
||||
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
|
||||
public static final String MODPACK_TYPE_MULTIMC = "multimc";
|
||||
public static final String MODPACK_TYPE_SERVER = "server";
|
||||
}
|
||||
@@ -132,7 +132,7 @@ class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
||||
}
|
||||
|
||||
String getSubtitle() {
|
||||
return StringUtils.parseColorEscapes(packInfo.getDescription());
|
||||
return StringUtils.parseColorEscapes(packInfo.getDescription().toString());
|
||||
}
|
||||
|
||||
Datapack.Pack getPackInfo() {
|
||||
|
||||
@@ -82,7 +82,7 @@ public class GameItem extends Control {
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
ModpackConfiguration<Void> config = profile.getRepository().readModpackConfiguration(version);
|
||||
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(version);
|
||||
if (config == null) return;
|
||||
tag.set(config.getVersion());
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
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;
|
||||
@@ -40,10 +39,8 @@ 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;
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
@@ -26,31 +25,25 @@ 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 Task<Void> {
|
||||
class HMCLDownloadTask extends FileDownloadTask {
|
||||
|
||||
private final FileDownloadTask task;
|
||||
private final RemoteVersion.Type archiveFormat;
|
||||
private final Path target;
|
||||
private RemoteVersion.Type archiveFormat;
|
||||
|
||||
public HMCLDownloadTask(RemoteVersion version, Path target) {
|
||||
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);
|
||||
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
|
||||
archiveFormat = version.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
super.execute();
|
||||
|
||||
try {
|
||||
Path target = getFile().toPath();
|
||||
|
||||
switch (archiveFormat) {
|
||||
case JAR:
|
||||
break;
|
||||
@@ -67,7 +60,7 @@ class HMCLDownloadTask extends Task<Void> {
|
||||
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Files.deleteIfExists(target);
|
||||
getFile().delete();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())) {
|
||||
if (Main.class.getName().equals(element.getClassName()) && "main".equals(element.getMethodName())) {
|
||||
// we've reached the main method
|
||||
return i + 1 != stacktrace.length;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility for Adding JavaFX to module path.
|
||||
*
|
||||
* @author ZekerZhayard
|
||||
*/
|
||||
public class JavaFXPatcher {
|
||||
public static void patch(Set<String> modules, Path... jarPaths) {
|
||||
// Nothing to do with Java 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinTask;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.lang.Class.forName;
|
||||
import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA;
|
||||
|
||||
/**
|
||||
* Utility for patching self when missing dependencies.
|
||||
* Copy from https://github.com/Col-E/Recaf/blob/master/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java
|
||||
*
|
||||
* @author Matt
|
||||
*/
|
||||
public class SelfDependencyPatcher {
|
||||
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
|
||||
private static final String DEFAULT_JFX_VERSION = "16";
|
||||
private static final Map<String, String> JFX_DEPENDENCIES = new HashMap<>();
|
||||
|
||||
static {
|
||||
addJfxDependency("base");
|
||||
addJfxDependency("controls");
|
||||
addJfxDependency("fxml");
|
||||
addJfxDependency("graphics");
|
||||
addJfxDependency("media");
|
||||
// Fix #874: Remove the dependency on javafx.swing
|
||||
// addJfxDependency("swing");
|
||||
addJfxDependency("web");
|
||||
}
|
||||
|
||||
private static void addJfxDependency(String name) {
|
||||
JFX_DEPENDENCIES.put("javafx." + name, jfxUrl(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
// Check if dependencies need to be downloaded
|
||||
if (!hasCachedDependencies()) {
|
||||
LOG.info(" - No local cache, downloading dependencies...");
|
||||
try {
|
||||
fetchDependencies();
|
||||
} catch (Exception ex) {
|
||||
throw new PatchException("Failed to download dependencies", ex);
|
||||
}
|
||||
} else {
|
||||
LOG.info(" - Local cache found!");
|
||||
}
|
||||
// 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...");
|
||||
List<Path> jarPaths = new ArrayList<>();
|
||||
List<String> jfxDepFile = JFX_DEPENDENCIES.values().stream().map(SelfDependencyPatcher::getFileName).collect(Collectors.toList());
|
||||
Files.walk(DEPENDENCIES_DIR_PATH).filter(p -> jfxDepFile.contains(p.getFileName().toString())).forEach(jarPaths::add);
|
||||
JavaFXPatcher.patch(JFX_DEPENDENCIES.keySet(), jarPaths.toArray(new Path[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download dependencies.
|
||||
*
|
||||
* @throws IOException When the files cannot be fetched or saved.
|
||||
*/
|
||||
private static void fetchDependencies() throws Exception {
|
||||
// Get dir to store dependencies in
|
||||
Path dependenciesDir = DEPENDENCIES_DIR_PATH;
|
||||
if (!Files.isDirectory(dependenciesDir)) {
|
||||
Files.createDirectories(dependenciesDir);
|
||||
}
|
||||
ProgressFrame dialog = new ProgressFrame(i18n("download.javafx"));
|
||||
|
||||
ForkJoinTask<Void> task = ForkJoinPool.commonPool().submit(() -> {
|
||||
// Download each dependency
|
||||
Collection<String> dependencies = JFX_DEPENDENCIES.values();
|
||||
int i = 1;
|
||||
for (String dependencyUrlPath : dependencies) {
|
||||
URL depURL = new URL(dependencyUrlPath);
|
||||
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(dependencyUrlPath));
|
||||
int finalI = i;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dialog.setStatus(dependencyUrlPath);
|
||||
dialog.setProgress(finalI, dependencies.size());
|
||||
});
|
||||
Files.copy(depURL.openStream(), dependencyFilePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
checksum(dependencyFilePath, dependencyUrlPath);
|
||||
i++;
|
||||
}
|
||||
|
||||
dialog.dispose();
|
||||
return null;
|
||||
});
|
||||
|
||||
dialog.setVisible(true);
|
||||
|
||||
task.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} when the dependencies directory has files in it.
|
||||
*/
|
||||
private static boolean hasCachedDependencies() {
|
||||
if (!Files.isDirectory(DEPENDENCIES_DIR_PATH))
|
||||
return false;
|
||||
|
||||
for (String url : JFX_DEPENDENCIES.values()) {
|
||||
Path dependencyFilePath = DEPENDENCIES_DIR_PATH.resolve(getFileName(url));
|
||||
if (!Files.exists(dependencyFilePath))
|
||||
return false;
|
||||
|
||||
try {
|
||||
checksum(dependencyFilePath, url);
|
||||
} catch (ChecksumMismatchException e) {
|
||||
return false;
|
||||
} catch (IOException ignored) {
|
||||
// Ignore other situations
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void checksum(Path dependencyFilePath, String dependencyUrlPath) throws IOException {
|
||||
String expectedHash = NetworkUtils.doGet(NetworkUtils.toURL(dependencyUrlPath + ".sha1"));
|
||||
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependencyFilePath));
|
||||
if (!expectedHash.equalsIgnoreCase(actualHash)) {
|
||||
throw new ChecksumMismatchException("SHA-1", expectedHash, actualHash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url Full url path.
|
||||
* @return Name of file at url.
|
||||
*/
|
||||
private static String getFileName(String url) {
|
||||
return url.substring(url.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param component Name of the component.
|
||||
* @return Formed URL for the component.
|
||||
*/
|
||||
private static String jfxUrl(String component) {
|
||||
return jfxUrl(component, DEFAULT_JFX_VERSION);
|
||||
}
|
||||
|
||||
private static String jfxUrl(String component, String version) {
|
||||
// https://repo1.maven.org/maven2/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar
|
||||
return String.format("https://maven.aliyun.com/repository/central/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
|
||||
component, version, component, version, getMvnName());
|
||||
// return String.format("https://bmclapi.bangbang93.com/maven/org/openjfx/javafx-%s/%s/javafx-%s-%s-%s.jar",
|
||||
// component, version, component, version, getMvnName());
|
||||
}
|
||||
|
||||
private static String getMvnName() {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX:
|
||||
return "linux";
|
||||
case OSX:
|
||||
return "mac";
|
||||
default:
|
||||
return "win";
|
||||
}
|
||||
}
|
||||
|
||||
public static class PatchException extends Exception {
|
||||
PatchException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IncompatibleVersionException extends Exception {
|
||||
}
|
||||
|
||||
public static class ProgressFrame extends JDialog {
|
||||
private int totalTasks = 0;
|
||||
private int finishedTasks = 0;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.module.Configuration;
|
||||
import java.lang.module.ModuleFinder;
|
||||
import java.lang.module.ModuleReference;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import jdk.internal.loader.BuiltinClassLoader;
|
||||
|
||||
/**
|
||||
* Utility for Adding JavaFX to module path.
|
||||
*
|
||||
* @author ZekerZhayard
|
||||
*/
|
||||
public class JavaFXPatcher {
|
||||
/**
|
||||
* 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 (
|
||||
FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + JavaFXPatcher.class.getProtectionDomain().getCodeSource().getLocation().toURI()), Map.of("create", "true"));
|
||||
InputStream stream = Files.newInputStream(fs.getPath("/META-INF/MANIFEST.MF"))
|
||||
) {
|
||||
// Some hacks
|
||||
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
|
||||
Attributes attributes = new Manifest(stream).getMainAttributes();
|
||||
addExportsOrOpens(attributes.getValue("Add-Exports"), layer, lookup.findVirtual(Module.class, "implAddExportsToAllUnnamed", MethodType.methodType(void.class, String.class)));
|
||||
addExportsOrOpens(attributes.getValue("Add-Opens"), layer, lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class)));
|
||||
} catch (Throwable t) {
|
||||
throw new ReflectiveOperationException(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addExportsOrOpens(String targets, ModuleLayer layer, MethodHandle handle) {
|
||||
for (String target : targets.split("\\s+")) {
|
||||
String[] name = target.split("/", 2); // <module>/<package>
|
||||
if (name[0].startsWith("javafx.")) {
|
||||
layer.findModule(name[0]).ifPresent(m -> {
|
||||
try {
|
||||
handle.invokeWithArguments(m, name[1]);
|
||||
} catch (Throwable throwable) {
|
||||
throw new RuntimeException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -4,8 +4,10 @@
|
||||
<?import com.jfoenix.validation.RequiredFieldValidator?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<?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?>
|
||||
<fx:root xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
type="StackPane">
|
||||
@@ -45,13 +47,13 @@
|
||||
</JFXButton>
|
||||
</HBox>
|
||||
|
||||
<Label text="%account.username" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
|
||||
<Label fx:id="lblUsername" 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>
|
||||
<RequiredFieldValidator message="%input.not_empty">
|
||||
</RequiredFieldValidator>
|
||||
<RequiredValidator message="%input.not_empty">
|
||||
</RequiredValidator>
|
||||
</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="btnHMCL" prefWidth="${list.width}">
|
||||
<JFXButton fx:id="btnMCBBS" prefWidth="${list.width}">
|
||||
<graphic>
|
||||
<BorderPane mouseTransparent="true">
|
||||
<left>
|
||||
<TwoLineListItem title="%modpack.type.hmcl" subtitle="%modpack.type.hmcl.export" />
|
||||
<TwoLineListItem title="%modpack.type.mcbbs" subtitle="%modpack.type.mcbbs.export" />
|
||||
</left>
|
||||
<right>
|
||||
<fx:include BorderPane.alignment="CENTER" source="/assets/svg/arrow-right.fxml"/>
|
||||
|
||||
@@ -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,13 +106,15 @@ 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.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.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_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.
|
||||
@@ -181,6 +183,7 @@ 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.
|
||||
@@ -263,6 +266,10 @@ 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.
|
||||
@@ -272,8 +279,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.hmcl=Hello Minecraft! Launcher
|
||||
modpack.type.hmcl.export=Can be imported by Hello Minecraft! Launcher
|
||||
modpack.type.mcbbs=MCBBS Standard
|
||||
modpack.type.mcbbs.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
|
||||
@@ -289,6 +296,7 @@ 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.
|
||||
@@ -404,6 +412,7 @@ 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
|
||||
|
||||
@@ -57,6 +57,7 @@ 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
|
||||
|
||||
@@ -57,6 +57,7 @@ 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=Без уч. записи
|
||||
|
||||
@@ -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,13 +106,15 @@ 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.missing_javafx=找不到 JavaFX。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
|
||||
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_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。
|
||||
|
||||
@@ -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.methods.yggdrasil=Mojang 账号
|
||||
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,13 +106,15 @@ 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.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。
|
||||
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_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。
|
||||
@@ -176,11 +178,12 @@ 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 参数有问题,可以在设置中开启无参数模式启动。
|
||||
@@ -237,11 +240,11 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
|
||||
modpack.choose.remote=从互联网下载整合包
|
||||
modpack.choose.remote.detail=需要提供整合包的下载链接
|
||||
modpack.choose.remote.tooltip=要下载的整合包的链接
|
||||
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 Markdown(图片请用网络图)。
|
||||
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录,支持 HTML(图片请用网络图)。
|
||||
modpack.description=整合包描述
|
||||
modpack.enter_name=给游戏起个你喜欢的名字
|
||||
modpack.export=导出整合包
|
||||
modpack.export.as=请选择整合包类型 (若无法决定,请选择 HMCL 类型)
|
||||
modpack.export.as=请选择整合包类型 (若无法决定,请选择我的世界中文论坛整合包标准)
|
||||
modpack.file_api=整合包下载链接前缀
|
||||
modpack.files.blueprints=BuildCraft 蓝图
|
||||
modpack.files.config=Mod 配置文件
|
||||
@@ -263,6 +266,10 @@ 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 整合包。
|
||||
@@ -272,8 +279,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.hmcl=HMCL
|
||||
modpack.type.hmcl.export=可以被 Hello Minecraft! Launcher (HMCL) 导入
|
||||
modpack.type.mcbbs=我的世界中文论坛整合包标准
|
||||
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher (HMCL) 导入
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher (HMCL) 和 MultiMC 导入
|
||||
modpack.type.server=服务器自动更新整合包
|
||||
@@ -289,6 +296,7 @@ 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整合包会保存您目前的下载源设置
|
||||
@@ -404,6 +412,7 @@ 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=查看日志
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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,6 +21,10 @@ 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;
|
||||
|
||||
@@ -92,6 +96,10 @@ 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,7 +25,52 @@ 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 {
|
||||
|
||||
|
||||
+22
@@ -21,6 +21,8 @@ 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;
|
||||
@@ -33,6 +35,8 @@ 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;
|
||||
@@ -156,4 +160,22 @@ 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);
|
||||
}
|
||||
}
|
||||
+5
@@ -44,6 +44,11 @@ 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);
|
||||
|
||||
+3
-10
@@ -17,11 +17,12 @@
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -55,7 +56,7 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
|
||||
|
||||
@Override
|
||||
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
|
||||
throw new UnsupportedOperationException();
|
||||
return toURL(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,12 +68,4 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -110,6 +110,7 @@ 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);
|
||||
@@ -145,6 +146,10 @@ public class AuthlibInjectorServer implements Observable {
|
||||
return links;
|
||||
}
|
||||
|
||||
public boolean isNonEmailLogin() {
|
||||
return nonEmailLogin;
|
||||
}
|
||||
|
||||
public String fetchMetadataResponse() throws IOException {
|
||||
if (metadataResponse == null || !metadataRefreshed) {
|
||||
refreshMetadata();
|
||||
@@ -194,6 +199,9 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,11 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
|
||||
private OfflineAccountFactory() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccountLoginType getLoginType() {
|
||||
return AccountLoginType.USERNAME;
|
||||
}
|
||||
|
||||
public OfflineAccount create(String username, UUID uuid) {
|
||||
return new OfflineAccount(username, uuid);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
*/
|
||||
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;
|
||||
@@ -38,7 +37,7 @@ public class CompleteGameProfile extends GameProfile {
|
||||
|
||||
public CompleteGameProfile(UUID id, String name, Map<String, String> properties) {
|
||||
super(id, name);
|
||||
this.properties = requireNonNull(properties);
|
||||
this.properties = Objects.requireNonNull(properties);
|
||||
}
|
||||
|
||||
public CompleteGameProfile(GameProfile profile, Map<String, String> properties) {
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
@@ -40,8 +39,8 @@ public class GameProfile implements Validation {
|
||||
private final String name;
|
||||
|
||||
public GameProfile(UUID id, String name) {
|
||||
this.id = requireNonNull(id);
|
||||
this.name = requireNonNull(name);
|
||||
this.id = Objects.requireNonNull(id);
|
||||
this.name = Objects.requireNonNull(name);
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
@@ -54,9 +53,7 @@ public class GameProfile implements Validation {
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (id == null)
|
||||
throw new JsonParseException("Game profile id cannot be null");
|
||||
if (name == null)
|
||||
throw new JsonParseException("Game profile name cannot be null");
|
||||
Validation.requireNonNull(id, "Game profile id cannot be null");
|
||||
Validation.requireNonNull(name, "Game profile name cannot be null");
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,14 @@ 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 {
|
||||
|
||||
@@ -189,6 +192,20 @@ 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);
|
||||
}
|
||||
@@ -209,6 +226,7 @@ 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;
|
||||
|
||||
+6
-1
@@ -36,12 +36,17 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
|
||||
public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
|
||||
|
||||
private YggdrasilService service;
|
||||
private final 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,7 +28,6 @@ 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;
|
||||
|
||||
@@ -52,7 +51,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class YggdrasilService {
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final ThreadPoolExecutor POOL = threadPool("YggdrasilProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
|
||||
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
|
||||
|
||||
@@ -164,10 +163,7 @@ public class YggdrasilService {
|
||||
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
|
||||
}
|
||||
}
|
||||
String response = IOUtils.readFullyAsString(con.getInputStream());
|
||||
if (response.startsWith("{")) {
|
||||
handleErrorMessage(fromJson(response, ErrorResponse.class));
|
||||
}
|
||||
requireEmpty(NetworkUtils.readData(con));
|
||||
} catch (IOException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
@@ -218,18 +214,14 @@ public class YggdrasilService {
|
||||
response.accessToken,
|
||||
response.selectedProfile,
|
||||
response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles),
|
||||
response.user);
|
||||
response.user == null ? null : response.user.getProperties());
|
||||
}
|
||||
|
||||
private static void requireEmpty(String response) throws AuthenticationException {
|
||||
if (StringUtils.isBlank(response))
|
||||
return;
|
||||
|
||||
try {
|
||||
handleErrorMessage(fromJson(response, ErrorResponse.class));
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
handleErrorMessage(fromJson(response, ErrorResponse.class));
|
||||
}
|
||||
|
||||
private static void handleErrorMessage(ErrorResponse response) throws AuthenticationException {
|
||||
@@ -253,7 +245,7 @@ public class YggdrasilService {
|
||||
try {
|
||||
return GSON.fromJson(text, typeOfT);
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException("Server response: " + text, e);
|
||||
throw new ServerResponseMalformedException(text, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ 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;
|
||||
@@ -36,14 +37,15 @@ public class YggdrasilSession {
|
||||
private final String accessToken;
|
||||
private final GameProfile selectedProfile;
|
||||
private final List<GameProfile> availableProfiles;
|
||||
private final User user;
|
||||
@Nullable
|
||||
private final Map<String, String> userProperties;
|
||||
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, User user) {
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, Map<String, String> userProperties) {
|
||||
this.clientToken = clientToken;
|
||||
this.accessToken = accessToken;
|
||||
this.selectedProfile = selectedProfile;
|
||||
this.availableProfiles = availableProfiles;
|
||||
this.user = user;
|
||||
this.userProperties = userProperties;
|
||||
}
|
||||
|
||||
public String getClientToken() {
|
||||
@@ -68,8 +70,8 @@ public class YggdrasilSession {
|
||||
return availableProfiles;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
public Map<String, String> getUserProperties() {
|
||||
return userProperties;
|
||||
}
|
||||
|
||||
public static YggdrasilSession fromStorage(Map<?, ?> storage) {
|
||||
@@ -77,34 +79,28 @@ 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, new User(userId, userProperties));
|
||||
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, 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("userid", user.getId()),
|
||||
pair("userProperties", user.getProperties()));
|
||||
pair("userProperties", userProperties));
|
||||
}
|
||||
|
||||
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(user.getProperties())
|
||||
Optional.ofNullable(userProperties)
|
||||
.map(properties -> properties.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey,
|
||||
e -> Collections.singleton(e.getValue()))))
|
||||
|
||||
@@ -78,6 +78,7 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
if (baseURL.contains("v1/products/java-runtime")) return baseURL;
|
||||
return baseURL
|
||||
.replace("https://bmclapi2.bangbang93.com", apiRoot)
|
||||
.replace("https://launchermeta.mojang.com", apiRoot)
|
||||
|
||||
@@ -24,7 +24,6 @@ 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;
|
||||
@@ -73,11 +72,8 @@ public final class ForgeInstallTask extends Task<Version> {
|
||||
installer = Files.createTempFile("forge-installer", ".jar");
|
||||
|
||||
dependent = new FileDownloadTask(
|
||||
new DownloadManager.DownloadTaskStateBuilder()
|
||||
.setUrls(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()))
|
||||
.setFile(installer.toFile())
|
||||
.setInitialParts(3)
|
||||
.build());
|
||||
dependencyManager.getDownloadProvider().injectURLsWithCandidates(remote.getUrls()),
|
||||
installer.toFile(), null);
|
||||
dependent.setCacheRepository(dependencyManager.getCacheRepository());
|
||||
dependent.setCaching(true);
|
||||
dependent.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.DownloadManager;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -57,11 +56,10 @@ public final class GameDownloadTask extends Task<Void> {
|
||||
public void execute() {
|
||||
File jar = dependencyManager.getGameRepository().getVersionJar(version);
|
||||
|
||||
FileDownloadTask task = new FileDownloadTask(new DownloadManager.DownloadTaskStateBuilder()
|
||||
.setUrls(dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()))
|
||||
.setFile(jar)
|
||||
.setInitialParts(3)
|
||||
.build(), IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
|
||||
FileDownloadTask task = new FileDownloadTask(
|
||||
dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()),
|
||||
jar,
|
||||
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()));
|
||||
task.setCaching(true);
|
||||
task.setCacheRepository(dependencyManager.getCacheRepository());
|
||||
|
||||
|
||||
@@ -168,8 +168,13 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
|
||||
@Override
|
||||
public void postExecute() throws Exception {
|
||||
if (!cached)
|
||||
cacheRepository.cacheLibrary(library, jar.toPath(), xz);
|
||||
if (!cached) {
|
||||
try {
|
||||
cacheRepository.cacheLibrary(library, jar.toPath(), xz);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to cache downloaded library " + library, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checksumValid(File libPath, List<String> checksums) {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.download.java;
|
||||
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.DownloadInfo;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.tukaani.xz.LZMAInputStream;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class JavaDownloadTask extends Task<Void> {
|
||||
private final GameJavaVersion javaVersion;
|
||||
private final Path rootDir;
|
||||
private String platform;
|
||||
private final Task<RemoteFiles> javaDownloadsTask;
|
||||
private JavaDownloads.JavaDownload download;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
|
||||
public JavaDownloadTask(GameJavaVersion javaVersion, Path rootDir, DownloadProvider downloadProvider) {
|
||||
this.javaVersion = javaVersion;
|
||||
this.rootDir = rootDir;
|
||||
this.javaDownloadsTask = new GetTask(NetworkUtils.toURL(
|
||||
"https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"))
|
||||
.thenComposeAsync(javaDownloadsJson -> {
|
||||
JavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, JavaDownloads.class);
|
||||
if (!allDownloads.getDownloads().containsKey(platform)) throw new UnsupportedPlatformException();
|
||||
Map<String, List<JavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
|
||||
if (!osDownloads.containsKey(javaVersion.getComponent())) throw new UnsupportedPlatformException();
|
||||
List<JavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.getComponent());
|
||||
for (JavaDownloads.JavaDownload download : candidates) {
|
||||
if (VersionNumber.VERSION_COMPARATOR.compare(download.getVersion().getName(), Integer.toString(javaVersion.getMajorVersion())) >= 0) {
|
||||
this.download = download;
|
||||
return new GetTask(NetworkUtils.toURL(download.getManifest().getUrl()));
|
||||
}
|
||||
}
|
||||
throw new UnsupportedPlatformException();
|
||||
})
|
||||
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, RemoteFiles.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
this.platform = JavaRepository.getCurrentJavaPlatform().orElseThrow(UnsupportedPlatformException::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependents() {
|
||||
return Collections.singleton(javaDownloadsTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Path jvmDir = rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent());
|
||||
for (Map.Entry<String, RemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
|
||||
Path dest = jvmDir.resolve(entry.getKey());
|
||||
if (entry.getValue() instanceof RemoteFiles.RemoteFile) {
|
||||
RemoteFiles.RemoteFile file = ((RemoteFiles.RemoteFile) entry.getValue());
|
||||
if (file.getDownloads().containsKey("lzma")) {
|
||||
DownloadInfo download = file.getDownloads().get("lzma");
|
||||
File tempFile = Files.createTempFile("hmcl", "tmp").toFile();
|
||||
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
|
||||
task.setName(entry.getKey());
|
||||
dependencies.add(task.thenRunAsync(() -> {
|
||||
try {
|
||||
Files.copy(new LZMAInputStream(new FileInputStream(tempFile)), dest);
|
||||
} catch (IOException e) {
|
||||
throw new ArtifactMalformedException("File " + entry.getKey() + " is malformed");
|
||||
}
|
||||
}));
|
||||
} else if (file.getDownloads().containsKey("raw")) {
|
||||
DownloadInfo download = file.getDownloads().get("raw");
|
||||
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(download.getUrl()), dest.toFile(), new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
|
||||
task.setName(entry.getKey());
|
||||
dependencies.add(task);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else if (entry.getValue() instanceof RemoteFiles.RemoteDirectory) {
|
||||
Files.createDirectories(dest);
|
||||
} else if (entry.getValue() instanceof RemoteFiles.RemoteLink) {
|
||||
RemoteFiles.RemoteLink link = ((RemoteFiles.RemoteLink) entry.getValue());
|
||||
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPostExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postExecute() throws Exception {
|
||||
FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(".version").toFile(), download.getVersion().getName());
|
||||
FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent() + ".sha1").toFile(),
|
||||
javaDownloadsTask.getResult().getFiles().entrySet().stream()
|
||||
.filter(entry -> entry.getValue() instanceof RemoteFiles.RemoteFile)
|
||||
.map(entry -> {
|
||||
RemoteFiles.RemoteFile file = (RemoteFiles.RemoteFile) entry.getValue();
|
||||
return entry.getKey() + " /#// " + file.getDownloads().get("raw").getSha1() + " " + file.getDownloads().get("raw").getSize();
|
||||
})
|
||||
.collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
||||
}
|
||||
|
||||
public static class UnsupportedPlatformException extends Exception {}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.download.java;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.game.DownloadInfo;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@JsonAdapter(JavaDownloads.Adapter.class)
|
||||
public class JavaDownloads {
|
||||
|
||||
private final Map<String, Map<String, List<JavaDownload>>> downloads;
|
||||
|
||||
public JavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
|
||||
this.downloads = downloads;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, List<JavaDownload>>> getDownloads() {
|
||||
return downloads;
|
||||
}
|
||||
|
||||
public static class Adapter implements JsonSerializer<JavaDownloads>, JsonDeserializer<JavaDownloads> {
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(JavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return context.serialize(src.downloads);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return new JavaDownloads(context.deserialize(json, new TypeToken<Map<String, Map<String, List<JavaDownload>>>>() {
|
||||
}.getType()));
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static class JavaDownload {
|
||||
private final Availability availability;
|
||||
private final DownloadInfo manifest;
|
||||
private final Version version;
|
||||
|
||||
public JavaDownload() {
|
||||
this(new Availability(), new DownloadInfo(), new Version());
|
||||
}
|
||||
|
||||
public JavaDownload(Availability availability, DownloadInfo manifest, Version version) {
|
||||
this.availability = availability;
|
||||
this.manifest = manifest;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public Availability getAvailability() {
|
||||
return availability;
|
||||
}
|
||||
|
||||
public DownloadInfo getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static class Availability {
|
||||
private final int group;
|
||||
private final int progress;
|
||||
|
||||
public Availability() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public Availability(int group, int progress) {
|
||||
this.group = group;
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public int getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static class Version {
|
||||
private final String name;
|
||||
private final String released;
|
||||
|
||||
public Version() {
|
||||
this("", "");
|
||||
}
|
||||
|
||||
public Version(String name, String released) {
|
||||
this.name = name;
|
||||
this.released = released;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getReleased() {
|
||||
return released;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.jackhuang.hmcl.download.java;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public final class JavaRepository {
|
||||
|
||||
public static Task<?> downloadJava(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
|
||||
return new JavaDownloadTask(javaVersion, getJavaStoragePath(), downloadProvider)
|
||||
.thenRunAsync(() -> {
|
||||
Optional<String> platform = getCurrentJavaPlatform();
|
||||
if (platform.isPresent()) {
|
||||
addJava(getJavaHome(javaVersion, platform.get()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void addJava(Path javaHome) throws InterruptedException, IOException {
|
||||
if (Files.isDirectory(javaHome)) {
|
||||
Path executable = JavaVersion.getExecutable(javaHome);
|
||||
if (Files.isRegularFile(executable)) {
|
||||
JavaVersion.getJavas().add(JavaVersion.fromExecutable(executable));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize() throws IOException, InterruptedException {
|
||||
Optional<String> platformOptional = getCurrentJavaPlatform();
|
||||
if (platformOptional.isPresent()) {
|
||||
String platform = platformOptional.get();
|
||||
for (Path component : Files.newDirectoryStream(getJavaStoragePath())) {
|
||||
Path javaHome = component.resolve(platform).resolve(component.getFileName());
|
||||
try {
|
||||
addJava(javaHome);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to determine Java at " + javaHome, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<String> getCurrentJavaPlatform() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
if (Architecture.CURRENT == Architecture.X86) {
|
||||
return Optional.of("linux-i386");
|
||||
} else if (Architecture.CURRENT == Architecture.X86_64) {
|
||||
return Optional.of("linux");
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
if (Architecture.CURRENT == Architecture.X86_64) {
|
||||
return Optional.of("mac-os");
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.CURRENT == Architecture.X86) {
|
||||
return Optional.of("windows-x86");
|
||||
} else if (Architecture.CURRENT == Architecture.X86_64) {
|
||||
return Optional.of("windows-x64");
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static Path getJavaStoragePath() {
|
||||
return CacheRepository.getInstance().getCacheDirectory().resolve("java");
|
||||
}
|
||||
|
||||
public static Path getJavaHome(GameJavaVersion javaVersion, String platform) {
|
||||
return getJavaStoragePath().resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.download.java;
|
||||
|
||||
import org.jackhuang.hmcl.game.DownloadInfo;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSubtype;
|
||||
import org.jackhuang.hmcl.util.gson.JsonType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public class RemoteFiles {
|
||||
private final Map<String, Remote> files;
|
||||
|
||||
public RemoteFiles(Map<String, Remote> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public Map<String, Remote> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
@JsonType(
|
||||
property = "type",
|
||||
subtypes = {
|
||||
@JsonSubtype(clazz = RemoteFile.class, name = "file"),
|
||||
@JsonSubtype(clazz = RemoteDirectory.class, name = "directory"),
|
||||
@JsonSubtype(clazz = RemoteLink.class, name = "link")
|
||||
}
|
||||
)
|
||||
public static class Remote {
|
||||
private final String type;
|
||||
|
||||
public Remote(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoteFile extends Remote {
|
||||
private final boolean executable;
|
||||
private final Map<String, DownloadInfo> downloads;
|
||||
|
||||
public RemoteFile(boolean executable, Map<String, DownloadInfo> downloads) {
|
||||
super("file");
|
||||
this.executable = executable;
|
||||
this.downloads = downloads;
|
||||
}
|
||||
|
||||
public boolean isExecutable() {
|
||||
return executable;
|
||||
}
|
||||
|
||||
public Map<String, DownloadInfo> getDownloads() {
|
||||
return downloads == null ? Collections.emptyMap() : downloads;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoteDirectory extends Remote {
|
||||
public RemoteDirectory() {
|
||||
super("directory");
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoteLink extends Remote {
|
||||
private final String target;
|
||||
|
||||
public RemoteLink(String target) {
|
||||
super("link");
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ public class ClassicVersion extends Version {
|
||||
|
||||
public ClassicVersion() {
|
||||
super(true, "Classic", null, null, "${auth_player_name} ${auth_session} --workDir ${game_directory}",
|
||||
null, "net.minecraft.client.Minecraft", null, null, null, null,
|
||||
null, "net.minecraft.client.Minecraft", null, null, null, null, null, null,
|
||||
Arrays.asList(new ClassicLibrary("lwjgl"), new ClassicLibrary("jinput"), new ClassicLibrary("lwjgl_util")),
|
||||
null, null, null, ReleaseType.UNKNOWN, new Date(), new Date(), 0, false, false, null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.game;
|
||||
|
||||
public class GameJavaVersion {
|
||||
private final String component;
|
||||
private final int majorVersion;
|
||||
|
||||
public GameJavaVersion() {
|
||||
this("", 0);
|
||||
}
|
||||
|
||||
public GameJavaVersion(String component, int majorVersion) {
|
||||
this.component = component;
|
||||
this.majorVersion = majorVersion;
|
||||
}
|
||||
|
||||
public String getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
public int getMajorVersion() {
|
||||
return majorVersion;
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -35,19 +32,23 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GameVersion {
|
||||
private static Optional<String> getVersionFromJson(Path versionJson) {
|
||||
try {
|
||||
MinecraftVersion version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), MinecraftVersion.class);
|
||||
return Optional.ofNullable(version.name);
|
||||
Map<?, ?> version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), Map.class);
|
||||
return tryCast(version.get("name"), String.class);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse version.json", e);
|
||||
return Optional.empty();
|
||||
@@ -112,24 +113,4 @@ public final class GameVersion {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MinecraftVersion {
|
||||
public String name;
|
||||
|
||||
@SerializedName("release_target")
|
||||
public String releaseTarget;
|
||||
|
||||
public String id;
|
||||
|
||||
public boolean stable;
|
||||
|
||||
@SerializedName("world_version")
|
||||
public int worldVersion;
|
||||
|
||||
@SerializedName("protocol_version")
|
||||
public int protocolVersion;
|
||||
|
||||
@SerializedName("pack_version")
|
||||
public int packVersion;
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,14 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -34,8 +38,8 @@ public class LaunchOptions implements Serializable {
|
||||
private String versionName;
|
||||
private String versionType;
|
||||
private String profileName;
|
||||
private String minecraftArgs;
|
||||
private String javaArgs;
|
||||
private List<String> gameArguments = new ArrayList<>();
|
||||
private List<String> javaArguments = new ArrayList<>();
|
||||
private Integer minMemory;
|
||||
private Integer maxMemory;
|
||||
private Integer metaspace;
|
||||
@@ -90,15 +94,17 @@ public class LaunchOptions implements Serializable {
|
||||
/**
|
||||
* User custom additional minecraft command line arguments.
|
||||
*/
|
||||
public String getMinecraftArgs() {
|
||||
return minecraftArgs;
|
||||
@NotNull
|
||||
public List<String> getGameArguments() {
|
||||
return Collections.unmodifiableList(gameArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* User custom additional java virtual machine command line arguments.
|
||||
*/
|
||||
public String getJavaArgs() {
|
||||
return javaArgs;
|
||||
@NotNull
|
||||
public List<String> getJavaArguments() {
|
||||
return Collections.unmodifiableList(javaArguments);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,6 +208,150 @@ public class LaunchOptions implements Serializable {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* The game directory
|
||||
*/
|
||||
public File getGameDir() {
|
||||
return options.gameDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Java Environment that Minecraft runs on.
|
||||
*/
|
||||
public JavaVersion getJava() {
|
||||
return options.java;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will shown in the left bottom corner of the main menu of Minecraft.
|
||||
* null if use the id of launch version.
|
||||
*/
|
||||
public String getVersionName() {
|
||||
return options.versionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will shown in the left bottom corner of the main menu of Minecraft.
|
||||
* null if use Version.versionType.
|
||||
*/
|
||||
public String getVersionType() {
|
||||
return options.versionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't know what the hell this is.
|
||||
*/
|
||||
public String getProfileName() {
|
||||
return options.profileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* User custom additional minecraft command line arguments.
|
||||
*/
|
||||
public List<String> getGameArguments() {
|
||||
return options.gameArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* User custom additional java virtual machine command line arguments.
|
||||
*/
|
||||
public List<String> getJavaArguments() {
|
||||
return options.javaArguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum memory that the JVM can allocate.
|
||||
*/
|
||||
public Integer getMinMemory() {
|
||||
return options.minMemory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum memory that the JVM can allocate.
|
||||
*/
|
||||
public Integer getMaxMemory() {
|
||||
return options.maxMemory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum metaspace memory that the JVM can allocate.
|
||||
* For Java 7 -XX:PermSize and Java 8 -XX:MetaspaceSize
|
||||
* Containing class instances.
|
||||
*/
|
||||
public Integer getMetaspace() {
|
||||
return options.metaspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial game window width
|
||||
*/
|
||||
public Integer getWidth() {
|
||||
return options.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial game window height
|
||||
*/
|
||||
public Integer getHeight() {
|
||||
return options.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is inital game window fullscreen.
|
||||
*/
|
||||
public boolean isFullscreen() {
|
||||
return options.fullscreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server ip that will connect to when enter game main menu.
|
||||
*/
|
||||
public String getServerIp() {
|
||||
return options.serverIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* i.e. optirun
|
||||
*/
|
||||
public String getWrapper() {
|
||||
return options.wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy settings
|
||||
*/
|
||||
public Proxy getProxy() {
|
||||
return options.proxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user name of the proxy, optional.
|
||||
*/
|
||||
public String getProxyUser() {
|
||||
return options.proxyUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* The password of the proxy, optional
|
||||
*/
|
||||
public String getProxyPass() {
|
||||
return options.proxyPass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent game launcher from generating default JVM arguments like max memory.
|
||||
*/
|
||||
public boolean isNoGeneratedJVMArgs() {
|
||||
return options.noGeneratedJVMArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called command line before launching the game.
|
||||
*/
|
||||
public String getPreLaunchCommand() {
|
||||
return options.preLaunchCommand;
|
||||
}
|
||||
|
||||
public Builder setGameDir(File gameDir) {
|
||||
options.gameDir = gameDir;
|
||||
return this;
|
||||
@@ -227,13 +377,15 @@ public class LaunchOptions implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMinecraftArgs(String minecraftArgs) {
|
||||
options.minecraftArgs = minecraftArgs;
|
||||
public Builder setGameArguments(List<String> gameArguments) {
|
||||
options.gameArguments.clear();
|
||||
options.gameArguments.addAll(gameArguments);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setJavaArgs(String javaArgs) {
|
||||
options.javaArgs = javaArgs;
|
||||
public Builder setJavaArguments(List<String> javaArguments) {
|
||||
options.javaArguments.clear();
|
||||
options.javaArguments.addAll(javaArguments);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
@@ -71,7 +72,7 @@ public final class OSRestriction {
|
||||
return false;
|
||||
|
||||
if (arch != null)
|
||||
return !Lang.test(() -> !Pattern.compile(arch).matcher(OperatingSystem.SYSTEM_ARCHITECTURE).matches());
|
||||
return !Lang.test(() -> !Pattern.compile(arch).matcher(Architecture.SYSTEM_ARCHITECTURE).matches());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -58,6 +58,9 @@ public class Version implements Comparable<Version>, Validation {
|
||||
private final String jar;
|
||||
private final AssetIndexInfo assetIndex;
|
||||
private final String assets;
|
||||
private final Integer complianceLevel;
|
||||
@Nullable
|
||||
private final GameJavaVersion javaVersion;
|
||||
private final List<Library> libraries;
|
||||
private final List<CompatibilityRule> compatibilityRules;
|
||||
private final JsonMap<DownloadType, DownloadInfo> downloads;
|
||||
@@ -73,23 +76,24 @@ public class Version implements Comparable<Version>, Validation {
|
||||
private transient final boolean resolved;
|
||||
|
||||
public Version(String id) {
|
||||
this(false, id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, true, null);
|
||||
this(false, id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for patch
|
||||
* @param id patch id
|
||||
* @param version patch version
|
||||
* @param priority patch priority
|
||||
*
|
||||
* @param id patch id
|
||||
* @param version patch version
|
||||
* @param priority patch priority
|
||||
* @param arguments patch additional arguments
|
||||
* @param mainClass main class to override
|
||||
* @param libraries additional libraries
|
||||
*/
|
||||
public Version(String id, String version, int priority, Arguments arguments, String mainClass, List<Library> libraries) {
|
||||
this(false, id, version, priority, null, arguments, mainClass, null, null, null, null, libraries, null, null, null, null, null, null, null, null, null, null);
|
||||
this(false, id, version, priority, null, arguments, mainClass, null, null, null, null, null, null, libraries, null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public Version(boolean resolved, String id, String version, Integer priority, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, List<Library> libraries, List<CompatibilityRule> compatibilityRules, Map<DownloadType, DownloadInfo> downloads, Map<DownloadType, LoggingInfo> logging, ReleaseType type, Date time, Date releaseTime, Integer minimumLauncherVersion, Boolean hidden, Boolean root, List<Version> patches) {
|
||||
public Version(boolean resolved, String id, String version, Integer priority, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, Integer complianceLevel, GameJavaVersion javaVersion, List<Library> libraries, List<CompatibilityRule> compatibilityRules, Map<DownloadType, DownloadInfo> downloads, Map<DownloadType, LoggingInfo> logging, ReleaseType type, Date time, Date releaseTime, Integer minimumLauncherVersion, Boolean hidden, Boolean root, List<Version> patches) {
|
||||
this.resolved = resolved;
|
||||
this.id = id;
|
||||
this.version = version;
|
||||
@@ -101,6 +105,8 @@ public class Version implements Comparable<Version>, Validation {
|
||||
this.jar = jar;
|
||||
this.assetIndex = assetIndex;
|
||||
this.assets = assets;
|
||||
this.complianceLevel = complianceLevel;
|
||||
this.javaVersion = javaVersion;
|
||||
this.libraries = Lang.copyList(libraries);
|
||||
this.compatibilityRules = Lang.copyList(compatibilityRules);
|
||||
this.downloads = downloads == null ? null : new JsonMap<>(downloads);
|
||||
@@ -168,11 +174,21 @@ public class Version implements Comparable<Version>, Validation {
|
||||
return minimumLauncherVersion == null ? 0 : minimumLauncherVersion;
|
||||
}
|
||||
|
||||
public Integer getComplianceLevel() {
|
||||
return complianceLevel;
|
||||
}
|
||||
|
||||
public GameJavaVersion getJavaVersion() {
|
||||
return javaVersion;
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return hidden == null ? false : hidden;
|
||||
}
|
||||
|
||||
public boolean isRoot() { return root == null ? false : root; }
|
||||
public boolean isRoot() {
|
||||
return root == null ? false : root;
|
||||
}
|
||||
|
||||
public boolean isResolved() {
|
||||
return resolved;
|
||||
@@ -239,6 +255,8 @@ public class Version implements Comparable<Version>, Validation {
|
||||
jar == null ? parent.jar : jar,
|
||||
assetIndex == null ? parent.assetIndex : assetIndex,
|
||||
assets == null ? parent.assets : assets,
|
||||
complianceLevel,
|
||||
javaVersion == null ? parent.javaVersion : javaVersion,
|
||||
Lang.merge(this.libraries, parent.libraries),
|
||||
Lang.merge(parent.compatibilityRules, this.compatibilityRules),
|
||||
downloads == null ? parent.downloads : downloads,
|
||||
@@ -319,59 +337,59 @@ public class Version implements Comparable<Version>, Validation {
|
||||
}
|
||||
|
||||
private Version markAsResolved() {
|
||||
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version markAsUnresolved() {
|
||||
return new Version(false, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(false, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
private Version setHidden(Boolean hidden) {
|
||||
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setId(String id) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setVersion(String version) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setPriority(Integer priority) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setMinecraftArguments(String minecraftArguments) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setArguments(Arguments arguments) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setMainClass(String mainClass) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setInheritsFrom(String inheritsFrom) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setJar(String jar) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setLibraries(List<Library> libraries) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setLogging(Map<DownloadType, LoggingInfo> logging) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version setPatches(List<Version> patches) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version addPatch(Version... additional) {
|
||||
@@ -381,15 +399,15 @@ public class Version implements Comparable<Version>, Validation {
|
||||
public Version addPatches(@Nullable List<Version> additional) {
|
||||
Set<String> patchIds = additional == null ? Collections.emptySet() : additional.stream().map(Version::getId).collect(Collectors.toSet());
|
||||
List<Version> patches = Lang.merge(this.patches == null ? null : this.patches.stream().filter(patch -> !patchIds.contains(patch.getId())).collect(Collectors.toList()), additional);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches);
|
||||
}
|
||||
|
||||
public Version clearPatches() {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root,null);
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, null);
|
||||
}
|
||||
|
||||
public Version removePatchById(String patchId) {
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root,
|
||||
return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root,
|
||||
patches == null ? null : patches.stream().filter(patch -> !patchId.equals(patch.getId())).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,7 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
res.add(options.getJava().getBinary().toString());
|
||||
|
||||
if (StringUtils.isNotBlank(options.getJavaArgs()))
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getJavaArgs()));
|
||||
res.addAllWithoutParsing(options.getJavaArguments());
|
||||
|
||||
// JVM Args
|
||||
if (!options.isNoGeneratedJVMArgs()) {
|
||||
@@ -100,7 +99,7 @@ public class DefaultLauncher extends Launcher {
|
||||
res.add("-Duser.home=" + options.getGameDir().getParent());
|
||||
|
||||
// Force using G1GC with its settings
|
||||
if (options.getJava().getParsedVersion() >= JavaVersion.JAVA_7) {
|
||||
if (options.getJava().getParsedVersion() >= JavaVersion.JAVA_8) {
|
||||
res.add("-XX:+UnlockExperimentalVMOptions");
|
||||
res.add("-XX:+UseG1GC");
|
||||
res.add("-XX:G1NewSizePercent=20");
|
||||
@@ -131,6 +130,9 @@ public class DefaultLauncher extends Launcher {
|
||||
if (options.getMinMemory() != null && options.getMinMemory() > 0)
|
||||
res.add("-Xms" + options.getMinMemory() + "m");
|
||||
|
||||
if (options.getJava().getParsedVersion() == JavaVersion.JAVA_16)
|
||||
res.add("--illegal-access=permit");
|
||||
|
||||
res.add("-Dfml.ignoreInvalidMinecraftCertificates=true");
|
||||
res.add("-Dfml.ignorePatchDiscrepancies=true");
|
||||
}
|
||||
@@ -218,8 +220,7 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(options.getMinecraftArgs()))
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getMinecraftArgs()));
|
||||
res.addAllWithoutParsing(options.getGameArguments());
|
||||
|
||||
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
|
||||
return res;
|
||||
@@ -233,7 +234,7 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
|
||||
private final Map<String, Supplier<Boolean>> forbiddens = mapOf(
|
||||
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9_AND_LATER)
|
||||
pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9)
|
||||
);
|
||||
|
||||
protected Map<String, Supplier<Boolean>> getForbiddens() {
|
||||
@@ -303,6 +304,10 @@ public class DefaultLauncher extends Launcher {
|
||||
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
|
||||
List<String> rawCommandLine = generateCommandLine(nativeFolder).asList();
|
||||
|
||||
if (rawCommandLine.stream().anyMatch(StringUtils::isBlank)) {
|
||||
throw new IllegalStateException("Illegal command line " + rawCommandLine);
|
||||
}
|
||||
|
||||
decompressNatives(nativeFolder);
|
||||
|
||||
File runDirectory = repository.getRunDirectory(version.getId());
|
||||
@@ -323,6 +328,9 @@ public class DefaultLauncher extends Launcher {
|
||||
Process process;
|
||||
try {
|
||||
ProcessBuilder builder = new ProcessBuilder(rawCommandLine).directory(runDirectory);
|
||||
if (listener == null) {
|
||||
builder.inheritIO();
|
||||
}
|
||||
String appdata = options.getGameDir().getAbsoluteFile().getParent();
|
||||
if (appdata != null) builder.environment().put("APPDATA", appdata);
|
||||
process = builder.start();
|
||||
|
||||
@@ -202,10 +202,10 @@ public class Datapack {
|
||||
private Path file;
|
||||
private final BooleanProperty active;
|
||||
private final String id;
|
||||
private final String description;
|
||||
private final ModInfo.Description description;
|
||||
private final Datapack datapack;
|
||||
|
||||
public Pack(Path file, String id, String description, Datapack datapack) {
|
||||
public Pack(Path file, String id, ModInfo.Description description, Datapack datapack) {
|
||||
this.file = file;
|
||||
this.id = id;
|
||||
this.description = description;
|
||||
@@ -235,7 +235,7 @@ public class Datapack {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
public ModInfo.Description getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ public final class FabricModMetadata {
|
||||
throw new IOException("File " + modFile + " is not a Fabric mod.");
|
||||
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
|
||||
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
|
||||
return new ModInfo(modManager, modFile, metadata.name, metadata.description,
|
||||
return new ModInfo(modManager, modFile, metadata.name, new ModInfo.Description(metadata.description),
|
||||
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ public final class ForgeModMetadata {
|
||||
authors = String.join(", ", metadata.getAuthorList());
|
||||
if (StringUtils.isBlank(authors))
|
||||
authors = metadata.getCredits();
|
||||
return new ModInfo(modManager, modFile, metadata.getName(), metadata.getDescription(),
|
||||
return new ModInfo(modManager, modFile, metadata.getName(), new ModInfo.Description(metadata.getDescription()),
|
||||
authors, metadata.getVersion(), metadata.getGameVersion(),
|
||||
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ public final class LiteModMetadata {
|
||||
LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class);
|
||||
if (metadata == null)
|
||||
throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
|
||||
return new ModInfo(modManager, modFile, metadata.getName(), metadata.getDescription(), metadata.getAuthor(),
|
||||
return new ModInfo(modManager, modFile, metadata.getName(), new ModInfo.Description(metadata.getDescription()), metadata.getAuthor(),
|
||||
metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import java.util.List;
|
||||
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
|
||||
public final class MinecraftInstanceTask<T> extends Task<Void> {
|
||||
public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>> {
|
||||
|
||||
private final File zipFile;
|
||||
private final Charset encoding;
|
||||
@@ -73,6 +73,8 @@ public final class MinecraftInstanceTask<T> extends Task<Void> {
|
||||
});
|
||||
}
|
||||
|
||||
FileUtils.writeText(jsonFile, JsonUtils.GSON.toJson(new ModpackConfiguration<>(manifest, type, name, version, overrides)));
|
||||
ModpackConfiguration<T> configuration = new ModpackConfiguration<>(manifest, type, name, version, overrides);
|
||||
FileUtils.writeText(jsonFile, JsonUtils.GSON.toJson(configuration));
|
||||
setResult(configuration);
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -38,7 +39,7 @@ public final class ModInfo implements Comparable<ModInfo> {
|
||||
|
||||
private Path file;
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final Description description;
|
||||
private final String authors;
|
||||
private final String version;
|
||||
private final String gameVersion;
|
||||
@@ -46,11 +47,11 @@ public final class ModInfo implements Comparable<ModInfo> {
|
||||
private final String fileName;
|
||||
private final BooleanProperty activeProperty;
|
||||
|
||||
public ModInfo(ModManager modManager, File file, String name, String description) {
|
||||
public ModInfo(ModManager modManager, File file, String name, Description description) {
|
||||
this(modManager, file, name, description, "", "", "", "");
|
||||
}
|
||||
|
||||
public ModInfo(ModManager modManager, File file, String name, String description, String authors, String version, String gameVersion, String url) {
|
||||
public ModInfo(ModManager modManager, File file, String name, Description description, String authors, String version, String gameVersion, String url) {
|
||||
this.file = file.toPath();
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
@@ -86,7 +87,7 @@ public final class ModInfo implements Comparable<ModInfo> {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
public Description getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@@ -136,4 +137,52 @@ public final class ModInfo implements Comparable<ModInfo> {
|
||||
public int hashCode() {
|
||||
return Objects.hash(getFileName());
|
||||
}
|
||||
|
||||
public static class Description {
|
||||
private final List<Part> parts;
|
||||
|
||||
public Description(String text) {
|
||||
this.parts = new ArrayList<>();
|
||||
this.parts.add(new Part(text, "black"));
|
||||
}
|
||||
|
||||
public Description(List<Part> parts) {
|
||||
this.parts = parts;
|
||||
}
|
||||
|
||||
public List<Part> getParts() {
|
||||
return parts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Part part : parts) {
|
||||
builder.append(part.text);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static class Part {
|
||||
private final String text;
|
||||
private final String color;
|
||||
|
||||
public Part(String text) {
|
||||
this(text, "");
|
||||
}
|
||||
|
||||
public Part(String text, String color) {
|
||||
this.text = Objects.requireNonNull(text);
|
||||
this.color = Objects.requireNonNull(color);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,14 @@ public final class ModManager {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public GameRepository getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private Path getModsDirectory() {
|
||||
return repository.getRunDirectory(id).toPath().resolve("mods");
|
||||
}
|
||||
@@ -86,7 +94,7 @@ public final class ModManager {
|
||||
default:
|
||||
throw new IllegalArgumentException("File " + modFile + " is not a mod file.");
|
||||
}
|
||||
return new ModInfo(this, modFile, FileUtils.getNameWithoutExtension(modFile), description);
|
||||
return new ModInfo(this, modFile, FileUtils.getNameWithoutExtension(modFile), new ModInfo.Description(description));
|
||||
}
|
||||
|
||||
public void refreshMods() throws IOException {
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ModpackExportInfo {
|
||||
|
||||
private final List<String> whitelist = new ArrayList<>();
|
||||
private String name;
|
||||
private String author;
|
||||
private String version;
|
||||
private String description;
|
||||
private String url;
|
||||
|
||||
private boolean forceUpdate;
|
||||
private boolean packWithLauncher;
|
||||
|
||||
private String fileApi;
|
||||
private int minMemory;
|
||||
private List<Integer> supportedJavaVersions;
|
||||
private String launchArguments;
|
||||
private String javaArguments;
|
||||
|
||||
private String authlibInjectorServer;
|
||||
|
||||
private List<McbbsModpackManifest.Origin> origins = new ArrayList<>();
|
||||
|
||||
public ModpackExportInfo() {}
|
||||
|
||||
public List<String> getWhitelist() {
|
||||
return whitelist;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setWhitelist(List<String> whitelist) {
|
||||
this.whitelist.clear();
|
||||
this.whitelist.addAll(whitelist);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of this modpack.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Author of this modpack.
|
||||
*/
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setAuthor(String author) {
|
||||
this.author = author;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version of this modpack.
|
||||
*/
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setVersion(String version) {
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of this modpack.
|
||||
*
|
||||
* Supports plain HTML text.
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFileApi() {
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setFileApi(String fileApi) {
|
||||
this.fileApi = fileApi;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modpack official introduction webpage link.
|
||||
*/
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setUrl(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isForceUpdate() {
|
||||
return forceUpdate;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setForceUpdate(boolean forceUpdate) {
|
||||
this.forceUpdate = forceUpdate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isPackWithLauncher() {
|
||||
return packWithLauncher;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setPackWithLauncher(boolean packWithLauncher) {
|
||||
this.packWithLauncher = packWithLauncher;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getMinMemory() {
|
||||
return minMemory;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setMinMemory(int minMemory) {
|
||||
this.minMemory = minMemory;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Integer> getSupportedJavaVersions() {
|
||||
return supportedJavaVersions;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setSupportedJavaVersions(List<Integer> supportedJavaVersions) {
|
||||
this.supportedJavaVersions = supportedJavaVersions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getLaunchArguments() {
|
||||
return launchArguments;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setLaunchArguments(String launchArguments) {
|
||||
this.launchArguments = launchArguments;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getJavaArguments() {
|
||||
return javaArguments;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setJavaArguments(String javaArguments) {
|
||||
this.javaArguments = javaArguments;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAuthlibInjectorServer() {
|
||||
return authlibInjectorServer;
|
||||
}
|
||||
|
||||
public ModpackExportInfo setAuthlibInjectorServer(String authlibInjectorServer) {
|
||||
this.authlibInjectorServer = authlibInjectorServer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<McbbsModpackManifest.Origin> getOrigins() {
|
||||
return Collections.unmodifiableList(origins);
|
||||
}
|
||||
|
||||
public ModpackExportInfo setOrigins(List<McbbsModpackManifest.Origin> origins) {
|
||||
this.origins.clear();
|
||||
this.origins.addAll(origins);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModpackExportInfo validate() throws NullPointerException {
|
||||
return this;
|
||||
}
|
||||
|
||||
public static class Options {
|
||||
private boolean requireUrl;
|
||||
private boolean requireForceUpdate;
|
||||
private boolean requireFileApi;
|
||||
private boolean validateFileApi;
|
||||
private boolean requireMinMemory;
|
||||
private boolean requireAuthlibInjectorServer;
|
||||
private boolean requireLaunchArguments;
|
||||
private boolean requireJavaArguments;
|
||||
private boolean requireOrigins;
|
||||
|
||||
public Options() {
|
||||
}
|
||||
|
||||
public boolean isRequireUrl() {
|
||||
return requireUrl;
|
||||
}
|
||||
|
||||
public boolean isRequireForceUpdate() {
|
||||
return requireForceUpdate;
|
||||
}
|
||||
|
||||
public boolean isRequireFileApi() {
|
||||
return requireFileApi;
|
||||
}
|
||||
|
||||
public boolean isValidateFileApi() {
|
||||
return validateFileApi;
|
||||
}
|
||||
|
||||
public boolean isRequireMinMemory() {
|
||||
return requireMinMemory;
|
||||
}
|
||||
|
||||
public boolean isRequireAuthlibInjectorServer() {
|
||||
return requireAuthlibInjectorServer;
|
||||
}
|
||||
|
||||
public boolean isRequireLaunchArguments() {
|
||||
return requireLaunchArguments;
|
||||
}
|
||||
|
||||
public boolean isRequireJavaArguments() {
|
||||
return requireJavaArguments;
|
||||
}
|
||||
|
||||
public boolean isRequireOrigins() {
|
||||
return requireOrigins;
|
||||
}
|
||||
|
||||
public Options requireUrl() {
|
||||
requireUrl = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options requireForceUpdate() {
|
||||
requireForceUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options requireFileApi(boolean optional) {
|
||||
requireFileApi = true;
|
||||
validateFileApi = !optional;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options requireMinMemory() {
|
||||
requireMinMemory = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options requireAuthlibInjectorServer() {
|
||||
requireAuthlibInjectorServer = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options requireLaunchArguments() {
|
||||
requireLaunchArguments = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options requireJavaArguments() {
|
||||
requireJavaArguments = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Options requireOrigins() {
|
||||
requireOrigins = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -27,9 +28,13 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Immutable
|
||||
public class PackMcMeta implements Validation {
|
||||
@@ -55,18 +60,19 @@ public class PackMcMeta implements Validation {
|
||||
throw new JsonParseException("pack cannot be null");
|
||||
}
|
||||
|
||||
@JsonAdapter(PackInfoDeserializer.class)
|
||||
public static class PackInfo {
|
||||
@SerializedName("pack_format")
|
||||
private final int packFormat;
|
||||
|
||||
@SerializedName("description")
|
||||
private final String description;
|
||||
private final ModInfo.Description description;
|
||||
|
||||
public PackInfo() {
|
||||
this(0, "");
|
||||
this(0, new ModInfo.Description(Collections.emptyList()));
|
||||
}
|
||||
|
||||
public PackInfo(int packFormat, String description) {
|
||||
public PackInfo(int packFormat, ModInfo.Description description) {
|
||||
this.packFormat = packFormat;
|
||||
this.description = description;
|
||||
}
|
||||
@@ -75,18 +81,76 @@ public class PackMcMeta implements Validation {
|
||||
return packFormat;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
public ModInfo.Description getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
|
||||
|
||||
private String parseText(JsonElement json) throws JsonParseException {
|
||||
if (json.isJsonPrimitive()) {
|
||||
JsonPrimitive primitive = json.getAsJsonPrimitive();
|
||||
if (primitive.isBoolean()) {
|
||||
return Boolean.toString(primitive.getAsBoolean());
|
||||
} else if (primitive.isNumber()) {
|
||||
return primitive.getAsNumber().toString();
|
||||
} else if (primitive.isString()) {
|
||||
return primitive.getAsString();
|
||||
} else {
|
||||
throw new JsonParseException("pack.mcmeta text not boolean nor number nor string???");
|
||||
}
|
||||
} else if (json.isJsonArray()) {
|
||||
JsonArray arr = json.getAsJsonArray();
|
||||
if (arr.size() == 0) {
|
||||
return "";
|
||||
} else {
|
||||
return parseText(arr.get(0));
|
||||
}
|
||||
} else {
|
||||
throw new JsonParseException("pack.mcmeta text should be a string, a boolean, a number or a list of raw JSON text components");
|
||||
}
|
||||
}
|
||||
|
||||
public ModInfo.Description.Part deserialize(JsonElement json, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json.isJsonPrimitive()) {
|
||||
return new ModInfo.Description.Part(parseText(json));
|
||||
} else if (json.isJsonObject()) {
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
String text = parseText(obj.get("text"));
|
||||
return new ModInfo.Description.Part(text);
|
||||
} else {
|
||||
throw new JsonParseException("pack.mcmeta Raw JSON text should be string or an object");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackInfo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
List<ModInfo.Description.Part> parts = new ArrayList<>();
|
||||
JsonObject packInfo = json.getAsJsonObject();
|
||||
int packFormat = packInfo.get("pack_format").getAsInt();
|
||||
JsonElement description = packInfo.get("description");
|
||||
if (description.isJsonPrimitive()) {
|
||||
parts.add(new ModInfo.Description.Part(parseText(description)));
|
||||
} else if (description.isJsonArray()) {
|
||||
for (JsonElement element : description.getAsJsonArray()) {
|
||||
JsonObject descriptionPart = element.getAsJsonObject();
|
||||
parts.add(new ModInfo.Description.Part(descriptionPart.get("text").getAsString(), descriptionPart.get("color").getAsString()));
|
||||
}
|
||||
} else {
|
||||
throw new JsonParseException("pack.mcmeta::pack::description should be String or array of text objects with text and color fields");
|
||||
}
|
||||
return new PackInfo(packFormat, new ModInfo.Description(parts));
|
||||
}
|
||||
}
|
||||
|
||||
public static ModInfo fromFile(ModManager modManager, File modFile) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
||||
Path mcmod = fs.getPath("pack.mcmeta");
|
||||
if (Files.notExists(mcmod))
|
||||
throw new IOException("File " + modFile + " is not a resource pack.");
|
||||
PackMcMeta metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), PackMcMeta.class);
|
||||
return new ModInfo(modManager, modFile, metadata.pack.description, "", "", "", "", "");
|
||||
return new ModInfo(modManager, modFile, FileUtils.getNameWithoutExtension(modFile), metadata.pack.description, "", "", "", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* 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.mod.mcbbs;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseMetaMod;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
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.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
|
||||
public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
|
||||
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final DefaultGameRepository repository;
|
||||
private final ModManager modManager;
|
||||
private final String version;
|
||||
private final File configurationFile;
|
||||
private ModpackConfiguration<McbbsModpackManifest> configuration;
|
||||
private GetTask dependent;
|
||||
private McbbsModpackManifest manifest;
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
|
||||
private final AtomicBoolean allNameKnown = new AtomicBoolean(true);
|
||||
private final AtomicInteger finished = new AtomicInteger(0);
|
||||
private final AtomicBoolean notFound = new AtomicBoolean(false);
|
||||
|
||||
public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version) {
|
||||
this(dependencyManager, version, null);
|
||||
}
|
||||
|
||||
public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version, ModpackConfiguration<McbbsModpackManifest> configuration) {
|
||||
this.dependency = dependencyManager;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
this.modManager = repository.getModManager(version);
|
||||
this.version = version;
|
||||
this.configurationFile = repository.getModpackConfiguration(version);
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> getFuture(TaskCompletableFuture executor) {
|
||||
return breakable(CompletableFuture.runAsync(wrap(() -> {
|
||||
if (configuration == null) {
|
||||
// Load configuration from disk
|
||||
try {
|
||||
configuration = JsonUtils.fromNonNullJson(FileUtils.readText(configurationFile), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
|
||||
}.getType());
|
||||
} catch (IOException | JsonParseException e) {
|
||||
throw new IOException("Malformed modpack configuration");
|
||||
}
|
||||
}
|
||||
manifest = configuration.getManifest();
|
||||
if (manifest == null) throw new CustomException();
|
||||
})).thenComposeAsync(unused -> {
|
||||
// we first download latest manifest
|
||||
return breakable(CompletableFuture.runAsync(wrap(() -> {
|
||||
if (StringUtils.isBlank(manifest.getFileApi())) {
|
||||
// skip this phase
|
||||
throw new CustomException();
|
||||
}
|
||||
})).thenComposeAsync(wrap(unused1 -> {
|
||||
return executor.one(new GetTask(new URL(manifest.getFileApi() + "/manifest.json")));
|
||||
})).thenComposeAsync(wrap(unused1 -> {
|
||||
McbbsModpackManifest remoteManifest;
|
||||
// We needs to update modpack from online server.
|
||||
try {
|
||||
remoteManifest = JsonUtils.fromNonNullJson(dependent.getResult(), McbbsModpackManifest.class);
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Unable to parse server manifest.json from " + manifest.getFileApi(), e);
|
||||
}
|
||||
|
||||
Path rootPath = repository.getVersionRoot(version).toPath();
|
||||
|
||||
Map<McbbsModpackManifest.File, McbbsModpackManifest.File> localFiles = manifest.getFiles().stream().collect(Collectors.toMap(Function.identity(), Function.identity()));
|
||||
|
||||
// for files in new modpack
|
||||
List<McbbsModpackManifest.File> newFiles = new ArrayList<>(remoteManifest.getFiles().size());
|
||||
List<Task<?>> tasks = new ArrayList<>();
|
||||
for (McbbsModpackManifest.File file : remoteManifest.getFiles()) {
|
||||
Path actualPath = getFilePath(file);
|
||||
McbbsModpackManifest.File oldFile = localFiles.remove(file);
|
||||
boolean download = false;
|
||||
if (oldFile == null) {
|
||||
// If old modpack does not have this entry, download it
|
||||
download = true;
|
||||
} else if (actualPath != null) {
|
||||
if (!Files.exists(actualPath)) {
|
||||
// If both old and new modpacks have this entry, but the file is missing...
|
||||
// Re-download it since network problem may cause file missing
|
||||
download = true;
|
||||
} else if (getFileHash(file) != null) {
|
||||
// If user modified this entry file, we will not replace this file since this modified file is what user expects.
|
||||
// Or we have downloaded latest file in previous completion task, this time we have no need to download it again.
|
||||
String fileHash = encodeHex(digest("SHA-1", actualPath));
|
||||
String oldHash = getFileHash(oldFile);
|
||||
String newHash = getFileHash(file);
|
||||
if (oldHash == null) {
|
||||
// We don't know whether the file is modified or not, just update it.
|
||||
download = true;
|
||||
} else if (!Objects.equals(fileHash, newHash)) {
|
||||
if (file.isForce()) {
|
||||
// this file is not allowed to be modified, required by modpack author.
|
||||
download = true;
|
||||
} else if (Objects.equals(oldHash, fileHash)) {
|
||||
download = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we resolve files with unknown path later.
|
||||
}
|
||||
|
||||
if (download) {
|
||||
tasks.add(downloadFile(remoteManifest, file));
|
||||
}
|
||||
|
||||
newFiles.add(mergeFile(oldFile, file));
|
||||
}
|
||||
|
||||
// If old modpack have this entry, and new modpack deleted it. Delete this file.
|
||||
// for-loop above removes still existing file in localFiles. Remaining elements
|
||||
// are files removed by next modpack version.
|
||||
// Notice that this loop will also remove Curse mods.
|
||||
for (McbbsModpackManifest.File file : localFiles.keySet()) {
|
||||
Path actualPath = getFilePath(file);
|
||||
if (actualPath != null && Files.exists(actualPath))
|
||||
Files.deleteIfExists(actualPath);
|
||||
}
|
||||
|
||||
manifest = remoteManifest.setFiles(newFiles);
|
||||
return executor.all(tasks.stream().filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
})).thenAcceptAsync(wrap(unused1 -> {
|
||||
File manifestFile = repository.getModpackConfiguration(version);
|
||||
FileUtils.writeText(manifestFile, JsonUtils.GSON.toJson(
|
||||
new ModpackConfiguration<>(manifest, this.configuration.getType(), this.manifest.getName(), this.manifest.getVersion(),
|
||||
this.manifest.getFiles().stream()
|
||||
.flatMap(file -> file instanceof McbbsModpackManifest.AddonFile
|
||||
? Stream.of((McbbsModpackManifest.AddonFile) file)
|
||||
: Stream.empty())
|
||||
.map(file -> new ModpackConfiguration.FileInformation(file.getPath(), file.getHash()))
|
||||
.collect(Collectors.toList()))));
|
||||
})));
|
||||
}).thenComposeAsync(unused -> {
|
||||
AtomicBoolean allNameKnown = new AtomicBoolean(true);
|
||||
AtomicInteger finished = new AtomicInteger(0);
|
||||
AtomicBoolean notFound = new AtomicBoolean(false);
|
||||
|
||||
return breakable(CompletableFuture.completedFuture(null)
|
||||
.thenComposeAsync(wrap(unused1 -> {
|
||||
List<Task<?>> dependencies = new ArrayList<>();
|
||||
// Because in China, Curse is too difficult to visit,
|
||||
// if failed, ignore it and retry next time.
|
||||
McbbsModpackManifest newManifest = manifest.setFiles(
|
||||
manifest.getFiles().parallelStream()
|
||||
.map(rawFile -> {
|
||||
updateProgress(finished.incrementAndGet(), manifest.getFiles().size());
|
||||
if (rawFile instanceof McbbsModpackManifest.CurseFile) {
|
||||
McbbsModpackManifest.CurseFile file = (McbbsModpackManifest.CurseFile) rawFile;
|
||||
if (StringUtils.isBlank(file.getFileName())) {
|
||||
try {
|
||||
return file.withFileName(NetworkUtils.detectFileName(file.getUrl()));
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID())));
|
||||
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
|
||||
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
|
||||
} catch (FileNotFoundException fof) {
|
||||
Logging.LOG.log(Level.WARNING, "Could not query cursemeta for deleted mods: " + file.getUrl(), fof);
|
||||
notFound.set(true);
|
||||
return file;
|
||||
} catch (IOException | JsonParseException e2) {
|
||||
try {
|
||||
String result = NetworkUtils.doGet(NetworkUtils.toURL(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID())));
|
||||
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
|
||||
return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
|
||||
} catch (FileNotFoundException fof) {
|
||||
Logging.LOG.log(Level.WARNING, "Could not query forgesvc for deleted mods: " + file.getUrl(), fof);
|
||||
notFound.set(true);
|
||||
return file;
|
||||
} catch (IOException | JsonParseException e3) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e);
|
||||
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e2);
|
||||
Logging.LOG.log(Level.WARNING, "Unable to fetch the file name of URL: " + file.getUrl(), e3);
|
||||
allNameKnown.set(false);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
} else {
|
||||
return rawFile;
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
manifest = newManifest;
|
||||
configuration = configuration.setManifest(newManifest);
|
||||
FileUtils.writeText(configurationFile, JsonUtils.GSON.toJson(configuration));
|
||||
|
||||
for (McbbsModpackManifest.File file : newManifest.getFiles())
|
||||
if (file instanceof McbbsModpackManifest.CurseFile) {
|
||||
McbbsModpackManifest.CurseFile curseFile = (McbbsModpackManifest.CurseFile) file;
|
||||
if (StringUtils.isNotBlank(curseFile.getFileName())) {
|
||||
if (!modManager.hasSimpleMod(curseFile.getFileName())) {
|
||||
FileDownloadTask task = new FileDownloadTask(curseFile.getUrl(), modManager.getSimpleModPath(curseFile.getFileName()).toFile());
|
||||
task.setCacheRepository(dependency.getCacheRepository());
|
||||
task.setCaching(true);
|
||||
dependencies.add(task.withCounter());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!dependencies.isEmpty()) {
|
||||
getProperties().put("total", dependencies.size());
|
||||
}
|
||||
|
||||
return executor.all(dependencies);
|
||||
})).whenComplete(wrap((unused1, ex) -> {
|
||||
// Let this task fail if the curse manifest has not been completed.
|
||||
// But continue other downloads.
|
||||
if (notFound.get())
|
||||
throw new CurseCompletionException(new FileNotFoundException());
|
||||
if (!allNameKnown.get() || ex != null)
|
||||
throw new CurseCompletionException();
|
||||
})));
|
||||
}));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Path getFilePath(McbbsModpackManifest.File file) {
|
||||
if (file instanceof McbbsModpackManifest.AddonFile) {
|
||||
return modManager.getRepository().getRunDirectory(modManager.getVersion()).toPath().resolve(((McbbsModpackManifest.AddonFile) file).getPath());
|
||||
} else if (file instanceof McbbsModpackManifest.CurseFile) {
|
||||
String fileName = ((McbbsModpackManifest.CurseFile) file).getFileName();
|
||||
if (fileName == null) return null;
|
||||
return modManager.getSimpleModPath(fileName);
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private String getFileHash(McbbsModpackManifest.File file) {
|
||||
if (file instanceof McbbsModpackManifest.AddonFile) {
|
||||
return ((McbbsModpackManifest.AddonFile) file).getHash();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<?> downloadFile(McbbsModpackManifest remoteManifest, McbbsModpackManifest.File file) throws IOException {
|
||||
if (file instanceof McbbsModpackManifest.AddonFile) {
|
||||
McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file;
|
||||
return new FileDownloadTask(
|
||||
new URL(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())),
|
||||
modManager.getSimpleModPath(addonFile.getPath()).toFile(),
|
||||
addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null);
|
||||
} else if (file instanceof McbbsModpackManifest.CurseFile) {
|
||||
// we download it later.
|
||||
return null;
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private McbbsModpackManifest.File mergeFile(@Nullable McbbsModpackManifest.File oldFile, @NotNull McbbsModpackManifest.File newFile) {
|
||||
if (newFile instanceof McbbsModpackManifest.AddonFile) {
|
||||
return newFile;
|
||||
} else if (newFile instanceof McbbsModpackManifest.CurseFile) {
|
||||
// Preserves prefetched file names and urls.
|
||||
return oldFile != null ? oldFile : newFile;
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.mod.mcbbs;
|
||||
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
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.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
|
||||
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
|
||||
public class McbbsModpackExportTask extends Task<Void> {
|
||||
private final DefaultGameRepository repository;
|
||||
private final String version;
|
||||
private final ModpackExportInfo info;
|
||||
private final File modpackFile;
|
||||
|
||||
public McbbsModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, File modpackFile) {
|
||||
this.repository = repository;
|
||||
this.version = version;
|
||||
this.info = info.validate();
|
||||
this.modpackFile = modpackFile;
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) modpackFile.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(modpackFile.toPath())) {
|
||||
Path runDirectory = repository.getRunDirectory(version).toPath();
|
||||
List<McbbsModpackManifest.File> files = new ArrayList<>();
|
||||
zip.putDirectory(runDirectory, "overrides", path -> {
|
||||
if (Modpack.acceptFile(path, blackList, info.getWhitelist())) {
|
||||
Path file = runDirectory.resolve(path);
|
||||
if (Files.isRegularFile(file)) {
|
||||
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
||||
files.add(new McbbsModpackManifest.AddonFile(true, relativePath, encodeHex(digest("SHA-1", file))));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version));
|
||||
String gameVersion = GameVersion.minecraftVersion(repository.getVersionJar(version))
|
||||
.orElseThrow(() -> new IOException("Cannot parse the version of " + version));
|
||||
|
||||
List<McbbsModpackManifest.Addon> addons = new ArrayList<>();
|
||||
addons.add(new McbbsModpackManifest.Addon(MINECRAFT.getPatchId(), gameVersion));
|
||||
analyzer.getVersion(FORGE).ifPresent(forgeVersion ->
|
||||
addons.add(new McbbsModpackManifest.Addon(FORGE.getPatchId(), forgeVersion)));
|
||||
analyzer.getVersion(LITELOADER).ifPresent(liteLoaderVersion ->
|
||||
addons.add(new McbbsModpackManifest.Addon(LITELOADER.getPatchId(), liteLoaderVersion)));
|
||||
analyzer.getVersion(OPTIFINE).ifPresent(optifineVersion ->
|
||||
addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
|
||||
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
||||
addons.add(new McbbsModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
|
||||
|
||||
List<Library> libraries = new ArrayList<>();
|
||||
// TODO libraries
|
||||
|
||||
List<McbbsModpackManifest.Origin> origins = new ArrayList<>();
|
||||
// TODO origins
|
||||
|
||||
McbbsModpackManifest.Settings settings = new McbbsModpackManifest.Settings();
|
||||
McbbsModpackManifest.LaunchInfo launchInfo = new McbbsModpackManifest.LaunchInfo(info.getMinMemory(), info.getSupportedJavaVersions(), StringUtils.tokenize(info.getLaunchArguments()), StringUtils.tokenize(info.getJavaArguments()));
|
||||
|
||||
McbbsModpackManifest manifest = new McbbsModpackManifest(McbbsModpackManifest.MANIFEST_TYPE, 1, info.getName(), info.getVersion(), info.getAuthor(), info.getDescription(), info.getFileApi() == null ? null : StringUtils.removeSuffix(info.getFileApi(), "/"), info.getUrl(), info.isForceUpdate(), origins, addons, libraries, files, settings, launchInfo);
|
||||
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "manifest.json");
|
||||
}
|
||||
}
|
||||
|
||||
public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options()
|
||||
.requireFileApi(true)
|
||||
.requireUrl()
|
||||
.requireForceUpdate()
|
||||
.requireMinMemory()
|
||||
.requireAuthlibInjectorServer()
|
||||
.requireJavaArguments()
|
||||
.requireLaunchArguments()
|
||||
.requireOrigins();
|
||||
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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.mod.mcbbs;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class McbbsModpackLocalInstallTask extends Task<Void> {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final File zipFile;
|
||||
private final Modpack modpack;
|
||||
private final McbbsModpackManifest manifest;
|
||||
private final String name;
|
||||
private final boolean update;
|
||||
private final DefaultGameRepository repository;
|
||||
private final MinecraftInstanceTask<McbbsModpackManifest> instanceTask;
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
private final List<Task<?>> dependents = new LinkedList<>();
|
||||
|
||||
public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, File zipFile, Modpack modpack, McbbsModpackManifest manifest, String name) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.zipFile = zipFile;
|
||||
this.modpack = modpack;
|
||||
this.manifest = manifest;
|
||||
this.name = name;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
File run = repository.getRunDirectory(name);
|
||||
|
||||
File json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && !json.exists())
|
||||
throw new IllegalArgumentException("Version " + name + " already exists.");
|
||||
this.update = repository.hasVersion(name);
|
||||
|
||||
|
||||
GameBuilder builder = dependencyManager.gameBuilder().name(name);
|
||||
for (McbbsModpackManifest.Addon addon : manifest.getAddons()) {
|
||||
builder.version(addon.getId(), addon.getVersion());
|
||||
}
|
||||
|
||||
dependents.add(builder.buildAsync());
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed())
|
||||
repository.removeVersionFromDisk(name);
|
||||
});
|
||||
|
||||
ModpackConfiguration<McbbsModpackManifest> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
|
||||
}.getType());
|
||||
|
||||
if (!MODPACK_TYPE.equals(config.getType()))
|
||||
throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version.");
|
||||
}
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/overrides", any -> true, config).withStage("hmcl.modpack"));
|
||||
instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/overrides", manifest, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name));
|
||||
dependents.add(instanceTask.withStage("hmcl.modpack"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Version version = repository.readVersionJson(name);
|
||||
Optional<Version> mcbbsPatch = version.getPatches().stream().filter(patch -> PATCH_NAME.equals(patch.getId())).findFirst();
|
||||
if (!update) {
|
||||
Version patch = new Version(PATCH_NAME).setLibraries(manifest.getLibraries());
|
||||
dependencies.add(repository.saveAsync(version.addPatch(patch)));
|
||||
} else if (mcbbsPatch.isPresent()) {
|
||||
// This mcbbs modpack was installed by HMCL.
|
||||
Version patch = mcbbsPatch.get().setLibraries(manifest.getLibraries());
|
||||
dependencies.add(repository.saveAsync(version.addPatch(patch)));
|
||||
} else {
|
||||
// This mcbbs modpack was installed by other launchers.
|
||||
// TODO: maintain libraries.
|
||||
}
|
||||
|
||||
dependencies.add(new McbbsModpackCompletionTask(dependencyManager, name, instanceTask.getResult()).withStage("hmcl.modpack.download"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStages() {
|
||||
return Stream.concat(
|
||||
dependents.stream().flatMap(task -> task.getStages().stream()),
|
||||
Stream.of("hmcl.modpack")
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static final String PATCH_NAME = "mcbbs";
|
||||
public static final String MODPACK_TYPE = "Mcbbs";
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
/*
|
||||
* 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.mod.mcbbs;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||
|
||||
public class McbbsModpackManifest implements Validation {
|
||||
public static final String MANIFEST_TYPE = "minecraftModpack";
|
||||
|
||||
private final String manifestType;
|
||||
private final int manifestVersion;
|
||||
private final String name;
|
||||
private final String version;
|
||||
private final String author;
|
||||
private final String description;
|
||||
|
||||
@Nullable
|
||||
private final String fileApi;
|
||||
private final String url;
|
||||
private final boolean forceUpdate;
|
||||
@SerializedName("origin")
|
||||
private final List<Origin> origins;
|
||||
private final List<Addon> addons;
|
||||
private final List<Library> libraries;
|
||||
private final List<File> files;
|
||||
private final Settings settings;
|
||||
private final LaunchInfo launchInfo;
|
||||
// sandbox and antiCheating are both not supported.
|
||||
|
||||
public McbbsModpackManifest() {
|
||||
this(MANIFEST_TYPE, 1, "", "", "", "", null, "", false, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), new Settings(), new LaunchInfo());
|
||||
}
|
||||
|
||||
public McbbsModpackManifest(String manifestType, int manifestVersion, String name, String version, String author, String description, @Nullable String fileApi, String url, boolean forceUpdate, List<Origin> origins, List<Addon> addons, List<Library> libraries, List<File> files, Settings settings, LaunchInfo launchInfo) {
|
||||
this.manifestType = manifestType;
|
||||
this.manifestVersion = manifestVersion;
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.author = author;
|
||||
this.description = description;
|
||||
this.fileApi = fileApi;
|
||||
this.url = url;
|
||||
this.forceUpdate = forceUpdate;
|
||||
this.origins = origins;
|
||||
this.addons = addons;
|
||||
this.libraries = libraries;
|
||||
this.files = files;
|
||||
this.settings = settings;
|
||||
this.launchInfo = launchInfo;
|
||||
}
|
||||
|
||||
public String getManifestType() {
|
||||
return manifestType;
|
||||
}
|
||||
|
||||
public int getManifestVersion() {
|
||||
return manifestVersion;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getFileApi() {
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public boolean isForceUpdate() {
|
||||
return forceUpdate;
|
||||
}
|
||||
|
||||
public List<Origin> getOrigins() {
|
||||
return origins;
|
||||
}
|
||||
|
||||
public List<Addon> getAddons() {
|
||||
return addons;
|
||||
}
|
||||
|
||||
public List<Library> getLibraries() {
|
||||
return libraries;
|
||||
}
|
||||
|
||||
public List<File> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
public Settings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
public LaunchInfo getLaunchInfo() {
|
||||
return launchInfo;
|
||||
}
|
||||
|
||||
public McbbsModpackManifest setFiles(List<File> files) {
|
||||
return new McbbsModpackManifest(manifestType, manifestVersion, name, version, author, description, fileApi, url, forceUpdate, origins, addons, libraries, files, settings, launchInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (!MANIFEST_TYPE.equals(manifestType))
|
||||
throw new JsonParseException("McbbsModpackManifest.manifestType must be 'minecraftModpack'");
|
||||
// if (manifestVersion > 1)
|
||||
// throw new JsonParseException("Only supports version 1 of McbbsModpackManifest");
|
||||
if (files == null)
|
||||
throw new JsonParseException("McbbsModpackManifest.files cannot be null");
|
||||
}
|
||||
|
||||
public static final class Origin {
|
||||
private final String type;
|
||||
private final int id;
|
||||
|
||||
public Origin() {
|
||||
this("", 0);
|
||||
}
|
||||
|
||||
public Origin(String type, int id) {
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Addon {
|
||||
private final String id;
|
||||
private final String version;
|
||||
|
||||
public Addon() {
|
||||
this("", "");
|
||||
}
|
||||
|
||||
public Addon(String id, String version) {
|
||||
this.id = id;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Settings {
|
||||
@SerializedName("install_modes")
|
||||
private final boolean installMods;
|
||||
|
||||
@SerializedName("install_resourcepack")
|
||||
private final boolean installResourcepack;
|
||||
|
||||
public Settings() {
|
||||
this(true, true);
|
||||
}
|
||||
|
||||
public Settings(boolean installMods, boolean installResourcepack) {
|
||||
this.installMods = installMods;
|
||||
this.installResourcepack = installResourcepack;
|
||||
}
|
||||
|
||||
public boolean isInstallMods() {
|
||||
return installMods;
|
||||
}
|
||||
|
||||
public boolean isInstallResourcepack() {
|
||||
return installResourcepack;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonType(
|
||||
property = "type",
|
||||
subtypes = {
|
||||
@JsonSubtype(clazz = AddonFile.class, name = "addon"),
|
||||
@JsonSubtype(clazz = CurseFile.class, name = "curse")
|
||||
}
|
||||
)
|
||||
public static abstract class File implements Validation {
|
||||
protected final boolean force;
|
||||
|
||||
public File(boolean force) {
|
||||
this.force = force;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
}
|
||||
|
||||
public boolean isForce() {
|
||||
return force;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class AddonFile extends File {
|
||||
private final String path;
|
||||
private final String hash;
|
||||
|
||||
public AddonFile(boolean force, String path, String hash) {
|
||||
super(force);
|
||||
this.path = Objects.requireNonNull(path);
|
||||
this.hash = Objects.requireNonNull(hash);
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
super.validate();
|
||||
|
||||
Validation.requireNonNull(path, "AddonFile.path cannot be null");
|
||||
Validation.requireNonNull(hash, "AddonFile.hash cannot be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
AddonFile addonFile = (AddonFile) o;
|
||||
return path.equals(addonFile.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(path);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CurseFile extends File {
|
||||
private final int projectID;
|
||||
private final int fileID;
|
||||
private final String fileName;
|
||||
private final String url;
|
||||
|
||||
public CurseFile() {
|
||||
this(false, 0, 0, "", "");
|
||||
}
|
||||
|
||||
public CurseFile(boolean force, int projectID, int fileID, String fileName, String url) {
|
||||
super(force);
|
||||
this.projectID = projectID;
|
||||
this.fileID = fileID;
|
||||
this.fileName = fileName;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public int getProjectID() {
|
||||
return projectID;
|
||||
}
|
||||
|
||||
public int getFileID() {
|
||||
return fileID;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public URL getUrl() {
|
||||
return url == null ? NetworkUtils.toURL("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file")
|
||||
: NetworkUtils.toURL(NetworkUtils.encodeLocation(url));
|
||||
}
|
||||
|
||||
public CurseFile withFileName(String fileName) {
|
||||
return new CurseFile(force, projectID, fileID, fileName, url);
|
||||
}
|
||||
|
||||
public CurseFile withURL(String url) {
|
||||
return new CurseFile(force, projectID, fileID, fileName, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
super.validate();
|
||||
|
||||
if (projectID == 0 || fileID == 0) {
|
||||
throw new JsonParseException("CurseFile.{projectID|fileID} cannot be empty.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CurseFile curseFile = (CurseFile) o;
|
||||
return projectID == curseFile.projectID && fileID == curseFile.fileID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(projectID, fileID);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class LaunchInfo {
|
||||
private final int minMemory;
|
||||
private final List<Integer> supportJava;
|
||||
@SerializedName("launchArgument")
|
||||
private final List<String> launchArguments;
|
||||
@SerializedName("javaArgument")
|
||||
private final List<String> javaArguments;
|
||||
|
||||
public LaunchInfo() {
|
||||
this(0, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public LaunchInfo(int minMemory, List<Integer> supportJava, List<String> launchArguments, List<String> javaArguments) {
|
||||
this.minMemory = minMemory;
|
||||
this.supportJava = supportJava;
|
||||
this.launchArguments = launchArguments;
|
||||
this.javaArguments = javaArguments;
|
||||
}
|
||||
|
||||
public int getMinMemory() {
|
||||
return minMemory;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Integer> getSupportJava() {
|
||||
return supportJava;
|
||||
}
|
||||
|
||||
public List<String> getLaunchArguments() {
|
||||
return Optional.ofNullable(launchArguments).orElseGet(Collections::emptyList);
|
||||
}
|
||||
|
||||
public List<String> getJavaArguments() {
|
||||
return Optional.ofNullable(javaArguments).orElseGet(Collections::emptyList);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ServerInfo {
|
||||
private final String authlibInjectorServer;
|
||||
|
||||
public ServerInfo() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ServerInfo(String authlibInjectorServer) {
|
||||
this.authlibInjectorServer = authlibInjectorServer;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getAuthlibInjectorServer() {
|
||||
return authlibInjectorServer;
|
||||
}
|
||||
}
|
||||
|
||||
public Modpack toModpack(Charset encoding) throws IOException {
|
||||
String gameVersion = addons.stream().filter(x -> MINECRAFT.getPatchId().equals(x.id)).findAny()
|
||||
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
|
||||
return new Modpack(name, author, version, gameVersion, description, encoding, this);
|
||||
}
|
||||
|
||||
public void injectLaunchOptions(LaunchOptions.Builder launchOptions) {
|
||||
launchOptions.getGameArguments().addAll(launchInfo.getLaunchArguments());
|
||||
launchOptions.getJavaArguments().addAll(launchInfo.getJavaArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param zip the CurseForge modpack file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the server-manifest.json is missing or malformed.
|
||||
* @return the manifest.
|
||||
*/
|
||||
public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = CompressingUtils.readTextZipEntry(zip, "manifest.json", encoding);
|
||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
|
||||
return manifest.toModpack(encoding);
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.mod.mcbbs;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class McbbsModpackRemoteInstallTask extends Task<Void> {
|
||||
|
||||
private final String name;
|
||||
private final DefaultDependencyManager dependency;
|
||||
private final DefaultGameRepository repository;
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
private final List<Task<?>> dependents = new LinkedList<>();
|
||||
private final McbbsModpackManifest manifest;
|
||||
|
||||
public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, McbbsModpackManifest manifest, String name) {
|
||||
this.name = name;
|
||||
this.dependency = dependencyManager;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
this.manifest = manifest;
|
||||
|
||||
File json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && !json.exists())
|
||||
throw new IllegalArgumentException("Version " + name + " already exists.");
|
||||
|
||||
GameBuilder builder = dependencyManager.gameBuilder().name(name);
|
||||
for (McbbsModpackManifest.Addon addon : manifest.getAddons()) {
|
||||
builder.version(addon.getId(), addon.getVersion());
|
||||
}
|
||||
|
||||
dependents.add(builder.buildAsync());
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed())
|
||||
repository.removeVersionFromDisk(name);
|
||||
});
|
||||
|
||||
ModpackConfiguration<McbbsModpackManifest> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
|
||||
}.getType());
|
||||
|
||||
if (!MODPACK_TYPE.equals(config.getType()))
|
||||
throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version.");
|
||||
}
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task<?>> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
dependencies.add(new McbbsModpackCompletionTask(dependency, name, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList())));
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "Server";
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -46,7 +47,7 @@ public class MultiMCModpackExportTask extends Task<Void> {
|
||||
private final File output;
|
||||
|
||||
/**
|
||||
* @param output mod pack file.
|
||||
* @param output mod pack file.
|
||||
* @param versionId to locate version.json
|
||||
*/
|
||||
public MultiMCModpackExportTask(DefaultGameRepository repository, String versionId, List<String> whitelist, MultiMCInstanceConfiguration configuration, File output) {
|
||||
@@ -91,4 +92,6 @@ public class MultiMCModpackExportTask extends Task<Void> {
|
||||
zip.putTextFile("", ".packignore");
|
||||
}
|
||||
}
|
||||
|
||||
public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options().requireMinMemory();
|
||||
}
|
||||
+19
-1
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.mod.server;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
@@ -46,6 +47,7 @@ import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
|
||||
public class ServerModpackCompletionTask extends Task<Void> {
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository repository;
|
||||
private final String version;
|
||||
private ModpackConfiguration<ServerModpackManifest> manifest;
|
||||
@@ -58,6 +60,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
}
|
||||
|
||||
public ServerModpackCompletionTask(DefaultDependencyManager dependencyManager, String version, ModpackConfiguration<ServerModpackManifest> manifest) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
this.version = version;
|
||||
|
||||
@@ -69,7 +72,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
}.getType());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to read CurseForge modpack manifest.json", e);
|
||||
Logging.LOG.log(Level.WARNING, "Unable to read Server modpack manifest.json", e);
|
||||
}
|
||||
} else {
|
||||
this.manifest = manifest;
|
||||
@@ -97,6 +100,10 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
return dependent == null ? Collections.emptySet() : Collections.singleton(dependent);
|
||||
}
|
||||
|
||||
private Map<String, String> toMap(Collection<ServerModpackManifest.Addon> addons) {
|
||||
return addons.stream().collect(Collectors.toMap(ServerModpackManifest.Addon::getId, ServerModpackManifest.Addon::getVersion));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
|
||||
@@ -107,6 +114,17 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
Map<String, String> oldAddons = toMap(manifest.getManifest().getAddons());
|
||||
Map<String, String> newAddons = toMap(remoteManifest.getAddons());
|
||||
if (!Objects.equals(oldAddons, newAddons)) {
|
||||
GameBuilder builder = dependencyManager.gameBuilder().name(version);
|
||||
for (ServerModpackManifest.Addon addon : remoteManifest.getAddons()) {
|
||||
builder.version(addon.getId(), addon.getVersion());
|
||||
}
|
||||
|
||||
dependencies.add(builder.buildAsync());
|
||||
}
|
||||
|
||||
Path rootPath = repository.getVersionRoot(version).toPath();
|
||||
Map<String, ModpackConfiguration.FileInformation> files = manifest.getManifest().getFiles().stream()
|
||||
.collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath,
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.game.GameVersion;
|
||||
import org.jackhuang.hmcl.mod.ModAdviser;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -43,27 +44,17 @@ import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
public class ServerModpackExportTask extends Task<Void> {
|
||||
private final DefaultGameRepository repository;
|
||||
private final String versionId;
|
||||
private final List<String> whitelist;
|
||||
private final File output;
|
||||
private final String modpackName;
|
||||
private final String modpackAuthor;
|
||||
private final String modpackVersion;
|
||||
private final String modpackDescription;
|
||||
private final String modpackFileApi;
|
||||
private final ModpackExportInfo exportInfo;
|
||||
private final File modpackFile;
|
||||
|
||||
public ServerModpackExportTask(DefaultGameRepository repository, String versionId, List<String> whitelist, String modpackName, String modpackAuthor, String modpackVersion, String modpackDescription, String modpackFileApi, File output) {
|
||||
public ServerModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo exportInfo, File modpackFile) {
|
||||
this.repository = repository;
|
||||
this.versionId = versionId;
|
||||
this.whitelist = whitelist;
|
||||
this.output = output;
|
||||
this.modpackName = modpackName;
|
||||
this.modpackAuthor = modpackAuthor;
|
||||
this.modpackVersion = modpackVersion;
|
||||
this.modpackDescription = modpackDescription;
|
||||
this.modpackFileApi = modpackFileApi;
|
||||
this.versionId = version;
|
||||
this.exportInfo = exportInfo.validate();
|
||||
this.modpackFile = modpackFile;
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) output.delete();
|
||||
if (event.isFailed()) modpackFile.delete();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,11 +64,11 @@ public class ServerModpackExportTask extends Task<Void> {
|
||||
blackList.add(versionId + ".jar");
|
||||
blackList.add(versionId + ".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())) {
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
Path runDirectory = repository.getRunDirectory(versionId).toPath();
|
||||
List<ModpackConfiguration.FileInformation> files = new ArrayList<>();
|
||||
zip.putDirectory(runDirectory, "overrides", path -> {
|
||||
if (Modpack.acceptFile(path, blackList, whitelist)) {
|
||||
if (Modpack.acceptFile(path, blackList, exportInfo.getWhitelist())) {
|
||||
Path file = runDirectory.resolve(path);
|
||||
if (Files.isRegularFile(file)) {
|
||||
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
||||
@@ -102,8 +93,11 @@ public class ServerModpackExportTask extends Task<Void> {
|
||||
addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
|
||||
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
||||
addons.add(new ServerModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
|
||||
ServerModpackManifest manifest = new ServerModpackManifest(modpackName, modpackAuthor, modpackVersion, modpackDescription, StringUtils.removeSuffix(modpackFileApi, "/"), files, addons);
|
||||
ServerModpackManifest manifest = new ServerModpackManifest(exportInfo.getName(), exportInfo.getAuthor(), exportInfo.getVersion(), exportInfo.getDescription(), StringUtils.removeSuffix(exportInfo.getFileApi(), "/"), files, addons);
|
||||
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json");
|
||||
}
|
||||
}
|
||||
|
||||
public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options()
|
||||
.requireFileApi(false);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -100,7 +99,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
future.cancel(true);
|
||||
}
|
||||
|
||||
private CompletableFuture<Exception> executeTasks(Task<?> parentTask, Collection<Task<?>> tasks) {
|
||||
private CompletableFuture<?> executeTasksExceptionally(Task<?> parentTask, Collection<Task<?>> tasks) {
|
||||
if (tasks == null || tasks.isEmpty())
|
||||
return CompletableFuture.completedFuture(null);
|
||||
|
||||
@@ -112,10 +111,14 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
return CompletableFuture.runAsync(this::checkCancellation);
|
||||
|
||||
return CompletableFuture.allOf(tasks.stream()
|
||||
.map(task -> ((CompletableFuture) CompletableFuture.completedFuture(null))
|
||||
.thenComposeAsync(unused2 -> (CompletableFuture) executeTask(parentTask, task))
|
||||
).toArray(CompletableFuture[]::new));
|
||||
})
|
||||
.map(task -> CompletableFuture.completedFuture(null)
|
||||
.thenComposeAsync(unused2 -> executeTask(parentTask, task))
|
||||
).toArray(CompletableFuture<?>[]::new));
|
||||
});
|
||||
}
|
||||
|
||||
private CompletableFuture<Exception> executeTasks(Task<?> parentTask, Collection<Task<?>> tasks) {
|
||||
return executeTasksExceptionally(parentTask, tasks)
|
||||
.thenApplyAsync(unused -> (Exception) null)
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolved = resolveException(throwable);
|
||||
@@ -128,48 +131,62 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<?> executeTask(Task<?> parentTask, Task<T> task) {
|
||||
private <T> CompletableFuture<T> executeCompletableFutureTask(Task<?> parentTask, CompletableFutureTask<T> task) {
|
||||
checkCancellation();
|
||||
|
||||
if (task instanceof CompletableFutureTask) {
|
||||
return CompletableFuture.completedFuture(null)
|
||||
.thenComposeAsync(unused -> {
|
||||
checkCancellation();
|
||||
return CompletableFuture.completedFuture(null)
|
||||
.thenComposeAsync(unused -> {
|
||||
checkCancellation();
|
||||
|
||||
task.setCancelled(this::isCancelled);
|
||||
task.setState(Task.TaskState.READY);
|
||||
if (parentTask != null && task.getStage() == null)
|
||||
task.setStage(parentTask.getStage());
|
||||
task.setCancelled(this::isCancelled);
|
||||
task.setState(Task.TaskState.READY);
|
||||
if (parentTask != null && task.getStage() == null)
|
||||
task.setStage(parentTask.getStage());
|
||||
|
||||
if (task.getSignificance().shouldLog())
|
||||
Logging.LOG.log(Level.FINE, "Executing task: " + task.getName());
|
||||
if (task.getSignificance().shouldLog())
|
||||
Logging.LOG.log(Level.FINE, "Executing task: " + task.getName());
|
||||
|
||||
taskListeners.forEach(it -> it.onRunning(task));
|
||||
taskListeners.forEach(it -> it.onReady(task));
|
||||
taskListeners.forEach(it -> it.onReady(task));
|
||||
|
||||
return ((CompletableFutureTask<T>) task).getCompletableFuture();
|
||||
}).whenComplete((result, throwable) -> {
|
||||
Throwable resolved = resolveException(throwable);
|
||||
task.setResult(result);
|
||||
task.setState(Task.TaskState.EXECUTED);
|
||||
if (isCancelled() || resolved instanceof InterruptedException || resolved instanceof CancellationException) {
|
||||
return task.getFuture(new TaskCompletableFuture() {
|
||||
@Override
|
||||
public <T2> CompletableFuture<T2> one(Task<T2> subtask) {
|
||||
return executeTask(task, subtask);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<?> all(Collection<Task<?>> tasks) {
|
||||
return executeTasksExceptionally(task, tasks);
|
||||
}
|
||||
});
|
||||
})
|
||||
.thenApplyAsync(result -> {
|
||||
checkCancellation();
|
||||
|
||||
if (task.getSignificance().shouldLog()) {
|
||||
Logging.LOG.log(Level.FINER, "Task finished: " + task.getName());
|
||||
}
|
||||
|
||||
task.setResult(result);
|
||||
task.onDone().fireEvent(new TaskEvent(this, task, false));
|
||||
taskListeners.forEach(it -> it.onFinished(task));
|
||||
|
||||
task.setState(Task.TaskState.SUCCEEDED);
|
||||
|
||||
return result;
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolved = resolveException(throwable);
|
||||
if (resolved instanceof Exception) {
|
||||
Exception e = (Exception) resolved;
|
||||
if (e instanceof InterruptedException || e instanceof CancellationException) {
|
||||
task.setException(null);
|
||||
if (task.getSignificance().shouldLog()) {
|
||||
Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName());
|
||||
}
|
||||
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
||||
taskListeners.forEach(it -> it.onFailed(task, resolved));
|
||||
} else if (resolved == null) {
|
||||
if (task.getSignificance().shouldLog()) {
|
||||
Logging.LOG.log(Level.FINER, "Task finished: " + task.getName());
|
||||
}
|
||||
|
||||
task.onDone().fireEvent(new TaskEvent(this, task, false));
|
||||
taskListeners.forEach(it -> it.onFinished(task));
|
||||
|
||||
task.setState(Task.TaskState.SUCCEEDED);
|
||||
} else if (resolved instanceof Exception) {
|
||||
Exception e = (Exception) resolved;
|
||||
taskListeners.forEach(it -> it.onFailed(task, e));
|
||||
} else {
|
||||
task.setException(e);
|
||||
exception = e;
|
||||
if (task.getSignificance().shouldLog()) {
|
||||
@@ -177,11 +194,17 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
}
|
||||
task.onDone().fireEvent(new TaskEvent(this, task, true));
|
||||
taskListeners.forEach(it -> it.onFailed(task, e));
|
||||
} else {
|
||||
throw new CompletionException(resolved); // rethrow error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
task.setState(Task.TaskState.FAILED);
|
||||
}
|
||||
|
||||
throw new CompletionException(resolved); // rethrow error
|
||||
});
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> executeNormalTask(Task<?> parentTask, Task<T> task) {
|
||||
checkCancellation();
|
||||
|
||||
return CompletableFuture.completedFuture(null)
|
||||
.thenComposeAsync(unused -> {
|
||||
@@ -238,7 +261,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
return CompletableFuture.completedFuture(dependenciesException);
|
||||
}
|
||||
})
|
||||
.thenAcceptAsync(dependenciesException -> {
|
||||
.thenApplyAsync(dependenciesException -> {
|
||||
boolean isDependenciesSucceeded = dependenciesException == null;
|
||||
|
||||
if (!isDependenciesSucceeded && task.isRelyingOnDependencies()) {
|
||||
@@ -257,6 +280,8 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
taskListeners.forEach(it -> it.onFinished(task));
|
||||
|
||||
task.setState(Task.TaskState.SUCCEEDED);
|
||||
|
||||
return task.getResult();
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolved = resolveException(throwable);
|
||||
@@ -286,14 +311,22 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
private static Throwable resolveException(@Nullable Throwable e) {
|
||||
private <T> CompletableFuture<T> executeTask(Task<?> parentTask, Task<T> task) {
|
||||
if (task instanceof CompletableFutureTask<?>) {
|
||||
return executeCompletableFutureTask(parentTask, (CompletableFutureTask<T>) task);
|
||||
} else {
|
||||
return executeNormalTask(parentTask, task);
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable resolveException(Throwable e) {
|
||||
if (e instanceof ExecutionException || e instanceof CompletionException)
|
||||
return resolveException(e.getCause());
|
||||
else
|
||||
return e;
|
||||
}
|
||||
|
||||
public static void rethrow(Throwable e) {
|
||||
private static void rethrow(Throwable e) {
|
||||
if (e == null)
|
||||
return;
|
||||
if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable
|
||||
@@ -305,7 +338,7 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
public static Runnable wrap(ExceptionalRunnable<?> runnable) {
|
||||
private static Runnable wrap(ExceptionalRunnable<?> runnable) {
|
||||
return () -> {
|
||||
try {
|
||||
runnable.run();
|
||||
|
||||
@@ -1,30 +1,92 @@
|
||||
/*
|
||||
* 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.task;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalBiConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class CompletableFutureTask<T> extends Task<T> {
|
||||
|
||||
public abstract CompletableFuture<T> getCompletableFuture();
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(getCompletableFuture().get());
|
||||
}
|
||||
|
||||
public abstract CompletableFuture<T> getFuture(TaskCompletableFuture executor);
|
||||
|
||||
protected static Runnable wrap(ExceptionalRunnable<?> runnable) {
|
||||
return () -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception e) {
|
||||
rethrow(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected static <T, R> Function<T, R> wrap(ExceptionalFunction<T, R, ?> fn) {
|
||||
return t -> {
|
||||
try {
|
||||
return fn.apply(t);
|
||||
} catch (Exception e) {
|
||||
rethrow(e);
|
||||
throw new InternalError("Unreachable code");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected static <T> Consumer<T> wrap(ExceptionalConsumer<T, ?> fn) {
|
||||
return t -> {
|
||||
try {
|
||||
fn.accept(t);
|
||||
} catch (Exception e) {
|
||||
rethrow(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected static <T, E> BiConsumer<T, E> wrap(ExceptionalBiConsumer<T, E, ?> fn) {
|
||||
return (t, e) -> {
|
||||
try {
|
||||
fn.accept(t, e);
|
||||
} catch (Exception ex) {
|
||||
rethrow(ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected static void rethrow(Throwable e) {
|
||||
if (e == null)
|
||||
return;
|
||||
if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable
|
||||
rethrow(e.getCause());
|
||||
} else if (e instanceof RuntimeException) {
|
||||
throw (RuntimeException) e;
|
||||
} else {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected static Throwable resolveException(Throwable e) {
|
||||
if (e instanceof ExecutionException || e instanceof CompletionException)
|
||||
return resolveException(e.getCause());
|
||||
else
|
||||
return e;
|
||||
}
|
||||
|
||||
public static class CustomException extends RuntimeException {}
|
||||
|
||||
protected static CompletableFuture<Void> breakable(CompletableFuture<?> future) {
|
||||
return future.thenApplyAsync(unused1 -> (Void) null).exceptionally(throwable -> {
|
||||
if (resolveException(throwable) instanceof CustomException) return null;
|
||||
else throw new CompletionException(throwable);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,732 +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.task;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class DownloadManager {
|
||||
|
||||
public static class DownloadTaskStateBuilder {
|
||||
private List<URL> urls;
|
||||
private Path file;
|
||||
private int retry = 3;
|
||||
private int initialParts = 1;
|
||||
|
||||
/**
|
||||
* Set the url of remote file to be downloaded.
|
||||
* @param url url of the remote file to be downloaded
|
||||
* @return this
|
||||
*/
|
||||
public DownloadTaskStateBuilder setUrl(URL url) {
|
||||
this.urls = Collections.singletonList(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set urls of the remote file to be downloaded, will be attempted in order.
|
||||
* @param urls urls of remote files to be downloaded
|
||||
* @return this
|
||||
*/
|
||||
public DownloadTaskStateBuilder setUrls(List<URL> urls) {
|
||||
this.urls = urls;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set location to save remote file.
|
||||
* @param file location to save the remote file.
|
||||
* @return this
|
||||
*/
|
||||
public DownloadTaskStateBuilder setFile(Path file) {
|
||||
this.file = file;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set location to save remote file.
|
||||
* @param file location to save the remote file.
|
||||
* @return this
|
||||
*/
|
||||
public DownloadTaskStateBuilder setFile(File file) {
|
||||
this.file = file.toPath();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set retry times of one url.
|
||||
* @param retry retry times of one url.
|
||||
* @return this
|
||||
*/
|
||||
public DownloadTaskStateBuilder setRetry(int retry) {
|
||||
this.retry = retry;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the remote file into multiple parts, and download in different
|
||||
* threads.
|
||||
*
|
||||
* @param initialParts number of threads to download the file.
|
||||
* @return this
|
||||
*/
|
||||
public DownloadTaskStateBuilder setInitialParts(int initialParts) {
|
||||
this.initialParts = initialParts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DownloadTaskState build() {
|
||||
if (file == null) {
|
||||
return DownloadTaskState.newWithLengthUnknown(urls, null, retry, initialParts);
|
||||
}
|
||||
|
||||
Path downloadingFile = file.resolveSibling(FileUtils.getName(file) + ".download");
|
||||
Path stateFile = file.resolveSibling(FileUtils.getName(file) + ".status");
|
||||
DownloadState state = null;
|
||||
if (Files.exists(downloadingFile) && Files.exists(stateFile)) {
|
||||
// Resume downloading from state
|
||||
try {
|
||||
String status = FileUtils.readText(stateFile);
|
||||
state = JsonUtils.fromNonNullJson(status, DownloadState.class);
|
||||
} catch (JsonParseException | IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse download state file", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (state == null || !urls.equals(state.urls)) {
|
||||
return DownloadTaskState.newWithLengthUnknown(urls, file, retry, initialParts);
|
||||
} else {
|
||||
return new DownloadTaskState(state, file, retry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static class SafeRegion implements AutoCloseable {
|
||||
final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
void begin() {
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
void end() {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Downloader<T> {
|
||||
|
||||
/**
|
||||
* do something before connection.
|
||||
*
|
||||
* @param segment segment
|
||||
* @param url currently ready URL
|
||||
*/
|
||||
protected void onBeforeConnection(DownloadSegment segment, URL url) {}
|
||||
|
||||
/**
|
||||
* Setup downloading environment, creates files, etc.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
protected abstract void onStart() throws IOException;
|
||||
|
||||
protected abstract void onContentLengthChanged(long contentLength) throws IOException;
|
||||
|
||||
/**
|
||||
* Accept recently downloaded data segment.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
protected abstract void write(long pos, byte[] buffer, int offset, int len) throws IOException;
|
||||
|
||||
/**
|
||||
* Verify if we should check etag, or even it is already cached,
|
||||
* and make use of it.
|
||||
*
|
||||
* @return whether we should check etag, or stop downloading if cached.
|
||||
*/
|
||||
protected abstract DownloadTask.EnumCheckETag shouldCheckETag();
|
||||
|
||||
/**
|
||||
* Make cached file as result of downloader.
|
||||
*
|
||||
* This method can fails, if failed, downloading should continue.
|
||||
*
|
||||
* @param cachedFile verified cached file
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
protected abstract T finishWithCachedResult(Path cachedFile) throws IOException;
|
||||
|
||||
/**
|
||||
* Inform downloader that downloading finished.
|
||||
*
|
||||
* Check state.isFinished(), and do necessary cleaning if downloading failed.
|
||||
*
|
||||
* @return result of downloader.
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
protected abstract T finish() throws IOException;
|
||||
|
||||
}
|
||||
|
||||
public static abstract class DownloadTask<T> extends CompletableFutureTask<T> {
|
||||
protected final DownloadTaskState state;
|
||||
protected boolean caching = false;
|
||||
private boolean doFinish = true;
|
||||
protected CacheRepository repository = CacheRepository.getInstance();
|
||||
private final CompletableFuture<T> future = new CompletableFuture<>();
|
||||
private EnumCheckETag checkETag;
|
||||
private Downloader<T> downloader;
|
||||
|
||||
public DownloadTask(DownloadTaskState state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public final void setCaching(boolean caching) {
|
||||
this.caching = caching;
|
||||
}
|
||||
|
||||
public final void setCacheRepository(CacheRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public final DownloadTaskState getDownloadState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
protected abstract Downloader<T> createDownloader();
|
||||
|
||||
protected final EnumCheckETag getCheckETag() { return checkETag; }
|
||||
|
||||
protected synchronized final void setContentLength(int contentLength) throws IOException {
|
||||
if (state.setContentLength(contentLength)) {
|
||||
downloader.onContentLengthChanged(contentLength);
|
||||
}
|
||||
}
|
||||
|
||||
protected synchronized final void finishWithCachedResult(Path cachedFile) throws IOException {
|
||||
setResult(downloader.finishWithCachedResult(cachedFile));
|
||||
|
||||
state.finished = true;
|
||||
doFinish = false;
|
||||
future.complete(null);
|
||||
}
|
||||
|
||||
public synchronized final void onSegmentFinished(DownloadSegment finishedSegment) {
|
||||
assert(state.segments.contains(finishedSegment));
|
||||
finishedSegment.finished = true;
|
||||
|
||||
int max = 0;
|
||||
|
||||
for (DownloadSegment segment : state.segments) {
|
||||
if (segment.endPosition <= max) {
|
||||
segment.setDownloaded();
|
||||
}
|
||||
if (!segment.isFinished())
|
||||
return;
|
||||
max = Math.max(max, segment.currentPosition);
|
||||
}
|
||||
|
||||
// All segments have finished downloading.
|
||||
state.finished = true;
|
||||
future.complete(null);
|
||||
}
|
||||
|
||||
public synchronized final void onSegmentFailed(DownloadSegment failedSegment, Throwable throwable) {
|
||||
assert(state.segments.contains(failedSegment));
|
||||
failedSegment.finished = true;
|
||||
|
||||
// All segments have finished downloading.
|
||||
state.finished = true;
|
||||
future.completeExceptionally(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final CompletableFuture<T> getCompletableFuture() {
|
||||
return CompletableFuture.runAsync(AsyncTaskExecutor.wrap(() -> {
|
||||
downloader = createDownloader();
|
||||
checkETag = downloader.shouldCheckETag();
|
||||
|
||||
downloader.onStart();
|
||||
|
||||
for (DownloadSegment segment : state.segments)
|
||||
segment.download(this, downloader);
|
||||
}))
|
||||
.thenCompose(unused -> future)
|
||||
.whenCompleteAsync((unused, exception) -> {
|
||||
if (doFinish) {
|
||||
try {
|
||||
setResult(downloader.finish());
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}
|
||||
AsyncTaskExecutor.rethrow(exception);
|
||||
})
|
||||
.thenApplyAsync(unused -> getResult());
|
||||
}
|
||||
|
||||
protected enum EnumCheckETag {
|
||||
CHECK_E_TAG,
|
||||
NOT_CHECK_E_TAG,
|
||||
CACHED
|
||||
}
|
||||
}
|
||||
|
||||
protected static final class DownloadTaskState {
|
||||
private final List<URL> urls;
|
||||
@Nullable
|
||||
private final Path file;
|
||||
private final List<DownloadSegment> segments;
|
||||
private URL fastestUrl;
|
||||
private boolean segmentSupported = false;
|
||||
private final int retry;
|
||||
private int retryUrl = 0;
|
||||
private boolean cancelled = false;
|
||||
private boolean finished = false;
|
||||
private int contentLength;
|
||||
private final int initialParts;
|
||||
private boolean waitingForContentLength;
|
||||
|
||||
private final SafeRegion connectionCheckRegion = new SafeRegion();
|
||||
private final SafeRegion writeRegion = new SafeRegion();
|
||||
|
||||
DownloadTaskState(DownloadState state, @NotNull Path file, int retry) {
|
||||
this.urls = new ArrayList<>(state.urls);
|
||||
this.file = file;
|
||||
this.retry = retry;
|
||||
this.segments = new ArrayList<>(state.segments);
|
||||
this.contentLength = state.getContentLength();
|
||||
this.initialParts = state.getSegments().size();
|
||||
this.waitingForContentLength = false;
|
||||
}
|
||||
|
||||
DownloadTaskState(List<URL> urls, @Nullable Path file, int retry, int contentLength, int initialParts) {
|
||||
if (urls == null || urls.size() == 0) {
|
||||
throw new IllegalArgumentException("DownloadTaskState requires at least one url candidate");
|
||||
}
|
||||
|
||||
this.urls = new ArrayList<>(urls);
|
||||
this.file = file;
|
||||
this.retry = retry;
|
||||
this.initialParts = initialParts;
|
||||
this.segments = new ArrayList<>(initialParts);
|
||||
int partLength = contentLength / initialParts;
|
||||
for (int i = 0; i < initialParts; i++) {
|
||||
int begin = partLength * i;
|
||||
int end = Math.min((partLength + 1) * i, contentLength);
|
||||
segments.add(new DownloadSegment(i, begin, end, 0));
|
||||
}
|
||||
this.waitingForContentLength = contentLength == 0;
|
||||
}
|
||||
|
||||
public static DownloadTaskState newWithLengthUnknown(List<URL> urls, Path file, int retry, int initialParts) {
|
||||
return new DownloadTaskState(urls, file, retry, 0, initialParts);
|
||||
}
|
||||
|
||||
public synchronized List<URL> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public Path getDownloadingFile() {
|
||||
return file == null ? null : file.resolveSibling(FileUtils.getName(file) + ".download");
|
||||
}
|
||||
|
||||
public Path getStateFile() {
|
||||
return file == null ? null : file.resolveSibling(FileUtils.getName(file) + ".status");
|
||||
}
|
||||
|
||||
public List<DownloadSegment> getSegments() {
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out actually content length
|
||||
* @param contentLength new content length
|
||||
* @return if content length changed
|
||||
*/
|
||||
protected synchronized boolean setContentLength(int contentLength) {
|
||||
if (!waitingForContentLength) {
|
||||
throw new IllegalStateException("ContentLength already set");
|
||||
}
|
||||
|
||||
if (this.contentLength == contentLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
waitingForContentLength = false;
|
||||
|
||||
this.contentLength = contentLength;
|
||||
if (contentLength < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int partLength = contentLength / initialParts;
|
||||
for (int i = 0; i < segments.size(); i++) {
|
||||
int begin = partLength * i;
|
||||
int end = Math.min(partLength * (i + 1), contentLength);
|
||||
segments.get(i).setDownloadRange(begin, end);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized int getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
public boolean isWaitingForContentLength() {
|
||||
return waitingForContentLength;
|
||||
}
|
||||
|
||||
public int getRetry() {
|
||||
return retry;
|
||||
}
|
||||
|
||||
public synchronized boolean isSegmentSupported() {
|
||||
return segmentSupported;
|
||||
}
|
||||
|
||||
public synchronized void setSegmentSupported(boolean segmentSupported) {
|
||||
this.segmentSupported = segmentSupported;
|
||||
}
|
||||
|
||||
public synchronized URL getFirstUrl() {
|
||||
return urls.get(0);
|
||||
}
|
||||
|
||||
public synchronized boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
protected synchronized void setFinished(boolean finished) {
|
||||
this.finished = finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* Next url for download runnable to retry.
|
||||
*
|
||||
* If some download runnable fails to connect to url, it will call this method
|
||||
* to acquire next url for retry. Making all download runnable try different
|
||||
* candidates concurrently to speed up finding fastest download source.
|
||||
*
|
||||
* If all URLs are tried and tested definitely negative for downloading,
|
||||
* returns null.
|
||||
*
|
||||
* @return next url to retry
|
||||
*/
|
||||
@Nullable
|
||||
public synchronized URL getNextUrlToRetry() {
|
||||
if (retryUrl < 0 || retryUrl >= urls.size()) return null;
|
||||
URL url = urls.get(retryUrl);
|
||||
retryUrl = (retryUrl + 1) % urls.size();
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* One candidate that is accessible and best network connection qualified.
|
||||
*
|
||||
* When some download runnable have started downloading, DownloadManager will
|
||||
* monitor download speed and make failed download runnable connect to the
|
||||
* fastest url directly without retry.
|
||||
*
|
||||
* In some times, the fastest url may be the first url suceeded in connection.
|
||||
*
|
||||
* @return fastest url, null if no url have successfully connected yet.
|
||||
*/
|
||||
public synchronized URL getFastestUrl() {
|
||||
return fastestUrl;
|
||||
}
|
||||
|
||||
public synchronized void setFastestUrl(URL fastestUrl) {
|
||||
if (this.fastestUrl == null)
|
||||
this.fastestUrl = fastestUrl;
|
||||
}
|
||||
|
||||
public synchronized void cancel() {
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
public synchronized boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
public SafeRegion checkingConnection() {
|
||||
connectionCheckRegion.begin();
|
||||
return connectionCheckRegion;
|
||||
}
|
||||
|
||||
public SafeRegion writing() {
|
||||
writeRegion.begin();
|
||||
return writeRegion;
|
||||
}
|
||||
|
||||
public synchronized void forbidURL(URL url) {
|
||||
int index;
|
||||
while ((index = urls.indexOf(url)) != -1) {
|
||||
if (retryUrl >= index) {
|
||||
retryUrl--;
|
||||
}
|
||||
urls.remove(index);
|
||||
}
|
||||
|
||||
if (url.equals(fastestUrl)) {
|
||||
fastestUrl = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static final class DownloadState {
|
||||
private final List<URL> urls;
|
||||
private final List<DownloadSegment> segments;
|
||||
private final int contentLength;
|
||||
|
||||
/**
|
||||
* Constructor for Gson
|
||||
*/
|
||||
public DownloadState() {
|
||||
this(Collections.emptyList(), Collections.emptyList(), 0);
|
||||
}
|
||||
|
||||
public DownloadState(List<URL> urls, List<DownloadSegment> segments, int contentLength) {
|
||||
this.urls = urls;
|
||||
this.segments = segments;
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
public List<URL> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
public List<DownloadSegment> getSegments() {
|
||||
return segments;
|
||||
}
|
||||
|
||||
public int getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
}
|
||||
|
||||
protected static final class DownloadSegment {
|
||||
private int index;
|
||||
private int startPosition;
|
||||
private int endPosition;
|
||||
private int currentPosition;
|
||||
private boolean finished;
|
||||
private URLConnection connection;
|
||||
private Future<?> future;
|
||||
|
||||
/**
|
||||
* Constructor for Gson
|
||||
*/
|
||||
public DownloadSegment() {
|
||||
this(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
public DownloadSegment(int index, int startPosition, int endPosition, int currentPosition) {
|
||||
if (startPosition > endPosition) {
|
||||
throw new IllegalArgumentException("Illegal download state: start " + startPosition + ", end " + endPosition + ", current " + currentPosition);
|
||||
}
|
||||
this.index = index;
|
||||
this.startPosition = startPosition;
|
||||
this.endPosition = endPosition;
|
||||
this.currentPosition = currentPosition;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public int getStartPosition() {
|
||||
return startPosition;
|
||||
}
|
||||
|
||||
public int getEndPosition() {
|
||||
return endPosition;
|
||||
}
|
||||
|
||||
public void setDownloadRange(int start, int end) {
|
||||
this.startPosition = start;
|
||||
this.endPosition = end;
|
||||
this.currentPosition = start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current downloaded position
|
||||
*
|
||||
* CurrentPosition may be less than startPosition or larger than endPosition
|
||||
* when segment unsupported.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getCurrentPosition() {
|
||||
return currentPosition;
|
||||
}
|
||||
|
||||
public void setDownloaded() {
|
||||
this.currentPosition = endPosition - startPosition;
|
||||
}
|
||||
|
||||
public void setCurrentPosition(int currentPosition) {
|
||||
this.currentPosition = currentPosition;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return endPosition - startPosition;
|
||||
}
|
||||
|
||||
public URLConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
protected void setConnection(URLConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished || currentPosition >= endPosition;
|
||||
}
|
||||
|
||||
public Future<?> download(DownloadTask<?> task, Downloader<?> downloader) {
|
||||
if (future == null) {
|
||||
future = DownloadManager.download().submit(new DownloadSegmentTask(task, downloader, this));
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DownloadSegment{" +
|
||||
"startPosition=" + startPosition +
|
||||
", endPosition=" + endPosition +
|
||||
", hash=" + hashCode() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
|
||||
private static final AtomicInteger downloadSpeed = new AtomicInteger(0);
|
||||
public static final EventBus speedEvent = new EventBus();
|
||||
|
||||
static {
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
speedEvent.channel(SpeedEvent.class).fireEvent(new SpeedEvent(speedEvent, downloadSpeed.getAndSet(0)));
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
public static void updateDownloadSpeed(int speed) {
|
||||
downloadSpeed.addAndGet(speed);
|
||||
}
|
||||
|
||||
public static class SpeedEvent extends Event {
|
||||
private final int speed;
|
||||
|
||||
public SpeedEvent(Object source, int speed) {
|
||||
super(source);
|
||||
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download speed in byte/sec.
|
||||
* @return download speed
|
||||
*/
|
||||
public int getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this).append("speed", speed).toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static int downloadExecutorConcurrency = Math.min(Runtime.getRuntime().availableProcessors() * 4, 64);
|
||||
private static volatile ThreadPoolExecutor DOWNLOAD_EXECUTOR;
|
||||
|
||||
/**
|
||||
* Get singleton instance of the thread pool for file downloading.
|
||||
*
|
||||
* @return Thread pool for FetchTask
|
||||
*/
|
||||
protected static ExecutorService download() {
|
||||
if (DOWNLOAD_EXECUTOR == null) {
|
||||
synchronized (Schedulers.class) {
|
||||
if (DOWNLOAD_EXECUTOR == null) {
|
||||
DOWNLOAD_EXECUTOR = new ThreadPoolExecutor(0, downloadExecutorConcurrency, 10, TimeUnit.SECONDS, new SynchronousQueue<>(),
|
||||
runnable -> {
|
||||
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DOWNLOAD_EXECUTOR;
|
||||
}
|
||||
|
||||
public static void setDownloadExecutorConcurrency(int concurrency) {
|
||||
synchronized (Schedulers.class) {
|
||||
downloadExecutorConcurrency = concurrency;
|
||||
if (DOWNLOAD_EXECUTOR != null) {
|
||||
DOWNLOAD_EXECUTOR.setMaximumPoolSize(concurrency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDownloadExecutorConcurrency() {
|
||||
synchronized (Schedulers.class) {
|
||||
return downloadExecutorConcurrency;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user