Compare commits

..
57 Commits
Author SHA1 Message Date
huanghongxun 54814181b9 Change xz compressing level to 6 since CI server does not have enough memory 2018-08-08 12:57:42 +08:00
huanghongxun 935c2bd6e5 Merge branch 'rewrite-upgrade' into javafx 2018-08-08 10:03:14 +08:00
huanghongxun a2b74b8e78 Fix getImplementationVersion returning null on Java 9. 2018-08-08 10:02:34 +08:00
huanghongxun 09730d74e0 Merge branch 'rewrite-upgrade' of https://github.com/yushijinhun/HMCL into rewrite-upgrade 2018-08-07 22:18:58 +08:00
huanghongxun 2bf03291ce Try Java8 for Minecraft 1.12.2 or older modded game 2018-08-07 22:10:34 +08:00
huanghongxun 93e5851167 Improve config compatibility with HMCL 2.x. Fix #411 2018-08-07 18:40:23 +08:00
huanghongxun 61fd581ef5 Hide the separator line between refreshNavButton and window buttons when refreshNavButton is hidden 2018-08-07 18:08:20 +08:00
huanghongxun 51afcf2dee Use ZipFileSystem instead of commons-compress 2018-08-07 11:53:05 +08:00
huanghongxun cadafe13e1 Remove IOUtils.closeQuietly 2018-08-07 11:53:05 +08:00
Haowei Wen 47031cb70e Merge pull request #413 from tsl0922/osx
修复在 OSX 下使用的一些问题
2018-08-06 18:36:53 +08:00
Shuanglei Tao b3e776b4bc Use native window border on osx (workaround for #403) 2018-08-06 14:17:41 +08:00
Shuanglei Tao e57a1a8da6 Make gradlew executable 2018-08-05 23:08:27 +08:00
yushijinhun 7f2a392a33 Use com.github.johnrengelman.shadow plugin 2018-08-05 11:06:50 +08:00
yushijinhun 055379f569 Add .pack.gz build back 2018-08-05 09:37:55 +08:00
yushijinhun 2b8843ca19 Replace .pack.gz with .pack.xz 2018-08-05 01:02:42 +08:00
yushijinhun ace830bced Refactor build.gradle 2018-08-05 00:42:05 +08:00
yushijinhun 3c2f232acb Rename PACK -> PACK_GZ 2018-08-04 23:11:31 +08:00
yushijinhun 86030e64ca Extract writeToStorage() 2018-08-04 23:10:14 +08:00
huanghongxun b1ce3c31f5 Build packed archive 2018-08-04 22:12:02 +08:00
huanghongxun 04553c2c56 Support packed update archive 2018-08-04 22:04:59 +08:00
yushijinhun bdb7afa860 Replace updateSource with updateChannel 2018-08-04 21:39:19 +08:00
yushijinhun 7096135cdf Change update source to hmcl.huangyuhui.net 2018-08-04 21:26:46 +08:00
yushijinhun be37014206 Use Paths.get(url.toURI()) in LocalVersion.current() 2018-08-04 21:17:52 +08:00
yushijinhun b78beb3305 Copy downloaded HMCL to .hmcl only after it's verified 2018-08-01 15:54:52 +08:00
yushijinhun 38f27d134d Change task.test() to executor.test() 2018-08-01 15:54:52 +08:00
yushijinhun 3b42ba9375 Add version query when checking for update 2018-08-01 15:54:52 +08:00
yushijinhun 4f52bb2ffe Add option hmcl.update_source.override 2018-07-31 19:41:50 +08:00
yushijinhun dd45e1b3db Add JAR integrity check 2018-07-31 19:39:21 +08:00
yushijinhun e38bfdfc73 Refactor build.gradle 2018-07-31 15:48:29 +08:00
yushijinhun 73caebff00 Remove unnecessary saveConfig() call 2018-07-31 14:33:03 +08:00
yushijinhun e0608cdf3e Remove unused files 2018-07-31 14:32:42 +08:00
yushijinhun c815fa80e8 Prevent /build being created 2018-07-31 14:06:54 +08:00
yushijinhun e91e1cab47 Remove coreCompile/Runtime.extendsFrom 2018-07-31 14:04:36 +08:00
yushijinhun b791878651 Remove generateSources task 2018-07-31 14:02:26 +08:00
yushijinhun 8192ffec03 Remove duplicated HMCLauncher.exe 2018-07-31 13:59:48 +08:00
yushijinhun afa6eb3bad Refactor build.gradle 2018-07-31 13:46:31 +08:00
yushijinhun 6f7d22518e Remove pack.gz build 2018-07-31 13:34:17 +08:00
yushijinhun 2663312dfb Auto-rename after upgrade 2018-07-31 13:28:39 +08:00
yushijinhun 1332517a9f Rewrite upgrade feature 2018-07-30 23:32:01 +08:00
yushijinhun 13e227851f Refactor IconedItem 2018-07-29 21:10:04 +08:00
yushijinhun e1b6c18017 Remove HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING 2018-07-29 19:07:48 +08:00
yushijinhun a88c8ed85f Extract class Metadata 2018-07-29 19:03:49 +08:00
yushijinhun 25a90fc481 Remove upgrade feature 2018-07-29 18:42:31 +08:00
huangyuhui 1371206323 Pack with exe launcher 2018-07-29 16:34:15 +08:00
huanghongxun 129f6bb5cc Merge pull request #407 from yushijinhun/config-permission
重构 Config
2018-07-29 14:44:23 +08:00
yushijinhun 545c17d052 Print current directory & hmcl directory when bootstrap 2018-07-29 14:27:50 +08:00
yushijinhun add00ddba1 Use configured locale as soon as possible 2018-07-29 14:21:43 +08:00
yushijinhun d9aeb0cc2b Use SupportedLocale in Config 2018-07-29 14:15:32 +08:00
yushijinhun 49e70e7195 Add ConfigHolder.isNewlyCreated() 2018-07-29 14:02:32 +08:00
yushijinhun f8abebf110 Remove isFirstLaunch 2018-07-29 13:45:22 +08:00
yushijinhun 8e2dfc7341 Show error dialog when hmcl.json is not accessible 2018-07-29 13:40:59 +08:00
yushijinhun e650ffffce Initialize ConfigHolder & Settings manually 2018-07-29 13:40:59 +08:00
yushijinhun d549a300ab Replace Settings.INSTANCE with Settings.instance() 2018-07-29 13:40:59 +08:00
yushijinhun 82a62e73c1 Replace CONFIG with config() 2018-07-29 13:40:57 +08:00
yushijinhun 45c7e0c231 Remove Settings.save() 2018-07-29 13:39:32 +08:00
huangyuhui b98a4b11a3 Fix #406 2018-07-29 13:31:18 +08:00
huangyuhui 484a103f27 Fix always showing the welcome page when upgrading from old versions 2018-07-29 13:25:15 +08:00
85 changed files with 2082 additions and 1578 deletions

No files matched your search

Binary file not shown.
+106 -90
View File
@@ -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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.
@@ -17,27 +17,29 @@
*/
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.stage.Stage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.AppDataUpgrader;
import org.jackhuang.hmcl.upgrade.IUpgrader;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
public final class Launcher extends Application {
@@ -53,12 +55,13 @@ 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();
thread(() -> {
try {
UpdateChecker.checkUpdate();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check for update", e);
}
});
primaryStage.show();
} catch (Throwable e) {
@@ -77,13 +80,14 @@ 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));
Logging.LOG.info("*** " + TITLE + " ***");
Logging.LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
Logging.LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
Logging.LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
Logging.LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -92,7 +96,7 @@ public final class Launcher extends Application {
}
public static void stopApplication() {
Logging.LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
LOG.info("Stopping application.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
@@ -106,7 +110,7 @@ public final class Launcher extends Application {
}
public static void stopWithoutPlatform() {
Logging.LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
LOG.info("Stopping application without JavaFX Toolkit.\n" + StringUtils.getStackTrace(Thread.currentThread().getStackTrace()));
JFXUtilities.runInFX(() -> {
if (Controllers.getStage() == null)
@@ -141,14 +145,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,6 +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.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -32,12 +34,22 @@ import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.swing.JOptionPane;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
public final class Main {
public static void main(String[] args) {
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
checkConfigPermission();
if (UpdateHandler.processArguments(args)) {
return;
}
ConfigHolder.init();
Launcher.main(args);
}
@@ -80,6 +92,22 @@ 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";
}
@@ -33,7 +33,7 @@ import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
* @author huangyuhui
@@ -63,7 +63,7 @@ public class HMCLGameDownloadTask extends Task {
// Force using common directory will not affect the behaviour that repository acts
// Since we always copy the downloaded jar to .minecraft/versions/<version>/
File cache = new File(Optional.ofNullable(Settings.INSTANCE.getCommonDirectory())
File cache = new File(Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(Settings.getDefaultCommonDirectory()),
"jars/" + gameVersion + ".jar");
if (cache.exists())
@@ -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;
}
}
@@ -34,7 +34,7 @@ import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@@ -58,10 +58,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
@Override
public File getAssetDirectory(String version, String assetId) {
if (Settings.INSTANCE.isCommonDirectoryDisabled() || useSelf(assetId))
if (Settings.instance().isCommonDirectoryDisabled() || useSelf(assetId))
return super.getAssetDirectory(version, assetId);
else
return new File(Settings.INSTANCE.getCommonDirectory(), "assets");
return new File(Settings.instance().getCommonDirectory(), "assets");
}
@Override
@@ -83,10 +83,10 @@ public class HMCLGameRepository extends DefaultGameRepository {
public File getLibraryFile(Version version, Library lib) {
VersionSetting vs = profile.getVersionSetting(version.getId());
File self = super.getLibraryFile(version, lib);
if (Settings.INSTANCE.isCommonDirectoryDisabled() || self.exists())
if (Settings.instance().isCommonDirectoryDisabled() || self.exists())
return self;
else
return new File(Settings.INSTANCE.getCommonDirectory(), "libraries/" + lib.getPath());
return new File(Settings.instance().getCommonDirectory(), "libraries/" + lib.getPath());
}
@@ -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;
}
@@ -378,7 +384,7 @@ public final class LauncherHelper {
System.out.print(log);
logs.add(pair(log, level));
if (logs.size() > Settings.INSTANCE.getLogLines())
if (logs.size() > Settings.instance().getLogLines())
logs.removeFirst();
if (setting.isShowLogs()) {
@@ -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);
}
@@ -44,7 +44,7 @@ import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
@@ -60,7 +60,7 @@ public final class Accounts {
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.INSTANCE.getDownloadProvider())::getArtifactInfo,
new AuthlibInjectorDownloader(Launcher.HMCL_DIRECTORY.toPath(), () -> Settings.instance().getDownloadProvider())::getArtifactInfo,
Accounts::getOrCreateAuthlibInjectorServer);
private static final String TYPE_OFFLINE = "offline";
@@ -125,7 +125,7 @@ public final class Accounts {
// selection is valid, store it
if (!initialized)
return;
CONFIG.setSelectedAccount(selected == null ? "" : accountId(selected));
config().setSelectedAccount(selected == null ? "" : accountId(selected));
}
};
@@ -144,7 +144,7 @@ public final class Accounts {
if (!initialized)
return;
// update storage
CONFIG.getAccountStorages().setAll(
config().getAccountStorages().setAll(
accounts.stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
@@ -155,14 +155,14 @@ public final class Accounts {
}
/**
* Called when it's ready to load accounts from {@link ConfigHolder#CONFIG}.
* Called when it's ready to load accounts from {@link ConfigHolder#config()}.
*/
static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
CONFIG.getAccountStorages().forEach(storage -> {
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
@@ -180,12 +180,12 @@ public final class Accounts {
initialized = true;
CONFIG.getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
// load selected account
selectedAccount.set(
accounts.stream()
.filter(it -> accountId(it).equals(CONFIG.getSelectedAccount()))
.filter(it -> accountId(it).equals(config().getSelectedAccount()))
.findFirst()
.orElse(null));
}
@@ -212,7 +212,7 @@ public final class Accounts {
// ==== authlib-injector ====
private static AuthlibInjectorServer getOrCreateAuthlibInjectorServer(String url) {
return CONFIG.getAuthlibInjectorServers().stream()
return config().getAuthlibInjectorServers().stream()
.filter(server -> url.equals(server.getUrl()))
.findFirst()
.orElseGet(() -> {
@@ -226,7 +226,7 @@ public final class Accounts {
LOG.log(Level.WARNING, "Failed to migrate authlib injector server " + url, e);
}
CONFIG.getAuthlibInjectorServers().add(server);
config().getAuthlibInjectorServers().add(server);
return server;
});
}
@@ -239,7 +239,7 @@ public final class Accounts {
accounts.stream()
.filter(AuthlibInjectorAccount.class::isInstance)
.map(AuthlibInjectorAccount.class::cast)
.filter(it -> !CONFIG.getAuthlibInjectorServers().contains(it.getServer()))
.filter(it -> !config().getAuthlibInjectorServers().contains(it.getServer()))
.collect(toList())
.forEach(accounts::remove);
}
@@ -33,6 +33,8 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.FileTypeAdapter;
import org.jackhuang.hmcl.util.ObservableHelper;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -67,6 +69,7 @@ public final class Config implements Cloneable, Observable {
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.registerTypeAdapter(Theme.class, new Theme.TypeAdapter())
.registerTypeAdapter(SupportedLocale.class, new SupportedLocale.TypeAdapter())
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
@@ -120,7 +123,7 @@ public final class Config implements Cloneable, Observable {
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
@SerializedName("localization")
private StringProperty localization = new SimpleStringProperty();
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@SerializedName("downloadtype")
private IntegerProperty downloadType = new SimpleIntegerProperty(1);
@@ -143,9 +146,6 @@ public final class Config implements Cloneable, Observable {
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(100);
@SerializedName("firstLaunch")
private BooleanProperty firstLaunch = new SimpleBooleanProperty(true);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@@ -351,15 +351,15 @@ public final class Config implements Cloneable, Observable {
return theme;
}
public String getLocalization() {
public SupportedLocale getLocalization() {
return localization.get();
}
public void setLocalization(String localization) {
public void setLocalization(SupportedLocale localization) {
this.localization.set(localization);
}
public StringProperty localizationProperty() {
public ObjectProperty<SupportedLocale> localizationProperty() {
return localization;
}
@@ -431,18 +431,6 @@ public final class Config implements Cloneable, Observable {
return logLines;
}
public boolean isFirstLaunch() {
return firstLaunch.get();
}
public void setFirstLaunch(boolean firstLaunch) {
this.firstLaunch.set(firstLaunch);
}
public BooleanProperty firstLaunchProperty() {
return firstLaunch;
}
public ObservableList<AuthlibInjectorServer> getAuthlibInjectorServers() {
return authlibInjectorServers;
}
@@ -29,6 +29,7 @@ import java.util.logging.Level;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.StringUtils;
public final class ConfigHolder {
@@ -36,40 +37,85 @@ public final class ConfigHolder {
public static final String CONFIG_FILENAME = "hmcl.json";
public static final Path CONFIG_PATH = Paths.get(CONFIG_FILENAME).toAbsolutePath();
public static final Config CONFIG = initSettings();
private static Config upgradeSettings(Config deserialized, Map rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
return deserialized;
private static Config configInstance;
private static boolean newlyCreated;
public static Config config() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return configInstance;
}
private static Config initSettings() {
Config config = new Config();
public static boolean isNewlyCreated() {
if (configInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return newlyCreated;
}
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configInstance = loadConfig();
configInstance.addListener(source -> saveConfig());
Settings.init();
if (newlyCreated) {
saveConfig();
}
}
private static Config loadConfig() {
if (Files.exists(CONFIG_PATH)) {
try {
String json = new String(Files.readAllBytes(CONFIG_PATH), UTF_8);
Map raw = new Gson().fromJson(json, Map.class);
Config deserialized = Config.fromJson(json);
String content = new String(Files.readAllBytes(CONFIG_PATH), UTF_8);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.finer("Settings file is empty, use the default settings.");
LOG.info("Config is empty");
} else {
config = upgradeSettings(deserialized, raw);
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
upgradeConfig(deserialized, raw);
return deserialized;
}
LOG.finest("Initialized settings.");
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Something happened wrongly when load settings.", e);
LOG.log(Level.WARNING, "Something went wrong when loading config.", e);
}
}
return config;
LOG.info("Creating an empty config");
newlyCreated = true;
return new Config();
}
static void saveConfig(Config config) {
static void saveConfig() {
LOG.info("Saving config");
try {
Files.write(CONFIG_PATH, config.toJson().getBytes(UTF_8));
Files.write(CONFIG_PATH, configInstance.toJson().getBytes(UTF_8));
} catch (IOException ex) {
LOG.log(Level.SEVERE, "Failed to save config", ex);
}
}
/**
* 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
* @return {@code deserialized}
*/
private static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
// Following is for the compatibility with HMCL 2.x
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
}
@@ -122,7 +122,7 @@ public final class Profile {
}
public HMCLDependencyManager getDependency() {
return new HMCLDependencyManager(this, Settings.INSTANCE.getDownloadProvider());
return new HMCLDependencyManager(this, Settings.instance().getDownloadProvider());
}
public VersionSetting getVersionSetting(String id) {
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.setting;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.net.Authenticator;
import java.net.InetSocketAddress;
@@ -38,18 +38,18 @@ public final class ProxyManager {
private static ObjectBinding<Proxy> proxyProperty = Bindings.createObjectBinding(
() -> {
String host = CONFIG.getProxyHost();
Integer port = Lang.toIntOrNull(CONFIG.getProxyPort());
if (!CONFIG.hasProxy() || StringUtils.isBlank(host) || port == null || CONFIG.getProxyType() == Proxy.Type.DIRECT) {
String host = config().getProxyHost();
Integer port = Lang.toIntOrNull(config().getProxyPort());
if (!config().hasProxy() || StringUtils.isBlank(host) || port == null || config().getProxyType() == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY;
} else {
return new Proxy(CONFIG.getProxyType(), new InetSocketAddress(host, port));
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
}
},
CONFIG.proxyTypeProperty(),
CONFIG.proxyHostProperty(),
CONFIG.proxyPortProperty(),
CONFIG.hasProxyProperty());
config().proxyTypeProperty(),
config().proxyHostProperty(),
config().proxyPortProperty(),
config().hasProxyProperty());
public static Proxy getProxy() {
return proxyProperty.get();
@@ -68,9 +68,9 @@ public final class ProxyManager {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (CONFIG.hasProxyAuth()) {
String username = CONFIG.getProxyUser();
String password = CONFIG.getProxyPass();
if (config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null && password != null) {
return new PasswordAuthentication(username, password.toCharArray());
}
@@ -25,25 +25,31 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class Settings {
public static final Settings INSTANCE = new Settings();
private static Settings instance;
private final boolean firstLaunch;
public static Settings instance() {
if (instance == null) {
throw new IllegalStateException("Settings hasn't been initialized");
}
return instance;
}
/**
* Should be called from {@link ConfigHolder#init()}.
*/
static void init() {
instance = new Settings();
}
private Settings() {
firstLaunch = CONFIG.isFirstLaunch();
CONFIG.setFirstLaunch(false);
ProxyManager.init();
Accounts.init();
@@ -52,51 +58,29 @@ 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 -> save());
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.saveConfig());
}
CONFIG.addListener(source -> save());
}
private void save() {
LOG.info("Saving config");
ConfigHolder.saveConfig(CONFIG);
}
public boolean isFirstLaunch() {
return firstLaunch;
}
private Locales.SupportedLocale locale = Locales.getLocaleByName(CONFIG.getLocalization());
public Locales.SupportedLocale getLocale() {
return locale;
}
public void setLocale(Locales.SupportedLocale locale) {
this.locale = locale;
CONFIG.setLocalization(Locales.getNameByLocale(locale));
}
public Font getFont() {
return Font.font(CONFIG.getFontFamily(), CONFIG.getFontSize());
return Font.font(config().getFontFamily(), config().getFontSize());
}
public void setFont(Font font) {
CONFIG.setFontFamily(font.getFamily());
CONFIG.setFontSize(font.getSize());
config().setFontFamily(font.getFamily());
config().setFontSize(font.getSize());
}
public int getLogLines() {
return Math.max(CONFIG.getLogLines(), 100);
return Math.max(config().getLogLines(), 100);
}
public void setLogLines(int logLines) {
CONFIG.setLogLines(logLines);
config().setLogLines(logLines);
}
public boolean isCommonDirectoryDisabled() {
return CONFIG.getCommonDirType() == EnumCommonDirectory.DISABLED;
return config().getCommonDirType() == EnumCommonDirectory.DISABLED;
}
public static String getDefaultCommonDirectory() {
@@ -104,13 +88,13 @@ public class Settings {
}
public String getCommonDirectory() {
switch (CONFIG.getCommonDirType()) {
switch (config().getCommonDirType()) {
case DISABLED:
return null;
case DEFAULT:
return getDefaultCommonDirectory();
case CUSTOM:
return CONFIG.getCommonDirectory();
return config().getCommonDirectory();
default:
return null;
}
@@ -121,14 +105,14 @@ public class Settings {
****************************************/
public DownloadProvider getDownloadProvider() {
return DownloadProviders.getDownloadProvider(CONFIG.getDownloadType());
return DownloadProviders.getDownloadProvider(config().getDownloadType());
}
public void setDownloadProvider(DownloadProvider downloadProvider) {
int index = DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(downloadProvider);
if (index == -1)
throw new IllegalArgumentException("Unknown download provider: " + downloadProvider);
CONFIG.setDownloadType(index);
config().setDownloadType(index);
}
/****************************************
@@ -138,18 +122,18 @@ public class Settings {
public Profile getSelectedProfile() {
checkProfileMap();
if (!hasProfile(CONFIG.getSelectedProfile())) {
if (!hasProfile(config().getSelectedProfile())) {
getProfileMap().keySet().stream().findFirst().ifPresent(selectedProfile -> {
CONFIG.setSelectedProfile(selectedProfile);
config().setSelectedProfile(selectedProfile);
});
Schedulers.computation().schedule(this::onProfileChanged);
}
return getProfile(CONFIG.getSelectedProfile());
return getProfile(config().getSelectedProfile());
}
public void setSelectedProfile(Profile selectedProfile) {
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), CONFIG.getSelectedProfile())) {
CONFIG.setSelectedProfile(selectedProfile.getName());
if (hasProfile(selectedProfile.getName()) && !Objects.equals(selectedProfile.getName(), config().getSelectedProfile())) {
config().setSelectedProfile(selectedProfile.getName());
Schedulers.computation().schedule(this::onProfileChanged);
}
}
@@ -166,7 +150,7 @@ public class Settings {
}
public Map<String, Profile> getProfileMap() {
return CONFIG.getConfigurations();
return config().getConfigurations();
}
public Collection<Profile> getProfiles() {
@@ -27,7 +27,7 @@ import org.jackhuang.hmcl.util.Logging;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.io.File;
import java.io.IOException;
@@ -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 {
@@ -120,7 +132,7 @@ public class Theme {
}
public static ObjectBinding<Color> foregroundFillBinding() {
return Bindings.createObjectBinding(() -> CONFIG.getTheme().getForegroundColor(), CONFIG.themeProperty());
return Bindings.createObjectBinding(() -> config().getTheme().getForegroundColor(), config().themeProperty());
}
public static ObjectBinding<Color> blackFillBinding() {
@@ -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.*;
@@ -30,7 +31,7 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
/**
*
@@ -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())
@@ -515,10 +516,10 @@ public final class VersionSetting {
.setFullscreen(isFullscreen())
.setServerIp(getServerIp())
.setWrapper(getWrapper())
.setProxyHost(CONFIG.getProxyHost())
.setProxyPort(CONFIG.getProxyPort())
.setProxyUser(CONFIG.getProxyUser())
.setProxyPass(CONFIG.getProxyPass())
.setProxyHost(config().getProxyHost())
.setProxyPort(config().getProxyPort())
.setProxyUser(config().getProxyUser())
.setProxyPass(config().getProxyPass())
.setPrecalledCommand(getPreLaunchCommand())
.setNoGeneratedJVMArgs(isNoJVMArgs())
.create();
@@ -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);
@@ -54,7 +54,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.jfxListCellFactory;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
@@ -79,7 +79,7 @@ public class AddAccountPane extends StackPane {
cboServers.setCellFactory(jfxListCellFactory(server -> new TwoLineListItem(server.getName(), server.getUrl())));
cboServers.setConverter(stringConverter(AuthlibInjectorServer::getName));
Bindings.bindContent(cboServers.getItems(), CONFIG.getAuthlibInjectorServers());
Bindings.bindContent(cboServers.getItems(), config().getAuthlibInjectorServers());
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
selectDefaultServer();
@@ -39,7 +39,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class AddAuthlibInjectorServerPane extends StackPane {
@@ -133,8 +133,8 @@ public class AddAuthlibInjectorServerPane extends StackPane {
@FXML
private void onAddFinish() {
if (!CONFIG.getAuthlibInjectorServers().contains(serverBeingAdded)) {
CONFIG.getAuthlibInjectorServers().add(serverBeingAdded);
if (!config().getAuthlibInjectorServers().contains(serverBeingAdded)) {
config().getAuthlibInjectorServers().add(serverBeingAdded);
}
fireEvent(new DialogCloseEvent());
}
@@ -34,7 +34,7 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", i18n("account.injector.manage.title"));
@@ -49,13 +49,13 @@ public class AuthlibInjectorServersPage extends StackPane implements DecoratorPa
loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
smoothScrolling(scrollPane);
serverItems = MappedObservableList.create(CONFIG.getAuthlibInjectorServers(), this::createServerItem);
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(listPane.getChildren(), serverItems);
}
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {
return new AuthlibInjectorServerItem(server,
item -> CONFIG.getAuthlibInjectorServers().remove(item.getServer()));
item -> config().getAuthlibInjectorServers().remove(item.getServer()));
}
@FXML
@@ -23,6 +23,7 @@ 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;
@@ -34,7 +35,7 @@ import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.JavaVersion;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.util.function.Consumer;
@@ -98,24 +99,24 @@ 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());
Settings.INSTANCE.onProfileLoading();
Settings.instance().onProfileLoading();
Task.of(JavaVersion::initialize).start();
decorator.setCustomMaximize(false);
scene = new Scene(decorator, 804, 521);
scene.getStylesheets().setAll(CONFIG.getTheme().getStylesheets());
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
stage.setMinWidth(804);
stage.setMaxWidth(804);
stage.setMinHeight(521);
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) {
@@ -178,12 +179,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);
@@ -55,8 +55,8 @@ import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -67,9 +67,10 @@ 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.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@@ -81,7 +82,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
public final class Decorator extends StackPane implements TaskExecutorDialogWizardDisplayer {
@@ -153,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);
@@ -168,7 +171,14 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
onCloseButtonAction = new SimpleObjectProperty<>(this, "onCloseButtonAction", Launcher::stopApplication);
primaryStage.initStyle(StageStyle.UNDECORATED);
switch (OperatingSystem.CURRENT_OS) {
case OSX:
titleContainer.setRight(null);
break;
default:
primaryStage.initStyle(StageStyle.UNDECORATED);
break;
}
btnClose.setGraphic(close);
btnMin.setGraphic(minus);
btnMax.setGraphic(resizeMax);
@@ -182,6 +192,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)));
@@ -200,7 +212,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
);
nowAnimation.play();
});
if (!Settings.INSTANCE.isFirstLaunch() || Settings.INSTANCE.getLocale().getLocale() != Locale.CHINA)
if (!ConfigHolder.isNewlyCreated() || config().getLocalization().getLocale() != Locale.CHINA)
drawerWrapper.getChildren().remove(welcomeView);
if (!min) buttonsContainer.getChildren().remove(btnMin);
@@ -228,8 +240,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
Bindings.createObjectBinding(
() -> {
Image image = null;
if (CONFIG.getBackgroundImageType() == EnumBackgroundImage.CUSTOM && CONFIG.getBackgroundImage() != null) {
image = tryLoadImage(Paths.get(CONFIG.getBackgroundImage()))
if (config().getBackgroundImageType() == EnumBackgroundImage.CUSTOM && config().getBackgroundImage() != null) {
image = tryLoadImage(Paths.get(config().getBackgroundImage()))
.orElse(null);
}
if (image == null) {
@@ -237,8 +249,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
return new Background(new BackgroundImage(image, BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.DEFAULT, new BackgroundSize(800, 480, false, false, true, true)));
},
CONFIG.backgroundImageTypeProperty(),
CONFIG.backgroundImageProperty()));
config().backgroundImageTypeProperty(),
config().backgroundImageProperty()));
}
private Image defaultBackground = new Image("/assets/img/background.jpg");
@@ -54,6 +54,7 @@ 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.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.MappedObservableList;
@@ -92,10 +93,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 -> {
@@ -216,11 +227,11 @@ public final class LeftPaneController {
private void onProfilesLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
for (Profile profile : Settings.INSTANCE.getProfiles()) {
for (Profile profile : Settings.instance().getProfiles()) {
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
ripplerContainer.setOnMouseClicked(e -> Settings.INSTANCE.setSelectedProfile(profile));
ripplerContainer.setOnMouseClicked(e -> Settings.instance().setSelectedProfile(profile));
ripplerContainer.getProperties().put("profile", profile.getName());
ripplerContainer.maxWidthProperty().bind(leftPane.widthProperty());
list.add(ripplerContainer);
@@ -228,11 +239,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;
@@ -43,7 +43,7 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.concurrent.CountDownLatch;
@@ -65,7 +65,7 @@ public final class LogWindow extends Stage {
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(CONFIG.getTheme().getStylesheets());
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -177,12 +177,12 @@ public final class LogWindow extends Stage {
engine = webView.getEngine();
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
.replace("${FONT}", Settings.INSTANCE.getFont().getSize() + "px \"" + Settings.INSTANCE.getFont().getFamily() + "\""));
.replace("${FONT}", Settings.instance().getFont().getSize() + "px \"" + Settings.instance().getFont().getFamily() + "\""));
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
document = engine.getDocument();
body = document.getElementsByTagName("body").item(0);
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
engine.executeScript("limitedLogs=" + Settings.instance().getLogLines());
latch.countDown();
onDone.fireEvent(new Event(LogWindow.this));
}
@@ -190,14 +190,14 @@ public final class LogWindow extends Stage {
boolean flag = false;
for (String i : cboLines.getItems())
if (Integer.toString(Settings.INSTANCE.getLogLines()).equals(i)) {
if (Integer.toString(Settings.instance().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
Settings.INSTANCE.setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
Settings.instance().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
engine.executeScript("limitedLogs=" + Settings.instance().getLogLines());
});
if (!flag)
@@ -101,7 +101,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
btnAdd.setOnMouseClicked(e -> Controllers.getDecorator().startWizard(new DownloadWizardProvider(), i18n("install")));
FXUtils.installTooltip(btnAdd, i18n("install"));
btnRefresh.setOnMouseClicked(e -> Settings.INSTANCE.getSelectedProfile().getRepository().refreshVersionsAsync().start());
btnRefresh.setOnMouseClicked(e -> Settings.instance().getSelectedProfile().getRepository().refreshVersionsAsync().start());
FXUtils.installTooltip(btnRefresh, i18n("button.refresh"));
}
@@ -80,7 +80,7 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
@FXML
private void onDelete() {
if (profile != null) {
Settings.INSTANCE.deleteProfile(profile);
Settings.instance().deleteProfile(profile);
Controllers.navigate(null);
}
}
@@ -99,10 +99,10 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
}
Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation()));
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
Settings.INSTANCE.putProfile(newProfile);
Settings.instance().putProfile(newProfile);
}
Settings.INSTANCE.onProfileLoading();
Settings.instance().onProfileLoading();
Controllers.navigate(null);
}
@@ -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,17 +39,19 @@ 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 org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.construct.FileItem;
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;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.net.Proxy;
@@ -106,52 +110,54 @@ 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");
FXUtils.smoothScrolling(scroll);
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.INSTANCE.getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
cboDownloadSource.getSelectionModel().select(DownloadProviders.DOWNLOAD_PROVIDERS.indexOf(Settings.instance().getDownloadProvider()));
cboDownloadSource.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.instance().setDownloadProvider(DownloadProviders.getDownloadProvider(newValue.intValue())));
cboFont.getSelectionModel().select(Settings.INSTANCE.getFont().getFamily());
cboFont.getSelectionModel().select(Settings.instance().getFont().getFamily());
cboFont.valueProperty().addListener((a, b, newValue) -> {
Font font = Font.font(newValue, Settings.INSTANCE.getFont().getSize());
Settings.INSTANCE.setFont(font);
Font font = Font.font(newValue, Settings.instance().getFont().getSize());
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
});
txtFontSize.setText(Double.toString(Settings.INSTANCE.getFont().getSize()));
txtFontSize.setText(Double.toString(Settings.instance().getFont().getSize()));
txtFontSize.getValidators().add(new Validator(it -> Lang.toDoubleOrNull(it) != null));
txtFontSize.textProperty().addListener((a, b, newValue) -> {
if (txtFontSize.validate()) {
Font font = Font.font(Settings.INSTANCE.getFont().getFamily(), Double.parseDouble(newValue));
Settings.INSTANCE.setFont(font);
Font font = Font.font(Settings.instance().getFont().getFamily(), Double.parseDouble(newValue));
Settings.instance().setFont(font);
lblDisplay.setStyle("-fx-font: " + font.getSize() + " \"" + font.getFamily() + "\";");
}
});
lblDisplay.setStyle("-fx-font: " + Settings.INSTANCE.getFont().getSize() + " \"" + Settings.INSTANCE.getFont().getFamily() + "\";");
lblDisplay.setStyle("-fx-font: " + Settings.instance().getFont().getSize() + " \"" + Settings.instance().getFont().getFamily() + "\";");
ObservableList<Label> list = FXCollections.observableArrayList();
for (Locales.SupportedLocale locale : Locales.LOCALES)
list.add(new Label(locale.getName(Settings.INSTANCE.getLocale().getResourceBundle())));
list.add(new Label(locale.getName(config().getLocalization().getResourceBundle())));
cboLanguage.setItems(list);
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(Settings.INSTANCE.getLocale()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setLocale(Locales.getLocale(newValue.intValue())));
cboLanguage.getSelectionModel().select(Locales.LOCALES.indexOf(config().getLocalization()));
cboLanguage.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> config().setLocalization(Locales.getLocale(newValue.intValue())));
// ==== Proxy ====
txtProxyHost.textProperty().bindBidirectional(CONFIG.proxyHostProperty());
txtProxyPort.textProperty().bindBidirectional(CONFIG.proxyPortProperty());
txtProxyUsername.textProperty().bindBidirectional(CONFIG.proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(CONFIG.proxyPassProperty());
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty());
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
proxyPane.disableProperty().bind(chkEnableProxy.selectedProperty().not());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkEnableProxy.selectedProperty().bindBidirectional(CONFIG.hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(CONFIG.hasProxyAuthProperty());
chkEnableProxy.selectedProperty().bindBidirectional(config().hasProxyProperty());
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
{
@@ -169,7 +175,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
}
}
};
selectedProxyType.bindBidirectional(CONFIG.proxyTypeProperty());
selectedProxyType.bindBidirectional(config().proxyTypeProperty());
ToggleGroup proxyConfigurationGroup = new ToggleGroup();
chkProxyHttp.setUserData(Proxy.Type.HTTP);
@@ -188,36 +194,57 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
fileCommonLocation.createChildren(i18n("launcher.common_directory.disabled"), EnumCommonDirectory.DISABLED),
fileCommonLocation.createChildren(i18n("launcher.common_directory.default"), EnumCommonDirectory.DEFAULT)
), EnumCommonDirectory.CUSTOM);
fileCommonLocation.selectedDataProperty().bindBidirectional(CONFIG.commonDirTypeProperty());
fileCommonLocation.customTextProperty().bindBidirectional(CONFIG.commonDirectoryProperty());
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.customTextProperty().bindBidirectional(config().commonDirectoryProperty());
fileCommonLocation.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.INSTANCE.getCommonDirectory())
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.common_directory.disabled")),
CONFIG.commonDirectoryProperty(), CONFIG.commonDirTypeProperty()));
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 {
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));
updateListener.invalidated(null);
// ====
// ==== Background ====
backgroundItem.loadChildren(Collections.singletonList(
backgroundItem.createChildren(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT)
), EnumBackgroundImage.CUSTOM);
backgroundItem.customTextProperty().bindBidirectional(CONFIG.backgroundImageProperty());
backgroundItem.selectedDataProperty().bindBidirectional(CONFIG.backgroundImageTypeProperty());
backgroundItem.customTextProperty().bindBidirectional(config().backgroundImageProperty());
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind(
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(CONFIG.backgroundImageProperty()));
.otherwise(config().backgroundImageProperty()));
// ====
// ==== Theme ====
JFXColorPicker picker = new JFXColorPicker(Color.web(CONFIG.getTheme().getColor()), null);
JFXColorPicker picker = new JFXColorPicker(Color.web(config().getTheme().getColor()), null);
picker.setCustomColorText(i18n("color.custom"));
picker.setRecentColorsText(i18n("color.recent"));
picker.getCustomColors().setAll(Theme.SUGGESTED_COLORS);
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
CONFIG.setTheme(theme);
config().setTheme(theme);
Controllers.getScene().getStylesheets().setAll(theme.getStylesheets());
});
themeColorPickerContainer.getChildren().setAll(picker);
@@ -238,26 +265,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);
}
}
@@ -22,14 +22,14 @@ import javafx.scene.image.Image;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class WebStage extends Stage {
private final WebView webView = new WebView();
public WebStage() {
setScene(new Scene(webView, 800, 480));
getScene().getStylesheets().addAll(CONFIG.getTheme().getStylesheets());
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
getIcons().add(new Image("/assets/img/icon.png"));
}
@@ -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;
}
}
@@ -41,7 +41,7 @@ public final class DownloadWizardProvider implements WizardProvider {
@Override
public void start(Map<String, Object> settings) {
profile = Settings.INSTANCE.getSelectedProfile();
profile = Settings.instance().getSelectedProfile();
settings.put(PROFILE, profile);
}
@@ -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;
@@ -37,7 +37,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.setting.ConfigHolder.CONFIG;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class ExportWizardProvider implements WizardProvider {
private final Profile profile;
@@ -81,19 +81,19 @@ 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());
exported.setTheme(CONFIG.getTheme());
exported.setDownloadType(CONFIG.getDownloadType());
exported.getAuthlibInjectorServers().setAll(CONFIG.getAuthlibInjectorServers());
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
exported.setTheme(config().getTheme());
exported.setDownloadType(config().getDownloadType());
exported.getAuthlibInjectorServers().setAll(config().getAuthlibInjectorServers());
zip.putTextFile(exported.toJson(), ConfigHolder.CONFIG_FILENAME);
zip.putFile(tempModpack, "modpack.zip");
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg);
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,149 @@
/*
* 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.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.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");
/**
* 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);
}
}
/**
* 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());
}
}
@@ -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,93 @@
*/
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 org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.NetworkUtils;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
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 SimpleStringProperty(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);
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 void checkUpdate() throws IOException {
if (!IntegrityChecker.isSelfVerified()) {
return;
}
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);
}
});
}
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);
@@ -23,24 +23,25 @@ import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.logging.Level;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
public final class I18n {
private I18n() {}
private static ResourceBundle RESOURCE_BUNDLE = null;
static {
private static SupportedLocale getCurrentLocale() {
try {
RESOURCE_BUNDLE = Settings.INSTANCE.getLocale().getResourceBundle();
} catch (Throwable e) {
LOG.log(Level.SEVERE, "Settings cannot be initialized", e);
return ConfigHolder.config().getLocalization();
} catch (IllegalStateException e) {
// e is thrown by ConfigHolder.config(), indicating the config hasn't been loaded
// fallback to use default locale
return Locales.DEFAULT;
}
}
public static ResourceBundle getResourceBundle() {
return RESOURCE_BUNDLE == null ? Locales.DEFAULT.getResourceBundle() : RESOURCE_BUNDLE;
return getCurrentLocale().getResourceBundle();
}
public static String i18n(String key, Object... formatArgs) {
@@ -19,6 +19,10 @@ package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.util.Lang;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@@ -107,5 +111,17 @@ public final class Locales {
if (name == null) return resourceBundle.getString("lang");
else return newResourceBundle.getString(name);
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
@Override
public void write(JsonWriter out, SupportedLocale value) throws IOException {
out.value(getNameByLocale(value));
}
@Override
public SupportedLocale read(JsonReader in) throws IOException {
return getLocaleByName(in.nextString());
}
}
}
}
File renamed without changes.
@@ -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>
@@ -31,7 +31,7 @@
</right>
</BorderPane>
<MultiFileItem fx:id="fileCommonLocation" title="%launcher.common_directory" chooserTitle="%launcher.common_directory.choose" hasSubtitle="true" customText="%settings.custom" />
<MultiFileItem fx:id="fileCommonLocation" title="%launcher.common_directory" directory="true" chooserTitle="%launcher.common_directory.choose" hasSubtitle="true" customText="%settings.custom" />
<MultiFileItem fx:id="backgroundItem" title="%launcher.background" chooserTitle="%launcher.background.choose"
hasSubtitle="true" customText="%settings.custom" />
@@ -93,6 +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.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
@@ -178,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.
@@ -308,7 +311,7 @@ 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.failed=Failed to perform upgrade
update.found=Update Available!
update.newest_version=Latest version: %s
update.latest=This is latest Version.
@@ -93,6 +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.migration_requires_manual_reboot=HMCL 即將完成升級,請重新打開 HMCL。
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們反饋該問題。
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
@@ -178,7 +181,7 @@ message.error=錯誤
message.info=提示
message.success=已完成
message.unknown=未知
message.warning=警告
modpack=整合包
modpack.choose=選擇要安裝的遊戲整合包文件
@@ -308,7 +311,7 @@ settings.type=版本設置類型
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
update=啓動器更新
update.failed=檢查更新失敗
update.failed=更新失敗
update.found=發現更新
update.newest_version=最新版本爲:%s
update.latest=當前版本爲最新版本
@@ -93,6 +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.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。
fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 HMCL 来完成升级。\n请考虑向我们反馈该问题。
folder.config=配置文件夹
folder.coremod=核心MOD文件夹
@@ -178,7 +181,7 @@ message.error=错误
message.info=提示
message.success=已完成
message.unknown=未知
message.warning=警告
modpack=整合包
modpack.choose=选择要安装的游戏整合包文件
@@ -308,7 +311,7 @@ settings.type=版本设置类型
settings.type.special=单独版本设置(不会影响到其他版本的设定)
update=启动器更新
update.failed=检查更新失败
update.failed=更新失败
update.found=发现更新
update.newest_version=最新版本为:%s
update.latest=当前版本为最新版本
@@ -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,16 @@
*/
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.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Predicate;
/**
* Utilities of compressing
@@ -38,176 +38,44 @@ public final class CompressingUtils {
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 FileSystems.newFileSystem(URI.create("jar:" + zipFile.toUri()), 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 +84,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)));
}
}
@@ -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));
}
}
+8 -6
View File
@@ -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'
Vendored Regular → Executable
View File
File mode changed.