Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47c4fc2f7e | ||
|
|
dfd177433a | ||
|
|
27381e9696 | ||
|
|
1c7827c53c | ||
|
|
9512bdb5d6 | ||
|
|
33d1f1d692 | ||
|
|
c6b49545ff | ||
|
|
a4a833bfdb | ||
|
|
2e1b6e4630 | ||
|
|
6911cfa387 | ||
|
|
aa29f890de | ||
|
|
0f4e6a01bc | ||
|
|
1d3d8d6710 | ||
|
|
005e06c56f | ||
|
|
0a7c32b3cb | ||
|
|
82916a04ea | ||
|
|
ba2e419894 | ||
|
|
0812c9025d | ||
|
|
4c59ed3ccf | ||
|
|
43d8d96608 | ||
|
|
26a6a7d883 | ||
|
|
2e9c237367 | ||
|
|
99d2816ea7 | ||
|
|
7c3853b0e6 | ||
|
|
53aecf38e0 | ||
|
|
41fab0a781 | ||
|
|
3df35aa373 | ||
|
|
543eaa4f66 | ||
|
|
3a851b7f1a | ||
|
|
089ae1e313 | ||
|
|
bfe5802d02 | ||
|
|
3ec6c97380 | ||
|
|
e376da208d | ||
|
|
3b02c5e4e0 | ||
|
|
1bcc79c415 | ||
|
|
9e2cb25d01 | ||
|
|
7e820ba2a5 | ||
|
|
b079940cd9 | ||
|
|
593ee24429 | ||
|
|
f7a91bf10c | ||
|
|
dad7ac706f | ||
|
|
13c48ce78c | ||
|
|
8ecbba1003 | ||
|
|
8bf6a9ec9d | ||
|
|
64092a4bd4 | ||
|
|
8e6bcd969e | ||
|
|
6777dd7ea7 | ||
|
|
f16885568e | ||
|
|
ab109bcfcc | ||
|
|
17f71eaa21 | ||
|
|
3a294e354a | ||
|
|
54814181b9 | ||
|
|
935c2bd6e5 | ||
|
|
a2b74b8e78 | ||
|
|
09730d74e0 | ||
|
|
2bf03291ce | ||
|
|
93e5851167 | ||
|
|
61fd581ef5 | ||
|
|
51afcf2dee | ||
|
|
cadafe13e1 | ||
|
|
47031cb70e | ||
|
|
b3e776b4bc | ||
|
|
e57a1a8da6 | ||
|
|
7f2a392a33 | ||
|
|
055379f569 | ||
|
|
2b8843ca19 | ||
|
|
ace830bced | ||
|
|
3c2f232acb | ||
|
|
86030e64ca | ||
|
|
b1ce3c31f5 | ||
|
|
04553c2c56 | ||
|
|
bdb7afa860 | ||
|
|
7096135cdf | ||
|
|
be37014206 | ||
|
|
b78beb3305 | ||
|
|
38f27d134d | ||
|
|
3b42ba9375 | ||
|
|
4f52bb2ffe | ||
|
|
dd45e1b3db | ||
|
|
e38bfdfc73 | ||
|
|
73caebff00 | ||
|
|
e0608cdf3e | ||
|
|
c815fa80e8 | ||
|
|
e91e1cab47 | ||
|
|
b791878651 | ||
|
|
8192ffec03 | ||
|
|
afa6eb3bad | ||
|
|
6f7d22518e | ||
|
|
2663312dfb | ||
|
|
1332517a9f | ||
|
|
13e227851f | ||
|
|
e1b6c18017 | ||
|
|
a88c8ed85f | ||
|
|
25a90fc481 | ||
|
|
1371206323 |
No files matched your search
Binary file not shown.
Binary file not shown.
+106
-90
@@ -1,121 +1,137 @@
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
import java.security.MessageDigest
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.Pack200
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
if (!hasProperty('mainClass')) {
|
||||
ext.mainClass = 'org.jackhuang.hmcl.Main'
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '2.0.4'
|
||||
}
|
||||
|
||||
def buildnumber = System.getenv("TRAVIS_BUILD_NUMBER")
|
||||
if (buildnumber == null)
|
||||
buildnumber = System.getenv("BUILD_NUMBER")
|
||||
if (buildnumber == null)
|
||||
buildnumber = "SNAPSHOT"
|
||||
import java.nio.file.FileSystems
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.Signature
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.jar.Pack200
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import java.util.zip.ZipFile
|
||||
import java.nio.file.Files
|
||||
|
||||
def versionroot = System.getenv("VERSION_ROOT")
|
||||
if (versionroot == null)
|
||||
versionroot = "3.1"
|
||||
import org.tukaani.xz.LZMA2Options
|
||||
import org.tukaani.xz.XZOutputStream
|
||||
|
||||
String mavenVersion = versionroot + '.' + buildnumber
|
||||
version = mavenVersion
|
||||
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
|
||||
def versionroot = System.getenv("VERSION_ROOT") ?: "3.1"
|
||||
version = versionroot + '.' + buildnumber
|
||||
|
||||
dependencies {
|
||||
compile project(":HMCLCore")
|
||||
compile rootProject.files("lib/JFoenix.jar")
|
||||
}
|
||||
|
||||
task generateSources(type: Sync) {
|
||||
from 'src/main/java'
|
||||
into "$buildDir/generated-src"
|
||||
filter(ReplaceTokens, tokens: [
|
||||
'HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING': mavenVersion
|
||||
])
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
return MessageDigest.getInstance(algorithm).digest(bytes)
|
||||
}
|
||||
|
||||
compileJava.setSource "$buildDir/generated-src"
|
||||
compileJava.dependsOn generateSources
|
||||
def createChecksum(File file) {
|
||||
def algorithm = "SHA-1"
|
||||
def suffix = "sha1"
|
||||
new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n"
|
||||
}
|
||||
|
||||
configurations {
|
||||
coreCompile.extendsFrom compile
|
||||
coreRuntime.extendsFrom runtime
|
||||
def attachSignature(File jar) {
|
||||
def keyLocation = System.getenv("HMCL_SIGNATURE_KEY");
|
||||
if (keyLocation == null)
|
||||
return
|
||||
def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes))
|
||||
|
||||
def signer = Signature.getInstance("SHA512withRSA")
|
||||
signer.initSign(privatekey)
|
||||
new ZipFile(jar).withCloseable { zip ->
|
||||
zip.stream()
|
||||
.sorted(Comparator.comparing { it.name })
|
||||
.filter { it.name != "META-INF/hmcl_signature" }
|
||||
.forEach {
|
||||
signer.update(digest("SHA-512", it.name.getBytes("UTF-8")))
|
||||
signer.update(digest("SHA-512", zip.getInputStream(it).bytes))
|
||||
}
|
||||
}
|
||||
def signature = signer.sign()
|
||||
|
||||
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs ->
|
||||
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it << signature }
|
||||
}
|
||||
}
|
||||
|
||||
ext.packer = Pack200.newPacker()
|
||||
packer.properties()["pack.effort"] = "9"
|
||||
ext.unpacker = Pack200.newUnpacker()
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
jar {
|
||||
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By' : 'Copyright(c) 2013-2018 huangyuhui.',
|
||||
'Main-Class' : mainClass,
|
||||
'Multi-Release': "true"
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2018 huangyuhui.',
|
||||
'Main-Class': 'org.jackhuang.hmcl.Main',
|
||||
'Multi-Release': 'true',
|
||||
'Implementation-Version': version
|
||||
}
|
||||
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 {
|
||||
def messageDigest = MessageDigest.getInstance("SHA1")
|
||||
archivePath.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
|
||||
messageDigest.update(buf, 0, bytesRead)
|
||||
repack(jar.archivePath)
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
def createExecutable(String suffix, String header) {
|
||||
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
|
||||
output.bytes = new File(project.projectDir, header).bytes
|
||||
output << jar.archivePath.bytes
|
||||
createChecksum(output)
|
||||
}
|
||||
|
||||
task makePack(dependsOn: jar) {
|
||||
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
|
||||
doLast {
|
||||
outputPath.newOutputStream().withCloseable { out ->
|
||||
new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) }
|
||||
}
|
||||
def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0')
|
||||
def fileEx = new File(project.buildDir, "libs/" + archivePath.getName() + ".sha1")
|
||||
if (!fileEx.exists()) fileEx.createNewFile()
|
||||
fileEx.append sha1Hex
|
||||
createChecksum(outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
from(sourceSets.main.resources.srcDirs) {
|
||||
exclude 'icon.icns'
|
||||
}
|
||||
task makePackXz(dependsOn: makePack) doLast {
|
||||
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
|
||||
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(6)).withCloseable { it << makePack.outputPath.bytes }
|
||||
createChecksum(packXz)
|
||||
}
|
||||
|
||||
task makePackGZ(dependsOn: jar) doLast {
|
||||
ext {
|
||||
jar.classifier = ''
|
||||
makeExecutableinjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
makeExecutableoutjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
}
|
||||
def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length()-4)+".pack.gz")
|
||||
def os = new GZIPOutputStream(new FileOutputStream(loc))
|
||||
Pack200.newPacker().pack new JarFile(makeExecutableinjar), os
|
||||
os.close()
|
||||
|
||||
def messageDigest = MessageDigest.getInstance("SHA1")
|
||||
loc.eachByte 1024 * 1024, { byte[] buf, int bytesRead ->
|
||||
messageDigest.update(buf, 0, bytesRead)
|
||||
}
|
||||
def sha1Hex = new BigInteger(1, messageDigest.digest()).toString(16).padLeft(40, '0')
|
||||
def fileEx = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length()-4)+".pack.gz.sha1")
|
||||
if (!fileEx.exists()) fileEx.createNewFile()
|
||||
fileEx.append sha1Hex
|
||||
task makePackGz(dependsOn: makePack) doLast {
|
||||
def packGz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".gz")
|
||||
new GZIPOutputStream(packGz.newOutputStream()).withCloseable { it << makePack.outputPath.bytes }
|
||||
createChecksum(packGz)
|
||||
}
|
||||
|
||||
task makeExecutable(dependsOn: jar) doLast {
|
||||
ext {
|
||||
jar.classifier = ''
|
||||
makeExecutableinjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
makeExecutableoutjar = jar.archivePath
|
||||
jar.classifier = ''
|
||||
}
|
||||
def loc = new File(project.buildDir, "libs/" + makeExecutableoutjar.getName().substring(0, makeExecutableoutjar.getName().length() - 4) + ".exe")
|
||||
def fos = new FileOutputStream(loc)
|
||||
def is = new FileInputStream(new File(project.buildDir, '../HMCLauncher.exe'))
|
||||
int read
|
||||
def bytes = new byte[8192]
|
||||
while((read = is.read(bytes)) != -1)
|
||||
fos.write(bytes, 0, read)
|
||||
is.close()
|
||||
is = new FileInputStream(makeExecutableoutjar)
|
||||
while((read = is.read(bytes)) != -1)
|
||||
fos.write(bytes, 0, read)
|
||||
is.close()
|
||||
fos.close()
|
||||
|
||||
task makeExecutables(dependsOn: jar) doLast {
|
||||
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
|
||||
}
|
||||
|
||||
build.dependsOn makePackGZ
|
||||
build.dependsOn makeExecutable
|
||||
build.dependsOn makePackXz
|
||||
build.dependsOn makePackGz
|
||||
build.dependsOn makeExecutables
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
@@ -25,19 +25,16 @@ import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.AppDataUpgrader;
|
||||
import org.jackhuang.hmcl.upgrade.IUpgrader;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -56,12 +53,7 @@ public final class Launcher extends Application {
|
||||
primaryStage.setResizable(false);
|
||||
primaryStage.setScene(Controllers.getScene());
|
||||
|
||||
UPDATE_CHECKER.process(false)
|
||||
.then(Task.of(Schedulers.javafx(), () -> {
|
||||
if (UPDATE_CHECKER.isOutOfDate())
|
||||
Controllers.showUpdate();
|
||||
}))
|
||||
.start();
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
} catch (Throwable e) {
|
||||
@@ -80,15 +72,17 @@ public final class Launcher extends Application {
|
||||
|
||||
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
|
||||
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
|
||||
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
|
||||
|
||||
LOG.info("*** " + TITLE + " ***");
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
|
||||
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
|
||||
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
|
||||
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
|
||||
launch(args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
@@ -146,14 +140,5 @@ public final class Launcher extends Application {
|
||||
public static final File HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
public static final File LOG_DIRECTORY = new File(Launcher.HMCL_DIRECTORY, "logs");
|
||||
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@");
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION));
|
||||
public static final IUpgrader UPGRADER = new AppDataUpgrader();
|
||||
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
|
||||
|
||||
public static final String UPDATE_SERVER = "https://www.huangyuhui.net";
|
||||
public static final String CONTACT = UPDATE_SERVER + "/hmcl.php";
|
||||
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -35,16 +35,27 @@ import javax.net.ssl.X509TrustManager;
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
|
||||
public final class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
|
||||
checkJavaFX();
|
||||
checkDirectoryPath();
|
||||
checkDSTRootCAX3();
|
||||
checkConfigPermission();
|
||||
|
||||
ConfigHolder.init();
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (IOException e) {
|
||||
showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
}
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
|
||||
@@ -87,22 +98,6 @@ public final class Main {
|
||||
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
|
||||
}
|
||||
|
||||
private static void checkConfigPermission() {
|
||||
Path config = ConfigHolder.CONFIG_PATH;
|
||||
if (Files.exists(config)) {
|
||||
if (Files.isReadable(config) && Files.isWritable(config)) {
|
||||
// we are able to read & write the existent config
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (Files.isWritable(config.getParent())) {
|
||||
// we are able to create a new config
|
||||
return;
|
||||
}
|
||||
}
|
||||
showErrorAndExit(i18n("fatal.config_access_denied", config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.JarUtils;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
*/
|
||||
public final class Metadata {
|
||||
private Metadata() {}
|
||||
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
|
||||
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String CONTACT_URL = "https://www.huangyuhui.net/hmcl.php";
|
||||
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
@@ -46,8 +46,8 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
@Override
|
||||
protected Map<String, String> getConfigurations() {
|
||||
Map<String, String> res = super.getConfigurations();
|
||||
res.put("${launcher_name}", Launcher.NAME);
|
||||
res.put("${launcher_version}", Launcher.VERSION);
|
||||
res.put("${launcher_name}", Metadata.NAME);
|
||||
res.put("${launcher_version}", Metadata.VERSION);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.ZipEngine;
|
||||
import org.jackhuang.hmcl.util.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -31,58 +30,44 @@ import java.util.List;
|
||||
/**
|
||||
* Export the game to a mod pack file.
|
||||
*/
|
||||
public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
|
||||
public class HMCLModpackExportTask extends Task {
|
||||
private final DefaultGameRepository repository;
|
||||
private final String version;
|
||||
private final List<String> whitelist;
|
||||
private final Modpack modpack;
|
||||
private final File output;
|
||||
private final String id;
|
||||
|
||||
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
|
||||
this(repository, version, whitelist, modpack, output, ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param output mod pack file.
|
||||
* @param version to locate version.json
|
||||
*/
|
||||
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output, String id) {
|
||||
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;
|
||||
this.id = id;
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) output.delete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.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 (ZipEngine zip = new ZipEngine(output)) {
|
||||
zip.putDirectory(repository.getRunDirectory(version), (String pathName, Boolean isDirectory) -> {
|
||||
try (Zipper zip = new Zipper(output.toPath())) {
|
||||
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
|
||||
for (String s : blackList)
|
||||
if (isDirectory) {
|
||||
if (pathName.startsWith(s + "/"))
|
||||
return null;
|
||||
} else if (pathName.equals(s))
|
||||
return null;
|
||||
if (path.equals(s))
|
||||
return false;
|
||||
for (String s : whitelist)
|
||||
if (pathName.equals(s + (isDirectory ? "/" : "")))
|
||||
return "minecraft/" + pathName;
|
||||
return null;
|
||||
if (path.equals(s))
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
Version mv = repository.getResolvedVersion(version);
|
||||
@@ -92,6 +77,4 @@ public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
|
||||
zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
|
||||
}
|
||||
}
|
||||
|
||||
public static final String ID = "zip_engine";
|
||||
}
|
||||
@@ -74,7 +74,7 @@ public final class HMCLModpackInstallTask extends Task {
|
||||
}
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), config));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, "/minecraft", it -> !"pack.json".equals(it), config));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,9 +90,9 @@ public final class HMCLModpackInstallTask extends Task {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
|
||||
Version version = Constants.GSON.fromJson(json, Version.class).setJar(null);
|
||||
Version version = Constants.GSON.fromJson(json, Version.class).setId(name).setJar(null);
|
||||
dependencies.add(new VersionJsonSaveTask(repository, version));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, "minecraft/", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "HMCL";
|
||||
|
||||
@@ -39,7 +39,7 @@ public final class HMCLModpackManager {
|
||||
"pack.json", "launcher.jar", "hmclmc.log", // HMCL
|
||||
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
|
||||
"minetweaker.log", // Mods
|
||||
"logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
|
||||
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
|
||||
"downloads", // Curse
|
||||
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
|
||||
);
|
||||
|
||||
@@ -65,6 +65,8 @@ public final class LauncherHelper {
|
||||
if (account == null)
|
||||
throw new IllegalArgumentException("No account");
|
||||
|
||||
Logging.LOG.info("Launching game version: " + selectedVersion);
|
||||
|
||||
GameRepository repository = profile.getRepository();
|
||||
|
||||
Version version = repository.getResolvedVersion(selectedVersion);
|
||||
@@ -218,7 +220,7 @@ public final class LauncherHelper {
|
||||
VersionNumber gameVersion = VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(version)).orElse("Unknown"));
|
||||
JavaVersion java = setting.getJavaVersion();
|
||||
if (java == null) {
|
||||
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.error"), MessageBox.WARNING_MESSAGE, onAccept);
|
||||
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
|
||||
setting.setJava(null);
|
||||
setting.setDefaultJavaPath(null);
|
||||
java = JavaVersion.fromCurrentEnvironment();
|
||||
@@ -232,7 +234,7 @@ public final class LauncherHelper {
|
||||
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
|
||||
} else {
|
||||
// Most mods require Java 8 or later version.
|
||||
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.error"), MessageBox.WARNING_MESSAGE, onAccept);
|
||||
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
|
||||
}
|
||||
flag = true;
|
||||
}
|
||||
@@ -240,7 +242,13 @@ public final class LauncherHelper {
|
||||
// LaunchWrapper will crash because of assuming the system class loader is an instance of URLClassLoader.
|
||||
// cpw has claimed that he will make MinecraftForge of 1.13 and later versions able to run on Java 9.
|
||||
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.12.5")) < 0 && version.getMainClass().contains("launchwrapper")) {
|
||||
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
|
||||
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
|
||||
if (java8.isPresent()) {
|
||||
setting.setJavaVersion(java8.get());
|
||||
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
|
||||
}
|
||||
flag = true;
|
||||
}
|
||||
|
||||
@@ -382,15 +390,18 @@ public final class LauncherHelper {
|
||||
logs.removeFirst();
|
||||
|
||||
if (setting.isShowLogs()) {
|
||||
Lang.invoke(() -> {
|
||||
try {
|
||||
latch.await();
|
||||
logWindow.waitForLoaded();
|
||||
});
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> logWindow.logLine(log, level));
|
||||
}
|
||||
|
||||
if (!lwjgl && log.contains("LWJGL Version: ") || !detectWindow) {
|
||||
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
|
||||
lwjgl = true;
|
||||
switch (visibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
|
||||
@@ -86,11 +86,13 @@ public final class ModpackHelper {
|
||||
profile.getRepository().markVersionAsModpack(name);
|
||||
|
||||
FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> {
|
||||
profile.getRepository().refreshVersions();
|
||||
VersionSetting vs = profile.specializeVersionSetting(name);
|
||||
profile.getRepository().undoMark(name);
|
||||
if (vs != null)
|
||||
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
|
||||
if (isDependentsSucceeded) {
|
||||
profile.getRepository().refreshVersions();
|
||||
VersionSetting vs = profile.specializeVersionSetting(name);
|
||||
profile.getRepository().undoMark(name);
|
||||
if (vs != null)
|
||||
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
|
||||
}
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof CurseManifest)
|
||||
@@ -102,7 +104,7 @@ public final class ModpackHelper {
|
||||
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
|
||||
.finalized(finalizeTask)
|
||||
.with(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
|
||||
.then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
|
||||
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ public final class Accounts {
|
||||
return type2factory.get(accountType(account));
|
||||
}
|
||||
|
||||
private static String accountId(Account account) {
|
||||
static String accountId(Account account) {
|
||||
return account.getUsername() + ":" + account.getCharacter();
|
||||
}
|
||||
|
||||
@@ -138,20 +138,19 @@ public final class Accounts {
|
||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||
}
|
||||
|
||||
static Map<Object, Object> getAccountStorage(Account account) {
|
||||
Map<Object, Object> storage = account.toStorage();
|
||||
storage.put("type", accountType(account));
|
||||
return storage;
|
||||
}
|
||||
|
||||
private static void updateAccountStorages() {
|
||||
// don't update the underlying storage before data loading is completed
|
||||
// otherwise it might cause data loss
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getAccountStorages().setAll(
|
||||
accounts.stream()
|
||||
.map(account -> {
|
||||
Map<Object, Object> storage = account.toStorage();
|
||||
storage.put("type", accountType(account));
|
||||
return storage;
|
||||
})
|
||||
.collect(toList()));
|
||||
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
|
||||
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
|
||||
import org.jackhuang.hmcl.util.FileTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.ObservableHelper;
|
||||
@@ -149,6 +150,9 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
|
||||
|
||||
@SerializedName("updateChannel")
|
||||
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
|
||||
|
||||
@SerializedName("_version")
|
||||
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
@@ -435,4 +439,15 @@ public final class Config implements Cloneable, Observable {
|
||||
return authlibInjectorServers;
|
||||
}
|
||||
|
||||
public UpdateChannel getUpdateChannel() {
|
||||
return updateChannel.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<UpdateChannel> updateChannelProperty() {
|
||||
return updateChannel;
|
||||
}
|
||||
|
||||
public void setUpdateChannel(UpdateChannel updateChannel) {
|
||||
this.updateChannel.set(updateChannel);
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,16 @@ import java.util.logging.Level;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import org.jackhuang.hmcl.util.OperatingSystem;
|
||||
|
||||
public final class ConfigHolder {
|
||||
|
||||
private ConfigHolder() {}
|
||||
|
||||
public static final String CONFIG_FILENAME = "hmcl.json";
|
||||
public static final Path CONFIG_PATH = Paths.get(CONFIG_FILENAME).toAbsolutePath();
|
||||
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
|
||||
|
||||
private static Path configLocation;
|
||||
private static Config configInstance;
|
||||
private static boolean newlyCreated;
|
||||
|
||||
@@ -54,34 +57,64 @@ public final class ConfigHolder {
|
||||
return newlyCreated;
|
||||
}
|
||||
|
||||
public synchronized static void init() {
|
||||
public synchronized static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
}
|
||||
|
||||
configLocation = locateConfig();
|
||||
configInstance = loadConfig();
|
||||
configInstance.addListener(source -> saveConfig());
|
||||
configInstance.addListener(source -> markConfigDirty());
|
||||
|
||||
Settings.init();
|
||||
|
||||
if (newlyCreated) {
|
||||
saveConfig();
|
||||
|
||||
// hide the config file on windows
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
Files.setAttribute(configLocation, "dos:hidden", true);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Files.isWritable(configLocation)) {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
}
|
||||
}
|
||||
|
||||
private static Config loadConfig() {
|
||||
if (Files.exists(CONFIG_PATH)) {
|
||||
private static Path locateConfig() {
|
||||
Path config = Paths.get(CONFIG_FILENAME);
|
||||
if (Files.isRegularFile(config))
|
||||
return config;
|
||||
|
||||
Path dotConfig = Paths.get(CONFIG_FILENAME_LINUX);
|
||||
if (Files.isRegularFile(dotConfig))
|
||||
return dotConfig;
|
||||
|
||||
// create new
|
||||
return OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? config : dotConfig;
|
||||
}
|
||||
|
||||
private static Config loadConfig() throws IOException {
|
||||
if (Files.exists(configLocation)) {
|
||||
try {
|
||||
String content = new String(Files.readAllBytes(CONFIG_PATH), UTF_8);
|
||||
String content = new String(Files.readAllBytes(configLocation), UTF_8);
|
||||
Config deserialized = Config.fromJson(content);
|
||||
if (deserialized == null) {
|
||||
LOG.info("Config is empty");
|
||||
} else {
|
||||
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
|
||||
return upgradeConfig(deserialized, raw);
|
||||
ConfigUpgrader.upgradeConfig(deserialized, raw);
|
||||
return deserialized;
|
||||
}
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Something went wrong when loading config.", e);
|
||||
} catch (JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Malformed config.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,18 +123,22 @@ public final class ConfigHolder {
|
||||
return new Config();
|
||||
}
|
||||
|
||||
static void saveConfig() {
|
||||
private static void saveConfig() throws IOException {
|
||||
LOG.info("Saving config");
|
||||
try {
|
||||
Files.write(CONFIG_PATH, configInstance.toJson().getBytes(UTF_8));
|
||||
} catch (IOException ex) {
|
||||
LOG.log(Level.SEVERE, "Failed to save config", ex);
|
||||
Files.write(configLocation, configInstance.toJson().getBytes(UTF_8));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to save config", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static Config upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
if (!rawJson.containsKey("commonDirType"))
|
||||
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
|
||||
return deserialized;
|
||||
static void markConfigDirty() {
|
||||
// TODO: change this to async
|
||||
try {
|
||||
saveConfig();
|
||||
} catch (IOException ignored) {
|
||||
// ignore it as it has been logged
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
final class ConfigUpgrader {
|
||||
private ConfigUpgrader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
*/
|
||||
static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
|
||||
upgradeV2(deserialized, rawJson);
|
||||
upgradeV3(deserialized, rawJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade configuration of HMCL 2.x
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
*/
|
||||
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
|
||||
// Convert OfflineAccounts whose stored uuid is important.
|
||||
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
|
||||
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
|
||||
List<OfflineAccount> accounts = new LinkedList<>();
|
||||
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
|
||||
((Map<?, ?>) uuidMap).forEach((key, value) -> {
|
||||
OfflineAccount account = OfflineAccountFactory.INSTANCE.create((String) key, UUIDTypeAdapter.fromString((String) value));
|
||||
accounts.add(account);
|
||||
deserialized.getAccountStorages().add(Accounts.getAccountStorage(account));
|
||||
});
|
||||
});
|
||||
|
||||
tryCast(offline.get("IAuthenticator_UserName"), String.class).ifPresent(selected -> {
|
||||
if (!rawJson.containsKey("selectedAccount"))
|
||||
accounts.stream().filter(account -> selected.equals(account.getUsername())).findAny().ifPresent(account ->
|
||||
deserialized.setSelectedAccount(Accounts.accountId(account)));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade configuration of HMCL earlier than 3.1.70
|
||||
*
|
||||
* @param deserialized deserialized config settings
|
||||
* @param rawJson raw json structure of the config settings without modification
|
||||
*/
|
||||
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
|
||||
if (!rawJson.containsKey("commonDirType"))
|
||||
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
|
||||
if (!rawJson.containsKey("backgroundType"))
|
||||
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
|
||||
if (!rawJson.containsKey("hasProxy"))
|
||||
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
|
||||
if (!rawJson.containsKey("hasProxyAuth"))
|
||||
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,5 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
public enum EnumBackgroundImage {
|
||||
DEFAULT,
|
||||
CUSTOM;
|
||||
|
||||
public static EnumBackgroundImage indexOf(int index) {
|
||||
if (index >= values().length || index < 0)
|
||||
return DEFAULT;
|
||||
else
|
||||
return values()[index];
|
||||
}
|
||||
CUSTOM
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public final class Profile {
|
||||
return globalProperty.get();
|
||||
}
|
||||
|
||||
public void setGlobal(VersionSetting global) {
|
||||
private void setGlobal(VersionSetting global) {
|
||||
if (global == null)
|
||||
global = new VersionSetting();
|
||||
globalProperty.set(global);
|
||||
@@ -175,6 +175,7 @@ public final class Profile {
|
||||
globalProperty.addListener(listener);
|
||||
gameDirProperty.addListener(listener);
|
||||
useRelativePathProperty.addListener(listener);
|
||||
globalProperty.get().addPropertyChangedListener(listener);
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
|
||||
|
||||
@@ -58,10 +58,8 @@ public class Settings {
|
||||
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
|
||||
profileEntry.getValue().setName(profileEntry.getKey());
|
||||
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
|
||||
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.saveConfig());
|
||||
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.markConfigDirty());
|
||||
}
|
||||
|
||||
config().addListener(source -> ConfigHolder.saveConfig());
|
||||
}
|
||||
|
||||
public Font getFont() {
|
||||
|
||||
@@ -88,8 +88,6 @@ public class Theme {
|
||||
}
|
||||
|
||||
return new String[]{
|
||||
Theme.class.getResource("/css/jfoenix-fonts.css").toExternalForm(),
|
||||
Theme.class.getResource("/css/jfoenix-design.css").toExternalForm(),
|
||||
css,
|
||||
Theme.class.getResource("/assets/css/root.css").toExternalForm()
|
||||
};
|
||||
@@ -104,6 +102,18 @@ public class Theme {
|
||||
public static Optional<Theme> getTheme(String name) {
|
||||
if (name == null)
|
||||
return Optional.empty();
|
||||
else if (name.equalsIgnoreCase("blue"))
|
||||
return Optional.of(custom("#5C6BC0"));
|
||||
else if (name.equalsIgnoreCase("darker_blue"))
|
||||
return Optional.of(custom("#283593"));
|
||||
else if (name.equalsIgnoreCase("green"))
|
||||
return Optional.of(custom("#43A047"));
|
||||
else if (name.equalsIgnoreCase("orange"))
|
||||
return Optional.of(custom("#E67E22"));
|
||||
else if (name.equalsIgnoreCase("purple"))
|
||||
return Optional.of(custom("#9C27B0"));
|
||||
else if (name.equalsIgnoreCase("red"))
|
||||
return Optional.of(custom("#F44336"));
|
||||
|
||||
if (name.startsWith("#"))
|
||||
try {
|
||||
|
||||
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
@@ -503,8 +504,8 @@ public final class VersionSetting {
|
||||
return new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Launcher.TITLE)
|
||||
.setProfileName(Launcher.TITLE)
|
||||
.setVersionName(Metadata.TITLE)
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setMinecraftArgs(getMinecraftArgs())
|
||||
.setJavaArgs(getJavaArgs())
|
||||
.setMaxMemory(getMaxMemory())
|
||||
@@ -601,7 +602,9 @@ public final class VersionSetting {
|
||||
}
|
||||
|
||||
private int parseJsonPrimitive(JsonPrimitive primitive, int defaultValue) {
|
||||
if (primitive.isNumber())
|
||||
if (primitive == null)
|
||||
return defaultValue;
|
||||
else if (primitive.isNumber())
|
||||
return primitive.getAsInt();
|
||||
else
|
||||
return Lang.parseInt(primitive.getAsString(), defaultValue);
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
|
||||
|
||||
@@ -49,7 +50,7 @@ import java.util.Optional;
|
||||
public class AccountPage extends StackPane implements DecoratorPage {
|
||||
private final StringProperty title;
|
||||
private final ObjectProperty<Runnable> onDelete = new SimpleObjectProperty<>(this, "onDelete");
|
||||
private final VersionListItem item;
|
||||
private final AdvancedListItem item;
|
||||
private final Account account;
|
||||
|
||||
@FXML
|
||||
@@ -73,7 +74,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
|
||||
@FXML
|
||||
private JFXProgressBar progressBar;
|
||||
|
||||
public AccountPage(Account account, VersionListItem item) {
|
||||
public AccountPage(Account account, AdvancedListItem item) {
|
||||
this.account = account;
|
||||
this.item = item;
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@@ -98,7 +98,7 @@ public final class Controllers {
|
||||
|
||||
stage.setOnCloseRequest(e -> Launcher.stopApplication());
|
||||
|
||||
decorator = new Decorator(stage, getMainPage(), Launcher.TITLE, false, true);
|
||||
decorator = new Decorator(stage, getMainPage(), Metadata.TITLE, false, true);
|
||||
decorator.showPage(null);
|
||||
leftPaneController = new LeftPaneController(decorator.getLeftPane());
|
||||
|
||||
@@ -115,7 +115,7 @@ public final class Controllers {
|
||||
stage.setMaxHeight(521);
|
||||
|
||||
stage.getIcons().add(new Image("/assets/img/icon.png"));
|
||||
stage.setTitle(Launcher.TITLE);
|
||||
stage.setTitle(Metadata.TITLE);
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
@@ -162,15 +162,6 @@ public final class Controllers {
|
||||
return pane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link DialogCloseEvent}
|
||||
*/
|
||||
public static void closeDialog(Region content) {
|
||||
if (stage == null) // shut down
|
||||
return;
|
||||
decorator.closeDialog(content);
|
||||
}
|
||||
|
||||
public static void navigate(Node node) {
|
||||
if (decorator.getNowPage() == node)
|
||||
decorator.showPage(null);
|
||||
@@ -178,12 +169,6 @@ public final class Controllers {
|
||||
decorator.showPage(node);
|
||||
}
|
||||
|
||||
public static void showUpdate() {
|
||||
if (stage == null) // shut down
|
||||
return;
|
||||
getLeftPaneController().showUpdate();
|
||||
}
|
||||
|
||||
public static boolean isStopped() {
|
||||
return decorator == null;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ import javafx.stage.Stage;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -39,7 +40,7 @@ public class CrashWindow extends Stage {
|
||||
|
||||
public CrashWindow(String text) {
|
||||
Label lblCrash = new Label();
|
||||
if (Launcher.UPDATE_CHECKER.isOutOfDate())
|
||||
if (UpdateChecker.isOutdated())
|
||||
lblCrash.setText(i18n("launcher.crash_out_dated"));
|
||||
else
|
||||
lblCrash.setText(i18n("launcher.crash"));
|
||||
@@ -51,7 +52,7 @@ public class CrashWindow extends Stage {
|
||||
|
||||
Button btnContact = new Button();
|
||||
btnContact.setText(i18n("launcher.contact"));
|
||||
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Launcher.CONTACT));
|
||||
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
|
||||
HBox box = new HBox();
|
||||
box.setStyle("-fx-padding: 8px;");
|
||||
box.getChildren().add(btnContact);
|
||||
|
||||
@@ -67,6 +67,8 @@ import org.jackhuang.hmcl.ui.construct.StackContainerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer;
|
||||
import org.jackhuang.hmcl.ui.wizard.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -152,6 +154,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
|
||||
private HBox navLeft;
|
||||
@FXML
|
||||
private ImageView welcomeView;
|
||||
@FXML
|
||||
private Rectangle separator;
|
||||
|
||||
public Decorator(Stage primaryStage, Node mainPage, String title) {
|
||||
this(primaryStage, mainPage, title, true, true);
|
||||
@@ -181,6 +185,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
|
||||
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), 15, 15));
|
||||
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), 15, 15));
|
||||
|
||||
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
|
||||
|
||||
lblTitle.setText(title);
|
||||
|
||||
buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
@@ -297,11 +303,22 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private boolean isMaximized() {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case OSX:
|
||||
Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
|
||||
return primaryStage.getWidth() >= bounds.getWidth() && primaryStage.getHeight() >= bounds.getHeight();
|
||||
default:
|
||||
return primaryStage.isMaximized();
|
||||
}
|
||||
}
|
||||
|
||||
// ====
|
||||
|
||||
@FXML
|
||||
private void onMouseMoved(MouseEvent mouseEvent) {
|
||||
if (!primaryStage.isMaximized() && !primaryStage.isFullScreen() && !maximized) {
|
||||
if (!isMaximized() && !primaryStage.isFullScreen() && !maximized) {
|
||||
if (!primaryStage.isResizable())
|
||||
updateInitMouseValues(mouseEvent);
|
||||
else {
|
||||
@@ -350,7 +367,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
|
||||
private void onMouseDragged(MouseEvent mouseEvent) {
|
||||
this.isDragging = true;
|
||||
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
|
||||
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) {
|
||||
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !isMaximized() && !this.maximized) {
|
||||
this.newX = mouseEvent.getScreenX();
|
||||
this.newY = mouseEvent.getScreenY();
|
||||
double deltaX = this.newX - this.initX;
|
||||
@@ -598,18 +615,20 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void closeDialog(Node node) {
|
||||
private void closeDialog(Node node) {
|
||||
FXUtils.checkFxUserThread();
|
||||
|
||||
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
|
||||
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
|
||||
|
||||
dialogPane.pop(node);
|
||||
if (dialog != null) {
|
||||
dialogPane.pop(node);
|
||||
|
||||
if (dialogPane.getChildren().isEmpty()) {
|
||||
dialog.close();
|
||||
dialog = null;
|
||||
dialogPane = null;
|
||||
if (dialogPane.getChildren().isEmpty()) {
|
||||
dialog.close();
|
||||
dialog = null;
|
||||
dialogPane = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,11 +49,8 @@ import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.ClassTitle;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedItem;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.MappedObservableList;
|
||||
|
||||
@@ -92,10 +89,20 @@ public final class LeftPaneController {
|
||||
public LeftPaneController(AdvancedListBox leftPane) {
|
||||
this.leftPane = leftPane;
|
||||
|
||||
this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), i18n("settings.launcher")), iconedItem -> {
|
||||
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
|
||||
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
});
|
||||
launcherSettingsItem = new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20));
|
||||
|
||||
launcherSettingsItem.getLabel().textProperty().bind(
|
||||
new When(UpdateChecker.outdatedProperty())
|
||||
.then(i18n("update.found"))
|
||||
.otherwise(i18n("settings.launcher")));
|
||||
|
||||
launcherSettingsItem.getLabel().textFillProperty().bind(
|
||||
new When(UpdateChecker.outdatedProperty())
|
||||
.then(Color.RED)
|
||||
.otherwise(Color.BLACK));
|
||||
|
||||
launcherSettingsItem.prefWidthProperty().bind(leftPane.widthProperty());
|
||||
launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
|
||||
|
||||
leftPane
|
||||
.add(new ClassTitle(i18n("account").toUpperCase(), Lang.apply(new JFXButton(), button -> {
|
||||
@@ -116,7 +123,7 @@ public final class LeftPaneController {
|
||||
|
||||
// ==== Accounts ====
|
||||
// Missing account item
|
||||
VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
|
||||
AdvancedListItem missingAccountItem = new AdvancedListItem(i18n("account.missing"), i18n("message.unknown"));
|
||||
RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem);
|
||||
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
|
||||
missingAccountRippler.setOnMouseClicked(e -> addNewAccount());
|
||||
@@ -151,7 +158,7 @@ public final class LeftPaneController {
|
||||
}
|
||||
|
||||
private RipplerContainer createAccountItem(Account account) {
|
||||
VersionListItem item = new VersionListItem(account.getCharacter(), accountSubtitle(account));
|
||||
AdvancedListItem item = new AdvancedListItem(account.getCharacter(), accountSubtitle(account));
|
||||
RipplerContainer rippler = new RipplerContainer(item);
|
||||
item.setOnSettingsButtonClicked(e -> {
|
||||
AccountPage accountPage = new AccountPage(account, item);
|
||||
@@ -208,7 +215,7 @@ public final class LeftPaneController {
|
||||
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof String) {
|
||||
boolean current = Objects.equals(node.getProperties().get("profile"), profile.getName());
|
||||
((RipplerContainer) node).setSelected(current);
|
||||
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? i18n("profile.selected") : "");
|
||||
((AdvancedListItem) ((RipplerContainer) node).getContainer()).setSubtitle(current ? i18n("profile.selected") : "");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -217,7 +224,7 @@ public final class LeftPaneController {
|
||||
private void onProfilesLoading() {
|
||||
LinkedList<RipplerContainer> list = new LinkedList<>();
|
||||
for (Profile profile : Settings.instance().getProfiles()) {
|
||||
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
|
||||
AdvancedListItem item = new AdvancedListItem(Profiles.getProfileDisplayName(profile));
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(item);
|
||||
item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
|
||||
ripplerContainer.setOnMouseClicked(e -> Settings.instance().setSelectedProfile(profile));
|
||||
@@ -228,11 +235,6 @@ public final class LeftPaneController {
|
||||
Platform.runLater(() -> profilePane.getChildren().setAll(list));
|
||||
}
|
||||
|
||||
public void showUpdate() {
|
||||
launcherSettingsItem.setText(i18n("update.found"));
|
||||
launcherSettingsItem.setTextFill(Color.RED);
|
||||
}
|
||||
|
||||
private boolean checkedModpack = false;
|
||||
private static boolean showNewAccount = true;
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import javafx.scene.layout.BorderPane;
|
||||
|
||||
import org.jackhuang.hmcl.mod.ModInfo;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
@@ -54,7 +56,14 @@ public final class ModItem extends BorderPane {
|
||||
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
modItem.setTitle(info.getFileName());
|
||||
modItem.setSubtitle(info.getName() + ", " + i18n("archive.version") + ": " + info.getVersion() + ", " + i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + i18n("archive.author") + ": " + info.getAuthors());
|
||||
StringBuilder message = new StringBuilder(info.getName());
|
||||
if (StringUtils.isNotBlank(info.getVersion()))
|
||||
message.append(", ").append(i18n("archive.version")).append(": ").append(info.getVersion());
|
||||
if (StringUtils.isNotBlank(info.getGameVersion()))
|
||||
message.append(", ").append(i18n("archive.game_version")).append(": ").append(info.getGameVersion());
|
||||
if (StringUtils.isNotBlank(info.getAuthors()))
|
||||
message.append(", ").append(i18n("archive.author")).append(": ").append(info.getAuthors());
|
||||
modItem.setSubtitle(message.toString());
|
||||
chkEnabled.setSelected(info.isActive());
|
||||
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
|
||||
info.activeProperty().set(newValue));
|
||||
|
||||
@@ -20,6 +20,8 @@ package org.jackhuang.hmcl.ui;
|
||||
import com.jfoenix.controls.*;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.When;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -37,12 +39,17 @@ import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import javafx.scene.text.Text;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.construct.FontComboBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
|
||||
@@ -83,6 +90,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
||||
@FXML
|
||||
private Label lblUpdateSub;
|
||||
@FXML
|
||||
private Text lblUpdateNote;
|
||||
@FXML
|
||||
private JFXRadioButton chkUpdateStable;
|
||||
@FXML
|
||||
private JFXRadioButton chkUpdateDev;
|
||||
@FXML
|
||||
private JFXButton btnUpdate;
|
||||
@FXML
|
||||
private ScrollPane scroll;
|
||||
@@ -91,7 +104,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
||||
@FXML
|
||||
private StackPane themeColorPickerContainer;
|
||||
@FXML
|
||||
private JFXCheckBox chkEnableProxy;
|
||||
private JFXCheckBox chkDisableProxy;
|
||||
@FXML
|
||||
private JFXRadioButton chkProxyHttp;
|
||||
@FXML
|
||||
@@ -105,6 +118,8 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
||||
|
||||
private ObjectProperty<Proxy.Type> selectedProxyType;
|
||||
|
||||
private InvalidationListener updateListener;
|
||||
|
||||
public SettingsPage() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
|
||||
|
||||
@@ -146,10 +161,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
||||
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
|
||||
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
|
||||
|
||||
proxyPane.disableProperty().bind(chkEnableProxy.selectedProperty().not());
|
||||
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
|
||||
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
|
||||
|
||||
chkEnableProxy.selectedProperty().bindBidirectional(config().hasProxyProperty());
|
||||
chkDisableProxy.setSelected(!config().hasProxy());
|
||||
chkDisableProxy.selectedProperty().addListener(o -> config().setHasProxy(!chkDisableProxy.isSelected()));
|
||||
config().hasProxyProperty().addListener(o -> chkDisableProxy.setSelected(!config().hasProxy()));
|
||||
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
|
||||
|
||||
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
|
||||
@@ -194,8 +211,60 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
||||
.orElse(i18n("launcher.common_directory.disabled")),
|
||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
|
||||
// ==== Update ====
|
||||
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
|
||||
checkUpdate();
|
||||
updateListener = any -> {
|
||||
btnUpdate.setVisible(UpdateChecker.isOutdated());
|
||||
|
||||
if (UpdateChecker.isOutdated()) {
|
||||
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
|
||||
lblUpdateSub.getStyleClass().setAll("update-label");
|
||||
|
||||
lblUpdate.setText(i18n("update.found"));
|
||||
lblUpdate.getStyleClass().setAll("update-label");
|
||||
} else if (UpdateChecker.isCheckingUpdate()) {
|
||||
lblUpdateSub.setText(i18n("update.checking"));
|
||||
lblUpdateSub.getStyleClass().setAll("subtitle-label");
|
||||
|
||||
lblUpdate.setText(i18n("update"));
|
||||
lblUpdate.getStyleClass().setAll();
|
||||
} else {
|
||||
lblUpdateSub.setText(i18n("update.latest"));
|
||||
lblUpdateSub.getStyleClass().setAll("subtitle-label");
|
||||
|
||||
lblUpdate.setText(i18n("update"));
|
||||
lblUpdate.getStyleClass().setAll();
|
||||
}
|
||||
};
|
||||
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
updateListener.invalidated(null);
|
||||
|
||||
lblUpdateNote.setWrappingWidth(470);
|
||||
|
||||
ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<UpdateChannel>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
UpdateChannel updateChannel = Objects.requireNonNull(get());
|
||||
chkUpdateDev.setSelected(updateChannel == UpdateChannel.DEVELOPMENT);
|
||||
chkUpdateStable.setSelected(updateChannel == UpdateChannel.STABLE);
|
||||
}
|
||||
};
|
||||
|
||||
ToggleGroup updateChannelGroup = new ToggleGroup();
|
||||
chkUpdateDev.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
|
||||
chkUpdateStable.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateStable.setUserData(UpdateChannel.STABLE);
|
||||
updateChannelGroup.getToggles().forEach(
|
||||
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
updateChannel.set((UpdateChannel) toggle.getUserData());
|
||||
}
|
||||
}));
|
||||
updateChannel.bindBidirectional(ConfigHolder.config().updateChannelProperty());
|
||||
// ====
|
||||
|
||||
// ==== Background ====
|
||||
backgroundItem.loadChildren(Collections.singletonList(
|
||||
@@ -237,26 +306,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
public void checkUpdate() {
|
||||
btnUpdate.setVisible(Launcher.UPDATE_CHECKER.isOutOfDate());
|
||||
|
||||
if (Launcher.UPDATE_CHECKER.isOutOfDate()) {
|
||||
lblUpdateSub.setText(i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString()));
|
||||
lblUpdateSub.getStyleClass().setAll("update-label");
|
||||
|
||||
lblUpdate.setText(i18n("update.found"));
|
||||
lblUpdate.getStyleClass().setAll("update-label");
|
||||
} else {
|
||||
lblUpdateSub.setText(i18n("update.latest"));
|
||||
lblUpdateSub.getStyleClass().setAll("subtitle-label");
|
||||
|
||||
lblUpdate.setText(i18n("update"));
|
||||
lblUpdate.getStyleClass().setAll();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void onUpdate() {
|
||||
Launcher.UPDATE_CHECKER.checkOutdate();
|
||||
RemoteVersion target = UpdateChecker.getLatestVersion();
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
UpdateHandler.updateFrom(target);
|
||||
}
|
||||
}
|
||||
@@ -26,16 +26,17 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title", null);
|
||||
|
||||
@@ -147,7 +148,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
sub = "saves";
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
return;
|
||||
}
|
||||
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
|
||||
}
|
||||
@@ -186,7 +187,12 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
|
||||
public static void deleteVersion(Profile profile, String version) {
|
||||
Controllers.confirmDialog(i18n("version.manage.remove.confirm", version), i18n("message.confirm"), () -> {
|
||||
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == EnumGameDirectory.VERSION_FOLDER;
|
||||
boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported();
|
||||
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
|
||||
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
|
||||
i18n("version.manage.remove.confirm", version);
|
||||
Controllers.confirmDialog(message, i18n("message.confirm"), () -> {
|
||||
if (profile.getRepository().removeVersionFromDisk(version)) {
|
||||
profile.getRepository().refreshVersionsAsync().start();
|
||||
Controllers.navigate(null);
|
||||
|
||||
+15
-14
@@ -15,7 +15,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.event.EventHandler;
|
||||
@@ -26,27 +26,28 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public final class VersionListItem extends StackPane {
|
||||
public final class AdvancedListItem extends StackPane {
|
||||
@FXML
|
||||
private StackPane imageViewContainer;
|
||||
@FXML
|
||||
private Label lblVersionName;
|
||||
private Label lblTitle;
|
||||
@FXML
|
||||
private Label lblGameVersion;
|
||||
private Label lblSubtitle;
|
||||
@FXML
|
||||
private ImageView imageView;
|
||||
@FXML private JFXButton btnSettings;
|
||||
|
||||
public VersionListItem(String versionName) {
|
||||
this(versionName, "");
|
||||
public AdvancedListItem(String title) {
|
||||
this(title, "");
|
||||
}
|
||||
|
||||
public VersionListItem(String versionName, String gameVersion) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/version-list-item.fxml");
|
||||
public AdvancedListItem(String title, String subtitle) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/advanced-list-item.fxml");
|
||||
|
||||
lblVersionName.setText(versionName);
|
||||
lblGameVersion.setText(gameVersion);
|
||||
lblTitle.setText(title);
|
||||
lblSubtitle.setText(subtitle);
|
||||
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
}
|
||||
@@ -55,12 +56,12 @@ public final class VersionListItem extends StackPane {
|
||||
btnSettings.setOnMouseClicked(handler);
|
||||
}
|
||||
|
||||
public void setVersionName(String versionName) {
|
||||
lblVersionName.setText(versionName);
|
||||
public void setTitle(String title) {
|
||||
lblTitle.setText(title);
|
||||
}
|
||||
|
||||
public void setGameVersion(String gameVersion) {
|
||||
lblGameVersion.setText(gameVersion);
|
||||
public void setSubtitle(String subtitle) {
|
||||
lblSubtitle.setText(subtitle);
|
||||
}
|
||||
|
||||
public void setImage(Image image, Rectangle2D viewport) {
|
||||
@@ -50,10 +50,6 @@ public class ComponentList extends StackPane {
|
||||
}
|
||||
|
||||
public void addChildren(Node node) {
|
||||
if (node instanceof ComponentList) {
|
||||
node.getProperties().put("title", ((ComponentList) node).getTitle());
|
||||
node.getProperties().put("subtitle", ((ComponentList) node).getSubtitle());
|
||||
}
|
||||
StackPane child = new StackPane();
|
||||
child.getChildren().add(new ComponentListCell(node));
|
||||
if (vbox.getChildren().isEmpty())
|
||||
|
||||
@@ -27,6 +27,8 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
@@ -75,31 +77,47 @@ public class ComponentListCell extends StackPane {
|
||||
content.getStyleClass().remove("options-list");
|
||||
content.getStyleClass().add("options-sublist");
|
||||
|
||||
StackPane groupNode = new StackPane();
|
||||
BorderPane groupNode = new BorderPane();
|
||||
groupNode.getStyleClass().add("options-list-item-header");
|
||||
|
||||
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
|
||||
JFXButton expandButton = new JFXButton();
|
||||
expandButton.setGraphic(expandIcon);
|
||||
expandButton.getStyleClass().add("options-list-item-expand-button");
|
||||
StackPane.setAlignment(expandButton, Pos.CENTER_RIGHT);
|
||||
|
||||
VBox labelVBox = new VBox();
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(list.titleProperty());
|
||||
label.setMouseTransparent(true);
|
||||
labelVBox.getChildren().add(label);
|
||||
|
||||
if (list.isHasSubtitle()) {
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.textProperty().bind(list.subtitleProperty());
|
||||
subtitleLabel.setMouseTransparent(true);
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
labelVBox.getChildren().add(subtitleLabel);
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
|
||||
if (leftNode != null)
|
||||
labelVBox.getChildren().setAll(leftNode);
|
||||
} else {
|
||||
Label label = new Label();
|
||||
label.textProperty().bind(list.titleProperty());
|
||||
label.setMouseTransparent(true);
|
||||
labelVBox.getChildren().add(label);
|
||||
|
||||
if (list.isHasSubtitle()) {
|
||||
Label subtitleLabel = new Label();
|
||||
subtitleLabel.textProperty().bind(list.subtitleProperty());
|
||||
subtitleLabel.setMouseTransparent(true);
|
||||
subtitleLabel.getStyleClass().add("subtitle-label");
|
||||
labelVBox.getChildren().add(subtitleLabel);
|
||||
}
|
||||
}
|
||||
|
||||
StackPane.setAlignment(labelVBox, Pos.CENTER_LEFT);
|
||||
groupNode.getChildren().setAll(labelVBox, expandButton);
|
||||
groupNode.setLeft(labelVBox);
|
||||
|
||||
HBox right = new HBox();
|
||||
if (list instanceof ComponentSublist) {
|
||||
Node rightNode = ((ComponentSublist) list).getHeaderRight();
|
||||
if (rightNode != null)
|
||||
right.getChildren().add(rightNode);
|
||||
}
|
||||
right.getChildren().add(expandButton);
|
||||
groupNode.setRight(right);
|
||||
labelVBox.setAlignment(Pos.CENTER_LEFT);
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
VBox container = new VBox();
|
||||
container.setStyle("-fx-padding: 8 0 0 0;");
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
|
||||
@DefaultProperty("content")
|
||||
public final class ComponentSublist extends ComponentList {
|
||||
|
||||
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
|
||||
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
|
||||
|
||||
public ComponentSublist() {
|
||||
super();
|
||||
}
|
||||
|
||||
public Node getHeaderLeft() {
|
||||
return headerLeft.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> headerLeftProperty() {
|
||||
return headerLeft;
|
||||
}
|
||||
|
||||
public void setHeaderLeft(Node headerLeft) {
|
||||
this.headerLeft.set(headerLeft);
|
||||
}
|
||||
|
||||
public Node getHeaderRight() {
|
||||
return headerRight.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> headerRightProperty() {
|
||||
return headerRight;
|
||||
}
|
||||
|
||||
public void setHeaderRight(Node headerRight) {
|
||||
this.headerRight.set(headerRight);
|
||||
}
|
||||
}
|
||||
@@ -21,18 +21,25 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.paint.Paint;
|
||||
|
||||
public class IconedItem extends RipplerContainer {
|
||||
|
||||
private Label label;
|
||||
|
||||
public IconedItem(Node icon, String text) {
|
||||
super(createHBox(icon, text));
|
||||
this(icon);
|
||||
label.setText(text);
|
||||
}
|
||||
|
||||
private static HBox createHBox(Node icon, String text) {
|
||||
public IconedItem(Node icon) {
|
||||
super(createHBox(icon));
|
||||
label = ((Label) lookup("#label"));
|
||||
}
|
||||
|
||||
private static HBox createHBox(Node icon) {
|
||||
HBox hBox = new HBox();
|
||||
icon.setMouseTransparent(true);
|
||||
Label textLabel = new Label(text);
|
||||
Label textLabel = new Label();
|
||||
textLabel.setId("label");
|
||||
textLabel.setAlignment(Pos.CENTER);
|
||||
textLabel.setMouseTransparent(true);
|
||||
@@ -42,11 +49,7 @@ public class IconedItem extends RipplerContainer {
|
||||
return hBox;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
((Label) lookup("#label")).setText(text);
|
||||
}
|
||||
|
||||
public void setTextFill(Paint paint) {
|
||||
((Label) lookup("#label")).setTextFill(paint);
|
||||
public Label getLabel() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
|
||||
}
|
||||
|
||||
private String getVersion(String id) {
|
||||
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion<?>) it).map(RemoteVersion::getSelfVersion).orElse(null);
|
||||
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion) it).map(RemoteVersion::getSelfVersion).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -49,16 +49,16 @@ public final class DownloadWizardProvider implements WizardProvider {
|
||||
GameBuilder builder = profile.getDependency().gameBuilder();
|
||||
|
||||
builder.name((String) settings.get("name"));
|
||||
builder.gameVersion(((RemoteVersion<?>) settings.get("game")).getGameVersion());
|
||||
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
|
||||
|
||||
if (settings.containsKey("forge"))
|
||||
builder.version((RemoteVersion<?>) settings.get("forge"));
|
||||
builder.version((RemoteVersion) settings.get("forge"));
|
||||
|
||||
if (settings.containsKey("liteloader"))
|
||||
builder.version((RemoteVersion<?>) settings.get("liteloader"));
|
||||
builder.version((RemoteVersion) settings.get("liteloader"));
|
||||
|
||||
if (settings.containsKey("optifine"))
|
||||
builder.version((RemoteVersion<?>) settings.get("optifine"));
|
||||
builder.version((RemoteVersion) settings.get("optifine"));
|
||||
|
||||
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions());
|
||||
}
|
||||
|
||||
@@ -85,16 +85,16 @@ public final class InstallerWizardProvider implements WizardProvider {
|
||||
settings.put("success_message", i18n("install.success"));
|
||||
settings.put("failure_message", i18n("install.failed"));
|
||||
|
||||
Task ret = Task.empty();
|
||||
Task ret = Task.ofResult("version", () -> version);
|
||||
|
||||
if (settings.containsKey("forge"))
|
||||
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("forge")));
|
||||
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
|
||||
|
||||
if (settings.containsKey("liteloader"))
|
||||
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("liteloader")));
|
||||
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
|
||||
|
||||
if (settings.containsKey("optifine"))
|
||||
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("optifine")));
|
||||
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
|
||||
|
||||
return ret.then(profile.getRepository().refreshVersionsAsync());
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class InstallersPage extends StackPane implements WizardPage {
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/installers.fxml");
|
||||
|
||||
String gameVersion = ((RemoteVersion<?>) controller.getSettings().get("game")).getGameVersion();
|
||||
String gameVersion = ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
|
||||
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
|
||||
hasVersion.setMessage(i18n("install.new_game.already_exists"));
|
||||
txtName.getValidators().add(hasVersion);
|
||||
@@ -103,7 +103,7 @@ public class InstallersPage extends StackPane implements WizardPage {
|
||||
}
|
||||
|
||||
private String getVersion(String id) {
|
||||
return ((RemoteVersion<?>) controller.getSettings().get(id)).getSelfVersion();
|
||||
return ((RemoteVersion) controller.getSettings().get(id)).getSelfVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -98,6 +98,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
|
||||
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
|
||||
} catch (UnsupportedModpackException e) {
|
||||
txtModpackName.setText(i18n("modpack.task.install.error"));
|
||||
btnInstall.setDisable(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
|
||||
import org.jackhuang.hmcl.download.game.GameVersionList;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
@@ -79,7 +78,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
|
||||
|
||||
if (versionList instanceof GameVersionList) {
|
||||
if (versionList.hasType()) {
|
||||
centrePane.getChildren().setAll(checkPane, list);
|
||||
} else
|
||||
centrePane.getChildren().setAll(list);
|
||||
@@ -99,21 +98,19 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
|
||||
}
|
||||
|
||||
private List<VersionsPageItem> loadVersions() {
|
||||
boolean isGameVersionList = versionList instanceof GameVersionList;
|
||||
return versionList.getVersions(gameVersion).stream()
|
||||
.filter(it -> {
|
||||
if (isGameVersionList)
|
||||
switch (((GameRemoteVersionTag) it.getTag()).getType()) {
|
||||
case RELEASE:
|
||||
return chkRelease.isSelected();
|
||||
case SNAPSHOT:
|
||||
return chkSnapshot.isSelected();
|
||||
default:
|
||||
return chkOld.isSelected();
|
||||
}
|
||||
else return true;
|
||||
switch (it.getVersionType()) {
|
||||
case RELEASE:
|
||||
return chkRelease.isSelected();
|
||||
case SNAPSHOT:
|
||||
return chkSnapshot.isSelected();
|
||||
case OLD:
|
||||
return chkOld.isSelected();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted()
|
||||
.map(VersionsPageItem::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersionTag;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -37,7 +38,7 @@ import java.util.Objects;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class VersionsPageItem extends StackPane {
|
||||
private final RemoteVersion<?> remoteVersion;
|
||||
private final RemoteVersion remoteVersion;
|
||||
@FXML
|
||||
private Label lblSelfVersion;
|
||||
@FXML
|
||||
@@ -49,14 +50,14 @@ public final class VersionsPageItem extends StackPane {
|
||||
@FXML
|
||||
private StackPane imageViewContainer;
|
||||
|
||||
public VersionsPageItem(RemoteVersion<?> remoteVersion) {
|
||||
public VersionsPageItem(RemoteVersion remoteVersion) {
|
||||
this.remoteVersion = Objects.requireNonNull(remoteVersion);
|
||||
|
||||
FXUtils.loadFXML(this, "/assets/fxml/download/versions-list-item.fxml");
|
||||
lblSelfVersion.setText(remoteVersion.getSelfVersion());
|
||||
|
||||
if (remoteVersion.getTag() instanceof GameRemoteVersionTag) {
|
||||
switch (((GameRemoteVersionTag) remoteVersion.getTag()).getType()) {
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
lblGameVersion.setText(i18n("version.game.release"));
|
||||
imageView.setImage(new Image("/assets/img/icon.png", 32, 32, false, true));
|
||||
@@ -70,13 +71,13 @@ public final class VersionsPageItem extends StackPane {
|
||||
imageView.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
|
||||
break;
|
||||
}
|
||||
} else if (remoteVersion.getTag() instanceof LiteLoaderRemoteVersionTag) {
|
||||
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
|
||||
imageView.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
|
||||
lblGameVersion.setText(remoteVersion.getGameVersion());
|
||||
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
|
||||
// optifine has no icon.
|
||||
lblGameVersion.setText(remoteVersion.getGameVersion());
|
||||
} else { // forge
|
||||
} else if (remoteVersion instanceof ForgeRemoteVersion) {
|
||||
imageView.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
|
||||
lblGameVersion.setText(remoteVersion.getGameVersion());
|
||||
}
|
||||
@@ -84,7 +85,7 @@ public final class VersionsPageItem extends StackPane {
|
||||
leftPane.getChildren().remove(imageViewContainer);
|
||||
}
|
||||
|
||||
public RemoteVersion<?> getRemoteVersion() {
|
||||
public RemoteVersion getRemoteVersion() {
|
||||
return remoteVersion;
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.ZipEngine;
|
||||
import org.jackhuang.hmcl.util.Zipper;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
@@ -81,7 +81,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
dependency = dependency.then(Task.of(() -> {
|
||||
boolean flag = true;
|
||||
|
||||
try (ZipEngine zip = new ZipEngine(modpackFile)) {
|
||||
try (Zipper zip = new Zipper(modpackFile.toPath())) {
|
||||
Config exported = new Config();
|
||||
exported.setBackgroundImageType(config().getBackgroundImageType());
|
||||
exported.setBackgroundImage(config().getBackgroundImage());
|
||||
@@ -93,7 +93,7 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
|
||||
File bg = new File("bg").getAbsoluteFile();
|
||||
if (bg.isDirectory())
|
||||
zip.putDirectory(bg);
|
||||
zip.putDirectory(bg.toPath(), "bg");
|
||||
|
||||
File background_png = new File("background.png").getAbsoluteFile();
|
||||
if (background_png.isFile())
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Pack200;
|
||||
import java.util.logging.Level;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class AppDataUpgrader extends IUpgrader {
|
||||
|
||||
private void launchNewerVersion(List<String> args, File jar) throws IOException, ReflectiveOperationException {
|
||||
try (JarFile jarFile = new JarFile(jar)) {
|
||||
String mainClass = jarFile.getManifest().getMainAttributes().getValue("Main-Class");
|
||||
if (mainClass == null)
|
||||
throw new ClassNotFoundException("Main-Class not found in manifest");
|
||||
ArrayList<String> al = new ArrayList<>(args);
|
||||
al.add("--noupdate");
|
||||
ClassLoader pre = Thread.currentThread().getContextClassLoader();
|
||||
try {
|
||||
Logging.stop();
|
||||
ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
|
||||
Thread.currentThread().setContextClassLoader(now);
|
||||
now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
|
||||
} finally {
|
||||
Logging.start(Launcher.LOG_DIRECTORY);
|
||||
Thread.currentThread().setContextClassLoader(pre);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseArguments(VersionNumber nowVersion, List<String> args) {
|
||||
File f = AppDataUpgraderPackGzTask.HMCL_VER_FILE;
|
||||
if (!args.contains("--noupdate"))
|
||||
try {
|
||||
if (f.exists()) {
|
||||
Map<String, String> m = Constants.GSON.fromJson(FileUtils.readText(f), new TypeToken<Map<String, String>>() {
|
||||
}.getType());
|
||||
String s = m.get("ver");
|
||||
if (s != null && VersionNumber.asVersion(s).compareTo(nowVersion) > 0) {
|
||||
String j = m.get("loc");
|
||||
if (j != null) {
|
||||
File jar = new File(j);
|
||||
if (jar.exists()) {
|
||||
launchNewerVersion(args, jar);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (JsonParseException ex) {
|
||||
f.delete();
|
||||
} catch (IOException | ReflectiveOperationException t) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to execute newer version application", t);
|
||||
AppDataUpgraderPackGzTask.HMCL_VER_FILE.delete(); // delete version json, let HMCL re-download the newer version.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(UpdateChecker checker, VersionNumber ver) {
|
||||
if (!(ver instanceof IntVersionNumber))
|
||||
return;
|
||||
IntVersionNumber version = (IntVersionNumber) ver;
|
||||
checker.requestDownloadLink().then(Task.of(variables -> {
|
||||
Map<String, String> map = variables.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID);
|
||||
|
||||
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
|
||||
try {
|
||||
String hash = null;
|
||||
if (map.containsKey("jarsha1"))
|
||||
hash = map.get("jarsha1");
|
||||
Task task = new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash);
|
||||
TaskExecutor executor = task.executor();
|
||||
AtomicReference<Region> region = new AtomicReference<>();
|
||||
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
|
||||
if (executor.test()) {
|
||||
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
|
||||
.directory(new File("").getAbsoluteFile()).start();
|
||||
System.exit(0);
|
||||
}
|
||||
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
|
||||
}
|
||||
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
|
||||
try {
|
||||
String hash = null;
|
||||
if (map.containsKey("packsha1"))
|
||||
hash = map.get("packsha1");
|
||||
Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash);
|
||||
TaskExecutor executor = task.executor();
|
||||
AtomicReference<Region> region = new AtomicReference<>();
|
||||
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
|
||||
if (executor.test()) {
|
||||
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
|
||||
.directory(new File("").getAbsoluteFile()).start();
|
||||
System.exit(0);
|
||||
}
|
||||
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
|
||||
}
|
||||
else {
|
||||
String url = Launcher.PUBLISH;
|
||||
if (map != null)
|
||||
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
|
||||
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
|
||||
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
|
||||
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(url));
|
||||
} catch (URISyntaxException | IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
|
||||
OperatingSystem.setClipboard(url);
|
||||
MessageBox.show(i18n("update.no_browser"));
|
||||
}
|
||||
}
|
||||
})).start();
|
||||
}
|
||||
|
||||
public static class AppDataUpgraderPackGzTask extends Task {
|
||||
|
||||
public static final File BASE_FOLDER = Launcher.HMCL_DIRECTORY;
|
||||
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
|
||||
|
||||
public static File getSelf(String ver) {
|
||||
return new File(BASE_FOLDER, "HMCL-" + ver + ".jar");
|
||||
}
|
||||
|
||||
private final URL downloadLink;
|
||||
private final String newestVersion, hash;
|
||||
File tempFile;
|
||||
|
||||
public AppDataUpgraderPackGzTask(URL downloadLink, String newestVersion, String hash) throws IOException {
|
||||
this.downloadLink = downloadLink;
|
||||
this.newestVersion = newestVersion;
|
||||
this.hash = hash;
|
||||
tempFile = File.createTempFile("hmcl", ".pack.gz");
|
||||
|
||||
setName("Upgrade");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependents() {
|
||||
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
HashMap<String, String> json = new HashMap<>();
|
||||
File f = getSelf(newestVersion);
|
||||
if (!FileUtils.makeDirectory(f.getParentFile()))
|
||||
throw new IOException("Failed to make directories: " + f.getParent());
|
||||
|
||||
for (int i = 0; f.exists() && !f.delete(); i++)
|
||||
f = new File(BASE_FOLDER, "HMCL-" + newestVersion + (i > 0 ? "-" + i : "") + ".jar");
|
||||
if (!f.createNewFile())
|
||||
throw new IOException("Failed to create new file: " + f);
|
||||
|
||||
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(f))) {
|
||||
Pack200.newUnpacker().unpack(new GZIPInputStream(new FileInputStream(tempFile)), jos);
|
||||
}
|
||||
json.put("ver", newestVersion);
|
||||
json.put("loc", f.getAbsolutePath());
|
||||
String result = Constants.GSON.toJson(json);
|
||||
FileUtils.writeText(HMCL_VER_FILE, result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class AppDataUpgraderJarTask extends Task {
|
||||
|
||||
public static final File BASE_FOLDER = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
|
||||
|
||||
public static File getSelf(String ver) {
|
||||
return new File(BASE_FOLDER, "HMCL-" + ver + ".jar");
|
||||
}
|
||||
|
||||
private final URL downloadLink;
|
||||
private final String newestVersion, hash;
|
||||
File tempFile;
|
||||
|
||||
public AppDataUpgraderJarTask(URL downloadLink, String newestVersion, String hash) throws IOException {
|
||||
this.downloadLink = downloadLink;
|
||||
this.newestVersion = newestVersion;
|
||||
this.hash = hash;
|
||||
tempFile = File.createTempFile("hmcl", ".jar");
|
||||
|
||||
setName("Upgrade");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependents() {
|
||||
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
HashMap<String, String> json = new HashMap<>();
|
||||
File f = getSelf(newestVersion);
|
||||
FileUtils.copyFile(tempFile, f);
|
||||
json.put("ver", newestVersion);
|
||||
json.put("loc", f.getAbsolutePath());
|
||||
String result = Constants.GSON.toJson(json);
|
||||
FileUtils.writeText(HMCL_VER_FILE, result);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
import static java.nio.file.StandardOpenOption.READ;
|
||||
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||
import static java.nio.file.StandardOpenOption.WRITE;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileChannel.MapMode;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
|
||||
/**
|
||||
* Helper class for adding/removing executable header from HMCL file.
|
||||
*
|
||||
* @author yushijinhun
|
||||
*/
|
||||
final class ExecutableHeaderHelper {
|
||||
private ExecutableHeaderHelper() {}
|
||||
|
||||
private static Map<String, String> suffix2header = mapOf(
|
||||
pair("exe", "assets/HMCLauncher.exe"));
|
||||
|
||||
private static Optional<String> getSuffix(Path file) {
|
||||
String filename = file.getFileName().toString();
|
||||
int idxDot = filename.lastIndexOf('.');
|
||||
if (idxDot < 0) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(filename.substring(idxDot + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<byte[]> readHeader(ZipFile zip, String suffix) throws IOException {
|
||||
String location = suffix2header.get(suffix);
|
||||
if (location != null) {
|
||||
ZipEntry entry = zip.getEntry(location);
|
||||
if (entry != null && !entry.isDirectory()) {
|
||||
try (InputStream in = zip.getInputStream(entry)) {
|
||||
return Optional.of(IOUtils.readFullyAsByteArray(in));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static int detectHeaderLength(ZipFile zip, FileChannel channel) throws IOException {
|
||||
ByteBuffer buf = channel.map(MapMode.READ_ONLY, 0, channel.size());
|
||||
suffixLoop: for (String suffix : suffix2header.keySet()) {
|
||||
Optional<byte[]> header = readHeader(zip, suffix);
|
||||
if (header.isPresent()) {
|
||||
buf.rewind();
|
||||
for (byte b : header.get()) {
|
||||
if (!buf.hasRemaining() || b != buf.get()) {
|
||||
continue suffixLoop;
|
||||
}
|
||||
}
|
||||
return header.get().length;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the executable and removes its header.
|
||||
*/
|
||||
public static void copyWithoutHeader(Path from, Path to) throws IOException {
|
||||
try (
|
||||
FileChannel in = FileChannel.open(from, READ);
|
||||
FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING);
|
||||
ZipFile zip = new ZipFile(from.toFile())
|
||||
) {
|
||||
in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the executable and appends the header according to the suffix.
|
||||
*/
|
||||
public static void copyWithHeader(Path from, Path to) throws IOException {
|
||||
try (
|
||||
FileChannel in = FileChannel.open(from, READ);
|
||||
FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING);
|
||||
ZipFile zip = new ZipFile(from.toFile())
|
||||
) {
|
||||
Optional<String> suffix = getSuffix(to);
|
||||
if (suffix.isPresent()) {
|
||||
Optional<byte[]> header = readHeader(zip, suffix.get());
|
||||
if (header.isPresent()) {
|
||||
out.write(ByteBuffer.wrap(header.get()));
|
||||
}
|
||||
}
|
||||
|
||||
in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.logging.Level;
|
||||
import java.util.TreeMap;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
|
||||
/**
|
||||
* A class that checks the integrity of HMCL.
|
||||
*
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public final class IntegrityChecker {
|
||||
private IntegrityChecker() {}
|
||||
|
||||
private static final String SIGNATURE_FILE = "META-INF/hmcl_signature";
|
||||
private static final String PUBLIC_KEY_FILE = "assets/hmcl_signature_publickey.der";
|
||||
|
||||
private static PublicKey getPublicKey() throws IOException {
|
||||
try (InputStream in = IntegrityChecker.class.getResourceAsStream("/" + PUBLIC_KEY_FILE)) {
|
||||
if (in == null) {
|
||||
throw new IOException("Public key not found");
|
||||
}
|
||||
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFullyAsByteArray(in)));
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("Failed to load public key", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean verifyJar(Path jarPath) throws IOException {
|
||||
PublicKey publickey = getPublicKey();
|
||||
|
||||
byte[] signature = null;
|
||||
Map<String, byte[]> fileFingerprints = new TreeMap<>();
|
||||
try (ZipFile zip = new ZipFile(jarPath.toFile())) {
|
||||
for (ZipEntry entry : zip.stream().toArray(ZipEntry[]::new)) {
|
||||
String filename = entry.getName();
|
||||
try (InputStream in = zip.getInputStream(entry)) {
|
||||
if (SIGNATURE_FILE.equals(filename)) {
|
||||
signature = IOUtils.readFullyAsByteArray(in);
|
||||
} else {
|
||||
fileFingerprints.put(filename, DigestUtils.digest("SHA-512", in));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (signature == null) {
|
||||
throw new IOException("Signature is missing");
|
||||
}
|
||||
|
||||
try {
|
||||
Signature verifier = Signature.getInstance("SHA512withRSA");
|
||||
verifier.initVerify(publickey);
|
||||
for (Entry<String, byte[]> entry : fileFingerprints.entrySet()) {
|
||||
verifier.update(DigestUtils.digest("SHA-512", entry.getKey().getBytes(UTF_8)));
|
||||
verifier.update(entry.getValue());
|
||||
}
|
||||
return verifier.verify(signature);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("Failed to verify signature", e);
|
||||
}
|
||||
}
|
||||
|
||||
static void requireVerifiedJar(Path jar) throws IOException {
|
||||
if (!verifyJar(jar)) {
|
||||
throw new IOException("Invalid signature: " + jar);
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean selfVerified = null;
|
||||
|
||||
/**
|
||||
* Checks whether the current application is verified.
|
||||
* This method is blocking.
|
||||
*/
|
||||
public static synchronized boolean isSelfVerified() {
|
||||
if (selfVerified != null) {
|
||||
return selfVerified;
|
||||
}
|
||||
try {
|
||||
verifySelf();
|
||||
LOG.info("Successfully verified current JAR");
|
||||
selfVerified = true;
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to verify myself, is the JAR corrupt?", e);
|
||||
selfVerified = false;
|
||||
}
|
||||
return selfVerified;
|
||||
}
|
||||
|
||||
private static void verifySelf() throws IOException {
|
||||
Path self = LocalVersion.current().orElseThrow(() -> new IOException("Failed to find myself"))
|
||||
.getLocation();
|
||||
requireVerifiedJar(self);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.JarUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Pack200;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
/**
|
||||
* A class used to manage the local HMCL repository.
|
||||
*
|
||||
* @author yushijinhun
|
||||
*/
|
||||
final class LocalRepository {
|
||||
private LocalRepository() {}
|
||||
|
||||
private static Path localStorage = Launcher.HMCL_DIRECTORY.toPath().resolve("hmcl.jar");
|
||||
private static Path hmclVersionJson = Launcher.HMCL_DIRECTORY.toPath().resolve("hmclver.json");
|
||||
|
||||
/**
|
||||
* Gets the current stored executable in local repository.
|
||||
*/
|
||||
public static Optional<LocalVersion> getStored() {
|
||||
if (!Files.isRegularFile(localStorage)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(localStorage)
|
||||
.flatMap(JarUtils::getImplementationVersion)
|
||||
.map(version -> new LocalVersion(version, localStorage));
|
||||
}
|
||||
|
||||
private static void writeToStorage(Path source, boolean checkHeaders) throws IOException {
|
||||
IntegrityChecker.requireVerifiedJar(source);
|
||||
Files.createDirectories(localStorage.getParent());
|
||||
if (checkHeaders) {
|
||||
ExecutableHeaderHelper.copyWithoutHeader(source, localStorage);
|
||||
} else {
|
||||
Files.copy(source, localStorage, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
Optional<LocalVersion> stored = getStored();
|
||||
if (stored.isPresent()) {
|
||||
Map<String, String> json = new HashMap<>();
|
||||
json.put("ver", stored.get().getVersion());
|
||||
json.put("loc", stored.get().getLocation().toString());
|
||||
FileUtils.writeText(hmclVersionJson.toFile(), Constants.GSON.toJson(json));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a task that downloads the given version to local repository.
|
||||
*/
|
||||
public static FileDownloadTask downloadFromRemote(RemoteVersion version) throws IOException {
|
||||
Path downloaded = Files.createTempFile("hmcl-update-", null);
|
||||
return new FileDownloadTask(new URL(version.getUrl()), downloaded.toFile(), version.getIntegrityCheck()) {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
super.execute();
|
||||
|
||||
try {
|
||||
switch (version.getType()) {
|
||||
case JAR:
|
||||
writeToStorage(downloaded, false);
|
||||
break;
|
||||
|
||||
case PACK_XZ:
|
||||
Path unpacked = Files.createTempFile("hmcl-update-unpack-", null);
|
||||
try {
|
||||
try (InputStream in = new XZInputStream(Files.newInputStream(downloaded));
|
||||
JarOutputStream out = new JarOutputStream(Files.newOutputStream(unpacked))) {
|
||||
Pack200.newUnpacker().unpack(in, out);
|
||||
}
|
||||
writeToStorage(unpacked, false);
|
||||
} finally {
|
||||
Files.deleteIfExists(unpacked);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown type: " + version.getType());
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(downloaded);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the current HMCL executable to local repository.
|
||||
*/
|
||||
public static void downloadFromCurrent() {
|
||||
Optional<LocalVersion> current = LocalVersion.current();
|
||||
if (current.isPresent()) {
|
||||
Path currentPath = current.get().getLocation();
|
||||
if (!Files.isRegularFile(currentPath)) {
|
||||
LOG.warning("Failed to download " + current.get() + ", it isn't a file");
|
||||
return;
|
||||
}
|
||||
if (isSameAsLocalStorage(currentPath)) {
|
||||
LOG.warning("Trying to download from self, ignored");
|
||||
return;
|
||||
}
|
||||
LOG.info("Downloading " + current.get());
|
||||
try {
|
||||
writeToStorage(current.get().getLocation(), true);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to download " + current.get(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the executable stored in local repository to the given location.
|
||||
*/
|
||||
public static void applyTo(Path target) throws IOException {
|
||||
if (isSameAsLocalStorage(target)) {
|
||||
throw new IOException("Cannot apply update to self");
|
||||
}
|
||||
|
||||
LOG.info("Applying update to " + target);
|
||||
IntegrityChecker.requireVerifiedJar(localStorage);
|
||||
ExecutableHeaderHelper.copyWithHeader(localStorage, target);
|
||||
}
|
||||
|
||||
private static boolean isSameAsLocalStorage(Path path) {
|
||||
return path.toAbsolutePath().equals(localStorage.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
+22
-23
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
*
|
||||
* 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
|
||||
@@ -15,39 +15,38 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.VersionNumber;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.JarUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* Result: Deny if do not upgrade HMCL.
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class OutOfDateEvent extends Event {
|
||||
private final VersionNumber version;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public OutOfDateEvent(Object source, VersionNumber version) {
|
||||
super(source);
|
||||
this.version = version;
|
||||
class LocalVersion {
|
||||
|
||||
public static Optional<LocalVersion> current() {
|
||||
return JarUtils.thisJar().map(path -> new LocalVersion(Metadata.VERSION, path));
|
||||
}
|
||||
|
||||
public VersionNumber getVersion() {
|
||||
private String version;
|
||||
private Path location;
|
||||
|
||||
public LocalVersion(String version, Path location) {
|
||||
this.version = version;
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return true;
|
||||
public Path getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("source", getSource())
|
||||
.append("version", getVersion())
|
||||
.toString();
|
||||
return "[" + version + " at " + location + "]";
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.VersionNumber;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class NewFileUpgrader extends IUpgrader {
|
||||
|
||||
@Override
|
||||
public void parseArguments(VersionNumber nowVersion, List<String> args) {
|
||||
int i = args.indexOf("--removeOldLauncher");
|
||||
if (i != -1 && i < args.size() - 1) {
|
||||
File f = new File(args.get(i + 1));
|
||||
if (f.exists())
|
||||
f.deleteOnExit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download(UpdateChecker checker, VersionNumber version) {
|
||||
URL url = requestDownloadLink();
|
||||
if (url == null) return;
|
||||
File newf = new File(url.getFile());
|
||||
Controllers.dialog(i18n("message.downloading"));
|
||||
Task task = new FileDownloadTask(url, newf);
|
||||
TaskExecutor executor = task.executor();
|
||||
AtomicReference<Region> region = new AtomicReference<>();
|
||||
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
|
||||
if (executor.test()) {
|
||||
try {
|
||||
new ProcessBuilder(newf.getCanonicalPath(), "--removeOldLauncher", getRealPath())
|
||||
.directory(new File("").getAbsoluteFile())
|
||||
.start();
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to start new app", ex);
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
|
||||
}
|
||||
|
||||
private static String getRealPath() {
|
||||
String realPath = NewFileUpgrader.class.getClassLoader().getResource("").getFile();
|
||||
File file = new File(realPath);
|
||||
realPath = file.getAbsolutePath();
|
||||
try {
|
||||
realPath = java.net.URLDecoder.decode(realPath, UTF_8.name());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return realPath;
|
||||
}
|
||||
|
||||
private URL requestDownloadLink() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.util.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
public class RemoteVersion {
|
||||
|
||||
public static RemoteVersion fetch(String url) throws IOException {
|
||||
try {
|
||||
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
|
||||
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
|
||||
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
|
||||
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
|
||||
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
|
||||
if (packXZUrl != null && packXZHash != null) {
|
||||
return new RemoteVersion(version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash));
|
||||
} else if (jarUrl != null && jarHash != null) {
|
||||
return new RemoteVersion(version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash));
|
||||
} else {
|
||||
throw new IOException("No download url is available");
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
throw new IOException("Malformed response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String version;
|
||||
private String url;
|
||||
private Type type;
|
||||
private IntegrityCheck integrityCheck;
|
||||
|
||||
public RemoteVersion(String version, String url, Type type, IntegrityCheck integrityCheck) {
|
||||
this.version = version;
|
||||
this.url = url;
|
||||
this.type = type;
|
||||
this.integrityCheck = integrityCheck;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public IntegrityCheck getIntegrityCheck() {
|
||||
return integrityCheck;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + version + " from " + url + "]";
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
PACK_XZ,
|
||||
JAR
|
||||
}
|
||||
}
|
||||
+8
-24
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
*
|
||||
* 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
|
||||
@@ -17,29 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.util.VersionNumber;
|
||||
public enum UpdateChannel {
|
||||
STABLE("stable"),
|
||||
DEVELOPMENT("dev");
|
||||
|
||||
import java.util.List;
|
||||
public final String channelName;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public abstract class IUpgrader {
|
||||
|
||||
/**
|
||||
* Paring arguments to decide on whether the upgrade is needed.
|
||||
*
|
||||
* @param nowVersion now launcher version
|
||||
* @param args Application CommandLine Arguments
|
||||
*/
|
||||
public abstract void parseArguments(VersionNumber nowVersion, List<String> args);
|
||||
|
||||
/**
|
||||
* Just download the new app.
|
||||
*
|
||||
* @param checker Should be VersionChecker
|
||||
* @param version the newest version
|
||||
*/
|
||||
public abstract void download(UpdateChecker checker, VersionNumber version);
|
||||
UpdateChannel(String channelName) {
|
||||
this.channelName = channelName;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
*
|
||||
* 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
|
||||
@@ -17,144 +17,119 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.OutOfDateEvent;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.VersionNumber;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.VersionNumber.asVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
|
||||
public final class UpdateChecker {
|
||||
private UpdateChecker() {}
|
||||
|
||||
private volatile boolean outOfDate = false;
|
||||
private final VersionNumber base;
|
||||
private String versionString;
|
||||
private Map<String, String> download_link = null;
|
||||
|
||||
public UpdateChecker(VersionNumber base) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
private VersionNumber value;
|
||||
|
||||
public boolean isOutOfDate() {
|
||||
return outOfDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the version number synchronously. When you execute this method
|
||||
* first, should leave "showMessage" false.
|
||||
*
|
||||
* @param showMessage If it is requested to warn the user that there is a
|
||||
* new version.
|
||||
*
|
||||
* @return the process observable.
|
||||
*/
|
||||
public TaskResult<VersionNumber> process(final boolean showMessage) {
|
||||
return new TaskResult<VersionNumber>() {
|
||||
GetTask http = new GetTask(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update.php?version=" + Launcher.VERSION));
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task> getDependents() {
|
||||
return value == null ? Collections.singleton(http) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (isDevelopmentVersion(Launcher.VERSION)) {
|
||||
LOG.info("Current version is a development version, skip updating");
|
||||
return;
|
||||
private static ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
|
||||
private static BooleanBinding outdated = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
RemoteVersion latest = latestVersion.get();
|
||||
if (latest == null || isDevelopmentVersion(Metadata.VERSION)) {
|
||||
return false;
|
||||
} else {
|
||||
return asVersion(latest.getVersion()).compareTo(asVersion(Metadata.VERSION)) > 0;
|
||||
}
|
||||
},
|
||||
latestVersion);
|
||||
private static ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
|
||||
|
||||
if (value == null) {
|
||||
versionString = http.getResult();
|
||||
value = VersionNumber.asVersion(versionString);
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
LOG.warning("Unable to check update...");
|
||||
if (showMessage)
|
||||
MessageBox.show(i18n("update.failed"));
|
||||
} else if (base.compareTo(value) < 0)
|
||||
outOfDate = true;
|
||||
if (outOfDate)
|
||||
setResult(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "update_checker.process";
|
||||
}
|
||||
};
|
||||
public static void init() {
|
||||
ConfigHolder.config().updateChannelProperty().addListener(onInvalidating(UpdateChecker::requestCheckUpdate));
|
||||
requestCheckUpdate();
|
||||
}
|
||||
|
||||
private boolean isDevelopmentVersion(String version) {
|
||||
return version.contains("@") || // eg. @HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@
|
||||
public static RemoteVersion getLatestVersion() {
|
||||
return latestVersion.get();
|
||||
}
|
||||
|
||||
public static ReadOnlyObjectProperty<RemoteVersion> latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public static boolean isOutdated() {
|
||||
return outdated.get();
|
||||
}
|
||||
|
||||
public static ObservableBooleanValue outdatedProperty() {
|
||||
return outdated;
|
||||
}
|
||||
|
||||
public static boolean isCheckingUpdate() {
|
||||
return checkingUpdate.get();
|
||||
}
|
||||
|
||||
public static ReadOnlyBooleanProperty checkingUpdateProperty() {
|
||||
return checkingUpdate.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
|
||||
if (!IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Self verification failed");
|
||||
}
|
||||
|
||||
String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf(
|
||||
pair("version", Metadata.VERSION),
|
||||
pair("channel", channel.channelName)));
|
||||
|
||||
return RemoteVersion.fetch(url);
|
||||
}
|
||||
|
||||
private static boolean isDevelopmentVersion(String version) {
|
||||
return version.contains("@") || // eg. @develop@
|
||||
version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the <b>cached</b> newest version number, use "process" method to
|
||||
* download!
|
||||
*
|
||||
* @return the newest version number
|
||||
*
|
||||
* @see #process(boolean)
|
||||
*/
|
||||
public VersionNumber getNewVersion() {
|
||||
return value;
|
||||
}
|
||||
public static void requestCheckUpdate() {
|
||||
Platform.runLater(() -> {
|
||||
if (isCheckingUpdate())
|
||||
return;
|
||||
checkingUpdate.set(true);
|
||||
UpdateChannel channel = config().getUpdateChannel();
|
||||
|
||||
/**
|
||||
* Get the download links.
|
||||
*
|
||||
* @return a JSON, which contains the server response.
|
||||
*/
|
||||
public synchronized TaskResult<Map<String, String>> requestDownloadLink() {
|
||||
return new TaskResult<Map<String, String>>() {
|
||||
@Override
|
||||
public void execute() {
|
||||
if (download_link == null) {
|
||||
try {
|
||||
download_link = Constants.GSON.<Map<String, String>>fromJson(NetworkUtils.doGet(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update_link.php")), Map.class);
|
||||
} catch (JsonSyntaxException | IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to get update link.", e);
|
||||
}
|
||||
thread(() -> {
|
||||
RemoteVersion result = null;
|
||||
try {
|
||||
result = checkUpdate(channel);
|
||||
LOG.info("Latest version (" + channel + ") is " + result);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to check for update", e);
|
||||
}
|
||||
setResult(download_link);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "update_checker.request_download_link";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static final String REQUEST_DOWNLOAD_LINK_ID = "update_checker.request_download_link";
|
||||
|
||||
public void checkOutdate() {
|
||||
if (outOfDate)
|
||||
if (EventBus.EVENT_BUS.fireEvent(new OutOfDateEvent(this, getNewVersion())) != Event.Result.DENY) {
|
||||
Launcher.UPGRADER.download(this, getNewVersion());
|
||||
}
|
||||
RemoteVersion finalResult = result;
|
||||
Platform.runLater(() -> {
|
||||
checkingUpdate.set(false);
|
||||
if (finalResult != null) {
|
||||
if (channel.equals(config().getUpdateChannel())) {
|
||||
latestVersion.set(finalResult);
|
||||
} else {
|
||||
// the channel has been changed during the period
|
||||
// check update again
|
||||
requestCheckUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
|
||||
import static org.jackhuang.hmcl.util.IntVersionNumber.isIntVersionNumber;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.VersionNumber.asVersion;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.util.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
public final class UpdateHandler {
|
||||
private UpdateHandler() {}
|
||||
|
||||
/**
|
||||
* @return whether to exit
|
||||
*/
|
||||
public static boolean processArguments(String[] args) {
|
||||
if (!isIntVersionNumber(Metadata.VERSION)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNestedApplication()) {
|
||||
// updated from old versions
|
||||
try {
|
||||
performMigration();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to perform migration", e);
|
||||
JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length == 2 && args[0].equals("--apply-to")) {
|
||||
try {
|
||||
applyUpdate(Paths.get(args[1]));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to apply update", e);
|
||||
JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isFirstLaunchAfterUpgrade()) {
|
||||
JOptionPane.showMessageDialog(null, i18n("fatal.migration_requires_manual_reboot"), "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
return true;
|
||||
}
|
||||
|
||||
Optional<LocalVersion> local = LocalRepository.getStored();
|
||||
if (local.isPresent()) {
|
||||
int difference = asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION));
|
||||
|
||||
if (difference < 0) {
|
||||
LocalRepository.downloadFromCurrent();
|
||||
|
||||
} else if (difference > 0) {
|
||||
Optional<LocalVersion> current = LocalVersion.current();
|
||||
if (current.isPresent() && IntegrityChecker.isSelfVerified()) {
|
||||
try {
|
||||
requestUpdate(local.get().getLocation(), current.get().getLocation());
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to update from local repository", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
LocalRepository.downloadFromCurrent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void requestUpdate(Path updateTo, Path self) throws IOException {
|
||||
IntegrityChecker.requireVerifiedJar(updateTo);
|
||||
startJava(updateTo, "--apply-to", self.toString());
|
||||
}
|
||||
|
||||
private static void applyUpdate(Path target) throws IOException {
|
||||
LocalRepository.applyTo(target);
|
||||
|
||||
Optional<String> newVersion = LocalRepository.getStored().map(LocalVersion::getVersion);
|
||||
if (newVersion.isPresent()) {
|
||||
Optional<Path> newFilename = tryRename(target, newVersion.get());
|
||||
if (newFilename.isPresent()) {
|
||||
LOG.info("Move " + target + " to " + newFilename.get());
|
||||
try {
|
||||
Files.move(target, newFilename.get());
|
||||
target = newFilename.get();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to move target", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG.warning("Failed to find local repository");
|
||||
}
|
||||
|
||||
startJava(target);
|
||||
}
|
||||
|
||||
private static void startJava(Path jar, String... appArgs) throws IOException {
|
||||
List<String> commandline = new ArrayList<>();
|
||||
commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath());
|
||||
commandline.add("-jar");
|
||||
commandline.add(jar.toAbsolutePath().toString());
|
||||
for (String arg : appArgs) {
|
||||
commandline.add(arg);
|
||||
}
|
||||
LOG.info("Starting process: " + commandline);
|
||||
new ProcessBuilder(commandline)
|
||||
.directory(Paths.get("").toAbsolutePath().toFile())
|
||||
.inheritIO()
|
||||
.start();
|
||||
}
|
||||
|
||||
public static void updateFrom(RemoteVersion version) {
|
||||
checkFxUserThread();
|
||||
|
||||
Task task;
|
||||
try {
|
||||
task = LocalRepository.downloadFromRemote(version);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to create upgrade download task", e);
|
||||
return;
|
||||
}
|
||||
TaskExecutor executor = task.executor();
|
||||
Region dialog = Controllers.taskDialog(executor, i18n("message.downloading"), "", null);
|
||||
thread(() -> {
|
||||
boolean success = executor.test();
|
||||
Platform.runLater(() -> dialog.fireEvent(new DialogCloseEvent()));
|
||||
if (success) {
|
||||
try {
|
||||
Optional<LocalVersion> current = LocalVersion.current();
|
||||
Optional<LocalVersion> stored = LocalRepository.getStored();
|
||||
if (!current.isPresent()) {
|
||||
throw new IOException("Failed to find current HMCL location");
|
||||
}
|
||||
if (!stored.isPresent()) {
|
||||
throw new IOException("Failed to find local repository, this shouldn't happen");
|
||||
}
|
||||
if (!IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Current JAR is not verified");
|
||||
}
|
||||
requestUpdate(stored.get().getLocation(), current.get().getLocation());
|
||||
System.exit(0);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to update to " + version, e);
|
||||
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Throwable e = task.getLastException();
|
||||
LOG.log(Level.WARNING, "Failed to update to " + version, e);
|
||||
Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageBox.ERROR_MESSAGE));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Optional<Path> tryRename(Path path, String newVersion) {
|
||||
String filename = path.getFileName().toString();
|
||||
Matcher matcher = Pattern.compile("^(?<prefix>[hH][mM][cC][lL][.-])(?<version>\\d+(?:\\.\\d+)*)(?<suffix>\\.[^.]+)$").matcher(filename);
|
||||
if (matcher.find()) {
|
||||
String newFilename = matcher.group("prefix") + newVersion + matcher.group("suffix");
|
||||
if (!newFilename.equals(filename)) {
|
||||
return Optional.of(path.resolveSibling(newFilename));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// ==== support for old versions ===
|
||||
private static void performMigration() throws IOException {
|
||||
LOG.info("Migrating from old versions");
|
||||
|
||||
Path location = getParentApplicationLocation()
|
||||
.orElseThrow(() -> new IOException("Failed to get parent application location"));
|
||||
|
||||
Optional<LocalVersion> local = LocalRepository.getStored();
|
||||
if (!local.isPresent() ||
|
||||
asVersion(local.get().getVersion()).compareTo(asVersion(Metadata.VERSION)) < 0) {
|
||||
LocalRepository.downloadFromCurrent();
|
||||
}
|
||||
local = LocalRepository.getStored();
|
||||
if (!local.isPresent()) {
|
||||
throw new IOException("Failed to find local repository");
|
||||
}
|
||||
|
||||
requestUpdate(local.get().getLocation(), location);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method must be called from the main thread.
|
||||
*/
|
||||
private static boolean isNestedApplication() {
|
||||
StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 0; i < stacktrace.length; i++) {
|
||||
StackTraceElement element = stacktrace[i];
|
||||
if (Main.class.getName().equals(element.getClassName())) {
|
||||
// we've reached the main method
|
||||
if (i + 1 == stacktrace.length) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Optional<Path> getParentApplicationLocation() {
|
||||
String command = System.getProperty("sun.java.command");
|
||||
if (command != null) {
|
||||
Path path = Paths.get(command);
|
||||
if (Files.isRegularFile(path)) {
|
||||
return Optional.of(path.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static boolean isFirstLaunchAfterUpgrade() {
|
||||
Optional<Path> currentPath = LocalVersion.current().map(LocalVersion::getLocation);
|
||||
if (currentPath.isPresent()) {
|
||||
Path updated = Launcher.HMCL_DIRECTORY.toPath().resolve("HMCL-" + Metadata.VERSION + ".jar");
|
||||
if (currentPath.get().toAbsolutePath().equals(updated.toAbsolutePath())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// ====
|
||||
}
|
||||
@@ -18,9 +18,13 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.ui.CrashWindow;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageBox;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
|
||||
import static java.util.Collections.newSetFromMap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -90,7 +94,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
CAUGHT_EXCEPTIONS.add(stackTrace);
|
||||
|
||||
String text = "---- Hello Minecraft! Crash Report ----\n" +
|
||||
" Version: " + Launcher.VERSION + "\n" +
|
||||
" Version: " + Metadata.VERSION + "\n" +
|
||||
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
|
||||
" Thread: " + t.toString() + "\n" +
|
||||
"\n Content: \n " +
|
||||
@@ -104,8 +108,9 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
if (checkThrowable(e)) {
|
||||
Platform.runLater(() -> new CrashWindow(text).show());
|
||||
if (!Launcher.UPDATE_CHECKER.isOutOfDate())
|
||||
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
|
||||
reportToServer(text);
|
||||
}
|
||||
}
|
||||
} catch (Throwable handlingException) {
|
||||
LOG.log(Level.SEVERE, "Unable to handle uncaught exception", handlingException);
|
||||
@@ -116,7 +121,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
Thread t = new Thread(() -> {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
map.put("crash_report", text);
|
||||
map.put("version", Launcher.VERSION);
|
||||
map.put("version", Metadata.VERSION);
|
||||
map.put("log", Logging.getLogs());
|
||||
try {
|
||||
String response = NetworkUtils.doPost(NetworkUtils.toURL("https://hmcl.huangyuhui.net/hmcl/crash.php"), map);
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,4 @@
|
||||
/*
|
||||
* Apply google Roboto font to all UI components
|
||||
* #5264AE
|
||||
*/
|
||||
|
||||
.root {
|
||||
-fx-base-font-family: "微软雅黑", "Microsoft YaHei", "Roboto";
|
||||
-fx-font-family: -fx-base-font-family;
|
||||
}
|
||||
|
||||
.disabled Label {
|
||||
|
||||
+2
-2
@@ -21,10 +21,10 @@
|
||||
</StackPane>
|
||||
<BorderPane style="-fx-padding: 0 0 0 10;">
|
||||
<top>
|
||||
<Label fx:id="lblVersionName" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
|
||||
<Label fx:id="lblTitle" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
|
||||
</top>
|
||||
<bottom>
|
||||
<Label fx:id="lblGameVersion" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
|
||||
<Label fx:id="lblSubtitle" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
</HBox>
|
||||
@@ -91,7 +91,6 @@
|
||||
<HBox fx:id="navRight" alignment="CENTER_LEFT">
|
||||
<JFXButton fx:id="refreshNavButton" onMouseClicked="#onRefresh"
|
||||
styleClass="jfx-decorator-button" ripplerFill="white" />
|
||||
<Rectangle height="${navBar.height}" width="1" fill="gray"/>
|
||||
</HBox>
|
||||
</right>
|
||||
</BorderPane>
|
||||
@@ -101,6 +100,7 @@
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="4.0"/>
|
||||
</padding>
|
||||
<Rectangle fx:id="separator" height="${navBar.height}" width="1" fill="gray"/>
|
||||
<JFXButton fx:id="btnMin" styleClass="jfx-decorator-button" ripplerFill="white"
|
||||
onAction="#onMin">
|
||||
</JFXButton>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import org.jackhuang.hmcl.ui.construct.*?>
|
||||
<?import org.jackhuang.hmcl.ui.FXUtils?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
type="StackPane">
|
||||
@@ -15,21 +16,28 @@
|
||||
<VBox fx:id="rootPane" style="-fx-padding: 20;">
|
||||
<ComponentList fx:id="settingsPane">
|
||||
|
||||
<BorderPane> <!-- Update -->
|
||||
<left>
|
||||
<ComponentSublist fx:id="updatePane" title="%update" hasSubtitle="true">
|
||||
<headerLeft>
|
||||
<VBox>
|
||||
<Label fx:id="lblUpdate" text="%update" BorderPane.alignment="CENTER_LEFT"/>
|
||||
<Label fx:id="lblUpdateSub" styleClass="subtitle-label"/>
|
||||
</VBox>
|
||||
</left>
|
||||
<right>
|
||||
</headerLeft>
|
||||
<headerRight>
|
||||
<JFXButton fx:id="btnUpdate" onMouseClicked="#onUpdate" styleClass="toggle-icon4">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/update.fxml" />
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
</right>
|
||||
</BorderPane>
|
||||
</headerRight>
|
||||
<VBox spacing="8">
|
||||
<JFXRadioButton fx:id="chkUpdateStable" text="%update.channel.stable" />
|
||||
<JFXRadioButton fx:id="chkUpdateDev" text="%update.channel.dev" />
|
||||
<VBox style="-fx-padding: 10 0 0 0;">
|
||||
<Text fx:id="lblUpdateNote" text="%update.note" />
|
||||
</VBox>
|
||||
</VBox>
|
||||
</ComponentSublist>
|
||||
|
||||
<MultiFileItem fx:id="fileCommonLocation" title="%launcher.common_directory" directory="true" chooserTitle="%launcher.common_directory.choose" hasSubtitle="true" customText="%settings.custom" />
|
||||
|
||||
@@ -55,7 +63,7 @@
|
||||
|
||||
<ComponentList title="%settings.launcher.proxy"> <!-- proxy -->
|
||||
<VBox spacing="10">
|
||||
<JFXCheckBox fx:id="chkEnableProxy" text="%settings.launcher.proxy.enable" />
|
||||
<JFXCheckBox fx:id="chkDisableProxy" text="%settings.launcher.proxy.disable" />
|
||||
<VBox fx:id="proxyPane" style="-fx-padding: 0 0 0 30;">
|
||||
<HBox>
|
||||
<JFXRadioButton fx:id="chkProxyHttp" text="%settings.launcher.proxy.http" />
|
||||
|
||||
Binary file not shown.
@@ -93,7 +93,9 @@ 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.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use HMCL, but HMCL will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause HMCL not to function properly.\nPlease upgrade your Java to 8u101 or later to resolve the problem.
|
||||
fatal.config_access_denied=The configuration at "%s" is not readable or writable.\nPlease grant HMCL read and write access to this file and its parent directory.
|
||||
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure HMCL has read and write access to "%s" and the files in it.
|
||||
fatal.migration_requires_manual_reboot=The upgrade is about to be completed. Please reopen HMCL.
|
||||
fatal.apply_update_failure=We're sorry that HMCL couldn't finish the upgrade because something went wrong.\nBut you can still manually finish the upgrade by downloading HMCL from %s.\nPlease consider reporting this issue to us.
|
||||
|
||||
folder.config=Configs
|
||||
folder.coremod=Core Mod
|
||||
@@ -179,7 +181,7 @@ message.error=Error
|
||||
message.info=Info
|
||||
message.success=Tasks succeeded
|
||||
message.unknown=Unknown
|
||||
|
||||
message.warning=Warning
|
||||
|
||||
modpack=Mod pack
|
||||
modpack.choose=Choose a modpack zip file which you want to install.
|
||||
@@ -291,7 +293,7 @@ settings.launcher.language=Language
|
||||
settings.launcher.log_font=Log Font
|
||||
settings.launcher.proxy=Proxy
|
||||
settings.launcher.proxy.authentication=Proxy Authentication
|
||||
settings.launcher.proxy.enable=Enable Proxy
|
||||
settings.launcher.proxy.disable=Use system proxies
|
||||
settings.launcher.proxy.host=Host
|
||||
settings.launcher.proxy.http=HTTP
|
||||
settings.launcher.proxy.password=Password
|
||||
@@ -309,9 +311,13 @@ settings.type=Version setting type
|
||||
settings.type.special=Specialized version settings(will not affect other versions)
|
||||
|
||||
update=Update
|
||||
update.failed=Failed to check for updates.
|
||||
update.channel.dev=Update to development version
|
||||
update.channel.stable=Update to stable version
|
||||
update.checking=Checking for updates
|
||||
update.failed=Failed to perform upgrade
|
||||
update.found=Update Available!
|
||||
update.newest_version=Latest version: %s
|
||||
update.note=Development version contains more functionality and bug fixes as well as more possible bugs. And this will affect all HMCL installations in your computer.
|
||||
update.latest=This is latest Version.
|
||||
update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update.
|
||||
update.tooltip=Update
|
||||
@@ -328,7 +334,9 @@ version.launch_script.save=Save the launch script
|
||||
version.launch_script.success=Finished script creation, %s.
|
||||
version.manage.redownload_assets_index=Redownload Assets Index
|
||||
version.manage.remove=Delete this game
|
||||
version.manage.remove.confirm=Sure to remove version %s?
|
||||
version.manage.remove.confirm=Sure to remove game %s? You cannot restore this game again!
|
||||
version.manage.remove.confirm.trash=Sure to remove game %s? You can restore the game with the name of %s in your system trash.
|
||||
version.manage.remove.confirm.independent=Since this game is in independent mode, deleting this game will result in all saved worlds belonging to this game removed simultaneously. Sure to remove game %s?
|
||||
version.manage.remove_libraries=Delete library files
|
||||
version.manage.rename=Rename this game
|
||||
version.manage.rename.message=Please enter the new name
|
||||
|
||||
@@ -93,7 +93,9 @@ extension.sh=Bash 腳本
|
||||
|
||||
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,請降級到 Java 8 或 10。\n如果您使用的是 OpenJDK,請確保其包含 OpenJFX。
|
||||
fatal.missing_dst_root_ca_x3=當前 Java 平台缺少 DST Root CA X3 證書。\n您依然可以使用 HMCL,但將無法連接到部分站點(如使用 Let's Encrypt 證書的站點),這可能會使 HMCL 無法正常工作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
|
||||
fatal.config_access_denied=HMCL 無法讀取或寫入位於 "%s" 的配置文件。\n請授予 HMCL 對該文件及其父目錄的讀寫權限。
|
||||
fatal.config_loading_failure=HMCL 無法加載配置文件。\n請確保 HMCL 對 "%s" 目錄及該目錄下的文件擁有讀寫權限。
|
||||
fatal.migration_requires_manual_reboot=HMCL 即將完成升級,請重新打開 HMCL。
|
||||
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們反饋該問題。
|
||||
|
||||
folder.config=配置文件夾
|
||||
folder.coremod=核心MOD文件夾
|
||||
@@ -179,7 +181,7 @@ message.error=錯誤
|
||||
message.info=提示
|
||||
message.success=已完成
|
||||
message.unknown=未知
|
||||
|
||||
message.warning=警告
|
||||
|
||||
modpack=整合包
|
||||
modpack.choose=選擇要安裝的遊戲整合包文件
|
||||
@@ -291,7 +293,7 @@ settings.launcher.language=語言
|
||||
settings.launcher.log_font=日誌字體
|
||||
settings.launcher.proxy=代理
|
||||
settings.launcher.proxy.authentication=身份驗證
|
||||
settings.launcher.proxy.enable=启用代理
|
||||
settings.launcher.proxy.disable=使用系統代理
|
||||
settings.launcher.proxy.host=主機
|
||||
settings.launcher.proxy.http=HTTP
|
||||
settings.launcher.proxy.password=密碼
|
||||
@@ -309,9 +311,13 @@ settings.type=版本設置類型
|
||||
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
|
||||
|
||||
update=啓動器更新
|
||||
update.failed=檢查更新失敗
|
||||
update.channel.dev=更新到開發版
|
||||
update.channel.stable=更新到推薦版本
|
||||
update.checking=正在檢查更新
|
||||
update.failed=更新失敗
|
||||
update.found=發現更新
|
||||
update.newest_version=最新版本爲:%s
|
||||
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。選擇更新到開發版導致你電腦中所有的HMCL更新至開發版
|
||||
update.latest=當前版本爲最新版本
|
||||
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼板了,您可以手動粘貼網址打開頁面
|
||||
update.tooltip=更新
|
||||
@@ -328,7 +334,9 @@ version.launch_script.save=保存啓動腳本
|
||||
version.launch_script.success=啓動腳本已生成完畢:%s
|
||||
version.manage.redownload_assets_index=重新下載資源配置(assets_index.json)
|
||||
version.manage.remove=刪除該版本
|
||||
version.manage.remove.confirm=真的要刪除版本 %s 嗎?
|
||||
version.manage.remove.confirm=真的要刪除版本 %s 嗎?你將無法找回被刪除的文件!
|
||||
version.manage.remove.confirm.trash=真的要刪除版本 %s 嗎?你可以在系統的回收站中恢復文件夾 %s 來找回該版本。
|
||||
version.manage.remove.confirm.independent=由於該遊戲使用了版本隔離,所以刪除該版本將導致該遊戲的存檔等數據一同被刪除,真的要刪除版本 %s 嗎?
|
||||
version.manage.remove_libraries=刪除所有庫文件
|
||||
version.manage.rename=重命名該版本
|
||||
version.manage.rename.message=請輸入要改成的名字
|
||||
|
||||
@@ -93,7 +93,9 @@ extension.sh=Bash 脚本
|
||||
|
||||
fatal.missing_javafx=JavaFX 缺失。\n如果您使用的是 Java 11 或更高版本,请降级到 Java 8 或 10。\n如果您使用的是 OpenJDK,请确保其包含 OpenJFX。
|
||||
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Let's Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
|
||||
fatal.config_access_denied=HMCL 无法读取或写入位于 "%s" 的配置文件。\n请授予 HMCL 对该文件及其父目录的读写权限。
|
||||
fatal.config_loading_failure=HMCL 无法加载配置文件。\n请确保 HMCL 对 "%s" 目录及该目录下的文件拥有读写权限。
|
||||
fatal.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。
|
||||
fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 HMCL 来完成升级。\n请考虑向我们反馈该问题。
|
||||
|
||||
folder.config=配置文件夹
|
||||
folder.coremod=核心MOD文件夹
|
||||
@@ -179,7 +181,7 @@ message.error=错误
|
||||
message.info=提示
|
||||
message.success=已完成
|
||||
message.unknown=未知
|
||||
|
||||
message.warning=警告
|
||||
|
||||
modpack=整合包
|
||||
modpack.choose=选择要安装的游戏整合包文件
|
||||
@@ -291,7 +293,7 @@ settings.launcher.language=语言
|
||||
settings.launcher.log_font=日志字体
|
||||
settings.launcher.proxy=代理
|
||||
settings.launcher.proxy.authentication=身份验证
|
||||
settings.launcher.proxy.enable=启用代理
|
||||
settings.launcher.proxy.disable=使用系统代理
|
||||
settings.launcher.proxy.host=主机
|
||||
settings.launcher.proxy.http=HTTP
|
||||
settings.launcher.proxy.password=密码
|
||||
@@ -309,9 +311,13 @@ settings.type=版本设置类型
|
||||
settings.type.special=单独版本设置(不会影响到其他版本的设定)
|
||||
|
||||
update=启动器更新
|
||||
update.failed=检查更新失败
|
||||
update.channel.dev=更新到开发版
|
||||
update.channel.stable=更新到推荐版本
|
||||
update.checking=正在检查更新
|
||||
update.failed=更新失败
|
||||
update.found=发现更新
|
||||
update.newest_version=最新版本为:%s
|
||||
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。选择更新到开发版导致你电脑中所有的HMCL更新至开发版
|
||||
update.latest=当前版本为最新版本
|
||||
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
|
||||
update.tooltip=更新
|
||||
@@ -328,7 +334,9 @@ version.launch_script.save=保存启动脚本
|
||||
version.launch_script.success=启动脚本已生成完毕:%s
|
||||
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
|
||||
version.manage.remove=删除该版本
|
||||
version.manage.remove.confirm=真的要删除版本 %s 吗?
|
||||
version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件!
|
||||
version.manage.remove.confirm.trash=真的要删除版本 %s 吗?你可以在系统的回收站中恢复文件夹 %s 来找回该版本。
|
||||
version.manage.remove.confirm.independent=由于该游戏使用了版本隔离,所以删除该版本将导致该游戏的存档等数据一同被删除,真的要删除版本 %s 吗?
|
||||
version.manage.remove_libraries=删除所有库文件
|
||||
version.manage.rename=重命名该版本
|
||||
version.manage.rename.message=请输入要改成的名字
|
||||
|
||||
@@ -37,6 +37,10 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
|
||||
private OfflineAccountFactory() {
|
||||
}
|
||||
|
||||
public OfflineAccount create(String username, UUID uuid) {
|
||||
return new OfflineAccount(username, uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OfflineAccount create(CharacterSelector selector, String username, String password, Object additionalData) {
|
||||
return new OfflineAccount(username, getUUIDFromUserName(username));
|
||||
|
||||
@@ -64,8 +64,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
return baseURL
|
||||
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
|
||||
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
|
||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven")
|
||||
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
|
||||
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven")
|
||||
.replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
|
||||
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
|
||||
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");
|
||||
|
||||
@@ -28,6 +28,8 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.ParallelTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.util.ExceptionalFunction;
|
||||
|
||||
/**
|
||||
* Note: This class has no state.
|
||||
@@ -77,7 +79,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion) {
|
||||
public Task installLibraryAsync(Version version, RemoteVersion libraryVersion) {
|
||||
if (libraryVersion instanceof ForgeRemoteVersion)
|
||||
return new ForgeInstallTask(this, version, (ForgeRemoteVersion) libraryVersion)
|
||||
.then(variables -> new LibrariesUniqueTask(variables.get("version")))
|
||||
@@ -97,4 +99,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
throw new IllegalArgumentException("Remote library " + libraryVersion + " is unrecognized.");
|
||||
}
|
||||
|
||||
|
||||
public ExceptionalFunction<AutoTypingMap<String>, Task, ?> installLibraryAsync(RemoteVersion libraryVersion) {
|
||||
return var -> installLibraryAsync(var.get("version"), libraryVersion);
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,8 @@ public class DefaultGameBuilder extends GameBuilder {
|
||||
if (toolVersions.containsKey("optifine"))
|
||||
result = result.then(libraryTaskHelper(gameVersion, "optifine"));
|
||||
|
||||
for (RemoteVersion<?> remoteVersion : remoteVersions)
|
||||
result = result.then(var -> dependencyManager.installLibraryAsync(var.get("version"), remoteVersion));
|
||||
for (RemoteVersion remoteVersion : remoteVersions)
|
||||
result = result.then(dependencyManager.installLibraryAsync(remoteVersion));
|
||||
|
||||
return result;
|
||||
}).finalized((variables, isDependentsSucceeded) -> {
|
||||
|
||||
@@ -67,7 +67,7 @@ public interface DependencyManager {
|
||||
* @param libraryVersion the remote version of being installed library.
|
||||
* @return the task to install the specific library.
|
||||
*/
|
||||
Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion);
|
||||
Task installLibraryAsync(Version version, RemoteVersion libraryVersion);
|
||||
|
||||
/**
|
||||
* Get registered version list.
|
||||
|
||||
@@ -31,7 +31,7 @@ public abstract class GameBuilder {
|
||||
protected String name = "";
|
||||
protected String gameVersion = "";
|
||||
protected final Map<String, String> toolVersions = new HashMap<>();
|
||||
protected final Set<RemoteVersion<?>> remoteVersions = new HashSet<>();
|
||||
protected final Set<RemoteVersion> remoteVersions = new HashSet<>();
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
@@ -64,7 +64,7 @@ public abstract class GameBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public GameBuilder version(RemoteVersion<?> remoteVersion) {
|
||||
public GameBuilder version(RemoteVersion remoteVersion) {
|
||||
remoteVersions.add(remoteVersion);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,9 @@ public class MaintainTask extends TaskResult<Version> {
|
||||
args.add("optifine.OptiFineForgeTweaker");
|
||||
}
|
||||
|
||||
return version.setMinecraftArguments(new CommandBuilder().addAll(args).toString());
|
||||
// Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping,
|
||||
// so we regenerate the minecraftArgument without escaping.
|
||||
return version.setMinecraftArguments(new CommandBuilder().addAllWithoutParsing(args).toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.download;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
|
||||
import org.jackhuang.hmcl.download.game.GameVersionList;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineVersionList;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
|
||||
|
||||
/**
|
||||
* @see <a href="http://wiki.vg">http://wiki,vg</a>
|
||||
@@ -48,7 +48,7 @@ public class MojangDownloadProvider implements DownloadProvider {
|
||||
case "liteloader":
|
||||
return LiteLoaderVersionList.INSTANCE;
|
||||
case "optifine":
|
||||
return OptiFineVersionList.INSTANCE;
|
||||
return OptiFineBMCLVersionList.INSTANCE;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized version list id: " + id);
|
||||
}
|
||||
|
||||
@@ -27,26 +27,36 @@ import java.util.Objects;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
|
||||
public class RemoteVersion implements Comparable<RemoteVersion> {
|
||||
|
||||
private final String gameVersion;
|
||||
private final String selfVersion;
|
||||
private final String url;
|
||||
private final T tag;
|
||||
private final Type type;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param gameVersion the Minecraft version that this remote version suits.
|
||||
* @param selfVersion the version string of the remote version.
|
||||
* @param url the installer or universal jar URL.
|
||||
* @param tag some necessary information for Installer Task.
|
||||
* @param url the installer or universal jar URL.
|
||||
*/
|
||||
public RemoteVersion(String gameVersion, String selfVersion, String url, T tag) {
|
||||
public RemoteVersion(String gameVersion, String selfVersion, String url) {
|
||||
this(gameVersion, selfVersion, url, Type.UNCATEGORIZED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param gameVersion the Minecraft version that this remote version suits.
|
||||
* @param selfVersion the version string of the remote version.
|
||||
* @param url the installer or universal jar URL.
|
||||
*/
|
||||
public RemoteVersion(String gameVersion, String selfVersion, String url, Type type) {
|
||||
this.gameVersion = Objects.requireNonNull(gameVersion);
|
||||
this.selfVersion = Objects.requireNonNull(selfVersion);
|
||||
this.url = Objects.requireNonNull(url);
|
||||
this.tag = tag;
|
||||
this.type = Objects.requireNonNull(type);
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
@@ -57,14 +67,14 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
|
||||
return selfVersion;
|
||||
}
|
||||
|
||||
public T getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Type getVersionType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof RemoteVersion && Objects.equals(selfVersion, ((RemoteVersion) obj).selfVersion);
|
||||
@@ -80,13 +90,19 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
|
||||
return new ToStringBuilder(this)
|
||||
.append("selfVersion", selfVersion)
|
||||
.append("gameVersion", gameVersion)
|
||||
.append("tag", tag)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(RemoteVersion<T> o) {
|
||||
public int compareTo(RemoteVersion o) {
|
||||
// newer versions are smaller than older versions
|
||||
return VersionNumber.asVersion(o.selfVersion).compareTo(VersionNumber.asVersion(selfVersion));
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
UNCATEGORIZED,
|
||||
RELEASE,
|
||||
SNAPSHOT,
|
||||
OLD
|
||||
}
|
||||
}
|
||||
@@ -26,18 +26,18 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
/**
|
||||
* The remote version list.
|
||||
*
|
||||
* @param <T> The type of {@code RemoteVersion<T>}, the type of tags.
|
||||
* @param <T> The subclass of {@code RemoteVersion}, the type of RemoteVersion.
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public abstract class VersionList<T> {
|
||||
public abstract class VersionList<T extends RemoteVersion> {
|
||||
|
||||
/**
|
||||
* the remote version list.
|
||||
* key: game version.
|
||||
* values: corresponding remote versions.
|
||||
*/
|
||||
protected final SimpleMultimap<String, RemoteVersion<T>> versions = new SimpleMultimap<String, RemoteVersion<T>>(HashMap::new, TreeSet::new);
|
||||
protected final SimpleMultimap<String, T> versions = new SimpleMultimap<String, T>(HashMap::new, TreeSet::new);
|
||||
|
||||
/**
|
||||
* True if the version list has been loaded.
|
||||
@@ -46,6 +46,8 @@ public abstract class VersionList<T> {
|
||||
return !versions.isEmpty();
|
||||
}
|
||||
|
||||
public abstract boolean hasType();
|
||||
|
||||
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
/**
|
||||
@@ -68,10 +70,10 @@ public abstract class VersionList<T> {
|
||||
});
|
||||
}
|
||||
|
||||
private Collection<RemoteVersion<T>> getVersionsImpl(String gameVersion) {
|
||||
private Collection<T> getVersionsImpl(String gameVersion) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
Collection<RemoteVersion<T>> ans = versions.get(gameVersion);
|
||||
Collection<T> ans = versions.get(gameVersion);
|
||||
return ans.isEmpty() ? versions.values() : ans;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
@@ -84,7 +86,7 @@ public abstract class VersionList<T> {
|
||||
* @param gameVersion the Minecraft version that remote versions belong to
|
||||
* @return the collection of specific remote versions
|
||||
*/
|
||||
public final Collection<RemoteVersion<T>> getVersions(String gameVersion) {
|
||||
public final Collection<T> getVersions(String gameVersion) {
|
||||
return Collections.unmodifiableCollection(getVersionsImpl(gameVersion));
|
||||
}
|
||||
|
||||
@@ -95,11 +97,11 @@ public abstract class VersionList<T> {
|
||||
* @param remoteVersion the version of the remote version.
|
||||
* @return the specific remote version, null if it is not found.
|
||||
*/
|
||||
public final Optional<RemoteVersion<T>> getVersion(String gameVersion, String remoteVersion) {
|
||||
public final Optional<T> getVersion(String gameVersion, String remoteVersion) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
RemoteVersion<T> result = null;
|
||||
for (RemoteVersion<T> it : versions.get(gameVersion))
|
||||
T result = null;
|
||||
for (T it : versions.get(gameVersion))
|
||||
if (remoteVersion.equals(it.getSelfVersion()))
|
||||
result = it;
|
||||
return Optional.ofNullable(result);
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.download.forge;
|
||||
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
|
||||
public class ForgeRemoteVersion extends RemoteVersion<Void> {
|
||||
public class ForgeRemoteVersion extends RemoteVersion {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@@ -28,6 +28,6 @@ public class ForgeRemoteVersion extends RemoteVersion<Void> {
|
||||
* @param url the installer or universal jar URL.
|
||||
*/
|
||||
public ForgeRemoteVersion(String gameVersion, String selfVersion, String url) {
|
||||
super(gameVersion, selfVersion, url, null);
|
||||
super(gameVersion, selfVersion, url);
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,18 @@ import java.util.*;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class ForgeVersionList extends VersionList<Void> {
|
||||
public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
|
||||
|
||||
public static final ForgeVersionList INSTANCE = new ForgeVersionList();
|
||||
|
||||
private ForgeVersionList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
final GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(FORGE_LIST)));
|
||||
|
||||
@@ -75,8 +75,6 @@ public final class GameAssetDownloadTask extends Task {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
int size = refreshTask.getResult().size();
|
||||
int downloaded = 0;
|
||||
for (Map.Entry<File, AssetObject> entry : refreshTask.getResult()) {
|
||||
if (Thread.interrupted())
|
||||
throw new InterruptedException();
|
||||
@@ -88,29 +86,11 @@ public final class GameAssetDownloadTask extends Task {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to create new file " + file + ", because parent directory cannot be created");
|
||||
continue;
|
||||
}
|
||||
if (file.isDirectory())
|
||||
if (file.isDirectory() || file.isFile() && file.exists())
|
||||
continue;
|
||||
boolean flag = true;
|
||||
try {
|
||||
// check the checksum of file to ensure that the file is not need to re-download.
|
||||
if (file.exists()) {
|
||||
String sha1 = encodeHex(digest("SHA-1", FileUtils.readBytes(file)));
|
||||
if (sha1.equals(assetObject.getHash())) {
|
||||
++downloaded;
|
||||
Logging.LOG.finest("File $file has been downloaded successfully, skipped downloading");
|
||||
updateProgress(downloaded, size);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to get hash code of file " + file, e);
|
||||
flag = !file.exists();
|
||||
}
|
||||
if (flag) {
|
||||
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
|
||||
task.setName(assetObject.getHash());
|
||||
dependencies.add(task);
|
||||
}
|
||||
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
|
||||
task.setName(assetObject.getHash());
|
||||
dependencies.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.game.ReleaseType;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.Validation;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@@ -30,64 +27,45 @@ import java.util.Date;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GameRemoteVersion implements Validation {
|
||||
@Immutable
|
||||
public final class GameRemoteVersion extends RemoteVersion {
|
||||
|
||||
@SerializedName("id")
|
||||
private final String gameVersion;
|
||||
|
||||
@SerializedName("time")
|
||||
private final ReleaseType type;
|
||||
private final Date time;
|
||||
|
||||
@SerializedName("releaseTime")
|
||||
private final Date releaseTime;
|
||||
|
||||
@SerializedName("type")
|
||||
private final ReleaseType type;
|
||||
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
public GameRemoteVersion() {
|
||||
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
|
||||
}
|
||||
|
||||
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
|
||||
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
|
||||
}
|
||||
|
||||
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
|
||||
this.gameVersion = gameVersion;
|
||||
this.time = time;
|
||||
this.releaseTime = releaseTime;
|
||||
public GameRemoteVersion(String gameVersion, String selfVersion, String url, ReleaseType type, Date time) {
|
||||
super(gameVersion, selfVersion, url, getReleaseType(type));
|
||||
this.type = type;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return gameVersion;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public Date getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public Date getReleaseTime() {
|
||||
return releaseTime;
|
||||
}
|
||||
|
||||
public ReleaseType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (StringUtils.isBlank(gameVersion))
|
||||
throw new JsonParseException("GameRemoteVersion id cannot be blank");
|
||||
if (StringUtils.isBlank(url))
|
||||
throw new JsonParseException("GameRemoteVersion url cannot be blank");
|
||||
public int compareTo(RemoteVersion o) {
|
||||
if (!(o instanceof GameRemoteVersion))
|
||||
return 0;
|
||||
|
||||
return ((GameRemoteVersion) o).getTime().compareTo(getTime());
|
||||
}
|
||||
|
||||
private static Type getReleaseType(ReleaseType type) {
|
||||
if (type == null) return Type.UNCATEGORIZED;
|
||||
switch (type) {
|
||||
case RELEASE:
|
||||
return Type.RELEASE;
|
||||
case SNAPSHOT:
|
||||
return Type.SNAPSHOT;
|
||||
case UNKNOWN:
|
||||
return Type.UNCATEGORIZED;
|
||||
default:
|
||||
return Type.OLD;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.game.ReleaseType;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.Validation;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GameRemoteVersionInfo implements Validation {
|
||||
|
||||
@SerializedName("id")
|
||||
private final String gameVersion;
|
||||
|
||||
@SerializedName("time")
|
||||
private final Date time;
|
||||
|
||||
@SerializedName("releaseTime")
|
||||
private final Date releaseTime;
|
||||
|
||||
@SerializedName("type")
|
||||
private final ReleaseType type;
|
||||
|
||||
@SerializedName("url")
|
||||
private final String url;
|
||||
|
||||
public GameRemoteVersionInfo() {
|
||||
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
|
||||
}
|
||||
|
||||
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
|
||||
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
|
||||
}
|
||||
|
||||
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
|
||||
this.gameVersion = gameVersion;
|
||||
this.time = time;
|
||||
this.releaseTime = releaseTime;
|
||||
this.type = type;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
public Date getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public Date getReleaseTime() {
|
||||
return releaseTime;
|
||||
}
|
||||
|
||||
public ReleaseType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (StringUtils.isBlank(gameVersion))
|
||||
throw new JsonParseException("GameRemoteVersion id cannot be blank");
|
||||
if (StringUtils.isBlank(url))
|
||||
throw new JsonParseException("GameRemoteVersion url cannot be blank");
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import org.jackhuang.hmcl.game.ReleaseType;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class GameRemoteVersionTag {
|
||||
|
||||
private final ReleaseType type;
|
||||
private final Date time;
|
||||
|
||||
/**
|
||||
* No-arg constructor for Gson.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public GameRemoteVersionTag() {
|
||||
this(ReleaseType.UNKNOWN, new Date());
|
||||
}
|
||||
|
||||
public GameRemoteVersionTag(ReleaseType type, Date time) {
|
||||
this.type = type;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public Date getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public ReleaseType getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import java.util.List;
|
||||
public final class GameRemoteVersions {
|
||||
|
||||
@SerializedName("versions")
|
||||
private final List<GameRemoteVersion> versions;
|
||||
private final List<GameRemoteVersionInfo> versions;
|
||||
|
||||
@SerializedName("latest")
|
||||
private final GameRemoteLatestVersions latest;
|
||||
@@ -44,7 +44,7 @@ public final class GameRemoteVersions {
|
||||
this(Collections.emptyList(), null);
|
||||
}
|
||||
|
||||
public GameRemoteVersions(List<GameRemoteVersion> versions, GameRemoteLatestVersions latest) {
|
||||
public GameRemoteVersions(List<GameRemoteVersionInfo> versions, GameRemoteLatestVersions latest) {
|
||||
this.versions = versions;
|
||||
this.latest = latest;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public final class GameRemoteVersions {
|
||||
return latest;
|
||||
}
|
||||
|
||||
public List<GameRemoteVersion> getVersions() {
|
||||
public List<GameRemoteVersionInfo> getVersions() {
|
||||
return versions;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,18 @@ import java.util.Collections;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
|
||||
public final class GameVersionList extends VersionList<GameRemoteVersion> {
|
||||
|
||||
public static final GameVersionList INSTANCE = new GameVersionList();
|
||||
|
||||
private GameVersionList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.getVersionListURL()));
|
||||
@@ -56,12 +61,12 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
|
||||
versions.clear();
|
||||
|
||||
GameRemoteVersions root = Constants.GSON.fromJson(task.getResult(), GameRemoteVersions.class);
|
||||
for (GameRemoteVersion remoteVersion : root.getVersions()) {
|
||||
versions.put(remoteVersion.getGameVersion(), new RemoteVersionGame(
|
||||
for (GameRemoteVersionInfo remoteVersion : root.getVersions()) {
|
||||
versions.put(remoteVersion.getGameVersion(), new GameRemoteVersion(
|
||||
remoteVersion.getGameVersion(),
|
||||
remoteVersion.getGameVersion(),
|
||||
remoteVersion.getUrl(),
|
||||
new GameRemoteVersionTag(remoteVersion.getType(), remoteVersion.getReleaseTime()))
|
||||
remoteVersion.getType(), remoteVersion.getReleaseTime())
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -70,15 +75,4 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static class RemoteVersionGame extends RemoteVersion<GameRemoteVersionTag> {
|
||||
public RemoteVersionGame(String gameVersion, String selfVersion, String url, GameRemoteVersionTag tag) {
|
||||
super(gameVersion, selfVersion, url, tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(RemoteVersion<GameRemoteVersionTag> o) {
|
||||
return o.getTag().getTime().compareTo(getTag().getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,11 +56,11 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
|
||||
String serialized = Constants.GSON.toJson(library);
|
||||
|
||||
if (multimap.containsKey(id)) {
|
||||
boolean hasEqualRule = false;
|
||||
boolean duplicate = false;
|
||||
for (Library otherLibrary : multimap.get(id)) {
|
||||
VersionNumber otherNumber = VersionNumber.asVersion(otherLibrary.getVersion());
|
||||
if (CompatibilityRule.equals(library.getRules(), otherLibrary.getRules())) { // rules equal, ignore older version.
|
||||
hasEqualRule = true;
|
||||
boolean flag = true;
|
||||
if (number.compareTo(otherNumber) > 0) { // if this library is newer
|
||||
multimap.removeValue(otherLibrary);
|
||||
multimap.put(id, library);
|
||||
@@ -76,12 +76,20 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
|
||||
multimap.put(id, library);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// for text2speech, which have same library id as well as version number,
|
||||
// but its library and native library does not equal
|
||||
flag = false;
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasEqualRule) {
|
||||
if (!duplicate) {
|
||||
multimap.put(id, library);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
|
||||
import org.jackhuang.hmcl.download.AbstractDependencyManager;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
@@ -10,9 +9,13 @@ import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
@@ -23,15 +26,15 @@ import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
import static org.jackhuang.hmcl.util.Hex.encodeHex;
|
||||
|
||||
public final class LibraryDownloadTask extends Task {
|
||||
private final FileDownloadTask xzTask;
|
||||
private final FileDownloadTask task;
|
||||
private final LibraryDownloadTaskHelper helperTask;
|
||||
private final File jar;
|
||||
private final File xzFile;
|
||||
private final Library library;
|
||||
|
||||
private boolean downloaded = false;
|
||||
private boolean xz;
|
||||
|
||||
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
|
||||
if (library.is("net.minecraftforge", "forge"))
|
||||
library = library.setClassifier("universal");
|
||||
|
||||
@@ -42,20 +45,12 @@ public final class LibraryDownloadTask extends Task {
|
||||
|
||||
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
|
||||
|
||||
xzTask = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"),
|
||||
xzFile, null, 1);
|
||||
xzTask.setSignificance(TaskSignificance.MINOR);
|
||||
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
|
||||
task = new FileDownloadTask(NetworkUtils.toURL(url),
|
||||
file,
|
||||
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
|
||||
helperTask = new LibraryDownloadTaskHelper(file, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task> getDependents() {
|
||||
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.singleton(task);
|
||||
return Collections.singleton(helperTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,32 +61,75 @@ public final class LibraryDownloadTask extends Task {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!isDependentsSucceeded()) {
|
||||
// Since FileDownloadTask wraps the actual exception with another IOException.
|
||||
// We should extract it letting the error message clearer.
|
||||
Throwable t = library.getChecksums() != null ? xzTask.getLastException() : task.getLastException();
|
||||
if (t.getCause() != null && t.getCause() != t)
|
||||
throw new LibraryDownloadException(library, t.getCause());
|
||||
else
|
||||
throw new LibraryDownloadException(library, t);
|
||||
if (helperTask.task == null) {
|
||||
// NetworkUtils.URLExists failed.
|
||||
throw new LibraryDownloadException(library, helperTask.getLastException());
|
||||
} else {
|
||||
// Since FileDownloadTask wraps the actual exception with another IOException.
|
||||
// We should extract it letting the error message clearer.
|
||||
Throwable t = helperTask.task.getLastException();
|
||||
if (t.getCause() != null && t.getCause() != t)
|
||||
throw new LibraryDownloadException(library, t.getCause());
|
||||
else
|
||||
throw new LibraryDownloadException(library, t);
|
||||
}
|
||||
} else {
|
||||
if (xz) unpackLibrary(jar, FileUtils.readBytes(xzFile));
|
||||
if (!checksumValid(jar, library.getChecksums())) {
|
||||
jar.delete();
|
||||
throw new IOException("Checksum failed for " + library);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LibraryDownloadTaskHelper extends Task {
|
||||
private FileDownloadTask task;
|
||||
private final File file;
|
||||
private final String url;
|
||||
|
||||
public LibraryDownloadTaskHelper(File file, String url) {
|
||||
this.file = file;
|
||||
this.url = url;
|
||||
|
||||
setName(library.getName());
|
||||
}
|
||||
|
||||
if (library.getChecksums() != null) {
|
||||
unpackLibrary(jar, FileUtils.readBytes(xzFile));
|
||||
if (!checksumValid(jar, library.getChecksums()))
|
||||
throw new IOException("Checksum failed for " + library);
|
||||
@Override
|
||||
public boolean isRelyingOnDependencies() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task> getDependencies() {
|
||||
return Collections.singleton(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
URL packXz = NetworkUtils.toURL(url + ".pack.xz");
|
||||
if (NetworkUtils.URLExists(packXz)) {
|
||||
task = new FileDownloadTask(packXz, xzFile, null);
|
||||
xz = true;
|
||||
} else {
|
||||
task = new FileDownloadTask(NetworkUtils.toURL(url),
|
||||
file,
|
||||
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
|
||||
xz = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checksumValid(File libPath, List<String> checksums) {
|
||||
try {
|
||||
if ((checksums == null) || (checksums.isEmpty())) {
|
||||
if (checksums == null || checksums.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
byte[] fileData = FileUtils.readBytes(libPath);
|
||||
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
|
||||
if ((!valid) && (libPath.getName().endsWith(".jar"))) {
|
||||
if (!valid && libPath.getName().endsWith(".jar")) {
|
||||
valid = validateJar(fileData, checksums);
|
||||
}
|
||||
return validateJar(fileData, checksums);
|
||||
return valid;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -104,7 +142,7 @@ public final class LibraryDownloadTask extends Task {
|
||||
JarInputStream jar = new JarInputStream(new ByteArrayInputStream(data));
|
||||
JarEntry entry = jar.getNextJarEntry();
|
||||
while (entry != null) {
|
||||
byte[] eData = IOUtils.readFullyAsByteArray(jar);
|
||||
byte[] eData = IOUtils.readFullyWithoutClosing(jar);
|
||||
if (entry.getName().equals("checksums.sha1")) {
|
||||
hashes = new String(eData, Charset.forName("UTF-8")).split("\n");
|
||||
}
|
||||
@@ -115,8 +153,8 @@ public final class LibraryDownloadTask extends Task {
|
||||
}
|
||||
jar.close();
|
||||
if (hashes != null) {
|
||||
boolean passed = !checksums.contains(files.get("checksums.sha1"));
|
||||
if (passed) {
|
||||
boolean failed = !checksums.contains(files.get("checksums.sha1"));
|
||||
if (!failed) {
|
||||
for (String hash : hashes) {
|
||||
if ((!hash.trim().equals("")) && (hash.contains(" "))) {
|
||||
String[] e = hash.split(" ");
|
||||
@@ -125,17 +163,17 @@ public final class LibraryDownloadTask extends Task {
|
||||
String checksum = files.get(target);
|
||||
if ((!files.containsKey(target)) || (checksum == null)) {
|
||||
Logging.LOG.warning(" " + target + " : missing");
|
||||
passed = false;
|
||||
failed = true;
|
||||
break;
|
||||
} else if (!checksum.equals(validChecksum)) {
|
||||
Logging.LOG.warning(" " + target + " : failed (" + checksum + ", " + validChecksum + ")");
|
||||
passed = false;
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return passed;
|
||||
return !failed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -145,7 +183,7 @@ public final class LibraryDownloadTask extends Task {
|
||||
if (!dest.delete())
|
||||
throw new IOException("Unable to delete file " + dest);
|
||||
|
||||
byte[] decompressed = IOUtils.readFullyAsByteArray(new XZCompressorInputStream(new ByteArrayInputStream(src)));
|
||||
byte[] decompressed = IOUtils.readFullyAsByteArray(new XZInputStream(new ByteArrayInputStream(src)));
|
||||
|
||||
String end = new String(decompressed, decompressed.length - 4, 4);
|
||||
if (!end.equals("SIGN"))
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ public final class VersionJsonDownloadTask extends Task {
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
RemoteVersion<?> remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
|
||||
RemoteVersion remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot find specific version " + gameVersion + " in remote repository"));
|
||||
String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl());
|
||||
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), ID));
|
||||
|
||||
+6
-1
@@ -39,13 +39,18 @@ import java.util.Optional;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
|
||||
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersion> {
|
||||
|
||||
public static final LiteLoaderBMCLVersionList INSTANCE = new LiteLoaderBMCLVersionList();
|
||||
|
||||
private LiteLoaderBMCLVersionList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
|
||||
|
||||
+2
-2
@@ -74,7 +74,7 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
|
||||
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()))
|
||||
);
|
||||
|
||||
Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Collections.singleton(library)));
|
||||
Version tempVersion = version.setLibraries(Lang.merge(remote.getLibraries(), Collections.singleton(library)));
|
||||
|
||||
String mcArg = version.getMinecraftArguments().orElse("");
|
||||
if (mcArg.contains("--tweakClass optifine.OptiFineTweaker"))
|
||||
@@ -84,7 +84,7 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
|
||||
.setMainClass("net.minecraft.launchwrapper.Launch")
|
||||
.setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries()))
|
||||
.setLogging(Collections.emptyMap())
|
||||
.setMinecraftArguments(mcArg + " --tweakClass " + remote.getTag().getTweakClass())
|
||||
.setMinecraftArguments(mcArg + " --tweakClass " + remote.getTweakClass())
|
||||
//.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass()))
|
||||
);
|
||||
|
||||
|
||||
+16
-2
@@ -22,7 +22,9 @@ import org.jackhuang.hmcl.game.Library;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersionTag> {
|
||||
public class LiteLoaderRemoteVersion extends RemoteVersion {
|
||||
private final String tweakClass;
|
||||
private final Collection<Library> libraries;
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@@ -31,6 +33,18 @@ public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersi
|
||||
* @param url the installer or universal jar URL.
|
||||
*/
|
||||
LiteLoaderRemoteVersion(String gameVersion, String selfVersion, String url, String tweakClass, Collection<Library> libraries) {
|
||||
super(gameVersion, selfVersion, url, new LiteLoaderRemoteVersionTag(tweakClass, libraries));
|
||||
super(gameVersion, selfVersion, url);
|
||||
|
||||
this.tweakClass = tweakClass;
|
||||
this.libraries = libraries;
|
||||
}
|
||||
|
||||
public Collection<Library> getLibraries() {
|
||||
return libraries;
|
||||
}
|
||||
|
||||
public String getTweakClass() {
|
||||
return tweakClass;
|
||||
}
|
||||
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.liteloader;
|
||||
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LiteLoaderRemoteVersionTag {
|
||||
private final String tweakClass;
|
||||
private final Collection<Library> libraries;
|
||||
|
||||
/**
|
||||
* No-arg constructor for Gson.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public LiteLoaderRemoteVersionTag() {
|
||||
this("", Collections.emptySet());
|
||||
}
|
||||
|
||||
public LiteLoaderRemoteVersionTag(String tweakClass, Collection<Library> libraries) {
|
||||
this.tweakClass = tweakClass;
|
||||
this.libraries = libraries;
|
||||
}
|
||||
|
||||
public Collection<Library> getLibraries() {
|
||||
return libraries;
|
||||
}
|
||||
|
||||
public String getTweakClass() {
|
||||
return tweakClass;
|
||||
}
|
||||
|
||||
}
|
||||
+6
-1
@@ -39,13 +39,18 @@ import java.util.Optional;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
|
||||
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersion> {
|
||||
|
||||
public static final LiteLoaderVersionList INSTANCE = new LiteLoaderVersionList();
|
||||
|
||||
private LiteLoaderVersionList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
|
||||
|
||||
+9
-4
@@ -33,13 +33,18 @@ import java.util.*;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class OptiFineBMCLVersionList extends VersionList<Void> {
|
||||
public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVersion> {
|
||||
|
||||
public static final OptiFineBMCLVersionList INSTANCE = new OptiFineBMCLVersionList();
|
||||
|
||||
private OptiFineBMCLVersionList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasType() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
GetTask task = new GetTask(NetworkUtils.toURL("http://bmclapi2.bangbang93.com/optifine/versionlist"));
|
||||
@@ -57,16 +62,16 @@ public final class OptiFineBMCLVersionList extends VersionList<Void> {
|
||||
}.getType());
|
||||
for (OptiFineVersion element : root) {
|
||||
String version = element.getType() + "_" + element.getPatch();
|
||||
if (element.getType() == null || "pre".equals(element.getPatch()))
|
||||
continue;
|
||||
String mirror = "http://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
|
||||
if (!duplicates.add(mirror))
|
||||
continue;
|
||||
|
||||
boolean isPre = element.getPatch() != null && (element.getPatch().equals("pre") || element.getPatch().startsWith("alpha"));
|
||||
|
||||
if (StringUtils.isBlank(element.getGameVersion()))
|
||||
continue;
|
||||
VersionNumber.parseVersion(element.getGameVersion())
|
||||
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror)));
|
||||
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+3
-3
@@ -21,11 +21,11 @@ import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class OptiFineRemoteVersion extends RemoteVersion<Void> {
|
||||
public class OptiFineRemoteVersion extends RemoteVersion {
|
||||
private final Supplier<String> url;
|
||||
|
||||
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url) {
|
||||
super(gameVersion, selfVersion, "", null);
|
||||
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url, boolean snapshot) {
|
||||
super(gameVersion, selfVersion, "", snapshot ? Type.SNAPSHOT : Type.RELEASE);
|
||||
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* 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 {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.optifine;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class OptiFineVersionList extends VersionList<Void> {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("OptiFine (.*?) ");
|
||||
private static final Pattern LINK_PATTERN = Pattern.compile("\"downloadx\\?f=OptiFine(.*)\"");
|
||||
|
||||
public static final OptiFineVersionList INSTANCE = new OptiFineVersionList();
|
||||
|
||||
private OptiFineVersionList() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task refreshAsync(DownloadProvider downloadProvider) {
|
||||
GetTask task = new GetTask(NetworkUtils.toURL("https://optifine.net/downloads"));
|
||||
return new Task() {
|
||||
@Override
|
||||
public Collection<Task> getDependents() {
|
||||
return Collections.singleton(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
lock.writeLock().lock();
|
||||
|
||||
try {
|
||||
versions.clear();
|
||||
|
||||
String html = task.getResult().replace(" ", " ").replace(">", ">").replace("<", "<").replace("<br>", "<br />");
|
||||
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder db = factory.newDocumentBuilder();
|
||||
Document doc = db.parse(new ByteArrayInputStream(html.getBytes("UTF-8")));
|
||||
Element r = doc.getDocumentElement();
|
||||
NodeList tables = r.getElementsByTagName("table");
|
||||
for (int i = 0; i < tables.getLength(); i++) {
|
||||
Element e = (Element) tables.item(i);
|
||||
if ("downloadTable".equals(e.getAttribute("class"))) {
|
||||
NodeList tr = e.getElementsByTagName("tr");
|
||||
for (int k = 0; k < tr.getLength(); k++) {
|
||||
NodeList downloadLine = ((Element) tr.item(k)).getElementsByTagName("td");
|
||||
String url = null, version = null, gameVersion = null;
|
||||
for (int j = 0; j < downloadLine.getLength(); j++) {
|
||||
Element td = (Element) downloadLine.item(j);
|
||||
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineMirror"))
|
||||
url = ((Element) td.getElementsByTagName("a").item(0)).getAttribute("href");
|
||||
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile"))
|
||||
version = td.getTextContent();
|
||||
}
|
||||
if (version == null || url == null)
|
||||
continue;
|
||||
|
||||
Matcher matcher = PATTERN.matcher(version);
|
||||
while (matcher.find())
|
||||
gameVersion = matcher.group(1);
|
||||
if (gameVersion == null)
|
||||
continue;
|
||||
|
||||
String finalURL = url;
|
||||
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, Lang.hideException(() -> getLink(finalURL))));
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String getLink(String url) throws IOException {
|
||||
String result = null;
|
||||
String content = NetworkUtils.doGet(NetworkUtils.toURL(url));
|
||||
Matcher m = LINK_PATTERN.matcher(content);
|
||||
while (m.find())
|
||||
result = m.group(1);
|
||||
if (result == null)
|
||||
throw new IllegalStateException("Cannot find version in " + content);
|
||||
return "https://optifine.net/downloadx?f=OptiFine" + result;
|
||||
}
|
||||
}
|
||||
@@ -45,12 +45,4 @@ public class ProcessStoppedEvent extends Event {
|
||||
public ManagedProcess getProcess() {
|
||||
return process;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
.append("source", source)
|
||||
.append("process", process)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public final class Arguments {
|
||||
}
|
||||
|
||||
public static List<String> parseStringArguments(List<String> arguments, Map<String, String> keys) {
|
||||
return arguments.stream().map(str -> keys.getOrDefault(str, str)).collect(Collectors.toList());
|
||||
return arguments.stream().flatMap(str -> new StringArgument(str).toString(keys, Collections.emptyMap()).stream()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static List<String> parseArguments(List<Argument> arguments, Map<String, String> keys) {
|
||||
|
||||
@@ -148,10 +148,10 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
|
||||
public boolean removeVersionFromDisk(String id) {
|
||||
if (!versions.containsKey(id))
|
||||
return true;
|
||||
if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY)
|
||||
return false;
|
||||
if (!versions.containsKey(id))
|
||||
return FileUtils.deleteDirectoryQuietly(getVersionRoot(id));
|
||||
File file = getVersionRoot(id);
|
||||
if (!file.exists())
|
||||
return true;
|
||||
@@ -160,14 +160,18 @@ public class DefaultGameRepository implements GameRepository {
|
||||
if (!file.renameTo(removedFile))
|
||||
return false;
|
||||
|
||||
versions.remove(id);
|
||||
|
||||
if (FileUtils.isMovingToTrashSupported()) {
|
||||
return FileUtils.moveToTrash(removedFile);
|
||||
}
|
||||
|
||||
// remove json files first to ensure HMCL will not recognize this folder as a valid version.
|
||||
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
|
||||
jsons.forEach(f -> {
|
||||
if (!f.delete())
|
||||
Logging.LOG.warning("Unable to delete file " + f);
|
||||
});
|
||||
|
||||
versions.remove(id);
|
||||
// remove the version from version list regardless of whether the directory was removed successfully or not.
|
||||
try {
|
||||
FileUtils.deleteDirectory(removedFile);
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||
import org.jenkinsci.constant_pool_scanner.ConstantType;
|
||||
@@ -27,6 +25,9 @@ import org.jenkinsci.constant_pool_scanner.StringConstant;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -36,8 +37,8 @@ import java.util.stream.StreamSupport;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class GameVersion {
|
||||
private static Optional<String> getVersionOfClassMinecraft(ZipFile file, ZipArchiveEntry entry) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING);
|
||||
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||
|
||||
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
||||
.map(StringConstant::get)
|
||||
@@ -46,8 +47,8 @@ public final class GameVersion {
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
private static Optional<String> getVersionFromClassMinecraftServer(ZipFile file, ZipArchiveEntry entry) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(file.getInputStream(entry), ConstantType.STRING);
|
||||
private static Optional<String> getVersionFromClassMinecraftServer(byte[] bytecode) throws IOException {
|
||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||
|
||||
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
||||
.map(StringConstant::get)
|
||||
@@ -72,23 +73,19 @@ public final class GameVersion {
|
||||
if (file == null || !file.exists() || !file.isFile() || !file.canRead())
|
||||
return Optional.empty();
|
||||
|
||||
ZipFile gameJar = null;
|
||||
try {
|
||||
gameJar = new ZipFile(file);
|
||||
ZipArchiveEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class");
|
||||
if (minecraft != null) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(gameJar, minecraft);
|
||||
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
|
||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||
if (Files.exists(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
}
|
||||
ZipArchiveEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class");
|
||||
if (minecraftServer != null)
|
||||
return getVersionFromClassMinecraftServer(gameJar, minecraftServer);
|
||||
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
|
||||
if (Files.exists(minecraftServer))
|
||||
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
|
||||
return Optional.empty();
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
} finally {
|
||||
IOUtils.closeQuietly(gameJar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +223,7 @@ public class Library implements Comparable<Library> {
|
||||
obj.addProperty("name", src.getName());
|
||||
obj.addProperty("url", src.url);
|
||||
obj.add("downloads", context.serialize(src.downloads));
|
||||
obj.add("checksums", context.serialize(src.getChecksums()));
|
||||
obj.add("extract", context.serialize(src.extract));
|
||||
obj.add("natives", context.serialize(src.natives, new TypeToken<Map<OperatingSystem, String>>() {
|
||||
}.getType()));
|
||||
|
||||
@@ -65,7 +65,7 @@ public class RuledArgument implements Argument {
|
||||
|
||||
@Override
|
||||
public List<String> toString(Map<String, String> keys, Map<String, Boolean> features) {
|
||||
if (CompatibilityRule.appliesToCurrentEnvironment(rules) && value != null)
|
||||
if (CompatibilityRule.appliesToCurrentEnvironment(rules, features) && value != null)
|
||||
return value.stream()
|
||||
.map(StringArgument::new)
|
||||
.map(str -> str.toString(keys, features).get(0))
|
||||
|
||||
@@ -151,8 +151,9 @@ public class Version implements Comparable<Version>, Validation {
|
||||
}
|
||||
|
||||
protected Version resolve(VersionProvider provider, Set<String> resolvedSoFar) throws VersionNotFoundException {
|
||||
if (inheritsFrom == null)
|
||||
return this;
|
||||
if (inheritsFrom == null) {
|
||||
return this.jar == null ? this.setJar(id) : this;
|
||||
}
|
||||
|
||||
// To maximize the compatibility.
|
||||
if (!resolvedSoFar.add(id)) {
|
||||
|
||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user