Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
@@ -17,30 +17,31 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
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.io.IOException;
|
||||
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;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class Launcher extends Application {
|
||||
|
||||
@@ -56,12 +57,24 @@ 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.updateChannelProperty().addListener(observable -> {
|
||||
thread(() -> {
|
||||
try {
|
||||
UpdateChecker.checkUpdate();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to check for update", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
UpdateChecker.updateChannelProperty().bind(Bindings.createStringBinding(() -> {
|
||||
switch (ConfigHolder.config().getUpdateChannel()) {
|
||||
case DEVELOPMENT:
|
||||
return UpdateChecker.CHANNEL_DEV;
|
||||
default:
|
||||
return UpdateChecker.CHANNEL_STABLE;
|
||||
}
|
||||
}, ConfigHolder.config().updateChannelProperty()));
|
||||
|
||||
primaryStage.show();
|
||||
} catch (Throwable e) {
|
||||
@@ -80,9 +93,8 @@ 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"));
|
||||
@@ -146,14 +158,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,6 +35,7 @@ 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 {
|
||||
|
||||
@@ -42,9 +43,17 @@ public final class Main {
|
||||
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 +96,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
|
||||
);
|
||||
|
||||
@@ -218,7 +218,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 +232,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 +240,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;
|
||||
}
|
||||
|
||||
@@ -390,7 +396,7 @@ public final class LauncherHelper {
|
||||
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()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -149,6 +149,9 @@ public final class Config implements Cloneable, Observable {
|
||||
@SerializedName("authlibInjectorServers")
|
||||
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
|
||||
|
||||
@SerializedName("updateChannel")
|
||||
private ObjectProperty<EnumUpdateChannel> updateChannel = new SimpleObjectProperty<>(EnumUpdateChannel.STABLE);
|
||||
|
||||
@SerializedName("_version")
|
||||
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
|
||||
|
||||
@@ -435,4 +438,15 @@ public final class Config implements Cloneable, Observable {
|
||||
return authlibInjectorServers;
|
||||
}
|
||||
|
||||
public EnumUpdateChannel getUpdateChannel() {
|
||||
return updateChannel.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<EnumUpdateChannel> updateChannelProperty() {
|
||||
return updateChannel;
|
||||
}
|
||||
|
||||
public void setUpdateChannel(EnumUpdateChannel 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
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 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;
|
||||
|
||||
public enum EnumUpdateChannel {
|
||||
STABLE,
|
||||
DEVELOPMENT
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -104,6 +104,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;
|
||||
|
||||
|
||||
@@ -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,16 @@ 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.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 +89,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;
|
||||
@@ -105,6 +117,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");
|
||||
|
||||
@@ -194,8 +208,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<EnumUpdateChannel> updateChannel = new SimpleObjectProperty<EnumUpdateChannel>() {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
EnumUpdateChannel updateChannel = Objects.requireNonNull(get());
|
||||
chkUpdateDev.setSelected(updateChannel == EnumUpdateChannel.DEVELOPMENT);
|
||||
chkUpdateStable.setSelected(updateChannel == EnumUpdateChannel.STABLE);
|
||||
}
|
||||
};
|
||||
|
||||
ToggleGroup updateChannelGroup = new ToggleGroup();
|
||||
chkUpdateDev.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateDev.setUserData(EnumUpdateChannel.DEVELOPMENT);
|
||||
chkUpdateStable.setToggleGroup(updateChannelGroup);
|
||||
chkUpdateStable.setUserData(EnumUpdateChannel.STABLE);
|
||||
updateChannelGroup.getToggles().forEach(
|
||||
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
updateChannel.set((EnumUpdateChannel) toggle.getUserData());
|
||||
}
|
||||
}));
|
||||
updateChannel.bindBidirectional(ConfigHolder.config().updateChannelProperty());
|
||||
// ====
|
||||
|
||||
// ==== Background ====
|
||||
backgroundItem.loadChildren(Collections.singletonList(
|
||||
@@ -237,26 +303,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);
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +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 org.jackhuang.hmcl.util.VersionNumber;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,109 @@
|
||||
*/
|
||||
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.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
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 com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.ImmediateStringProperty;
|
||||
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 static final String CHANNEL_STABLE = "stable";
|
||||
public static final String CHANNEL_DEV = "dev";
|
||||
|
||||
public UpdateChecker(VersionNumber base) {
|
||||
this.base = base;
|
||||
}
|
||||
private static StringProperty updateChannel = new ImmediateStringProperty(null, "updateChannel", CHANNEL_STABLE);
|
||||
|
||||
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 String getUpdateChannel() {
|
||||
return updateChannel.get();
|
||||
}
|
||||
|
||||
private boolean isDevelopmentVersion(String version) {
|
||||
return version.contains("@") || // eg. @HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@
|
||||
public static void setUpdateChannel(String updateChannel) {
|
||||
UpdateChecker.updateChannel.set(updateChannel);
|
||||
}
|
||||
|
||||
public static StringProperty updateChannelProperty() {
|
||||
return updateChannel;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public static void checkUpdate() throws IOException {
|
||||
if (!IntegrityChecker.isSelfVerified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
JFXUtilities.runInFXAndWait(() -> {
|
||||
checkingUpdate.set(true);
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
String channel = getUpdateChannel();
|
||||
String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf(
|
||||
pair("version", Metadata.VERSION),
|
||||
pair("channel", channel)));
|
||||
|
||||
RemoteVersion fetched = RemoteVersion.fetch(url);
|
||||
Platform.runLater(() -> {
|
||||
if (channel.equals(getUpdateChannel())) {
|
||||
latestVersion.set(fetched);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
Platform.runLater(() -> checkingUpdate.set(false));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
+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" />
|
||||
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
|
||||
@@ -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=選擇要安裝的遊戲整合包文件
|
||||
@@ -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=更新
|
||||
|
||||
@@ -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=选择要安装的游戏整合包文件
|
||||
@@ -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=更新
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,6 +9,7 @@ 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.nio.charset.Charset;
|
||||
@@ -145,7 +145,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"))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,11 +229,9 @@ public class DefaultLauncher extends Launcher {
|
||||
try {
|
||||
for (Library library : version.getLibraries())
|
||||
if (library.isNative())
|
||||
CompressingUtils.unzip(repository.getLibraryFile(version, library),
|
||||
destination,
|
||||
"",
|
||||
library.getExtract()::shouldExtract,
|
||||
false);
|
||||
new Unzipper(repository.getLibraryFile(version, library), destination)
|
||||
.setFilter((destFile, isDirectory, zipEntry, path) -> library.getExtract().shouldExtract(path))
|
||||
.setReplaceExistentFile(true).unzip();
|
||||
} catch (IOException e) {
|
||||
throw new NotDecompressingNativesException(e);
|
||||
}
|
||||
|
||||
@@ -49,17 +49,15 @@ final class StreamPump implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Constants.SYSTEM_CHARSET))) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
|
||||
callback.accept(line);
|
||||
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Constants.SYSTEM_CHARSET))) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
|
||||
callback.accept(line);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "An error occurred when reading stream", e);
|
||||
|
||||
@@ -76,6 +76,10 @@ public final class CurseInstallTask extends Task {
|
||||
builder.version("forge", modLoader.getId().substring("forge-".length()));
|
||||
dependents.add(builder.buildAsync());
|
||||
|
||||
onDone().register(event -> {
|
||||
if (event.isFailed()) repository.removeVersionFromDisk(name);
|
||||
});
|
||||
|
||||
ModpackConfiguration<CurseManifest> config = null;
|
||||
try {
|
||||
if (json.exists()) {
|
||||
|
||||
@@ -20,15 +20,13 @@ package org.jackhuang.hmcl.mod;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -114,11 +112,11 @@ public final class ForgeModMetadata {
|
||||
}
|
||||
|
||||
public static ModInfo fromFile(File modFile) throws IOException, JsonParseException {
|
||||
try (ZipFile zipFile = new ZipFile(modFile)) {
|
||||
ZipArchiveEntry entry = zipFile.getEntry("mcmod.info");
|
||||
if (entry == null)
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile.toPath())) {
|
||||
Path mcmod = fs.getPath("mcmod.info");
|
||||
if (Files.notExists(mcmod))
|
||||
throw new IOException("File " + modFile + " is not a Forge mod.");
|
||||
List<ForgeModMetadata> modList = Constants.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)),
|
||||
List<ForgeModMetadata> modList = Constants.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(mcmod)),
|
||||
new TypeToken<List<ForgeModMetadata>>() {
|
||||
}.getType());
|
||||
if (modList == null || modList.isEmpty())
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -41,7 +42,7 @@ public final class MinecraftInstanceTask<T> extends Task {
|
||||
|
||||
public MinecraftInstanceTask(File zipFile, String subDirectory, T manifest, String type, File jsonFile) {
|
||||
this.zipFile = zipFile;
|
||||
this.subDirectory = subDirectory;
|
||||
this.subDirectory = FileUtils.normalizePath(subDirectory);
|
||||
this.manifest = manifest;
|
||||
this.jsonFile = jsonFile;
|
||||
this.type = type;
|
||||
@@ -54,18 +55,18 @@ public final class MinecraftInstanceTask<T> extends Task {
|
||||
public void execute() throws Exception {
|
||||
List<ModpackConfiguration.FileInformation> overrides = new LinkedList<>();
|
||||
|
||||
try (ZipArchiveInputStream zip = new ZipArchiveInputStream(new FileInputStream(zipFile), null, true, true)) {
|
||||
ArchiveEntry entry;
|
||||
while ((entry = zip.getNextEntry()) != null) {
|
||||
String path = entry.getName();
|
||||
if (!path.startsWith(subDirectory) || entry.isDirectory())
|
||||
continue;
|
||||
path = path.substring(subDirectory.length());
|
||||
if (path.startsWith("/") || path.startsWith("\\"))
|
||||
path = path.substring(1);
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
Path root = fs.getPath(subDirectory);
|
||||
|
||||
overrides.add(new ModpackConfiguration.FileInformation(path, encodeHex(digest("SHA-1", zip))));
|
||||
}
|
||||
if (Files.exists(root))
|
||||
Files.walkFileTree(fs.getPath(subDirectory), new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
String relativePath = root.relativize(file).normalize().toString();
|
||||
overrides.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", Files.newInputStream(file)))));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FileUtils.writeText(jsonFile, Constants.GSON.toJson(new ModpackConfiguration<>(manifest, type, overrides)));
|
||||
|
||||
@@ -17,17 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.Unzipper;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.jackhuang.hmcl.util.DigestUtils.digest;
|
||||
@@ -56,61 +54,37 @@ public class ModpackInstallTask<T> extends Task {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
Set<String> entries = new HashSet<>();
|
||||
byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
if (!FileUtils.makeDirectory(dest))
|
||||
throw new IOException("Unable to make directory " + dest);
|
||||
|
||||
HashSet<String> files = new HashSet<>();
|
||||
HashMap<String, ModpackConfiguration.FileInformation> files = new HashMap<>();
|
||||
for (ModpackConfiguration.FileInformation file : overrides)
|
||||
files.add(file.getPath());
|
||||
files.put(file.getPath(), file);
|
||||
|
||||
try (ZipArchiveInputStream zipStream = new ZipArchiveInputStream(new FileInputStream(modpackFile), null, true, true)) {
|
||||
ArchiveEntry entry;
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
String path = entry.getName();
|
||||
new Unzipper(modpackFile, dest)
|
||||
.setSubDirectory(subDirectory)
|
||||
.setTerminateIfSubDirectoryNotExists()
|
||||
.setReplaceExistentFile(true)
|
||||
.setFilter((destPath, isDirectory, zipEntry, entryPath) -> {
|
||||
if (isDirectory) return true;
|
||||
if (!callback.test(entryPath)) return false;
|
||||
entries.add(entryPath);
|
||||
|
||||
if (!path.startsWith(subDirectory))
|
||||
continue;
|
||||
path = path.substring(subDirectory.length());
|
||||
if (path.startsWith("/") || path.startsWith("\\"))
|
||||
path = path.substring(1);
|
||||
File entryFile = new File(dest, path);
|
||||
|
||||
if (callback != null)
|
||||
if (!callback.test(path))
|
||||
continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!FileUtils.makeDirectory(entryFile))
|
||||
throw new IOException("Unable to make directory: " + entryFile);
|
||||
} else {
|
||||
if (!FileUtils.makeDirectory(entryFile.getAbsoluteFile().getParentFile()))
|
||||
throw new IOException("Unable to make parent directory for file " + entryFile);
|
||||
|
||||
entries.add(path);
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE);
|
||||
IOUtils.copyTo(zipStream, os, buf);
|
||||
byte[] data = os.toByteArray();
|
||||
|
||||
if (files.contains(path) && entryFile.exists()) {
|
||||
String oldHash = encodeHex(digest("SHA-1", new FileInputStream(entryFile)));
|
||||
String newHash = encodeHex(digest("SHA-1", new ByteArrayInputStream(data)));
|
||||
if (!oldHash.equals(newHash)) {
|
||||
try (FileOutputStream fos = new FileOutputStream(entryFile)) {
|
||||
IOUtils.copyTo(new ByteArrayInputStream(data), fos, buf);
|
||||
}
|
||||
}
|
||||
} else if (!files.contains(path)) {
|
||||
try (FileOutputStream fos = new FileOutputStream(entryFile)) {
|
||||
IOUtils.copyTo(new ByteArrayInputStream(data), fos, buf);
|
||||
}
|
||||
if (!files.containsKey(entryPath)) {
|
||||
// If old modpack does not have this entry, add this entry or override the file that user added.
|
||||
return true;
|
||||
} else if (!Files.exists(destPath)) {
|
||||
// If both old and new modpacks have this entry, but the file is deleted by user, leave it missing.
|
||||
return false;
|
||||
} else {
|
||||
// If user modified this entry file, we will not replace this file since this modified file is that user expects.
|
||||
String fileHash = encodeHex(digest("SHA-1", Files.newInputStream(destPath)));
|
||||
String oldHash = files.get(entryPath).getHash();
|
||||
return Objects.equals(oldHash, fileHash);
|
||||
}
|
||||
}).unzip();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If old modpack have this entry, and new modpack deleted it. Delete this file.
|
||||
for (ModpackConfiguration.FileInformation file : overrides) {
|
||||
File original = new File(dest, file.getPath());
|
||||
if (original.exists() && !entries.contains(file.getPath()))
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
@@ -262,15 +263,17 @@ public final class MultiMCInstanceConfiguration {
|
||||
return mmcPack;
|
||||
}
|
||||
|
||||
public static Modpack readMultiMCModpackManifest(File f) throws IOException {
|
||||
try (ZipFile zipFile = new ZipFile(f)) {
|
||||
ZipArchiveEntry firstEntry = zipFile.getEntries().nextElement();
|
||||
String name = StringUtils.substringBefore(firstEntry.getName(), '/');
|
||||
ZipArchiveEntry entry = zipFile.getEntry(name + "/instance.cfg");
|
||||
if (entry == null)
|
||||
throw new IOException("`instance.cfg` not found, " + f + " is not a valid MultiMC modpack.");
|
||||
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(f);
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, zipFile.getInputStream(entry), manifest);
|
||||
public static Modpack readMultiMCModpackManifest(File modpackFile) throws IOException {
|
||||
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(modpackFile);
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modpackFile.toPath())) {
|
||||
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
||||
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
|
||||
String name = root.normalize().getFileName().toString();
|
||||
|
||||
Path instancePath = root.resolve("instance.cfg");
|
||||
if (Files.notExists(instancePath))
|
||||
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, Files.newInputStream(instancePath), manifest);
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), cfg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.JsonUtils;
|
||||
|
||||
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;
|
||||
|
||||
@Immutable
|
||||
@@ -49,14 +52,14 @@ public final class MultiMCManifest {
|
||||
return components;
|
||||
}
|
||||
|
||||
public static MultiMCManifest readMultiMCModpackManifest(File f) throws IOException {
|
||||
try (ZipFile zipFile = new ZipFile(f)) {
|
||||
ZipArchiveEntry firstEntry = zipFile.getEntries().nextElement();
|
||||
String name = StringUtils.substringBefore(firstEntry.getName(), '/');
|
||||
ZipArchiveEntry entry = zipFile.getEntry(name + "/mmc-pack.json");
|
||||
if (entry == null)
|
||||
public static MultiMCManifest readMultiMCModpackManifest(File zipFile) throws IOException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
||||
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
|
||||
Path mmcPack = root.resolve("mmc-pack.json");
|
||||
if (Files.notExists(mmcPack))
|
||||
return null;
|
||||
String json = IOUtils.readFullyAsString(zipFile.getInputStream(entry));
|
||||
String json = IOUtils.readFullyAsString(Files.newInputStream(mmcPack));
|
||||
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
|
||||
|
||||
if (manifest != null && manifest.getComponents() == null)
|
||||
|
||||
@@ -19,8 +19,6 @@ package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
|
||||
@@ -28,13 +26,13 @@ import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
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.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -98,7 +96,7 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, manifest.getName() + "/minecraft/", Constants.truePredicate(), config));
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, "/" + manifest.getName() + "/minecraft", Constants.truePredicate(), config));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -115,26 +113,30 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
public void execute() throws Exception {
|
||||
Version version = Objects.requireNonNull(repository.readVersionJson(name));
|
||||
|
||||
try (ZipFile zip = new ZipFile(zipFile)) {
|
||||
for (ZipArchiveEntry entry : Lang.asIterable(zip.getEntries())) {
|
||||
// ensure that this entry is in folder 'patches' and is a json file.
|
||||
if (!entry.isDirectory() && entry.getName().startsWith(manifest.getName() + "/patches/") && entry.getName().endsWith(".json")) {
|
||||
MultiMCInstancePatch patch = Constants.GSON.fromJson(IOUtils.readFullyAsString(zip.getInputStream(entry)), MultiMCInstancePatch.class);
|
||||
List<String> newArguments = new LinkedList<>();
|
||||
for (String arg : patch.getTweakers()) {
|
||||
newArguments.add("--tweakClass");
|
||||
newArguments.add(arg);
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
||||
.orElseThrow(() -> new IOException("Not a valid MultiMC modpack"));
|
||||
Path patches = root.resolve("patches");
|
||||
|
||||
if (Files.exists(patches))
|
||||
for (Path patchJson : Files.newDirectoryStream(patches)) {
|
||||
if (patchJson.endsWith(".json")) {
|
||||
MultiMCInstancePatch patch = Constants.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(patchJson)), MultiMCInstancePatch.class);
|
||||
List<String> newArguments = new LinkedList<>();
|
||||
for (String arg : patch.getTweakers()) {
|
||||
newArguments.add("--tweakClass");
|
||||
newArguments.add(arg);
|
||||
}
|
||||
version = version
|
||||
.setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries()))
|
||||
.setMainClass(patch.getMainClass())
|
||||
.setArguments(version.getArguments().orElseGet(Arguments::new).addGameArguments(newArguments));
|
||||
}
|
||||
version = version
|
||||
.setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries()))
|
||||
.setMainClass(patch.getMainClass())
|
||||
.setArguments(version.getArguments().orElseGet(Arguments::new).addGameArguments(newArguments));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies.add(new VersionJsonSaveTask(repository, version));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, manifest.getName() + "/minecraft/", manifest, MODPACK_TYPE, repository.getModpackConfiguration(name)));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, "/" + manifest.getName() + "/minecraft", manifest, MODPACK_TYPE, repository.getModpackConfiguration(name)));
|
||||
}
|
||||
|
||||
public static final String MODPACK_TYPE = "MultiMC";
|
||||
|
||||
@@ -115,9 +115,21 @@ public class FileDownloadTask extends Task {
|
||||
}
|
||||
|
||||
private void closeFiles() {
|
||||
IOUtils.closeQuietly(rFile);
|
||||
if (rFile != null)
|
||||
try {
|
||||
rFile.close();
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to close file: " + rFile, e);
|
||||
}
|
||||
|
||||
rFile = null;
|
||||
IOUtils.closeQuietly(stream);
|
||||
|
||||
if (stream != null)
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to close stream", e);
|
||||
}
|
||||
stream = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
|
||||
import java.io.*;
|
||||
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.nio.file.spi.FileSystemProvider;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Utilities of compressing
|
||||
@@ -35,179 +34,52 @@ import java.util.function.Predicate;
|
||||
*/
|
||||
public final class CompressingUtils {
|
||||
|
||||
private static final FileSystemProvider ZIPFS_PROVIDER = FileSystemProvider.installedProviders().stream()
|
||||
.filter(it -> "jar".equalsIgnoreCase(it.getScheme()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Zipfs not supported"));
|
||||
|
||||
private CompressingUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress the given directory to a zip file.
|
||||
*
|
||||
* @param sourceDir the source directory or a file.
|
||||
* @param zipFile the location of dest zip file.
|
||||
* @param pathNameCallback callback(pathName, isDirectory) returns your modified pathName
|
||||
* @throws IOException if there is filesystem error.
|
||||
*/
|
||||
public static void zip(File sourceDir, File zipFile, BiFunction<String, Boolean, String> pathNameCallback) throws IOException {
|
||||
try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream(new FileOutputStream(zipFile))) {
|
||||
String basePath;
|
||||
if (sourceDir.isDirectory())
|
||||
basePath = sourceDir.getPath();
|
||||
else
|
||||
basePath = sourceDir.getParent();
|
||||
zipFile(sourceDir, basePath, zos, pathNameCallback);
|
||||
zos.closeArchiveEntry();
|
||||
}
|
||||
public static FileSystem createReadOnlyZipFileSystem(Path zipFile) throws IOException {
|
||||
return createReadOnlyZipFileSystem(zipFile, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip file.
|
||||
*
|
||||
* @param src source directory to be compressed.
|
||||
* @param basePath the file directory to be compressed, if [src] is a file, this is the parent directory of [src]
|
||||
* @param zos the [ZipOutputStream] of dest zip file.
|
||||
* @param pathNameCallback callback(pathName, isDirectory) returns your modified pathName, null if you dont want this file zipped
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
private static void zipFile(File src, String basePath,
|
||||
ZipArchiveOutputStream zos, BiFunction<String, Boolean, String> pathNameCallback) throws IOException {
|
||||
File[] files = src.isDirectory() ? src.listFiles() : new File[] { src };
|
||||
String pathName;// the relative path (relative to the root directory to be compressed)
|
||||
byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
|
||||
if (files == null) return;
|
||||
for (File file : files)
|
||||
if (file.isDirectory()) {
|
||||
pathName = file.getPath().substring(basePath.length() + 1) + "/";
|
||||
if (pathNameCallback != null)
|
||||
pathName = pathNameCallback.apply(pathName, true);
|
||||
if (pathName == null)
|
||||
continue;
|
||||
zos.putArchiveEntry(new ZipArchiveEntry(pathName));
|
||||
zipFile(file, basePath, zos, pathNameCallback);
|
||||
} else {
|
||||
pathName = file.getPath().substring(basePath.length() + 1);
|
||||
if (pathNameCallback != null)
|
||||
pathName = pathNameCallback.apply(pathName, true);
|
||||
if (pathName == null)
|
||||
continue;
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
zos.putArchiveEntry(new ZipArchiveEntry(pathName));
|
||||
IOUtils.copyTo(is, zos, buf);
|
||||
}
|
||||
}
|
||||
public static FileSystem createReadOnlyZipFileSystem(Path zipFile, String encoding) throws IOException {
|
||||
return createZipFileSystem(zipFile, false, false, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @param src the input zip file.
|
||||
* @param dest the dest directory.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public static void unzip(File src, File dest) throws IOException {
|
||||
unzip(src, dest, "");
|
||||
public static FileSystem createWritableZipFileSystem(Path zipFile) throws IOException {
|
||||
return createWritableZipFileSystem(zipFile, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @param src the input zip file.
|
||||
* @param dest the dest directory.
|
||||
* @param subDirectory the subdirectory of the zip file to be decompressed.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public static void unzip(File src, File dest, String subDirectory) throws IOException {
|
||||
unzip(src, dest, subDirectory, null);
|
||||
public static FileSystem createWritableZipFileSystem(Path zipFile, String encoding) throws IOException {
|
||||
return createZipFileSystem(zipFile, true, true, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @param src the input zip file.
|
||||
* @param dest the dest directory.
|
||||
* @param subDirectory the subdirectory of the zip file to be decompressed.
|
||||
* @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public static void unzip(File src, File dest, String subDirectory, Predicate<String> callback) throws IOException {
|
||||
unzip(src, dest, subDirectory, callback, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @param src the input zip file.
|
||||
* @param dest the dest directory.
|
||||
* @param subDirectory the subdirectory of the zip file to be decompressed.
|
||||
* @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed.
|
||||
* @param ignoreExistentFile true if skip all existent files.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public static void unzip(File src, File dest, String subDirectory, Predicate<String> callback, boolean ignoreExistentFile) throws IOException {
|
||||
unzip(src, dest, subDirectory, callback, ignoreExistentFile, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @param src the input zip file.
|
||||
* @param dest the dest directory.
|
||||
* @param subDirectory the subdirectory of the zip file to be decompressed.
|
||||
* @param callback will be called for every entry in the zip file, returns false if you dont want this file being uncompressed.
|
||||
* @param ignoreExistentFile true if skip all existent files.
|
||||
* @param allowStoredEntriesWithDataDescriptor whether the zip stream will try to read STORED entries that use a data descriptor
|
||||
* @throws IOException if zip file is malformed or filesystem error.
|
||||
*/
|
||||
public static void unzip(File src, File dest, String subDirectory, Predicate<String> callback, boolean ignoreExistentFile, boolean allowStoredEntriesWithDataDescriptor) throws IOException {
|
||||
byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
if (!FileUtils.makeDirectory(dest))
|
||||
throw new IOException("Unable to make directory " + dest);
|
||||
try (ZipArchiveInputStream zipStream = new ZipArchiveInputStream(new FileInputStream(src), null, true, allowStoredEntriesWithDataDescriptor)) {
|
||||
ArchiveEntry entry;
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
String path = entry.getName();
|
||||
|
||||
if (!path.startsWith(subDirectory))
|
||||
continue;
|
||||
path = path.substring(subDirectory.length());
|
||||
if (path.startsWith("/") || path.startsWith("\\"))
|
||||
path = path.substring(1);
|
||||
File entryFile = new File(dest, path);
|
||||
|
||||
if (callback != null)
|
||||
if (!callback.test(path))
|
||||
continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!FileUtils.makeDirectory(entryFile))
|
||||
throw new IOException("Unable to make directory: " + entryFile);
|
||||
} else {
|
||||
if (!FileUtils.makeDirectory(entryFile.getAbsoluteFile().getParentFile()))
|
||||
throw new IOException("Unable to make parent directory for file " + entryFile);
|
||||
|
||||
if (ignoreExistentFile && entryFile.exists())
|
||||
continue;
|
||||
try (FileOutputStream fos = new FileOutputStream(entryFile)) {
|
||||
IOUtils.copyTo(zipStream, fos, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public static FileSystem createZipFileSystem(Path zipFile, boolean create, boolean useTempFile, String encoding) throws IOException {
|
||||
Map<String, Object> env = new HashMap<>();
|
||||
if (create)
|
||||
env.put("create", "true");
|
||||
if (encoding != null)
|
||||
env.put("encoding", encoding);
|
||||
if (useTempFile)
|
||||
env.put("useTempFile", true);
|
||||
return ZIPFS_PROVIDER.newFileSystem(zipFile, env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the text content of a file in zip.
|
||||
*
|
||||
* @param file the zip file
|
||||
* @param zipFile the zip file
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @return the content of given file.
|
||||
* @return the plain text content of given file.
|
||||
*/
|
||||
public static String readTextZipEntry(File file, String name) throws IOException {
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
ZipArchiveEntry entry = zipFile.getEntry(name);
|
||||
if (entry == null)
|
||||
throw new IOException("ZipEntry `" + name + "` not found in " + file);
|
||||
return IOUtils.readFullyAsString(zipFile.getInputStream(entry));
|
||||
public static String readTextZipEntry(File zipFile, String name) throws IOException {
|
||||
try (FileSystem fs = createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
return new String(Files.readAllBytes(fs.getPath(name)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +88,7 @@ public final class CompressingUtils {
|
||||
*
|
||||
* @param file the zip file
|
||||
* @param name the location of the text in zip file, something like A/B/C/D.txt
|
||||
* @return the content of given file.
|
||||
* @return the plain text content of given file.
|
||||
*/
|
||||
public static Optional<String> readTextZipEntryQuietly(File file, String name) {
|
||||
try {
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.nio.file.*;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -45,6 +45,13 @@ public final class FileUtils {
|
||||
return StringUtils.substringAfterLast(file.getName(), '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is for normalizing ZipPath since Path.normalize of ZipFileSystem does not work properly.
|
||||
*/
|
||||
public static String normalizePath(String path) {
|
||||
return StringUtils.addPrefix(StringUtils.removeSuffix(path, "/", "\\"), "/");
|
||||
}
|
||||
|
||||
public static String readText(File file) throws IOException {
|
||||
return readText(file, UTF_8);
|
||||
}
|
||||
@@ -54,11 +61,7 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static byte[] readBytes(File file) throws IOException {
|
||||
try (FileInputStream input = new FileInputStream(file)) {
|
||||
if (file.length() > Integer.MAX_VALUE)
|
||||
throw new OutOfMemoryError("File " + file + " is too big (" + file.length() + " bytes) to fit in memory.");
|
||||
return IOUtils.readFullyAsByteArray(input);
|
||||
}
|
||||
return Files.readAllBytes(file.toPath());
|
||||
}
|
||||
|
||||
public static void writeText(File file, String text) throws IOException {
|
||||
@@ -70,9 +73,7 @@ public final class FileUtils {
|
||||
}
|
||||
|
||||
public static void writeBytes(File file, byte[] array) throws IOException {
|
||||
try (FileOutputStream stream = new FileOutputStream(file)) {
|
||||
stream.write(array);
|
||||
}
|
||||
Files.write(file.toPath(), array);
|
||||
}
|
||||
|
||||
public static void deleteDirectory(File directory)
|
||||
@@ -94,10 +95,6 @@ public final class FileUtils {
|
||||
return Lang.test(() -> deleteDirectory(directory));
|
||||
}
|
||||
|
||||
public static boolean cleanDirectoryQuietly(File directory) {
|
||||
return Lang.test(() -> cleanDirectory(directory));
|
||||
}
|
||||
|
||||
public static void cleanDirectory(File directory)
|
||||
throws IOException {
|
||||
if (!directory.exists()) {
|
||||
@@ -129,16 +126,14 @@ public final class FileUtils {
|
||||
|
||||
public static void forceDelete(File file)
|
||||
throws IOException {
|
||||
if (file.isDirectory())
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file);
|
||||
else {
|
||||
} else {
|
||||
boolean filePresent = file.exists();
|
||||
if (!file.delete()) {
|
||||
if (!filePresent)
|
||||
throw new FileNotFoundException("File does not exist: " + file);
|
||||
String message = "Unable to delete file: " + file;
|
||||
|
||||
throw new IOException(message);
|
||||
throw new IOException("Unable to delete file: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,67 +154,6 @@ public final class FileUtils {
|
||||
return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
|
||||
}
|
||||
|
||||
public static void copyDirectory(File srcDir, File destDir)
|
||||
throws IOException {
|
||||
copyDirectory(srcDir, destDir, null);
|
||||
}
|
||||
|
||||
public static void copyDirectory(File srcDir, File destDir, FileFilter filter)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(srcDir, "Source must not be null");
|
||||
Objects.requireNonNull(destDir, "Destination must not be null");
|
||||
if (!srcDir.exists())
|
||||
throw new FileNotFoundException("Source '" + srcDir + "' does not exist");
|
||||
if (!srcDir.isDirectory())
|
||||
throw new IOException("Source '" + srcDir + "' exists but is not a directory");
|
||||
if (srcDir.getCanonicalPath().equals(destDir.getCanonicalPath()))
|
||||
throw new IOException("Source '" + srcDir + "' and destination '" + destDir + "' are the same");
|
||||
|
||||
List<String> exclusionList = null;
|
||||
if (destDir.getCanonicalPath().startsWith(srcDir.getCanonicalPath())) {
|
||||
File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter);
|
||||
if ((srcFiles != null) && (srcFiles.length > 0)) {
|
||||
exclusionList = new ArrayList<>(srcFiles.length);
|
||||
for (File srcFile : srcFiles) {
|
||||
File copiedFile = new File(destDir, srcFile.getName());
|
||||
exclusionList.add(copiedFile.getCanonicalPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
doCopyDirectory(srcDir, destDir, filter, exclusionList);
|
||||
}
|
||||
|
||||
private static void doCopyDirectory(File srcDir, File destDir, FileFilter filter, List<String> exclusionList)
|
||||
throws IOException {
|
||||
File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter);
|
||||
if (srcFiles == null)
|
||||
throw new IOException("Failed to list contents of " + srcDir);
|
||||
if (destDir.exists()) {
|
||||
if (!destDir.isDirectory())
|
||||
throw new IOException("Destination '" + destDir + "' exists but is not a directory");
|
||||
} else if (!FileUtils.makeDirectory(destDir))
|
||||
throw new IOException("Destination '" + destDir + "' directory cannot be created");
|
||||
|
||||
if (!destDir.canWrite())
|
||||
throw new IOException("Destination '" + destDir + "' cannot be written to");
|
||||
for (File srcFile : srcFiles) {
|
||||
File dstFile = new File(destDir, srcFile.getName());
|
||||
if ((exclusionList == null) || (!exclusionList.contains(srcFile.getCanonicalPath())))
|
||||
if (srcFile.isDirectory())
|
||||
doCopyDirectory(srcFile, dstFile, filter, exclusionList);
|
||||
else
|
||||
doCopyFile(srcFile, dstFile);
|
||||
}
|
||||
destDir.setLastModified(srcDir.lastModified());
|
||||
}
|
||||
|
||||
public static void copyFileQuietly(File srcFile, File destFile) {
|
||||
try {
|
||||
copyFile(srcFile, destFile);
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyFile(File srcFile, File destFile)
|
||||
throws IOException {
|
||||
Objects.requireNonNull(srcFile, "Source must not be null");
|
||||
@@ -235,7 +169,8 @@ public final class FileUtils {
|
||||
throw new IOException("Destination '" + parentFile + "' directory cannot be created");
|
||||
if (destFile.exists() && !destFile.canWrite())
|
||||
throw new IOException("Destination '" + destFile + "' exists but is read-only");
|
||||
doCopyFile(srcFile, destFile);
|
||||
|
||||
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static void moveFile(File srcFile, File destFile) throws IOException {
|
||||
@@ -243,11 +178,6 @@ public final class FileUtils {
|
||||
srcFile.delete();
|
||||
}
|
||||
|
||||
private static void doCopyFile(File srcFile, File destFile)
|
||||
throws IOException {
|
||||
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
public static boolean makeDirectory(File directory) {
|
||||
return directory.isDirectory() || directory.mkdirs();
|
||||
}
|
||||
|
||||
@@ -31,22 +31,11 @@ public final class IOUtils {
|
||||
|
||||
public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
|
||||
|
||||
public static void closeQuietly(Closeable closeable) {
|
||||
try {
|
||||
if (closeable != null)
|
||||
closeable.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
|
||||
try {
|
||||
try (InputStream is = stream) {
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
copyTo(stream, result);
|
||||
copyTo(is, result);
|
||||
return result;
|
||||
} finally {
|
||||
closeQuietly(stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.CodeSource;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
public final class JarUtils {
|
||||
|
||||
public static Optional<Path> thisJar() {
|
||||
CodeSource codeSource = JarUtils.class.getProtectionDomain().getCodeSource();
|
||||
if (codeSource == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
URL url = codeSource.getLocation();
|
||||
if (url == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Path path;
|
||||
try {
|
||||
path = Paths.get(url.toURI());
|
||||
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(path);
|
||||
}
|
||||
|
||||
public static Optional<Manifest> getManifest(Path jar) {
|
||||
try (JarFile file = new JarFile(jar.toFile())) {
|
||||
return Optional.ofNullable(file.getManifest());
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<String> getImplementationVersion(Path jar) {
|
||||
return Optional.of(jar).flatMap(JarUtils::getManifest)
|
||||
.flatMap(manifest -> Optional.ofNullable(manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION)));
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,9 @@ import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -38,7 +37,7 @@ import java.util.stream.Stream;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class JavaVersion implements Serializable {
|
||||
public final class JavaVersion {
|
||||
|
||||
private final File binary;
|
||||
private final String longVersion;
|
||||
@@ -67,12 +66,11 @@ public final class JavaVersion implements Serializable {
|
||||
/**
|
||||
* The major version of Java installation.
|
||||
*
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_X
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_11
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_10
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_9
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_8
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_7
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_6
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#JAVA_5
|
||||
* @see org.jackhuang.hmcl.util.JavaVersion#UNKNOWN
|
||||
*/
|
||||
public int getParsedVersion() {
|
||||
@@ -82,52 +80,44 @@ public final class JavaVersion implements Serializable {
|
||||
private static final Pattern REGEX = Pattern.compile("version \"(?<version>(.*?))\"");
|
||||
|
||||
public static final int UNKNOWN = -1;
|
||||
public static final int JAVA_5 = 50;
|
||||
public static final int JAVA_6 = 60;
|
||||
public static final int JAVA_7 = 70;
|
||||
public static final int JAVA_8 = 80;
|
||||
public static final int JAVA_9 = 90;
|
||||
public static final int JAVA_X = 100;
|
||||
public static final int JAVA_10 = 100;
|
||||
public static final int JAVA_11 = 110;
|
||||
|
||||
private static int parseVersion(String version) {
|
||||
if (version.startsWith("10") || version.startsWith("X"))
|
||||
return JAVA_X;
|
||||
else if (version.contains("1.9.") || version.startsWith("9"))
|
||||
if (version.startsWith("11"))
|
||||
return JAVA_11;
|
||||
else if (version.startsWith("10"))
|
||||
return JAVA_10;
|
||||
else if (version.startsWith("9"))
|
||||
return JAVA_9;
|
||||
else if (version.contains("1.8"))
|
||||
return JAVA_8;
|
||||
else if (version.contains("1.7"))
|
||||
return JAVA_7;
|
||||
else if (version.contains("1.6"))
|
||||
return JAVA_6;
|
||||
else if (version.contains("1.5"))
|
||||
return JAVA_5;
|
||||
else
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
public static JavaVersion fromExecutable(File executable) throws IOException {
|
||||
File actualFile = executable;
|
||||
Platform platform = Platform.BIT_32;
|
||||
String version = null;
|
||||
|
||||
if ("javaw".equals(FileUtils.getNameWithoutExtension(actualFile)))
|
||||
actualFile = new File(actualFile.getAbsoluteFile().getParentFile(), "java");
|
||||
// javaw is only used on windows
|
||||
if ("javaw.exe".equalsIgnoreCase(executable.getName()))
|
||||
executable = new File(executable.getAbsoluteFile().getParentFile(), "java.exe");
|
||||
|
||||
try {
|
||||
Process process = new ProcessBuilder(actualFile.getAbsolutePath(), "-version").start();
|
||||
process.waitFor();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
||||
for (String line; (line = reader.readLine()) != null; ) {
|
||||
Matcher m = REGEX.matcher(line);
|
||||
if (m.find())
|
||||
version = m.group("version");
|
||||
if (line.contains("64-Bit"))
|
||||
platform = Platform.BIT_64;
|
||||
}
|
||||
Process process = new ProcessBuilder(executable.getAbsolutePath(), "-version").start();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
||||
for (String line; (line = reader.readLine()) != null;) {
|
||||
Matcher m = REGEX.matcher(line);
|
||||
if (m.find())
|
||||
version = m.group("version");
|
||||
if (line.contains("64-Bit"))
|
||||
platform = Platform.BIT_64;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException("Interrupted scanning the java version.", e);
|
||||
}
|
||||
|
||||
if (version == null)
|
||||
@@ -135,24 +125,20 @@ public final class JavaVersion implements Serializable {
|
||||
|
||||
if (parseVersion(version) == UNKNOWN)
|
||||
throw new IOException("Unrecognized Java version " + version);
|
||||
return new JavaVersion(actualFile, version, platform);
|
||||
}
|
||||
|
||||
private static JavaVersion fromKnownExecutable(File file, String version) {
|
||||
return new JavaVersion(file, version, Platform.UNKNOWN);
|
||||
return new JavaVersion(executable, version, platform);
|
||||
}
|
||||
|
||||
public static JavaVersion fromJavaHome(File home) throws IOException {
|
||||
return fromExecutable(getJavaFile(home));
|
||||
return fromExecutable(getExecutable(home));
|
||||
}
|
||||
|
||||
private static File getJavaFile(File home) {
|
||||
File path = new File(home, "bin");
|
||||
File javaw = new File(path, "javae.exe");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && javaw.isFile())
|
||||
return javaw;
|
||||
else
|
||||
return new File(path, "java"); // Both linux and windows allow this.
|
||||
private static File getExecutable(File javaHome) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
return new File(javaHome, "bin/java.exe");
|
||||
} else {
|
||||
return new File(javaHome, "bin/java");
|
||||
}
|
||||
}
|
||||
|
||||
public static JavaVersion fromCurrentEnvironment() {
|
||||
@@ -160,7 +146,7 @@ public final class JavaVersion implements Serializable {
|
||||
}
|
||||
|
||||
public static final JavaVersion THIS_JAVA = new JavaVersion(
|
||||
getJavaFile(new File(System.getProperty("java.home"))),
|
||||
getExecutable(new File(System.getProperty("java.home"))),
|
||||
System.getProperty("java.version"),
|
||||
Platform.PLATFORM
|
||||
);
|
||||
@@ -168,10 +154,6 @@ public final class JavaVersion implements Serializable {
|
||||
private static List<JavaVersion> JAVAS;
|
||||
private static final CountDownLatch LATCH = new CountDownLatch(1);
|
||||
|
||||
public static Optional<List<JavaVersion>> getJREsImmediately() {
|
||||
return Optional.ofNullable(JAVAS);
|
||||
}
|
||||
|
||||
public static List<JavaVersion> getJREs() throws InterruptedException {
|
||||
if (JAVAS != null)
|
||||
return JAVAS;
|
||||
@@ -194,7 +176,7 @@ public final class JavaVersion implements Serializable {
|
||||
javaVersions = queryMacintosh();
|
||||
break;
|
||||
default:
|
||||
javaVersions = Collections.emptyList();
|
||||
javaVersions = new ArrayList<>();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -214,6 +196,7 @@ public final class JavaVersion implements Serializable {
|
||||
LATCH.countDown();
|
||||
}
|
||||
|
||||
// ==== Linux ====
|
||||
private static List<JavaVersion> queryLinux() throws IOException {
|
||||
Path jvmDir = Paths.get("/usr/lib/jvm");
|
||||
if (Files.isDirectory(jvmDir)) {
|
||||
@@ -234,9 +217,11 @@ public final class JavaVersion implements Serializable {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== OSX ====
|
||||
private static List<JavaVersion> queryMacintosh() throws IOException {
|
||||
LinkedList<JavaVersion> res = new LinkedList<>();
|
||||
List<JavaVersion> res = new ArrayList<>();
|
||||
|
||||
File currentJRE = new File("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home");
|
||||
if (currentJRE.exists())
|
||||
@@ -248,18 +233,32 @@ public final class JavaVersion implements Serializable {
|
||||
|
||||
return res;
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== Windows ====
|
||||
private static List<JavaVersion> queryWindows() {
|
||||
LinkedList<JavaVersion> res = new LinkedList<>();
|
||||
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\")));
|
||||
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\")));
|
||||
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\")));
|
||||
Lang.ignoringException(() -> res.addAll(queryRegisterKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\")));
|
||||
List<JavaVersion> res = new ArrayList<>();
|
||||
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\")));
|
||||
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\")));
|
||||
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\")));
|
||||
Lang.ignoringException(() -> res.addAll(queryJavaInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\")));
|
||||
return res;
|
||||
}
|
||||
|
||||
private static List<JavaVersion> queryJavaInRegistryKey(String location) throws IOException, InterruptedException {
|
||||
List<JavaVersion> res = new ArrayList<>();
|
||||
for (String java : querySubFolders(location)) {
|
||||
if (!querySubFolders(java).contains(java + "\\MSI")) continue;
|
||||
String home = queryRegisterValue(java, "JavaHome");
|
||||
if (home != null)
|
||||
res.add(fromJavaHome(new File(home)));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Registry utilities
|
||||
private static List<String> querySubFolders(String location) throws IOException, InterruptedException {
|
||||
List<String> res = new LinkedList<>();
|
||||
List<String> res = new ArrayList<>();
|
||||
String[] cmd = new String[] { "cmd", "/c", "reg", "query", location };
|
||||
Process process = Runtime.getRuntime().exec(cmd);
|
||||
process.waitFor();
|
||||
@@ -272,16 +271,6 @@ public final class JavaVersion implements Serializable {
|
||||
return res;
|
||||
}
|
||||
|
||||
private static List<JavaVersion> queryRegisterKey(String location) throws IOException, InterruptedException {
|
||||
List<JavaVersion> res = new LinkedList<>();
|
||||
for (String java : querySubFolders(location)) {
|
||||
String home = queryRegisterValue(java, "JavaHome");
|
||||
if (home != null)
|
||||
res.add(fromJavaHome(new File(home)));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private static String queryRegisterValue(String location, String name) throws IOException, InterruptedException {
|
||||
String[] cmd = new String[] { "cmd", "/c", "reg", "query", location, "/v", name };
|
||||
boolean last = false;
|
||||
@@ -306,4 +295,5 @@ public final class JavaVersion implements Serializable {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// ====
|
||||
}
|
||||
@@ -20,9 +20,11 @@ package org.jackhuang.hmcl.util;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -48,6 +50,27 @@ public final class NetworkUtils {
|
||||
NetworkUtils.userAgentSupplier = Objects.requireNonNull(userAgentSupplier);
|
||||
}
|
||||
|
||||
public static String withQuery(String baseUrl, Map<String, String> params) {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder(baseUrl);
|
||||
boolean first = true;
|
||||
for (Entry<String, String> param : params.entrySet()) {
|
||||
if (first) {
|
||||
sb.append('?');
|
||||
first = false;
|
||||
} else {
|
||||
sb.append('&');
|
||||
}
|
||||
sb.append(URLEncoder.encode(param.getKey(), "UTF-8"));
|
||||
sb.append('=');
|
||||
sb.append(URLEncoder.encode(param.getValue(), "UTF-8"));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpURLConnection createConnection(URL url) throws IOException {
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
@@ -84,30 +107,22 @@ public final class NetworkUtils {
|
||||
con.setDoOutput(true);
|
||||
con.setRequestProperty("Content-Type", contentType + "; charset=utf-8");
|
||||
con.setRequestProperty("Content-Length", "" + bytes.length);
|
||||
OutputStream os = null;
|
||||
try {
|
||||
os = con.getOutputStream();
|
||||
if (os != null)
|
||||
os.write(bytes);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(os);
|
||||
try (OutputStream os = con.getOutputStream()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
return readData(con);
|
||||
}
|
||||
|
||||
public static String readData(HttpURLConnection con) throws IOException {
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = con.getInputStream();
|
||||
return IOUtils.readFullyAsString(is, UTF_8);
|
||||
try (InputStream stdout = con.getInputStream()) {
|
||||
return IOUtils.readFullyAsString(stdout, UTF_8);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
IOUtils.closeQuietly(is);
|
||||
is = con.getErrorStream();
|
||||
if (is != null)
|
||||
return IOUtils.readFullyAsString(is, UTF_8);
|
||||
throw e;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(is);
|
||||
try (InputStream stderr = con.getErrorStream()) {
|
||||
if (stderr == null) throw e;
|
||||
return IOUtils.readFullyAsString(stderr, UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,18 +137,28 @@ public final class StringUtils {
|
||||
return str;
|
||||
}
|
||||
|
||||
public static String removePrefix(String str, String prefix) {
|
||||
public static String addPrefix(String str, String prefix) {
|
||||
if (str.startsWith(prefix))
|
||||
return str.substring(prefix.length(), str.length());
|
||||
else
|
||||
return str;
|
||||
else
|
||||
return prefix + str;
|
||||
}
|
||||
|
||||
public static String removeSuffix(String str, String suffix) {
|
||||
if (str.endsWith(suffix))
|
||||
return str.substring(0, str.length() - suffix.length());
|
||||
else
|
||||
return str;
|
||||
public static String removePrefix(String str, String... prefixes) {
|
||||
for (String prefix : prefixes)
|
||||
if (str.startsWith(prefix))
|
||||
return str.substring(prefix.length());
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one suffix of the suffixes of the string.
|
||||
*/
|
||||
public static String removeSuffix(String str, String... suffixes) {
|
||||
for (String suffix : suffixes)
|
||||
if (str.endsWith(suffix))
|
||||
return str.substring(0, str.length() - suffix.length());
|
||||
return str;
|
||||
}
|
||||
|
||||
public static boolean containsOne(Collection<String> patterns, String... targets) {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
public class ToStringBuilder {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
public class Unzipper {
|
||||
private final Path zipFile, dest;
|
||||
private boolean replaceExistentFile = false;
|
||||
private boolean terminateIfSubDirectoryNotExists = false;
|
||||
private String subDirectory = "/";
|
||||
private FileFilter filter = null;
|
||||
private String encoding;
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @param zipFile the input zip file to be uncompressed
|
||||
* @param destDir the dest directory to hold uncompressed files
|
||||
*/
|
||||
public Unzipper(Path zipFile, Path destDir) {
|
||||
this.zipFile = zipFile;
|
||||
this.dest = destDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @param zipFile the input zip file to be uncompressed
|
||||
* @param destDir the dest directory to hold uncompressed files
|
||||
*/
|
||||
public Unzipper(File zipFile, File destDir) {
|
||||
this(zipFile.toPath(), destDir.toPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* True if replace the existent files in destination directory,
|
||||
* otherwise those conflict files will be ignored.
|
||||
*/
|
||||
public Unzipper setReplaceExistentFile(boolean replaceExistentFile) {
|
||||
this.replaceExistentFile = replaceExistentFile;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be called for every entry in the zip file.
|
||||
* Callback returns false if you want leave the specific file uncompressed.
|
||||
*/
|
||||
public Unzipper setFilter(FileFilter filter) {
|
||||
this.filter = filter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will only uncompress files in the "subDirectory", their path will be also affected.
|
||||
*
|
||||
* For example, if you set subDirectory to /META-INF, files in /META-INF/ will be
|
||||
* uncompressed to the destination directory without creating META-INF folder.
|
||||
*
|
||||
* Default value: "/"
|
||||
*/
|
||||
public Unzipper setSubDirectory(String subDirectory) {
|
||||
this.subDirectory = FileUtils.normalizePath(subDirectory);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Unzipper setEncoding(String encoding) {
|
||||
this.encoding = encoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Unzipper setTerminateIfSubDirectoryNotExists() {
|
||||
this.terminateIfSubDirectoryNotExists = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompress the given zip file to a directory.
|
||||
*
|
||||
* @throws IOException if zip file is malformed or filesystem error.
|
||||
*/
|
||||
public void unzip() throws IOException {
|
||||
Files.createDirectories(dest);
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile, encoding)) {
|
||||
Path root = fs.getPath(subDirectory);
|
||||
if (!root.isAbsolute() || (subDirectory.length() > 1 && subDirectory.endsWith("/")))
|
||||
throw new IllegalArgumentException("Subdirectory for unzipper must be absolute");
|
||||
|
||||
if (terminateIfSubDirectoryNotExists && Files.notExists(root))
|
||||
return;
|
||||
|
||||
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file,
|
||||
BasicFileAttributes attrs) throws IOException {
|
||||
String relativePath = root.relativize(file).toString();
|
||||
Path destFile = dest.resolve(relativePath);
|
||||
if (filter != null && !filter.accept(file, false, destFile, relativePath))
|
||||
return FileVisitResult.CONTINUE;
|
||||
if (replaceExistentFile || Files.notExists(destFile))
|
||||
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir,
|
||||
BasicFileAttributes attrs) throws IOException {
|
||||
String relativePath = root.relativize(dir).toString();
|
||||
Path dirToCreate = dest.resolve(relativePath);
|
||||
if (filter != null && !filter.accept(dir, true, dirToCreate, relativePath))
|
||||
return FileVisitResult.CONTINUE;
|
||||
if (Files.notExists(dirToCreate)) {
|
||||
Files.createDirectory(dirToCreate);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public interface FileFilter {
|
||||
boolean accept(Path destPath, boolean isDirectory, Path zipEntry, String entryPath) throws IOException;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +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.util;
|
||||
|
||||
|
||||
import java.io.*;
|
||||
import java.util.HashSet;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Non thread-safe
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class ZipEngine implements Closeable {
|
||||
|
||||
private final byte[] buf = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
ZipOutputStream zos;
|
||||
|
||||
public ZipEngine(File f) throws IOException {
|
||||
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
zos.closeEntry();
|
||||
zos.close();
|
||||
}
|
||||
|
||||
public void putDirectory(File sourceDir) throws IOException {
|
||||
putDirectory(sourceDir, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress all the files in sourceDir
|
||||
*
|
||||
* @param sourceDir the directory to be compressed.
|
||||
* @param pathNameCallback callback(pathName, isDirectory) returns your
|
||||
* modified pathName
|
||||
*
|
||||
* @throws java.io.IOException if unable to compress or read files.
|
||||
*/
|
||||
public void putDirectory(File sourceDir, BiFunction<String, Boolean, String> pathNameCallback) throws IOException {
|
||||
putDirectoryImpl(sourceDir, sourceDir.isDirectory() ? sourceDir.getPath() : sourceDir.getParent(), pathNameCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress all the files in sourceDir
|
||||
*
|
||||
* @param source the file in basePath to be compressed
|
||||
* @param basePath the root directory to be compressed.
|
||||
* @param pathNameCallback callback(pathName, isDirectory) returns your
|
||||
* modified pathName, null if you dont want this file zipped
|
||||
*/
|
||||
private void putDirectoryImpl(File source, String basePath, BiFunction<String, Boolean, String> pathNameCallback) throws IOException {
|
||||
File[] files = null;
|
||||
if (source.isDirectory())
|
||||
files = source.listFiles();
|
||||
else if (source.isFile())
|
||||
files = new File[] { source };
|
||||
|
||||
if (files == null)
|
||||
return;
|
||||
String pathName; // the relative path (relative to basePath)
|
||||
for (File file : files)
|
||||
if (file.isDirectory()) {
|
||||
pathName = file.getPath().substring(basePath.length() + 1)
|
||||
+ "/";
|
||||
pathName = pathName.replace('\\', '/');
|
||||
if (pathNameCallback != null)
|
||||
pathName = pathNameCallback.apply(pathName, true);
|
||||
if (pathName == null)
|
||||
continue;
|
||||
put(new ZipEntry(pathName));
|
||||
putDirectoryImpl(file, basePath, pathNameCallback);
|
||||
} else {
|
||||
if (".DS_Store".equals(file.getName())) // For Mac computers.
|
||||
continue;
|
||||
pathName = file.getPath().substring(basePath.length() + 1);
|
||||
pathName = pathName.replace('\\', '/');
|
||||
if (pathNameCallback != null)
|
||||
pathName = pathNameCallback.apply(pathName, false);
|
||||
if (pathName == null)
|
||||
continue;
|
||||
putFile(file, pathName);
|
||||
}
|
||||
}
|
||||
|
||||
public void putFile(File file, String pathName) throws IOException {
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
putStream(fis, pathName);
|
||||
}
|
||||
}
|
||||
|
||||
public void putStream(InputStream is, String pathName) throws IOException {
|
||||
put(new ZipEntry(pathName));
|
||||
IOUtils.copyTo(is, zos, buf);
|
||||
}
|
||||
|
||||
public void putTextFile(String text, String pathName) throws IOException {
|
||||
putTextFile(text, "UTF-8", pathName);
|
||||
}
|
||||
|
||||
public void putTextFile(String text, String encoding, String pathName) throws IOException {
|
||||
putStream(new ByteArrayInputStream(text.getBytes(encoding)), pathName);
|
||||
}
|
||||
|
||||
private final HashSet<String> names = new HashSet<>();
|
||||
|
||||
public void put(ZipEntry entry) throws IOException {
|
||||
if (names.add(entry.getName()))
|
||||
zos.putNextEntry(entry);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Non thread-safe
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class Zipper implements Closeable {
|
||||
|
||||
private final FileSystem fs;
|
||||
|
||||
public Zipper(Path zipFile) throws IOException {
|
||||
this(zipFile, null);
|
||||
}
|
||||
|
||||
public Zipper(Path zipFile, String encoding) throws IOException {
|
||||
Files.deleteIfExists(zipFile);
|
||||
fs = CompressingUtils.createWritableZipFileSystem(zipFile, encoding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
fs.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress all the files in sourceDir
|
||||
*
|
||||
* @param source the file in basePath to be compressed
|
||||
* @param rootDir the path of the directory in this zip file.
|
||||
*/
|
||||
public void putDirectory(Path source, String rootDir) throws IOException {
|
||||
putDirectory(source, rootDir, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress all the files in sourceDir
|
||||
*
|
||||
* @param source the file in basePath to be compressed
|
||||
* @param targetDir the path of the directory in this zip file.
|
||||
* @param filter returns false if you do not want that file or directory
|
||||
*/
|
||||
public void putDirectory(Path source, String targetDir, Predicate<String> filter) throws IOException {
|
||||
File[] files = null;
|
||||
|
||||
Path root = fs.getPath(targetDir);
|
||||
Files.createDirectories(root);
|
||||
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (".DS_Store".equals(file.getFileName().toString())) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
String relativePath = source.relativize(file).normalize().toString();
|
||||
if (filter != null && !filter.test(relativePath)) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
Files.copy(file, root.resolve(relativePath));
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
String relativePath = source.relativize(dir).normalize().toString();
|
||||
if (filter != null && !filter.test(relativePath)) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
Path path = root.resolve(relativePath);
|
||||
if (Files.notExists(path)) {
|
||||
Files.createDirectory(path);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void putFile(File file, String path) throws IOException {
|
||||
putFile(file.toPath(), path);
|
||||
}
|
||||
|
||||
public void putFile(Path file, String path) throws IOException {
|
||||
Files.copy(file, fs.getPath(path));
|
||||
}
|
||||
|
||||
public void putStream(InputStream in, String path) throws IOException {
|
||||
Files.copy(in, fs.getPath(path));
|
||||
}
|
||||
|
||||
public void putTextFile(String text, String path) throws IOException {
|
||||
putTextFile(text, "UTF-8", path);
|
||||
}
|
||||
|
||||
public void putTextFile(String text, String encoding, String pathName) throws IOException {
|
||||
Files.write(fs.getPath(pathName), text.getBytes(encoding));
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -103,7 +103,7 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -116,11 +116,13 @@
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>MinSpace</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<IntrinsicFunctions>false</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<InlineFunctionExpansion>Disabled</InlineFunctionExpansion>
|
||||
<FavorSizeOrSpeed>Size</FavorSizeOrSpeed>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
@@ -133,12 +135,14 @@
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<Optimization>MinSpace</Optimization>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<IntrinsicFunctions>false</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<ConformanceMode>false</ConformanceMode>
|
||||
<InlineFunctionExpansion>Disabled</InlineFunctionExpansion>
|
||||
<FavorSizeOrSpeed>Size</FavorSizeOrSpeed>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
|
||||
Binary file not shown.
+8
-6
@@ -19,14 +19,17 @@
|
||||
group 'org.jackhuang'
|
||||
version '3.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'org.tukaani:xz:1.8'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group 'org.jackhuang'
|
||||
version '3.0'
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'idea'
|
||||
|
||||
@@ -39,7 +42,6 @@ allprojects {
|
||||
|
||||
dependencies {
|
||||
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
|
||||
compile group: 'org.apache.commons', name: 'commons-compress', version: '1.17'
|
||||
compile group: 'org.tukaani', name: 'xz', version: '1.8'
|
||||
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
|
||||
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
|
||||
|
||||
Reference in new issue
Block a user