Compare commits

..
111 Commits
Author SHA1 Message Date
huanghongxun f4f89fac1f Merge pull request #438 from yushijinhun/force-update
移除「更新一次 HMCL,电脑上所有 HMCL 都得到更新」的功能
2018-08-26 19:51:20 +08:00
yushijinhun 5262150d3c Prevent 2.x being updated to 3.x 2018-08-26 19:34:10 +08:00
yushijinhun b5f56eb2b5 Remove force update feature 2018-08-26 19:02:47 +08:00
huangyuhui 9a375fd0d1 Add hiding versions. Closes #437 2018-08-26 11:50:42 +08:00
huangyuhui 6ca4aeb5a2 Support 1.13 auto-installing 2018-08-26 11:48:27 +08:00
huangyuhui 8ed4a9a5f2 Fix not showing Optifine 1.13 alpha 2018-08-25 19:53:29 +08:00
huangyuhui a4578405cf Remove RandomUserAgent 2018-08-25 19:39:00 +08:00
huangyuhui 73f147b7e2 Update Chinese localization 2018-08-23 21:45:56 +08:00
huangyuhui 5c3d735315 Prevent HMCL 2.x updating 2018-08-23 21:45:47 +08:00
huanghongxun 200621996d Merge pull request #431 from pan93412/javafx
Update Chinese (Taiwan) Translation
2018-08-22 20:56:12 +08:00
byStarTW a912a0a07e Update Chinese (Taiwan) Translation 2018-08-22 20:54:49 +08:00
huangyuhui 4146ceb048 Fix not downloading the correct game version 2018-08-22 04:25:35 +08:00
huangyuhui ad568d2b9f Fix #429 2018-08-22 02:08:49 +08:00
huangyuhui c7bfb9a11d Suggest user to install latest Java 8 to avoid potentional game crashing 2018-08-21 21:12:05 +08:00
huangyuhui c9391181bd Display empty message when loading 1.13 forge version list 2018-08-21 19:31:04 +08:00
huangyuhui b08eb519bf Remove logging support which is meaningless 2018-08-21 11:48:19 +08:00
huangyuhui 47c4fc2f7e Fix #426 2018-08-20 11:53:02 +08:00
huangyuhui dfd177433a Fix not replacing tokens metioned in #426 2018-08-20 11:38:06 +08:00
huanghongxun 27381e9696 Merge pull request #425 from yushijinhun/yushijinhun/javafx
修复 UpdateChecker 潜在问题 & 弃用 ImmediateXXXProperty
2018-08-19 19:47:30 +08:00
yushijinhun 1c7827c53c Deprecate and partially remove ImmediateProperty 2018-08-19 15:55:18 +08:00
yushijinhun 9512bdb5d6 Remove UpdateChecker.updateChannel & Fix concurrent problems in update checking 2018-08-19 15:43:34 +08:00
huangyuhui 33d1f1d692 Not showing 'unknown' in mod info 2018-08-19 12:19:11 +08:00
huangyuhui c6b49545ff Fix #424 2018-08-19 12:13:39 +08:00
huangyuhui a4a833bfdb Fix auto-installing: missing checksums in game json 2018-08-19 11:58:02 +08:00
huangyuhui 2e1b6e4630 Fix not correctly loading remote game version list 2018-08-19 11:56:39 +08:00
huangyuhui 6911cfa387 try to move deleting version to trash 2018-08-19 11:34:07 +08:00
huangyuhui aa29f890de Log memory size and metaspace 2018-08-19 02:03:05 +08:00
huangyuhui 0f4e6a01bc Fix unable to install more than one libraries 2018-08-19 01:18:57 +08:00
huangyuhui 1d3d8d6710 Show OptiFine versions in dividually 2018-08-19 00:39:47 +08:00
huangyuhui 005e06c56f Always download Optifine from BMCLAPI 2018-08-19 00:35:37 +08:00
huangyuhui 0a7c32b3cb Friendly prompt warning user about independent version deletion 2018-08-18 22:40:08 +08:00
huangyuhui 82916a04ea Check if forge libraries' checksums are valid 2018-08-18 22:25:13 +08:00
huangyuhui ba2e419894 Fix #422 2018-08-18 22:09:16 +08:00
huangyuhui 0812c9025d Do not check game asset hash code for every launching 2018-08-18 02:00:00 +08:00
huangyuhui 4c59ed3ccf Fix installing MultiMC modpack 2018-08-18 01:12:22 +08:00
huangyuhui 43d8d96608 Add doublequotes when doublequotes exist in windows batch 2018-08-18 00:43:55 +08:00
huangyuhui 26a6a7d883 Add doublequotes when special characters exist in launch script. Closes #420 2018-08-18 00:32:37 +08:00
huangyuhui 2e9c237367 Use system proxy by default 2018-08-17 23:48:01 +08:00
huangyuhui 99d2816ea7 Suppress RuntimeException since -1 is possible 2018-08-17 22:11:46 +08:00
huangyuhui 7c3853b0e6 Not logging command line in ProcessStoppedEvent 2018-08-17 22:10:11 +08:00
huangyuhui 53aecf38e0 Support displaying Rift mod information 2018-08-17 22:09:37 +08:00
huangyuhui 41fab0a781 Log launching game version name 2018-08-17 21:56:52 +08:00
huangyuhui 3df35aa373 Translate ZipError into ZipException since ZipError is not IOException 2018-08-17 21:53:29 +08:00
huangyuhui 543eaa4f66 Force cancel using custom log4j config 2018-08-17 21:52:35 +08:00
huangyuhui 3a851b7f1a Fix unable to launch 1.13 with Rift 2018-08-17 21:51:25 +08:00
huangyuhui 089ae1e313 Suppress InterruptedException 2018-08-17 21:51:12 +08:00
huangyuhui bfe5802d02 Remove roboto font 2018-08-16 02:46:49 +08:00
huangyuhui 3ec6c97380 Replace 2.x with existent 3.x 2018-08-15 01:40:18 +08:00
huangyuhui e376da208d Fix duplicate JavaVersions in Windows. Closes #419 2018-08-14 10:44:24 +08:00
huangyuhui 3b02c5e4e0 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-08-14 00:10:48 +08:00
huangyuhui 1bcc79c415 Fix always trying to close launcher when game version cannot be obtained 2018-08-14 00:09:20 +08:00
huangyuhui 9e2cb25d01 Convert old offline account setings 2018-08-14 00:08:35 +08:00
yushijinhun 7e820ba2a5 Fix closeDialog() throws NPE 2018-08-13 22:18:18 +08:00
huanghongxun b079940cd9 Fix uniquing text2speech and its native library. Closes #418 2018-08-11 09:45:31 +08:00
huanghongxun 593ee24429 Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2018-08-10 19:05:26 +08:00
huanghongxun f7a91bf10c Fix version setting not saved 2018-08-10 19:04:33 +08:00
huanghongxun dad7ac706f Add update channel selection 2018-08-10 12:53:49 +08:00
Haowei Wen 13c48ce78c Merge pull request #417 from tsl0922/osx-maximized
Mac 下恢复使用自定义窗口边框按钮
2018-08-10 08:26:56 +08:00
Shuanglei Tao 8ecbba1003 Remove native window border on osx 2018-08-09 23:41:56 +08:00
huanghongxun 8bf6a9ec9d Rename AdvancedListItem 2018-08-09 20:22:57 +08:00
huanghongxun 64092a4bd4 Merge branch 'yushijinhun-java' into javafx 2018-08-09 13:54:54 +08:00
yushijinhun 8e6bcd969e Refactor JavaVersion 2018-08-09 13:49:16 +08:00
yushijinhun 6777dd7ea7 Fix UnsupportedOperationException in java lookup
Mentioned in #412
2018-08-09 13:22:49 +08:00
yushijinhun f16885568e Make config files hidden 2018-08-09 12:56:03 +08:00
huanghongxun ab109bcfcc Merge pull request #414 from yushijinhun/javafx
Fixes FileSystemNotFoundException in createZipFileSystem
2018-08-09 11:41:27 +08:00
yushijinhun 17f71eaa21 Fixes FileSystemNotFoundException in createZipFileSystem 2018-08-09 11:34:38 +08:00
huanghongxun 3a294e354a ExeLauncher: Search Java in C:/Program Files 2018-08-08 21:16:57 +08:00
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
146 changed files with 3190 additions and 4552 deletions

No files matched your search

Binary file not shown.
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.
@@ -25,19 +25,16 @@ import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.AppDataUpgrader;
import org.jackhuang.hmcl.upgrade.IUpgrader;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.*;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -56,12 +53,7 @@ public final class Launcher extends Application {
primaryStage.setResizable(false);
primaryStage.setScene(Controllers.getScene());
UPDATE_CHECKER.process(false)
.then(Task.of(Schedulers.javafx(), () -> {
if (UPDATE_CHECKER.isOutOfDate())
Controllers.showUpdate();
}))
.start();
UpdateChecker.init();
primaryStage.show();
} catch (Throwable e) {
@@ -80,15 +72,17 @@ public final class Launcher extends Application {
// NetworkUtils.setUserAgentSupplier(() -> "Hello Minecraft! Launcher");
Constants.UI_THREAD_SCHEDULER = Constants.JAVAFX_UI_THREAD_SCHEDULER;
UPGRADER.parseArguments(VersionNumber.asVersion(VERSION), Arrays.asList(args));
LOG.info("*** " + TITLE + " ***");
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + HMCL_DIRECTORY);
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
launch(args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
@@ -146,14 +140,5 @@ public final class Launcher extends Application {
public static final File HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
public static final File LOG_DIRECTORY = new File(Launcher.HMCL_DIRECTORY, "logs");
public static final String VERSION = System.getProperty("hmcl.version.override", "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@");
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final UpdateChecker UPDATE_CHECKER = new UpdateChecker(VersionNumber.asVersion(VERSION));
public static final IUpgrader UPGRADER = new AppDataUpgrader();
public static final CrashReporter CRASH_REPORTER = new CrashReporter();
public static final String UPDATE_SERVER = "https://www.huangyuhui.net";
public static final String CONTACT = UPDATE_SERVER + "/hmcl.php";
public static final String PUBLISH = "http://www.mcbbs.net/thread-142335-1-1.html";
}
+15 -20
View File
@@ -21,8 +21,8 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -35,16 +35,27 @@ import javax.net.ssl.X509TrustManager;
import javax.swing.JOptionPane;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
public final class Main {
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
checkJavaFX();
checkDirectoryPath();
checkDSTRootCAX3();
checkConfigPermission();
ConfigHolder.init();
if (UpdateHandler.processArguments(args)) {
return;
}
try {
ConfigHolder.init();
} catch (IOException e) {
showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
}
Launcher.main(args);
}
@@ -87,22 +98,6 @@ public final class Main {
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
}
private static void checkConfigPermission() {
Path config = ConfigHolder.CONFIG_PATH;
if (Files.exists(config)) {
if (Files.isReadable(config) && Files.isWritable(config)) {
// we are able to read & write the existent config
return;
}
} else {
if (Files.isWritable(config.getParent())) {
// we are able to create a new config
return;
}
}
showErrorAndExit(i18n("fatal.config_access_denied", config));
}
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
@@ -0,0 +1,35 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.JarUtils;
/**
* Stores metadata about this application.
*/
public final class Metadata {
private Metadata() {}
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
public static final String NAME = "HMCL";
public static final String TITLE = NAME + " " + VERSION;
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://www.huangyuhui.net/hmcl.php";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
}
@@ -1,53 +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.event;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.VersionNumber;
/**
*
* Result: Deny if do not upgrade HMCL.
*
* @author huangyuhui
*/
public final class OutOfDateEvent extends Event {
private final VersionNumber version;
public OutOfDateEvent(Object source, VersionNumber version) {
super(source);
this.version = version;
}
public VersionNumber getVersion() {
return version;
}
@Override
public boolean hasResult() {
return true;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", getSource())
.append("version", getVersion())
.toString();
}
}
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
@@ -46,8 +46,8 @@ public final class HMCLGameLauncher extends DefaultLauncher {
@Override
protected Map<String, String> getConfigurations() {
Map<String, String> res = super.getConfigurations();
res.put("${launcher_name}", Launcher.NAME);
res.put("${launcher_version}", Launcher.VERSION);
res.put("${launcher_name}", Metadata.NAME);
res.put("${launcher_version}", Metadata.VERSION);
return res;
}
}
@@ -17,12 +17,11 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ZipEngine;
import org.jackhuang.hmcl.util.Zipper;
import java.io.File;
import java.util.ArrayList;
@@ -31,58 +30,44 @@ import java.util.List;
/**
* Export the game to a mod pack file.
*/
public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
public class HMCLModpackExportTask extends Task {
private final DefaultGameRepository repository;
private final String version;
private final List<String> whitelist;
private final Modpack modpack;
private final File output;
private final String id;
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this(repository, version, whitelist, modpack, output, ID);
}
/**
* @param output mod pack file.
* @param version to locate version.json
*/
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output, String id) {
public HMCLModpackExportTask(DefaultGameRepository repository, String version, List<String> whitelist, Modpack modpack, File output) {
this.repository = repository;
this.version = version;
this.whitelist = whitelist;
this.modpack = modpack;
this.output = output;
this.id = id;
onDone().register(event -> {
if (event.isFailed()) output.delete();
});
}
@Override
public String getId() {
return id;
}
@Override
public void execute() throws Exception {
ArrayList<String> blackList = new ArrayList<>(HMCLModpackManager.MODPACK_BLACK_LIST);
blackList.add(version + ".jar");
blackList.add(version + ".json");
Logging.LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker");
try (ZipEngine zip = new ZipEngine(output)) {
zip.putDirectory(repository.getRunDirectory(version), (String pathName, Boolean isDirectory) -> {
try (Zipper zip = new Zipper(output.toPath())) {
zip.putDirectory(repository.getRunDirectory(version).toPath(), "minecraft", path -> {
for (String s : blackList)
if (isDirectory) {
if (pathName.startsWith(s + "/"))
return null;
} else if (pathName.equals(s))
return null;
if (path.equals(s))
return false;
for (String s : whitelist)
if (pathName.equals(s + (isDirectory ? "/" : "")))
return "minecraft/" + pathName;
return null;
if (path.equals(s))
return true;
return false;
});
Version mv = repository.getResolvedVersion(version);
@@ -92,6 +77,4 @@ public class HMCLModpackExportTask extends TaskResult<ZipEngine> {
zip.putTextFile(Constants.GSON.toJson(modpack.setGameVersion(gameVersion)), "modpack.json"); // Newer HMCL only reads 'gameVersion' field.
}
}
public static final String ID = "zip_engine";
}
@@ -74,7 +74,7 @@ public final class HMCLModpackInstallTask extends Task {
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, "minecraft/", it -> !Objects.equals(it, "minecraft/pack.json"), config));
dependents.add(new ModpackInstallTask<>(zipFile, run, "/minecraft", it -> !"pack.json".equals(it), config));
}
@Override
@@ -90,9 +90,9 @@ public final class HMCLModpackInstallTask extends Task {
@Override
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version version = Constants.GSON.fromJson(json, Version.class).setJar(null);
Version version = Constants.GSON.fromJson(json, Version.class).setId(name).setJar(null);
dependencies.add(new VersionJsonSaveTask(repository, version));
dependencies.add(new MinecraftInstanceTask<>(zipFile, "minecraft/", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
dependencies.add(new MinecraftInstanceTask<>(zipFile, "/minecraft", modpack, MODPACK_TYPE, repository.getModpackConfiguration(name)));
}
public static final String MODPACK_TYPE = "HMCL";
@@ -39,7 +39,7 @@ public final class HMCLModpackManager {
"pack.json", "launcher.jar", "hmclmc.log", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"minetweaker.log", // Mods
"logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data" // Mods
);
@@ -65,6 +65,8 @@ public final class LauncherHelper {
if (account == null)
throw new IllegalArgumentException("No account");
Logging.LOG.info("Launching game version: " + selectedVersion);
GameRepository repository = profile.getRepository();
Version version = repository.getResolvedVersion(selectedVersion);
@@ -218,30 +220,57 @@ 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();
flag = true;
}
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8) {
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
// Game later than 1.7.2 accepts Java 8.
if (!flag && java.getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
} else {
// Most mods require Java 8 or later version.
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.error"), MessageBox.WARNING_MESSAGE, onAccept);
if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) {
// Minecraft 1.13 and later versions only support Java 8 or later.
// Terminate launching operation.
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.warning"), MessageBox.WARNING_MESSAGE, onAccept);
}
flag = true;
}
flag = true;
}
// 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);
flag = true;
if (!flag && java.getParsedVersion() >= JavaVersion.JAVA_9 && gameVersion.compareTo(VersionNumber.asVersion("1.13")) < 0 && version.getMainClass().contains("launchwrapper")) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny();
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java9"), i18n("message.error"), MessageBox.ERROR_MESSAGE, null);
flag = true;
}
}
// Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51
VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0.51");
if (!flag && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && java.getParsedVersion() == JavaVersion.JAVA_8 && java.getVersionNumber().compareTo(JAVA_8) < 0) {
Optional<JavaVersion> java8 = JavaVersion.getJREs().stream()
.filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0)
.max(Comparator.comparing(JavaVersion::getVersionNumber));
if (java8.isPresent()) {
setting.setJavaVersion(java8.get());
} else {
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageBox.WARNING_MESSAGE, onAccept);
flag = true;
}
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
@@ -249,6 +278,7 @@ public final class LauncherHelper {
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (!flag && java.getPlatform() == org.jackhuang.hmcl.util.Platform.BIT_32 &&
setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
@@ -256,6 +286,7 @@ public final class LauncherHelper {
Controllers.dialog(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
}
if (!flag && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
Controllers.dialog(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), MessageBox.ERROR_MESSAGE, onAccept);
flag = true;
@@ -382,15 +413,18 @@ public final class LauncherHelper {
logs.removeFirst();
if (setting.isShowLogs()) {
Lang.invoke(() -> {
try {
latch.await();
logWindow.waitForLoaded();
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
Platform.runLater(() -> logWindow.logLine(log, level));
}
if (!lwjgl && log.contains("LWJGL Version: ") || !detectWindow) {
if (!lwjgl && (log.contains("LWJGL Version: ") || !detectWindow)) {
lwjgl = true;
switch (visibility) {
case HIDE_AND_REOPEN:
@@ -86,11 +86,13 @@ public final class ModpackHelper {
profile.getRepository().markVersionAsModpack(name);
FinalizedCallback finalizeTask = (variables, isDependentsSucceeded) -> {
profile.getRepository().refreshVersions();
VersionSetting vs = profile.specializeVersionSetting(name);
profile.getRepository().undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
if (isDependentsSucceeded) {
profile.getRepository().refreshVersions();
VersionSetting vs = profile.specializeVersionSetting(name);
profile.getRepository().undoMark(name);
if (vs != null)
vs.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
}
};
if (modpack.getManifest() instanceof CurseManifest)
@@ -102,7 +104,7 @@ public final class ModpackHelper {
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name)
.finalized(finalizeTask)
.with(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
.then(new MultiMCInstallVersionSettingTask(profile, ((MultiMCInstanceConfiguration) modpack.getManifest()), name));
else throw new IllegalStateException("Unrecognized modpack: " + modpack);
}
@@ -87,7 +87,7 @@ public final class Accounts {
return type2factory.get(accountType(account));
}
private static String accountId(Account account) {
static String accountId(Account account) {
return account.getUsername() + ":" + account.getCharacter();
}
@@ -138,20 +138,19 @@ public final class Accounts {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
}
private static void updateAccountStorages() {
// don't update the underlying storage before data loading is completed
// otherwise it might cause data loss
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(
accounts.stream()
.map(account -> {
Map<Object, Object> storage = account.toStorage();
storage.put("type", accountType(account));
return storage;
})
.collect(toList()));
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
}
/**
@@ -30,6 +30,7 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.util.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.FileTypeAdapter;
import org.jackhuang.hmcl.util.ObservableHelper;
@@ -149,6 +150,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList();
@SerializedName("updateChannel")
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -435,4 +439,15 @@ public final class Config implements Cloneable, Observable {
return authlibInjectorServers;
}
public UpdateChannel getUpdateChannel() {
return updateChannel.get();
}
public ObjectProperty<UpdateChannel> updateChannelProperty() {
return updateChannel;
}
public void setUpdateChannel(UpdateChannel updateChannel) {
this.updateChannel.set(updateChannel);
}
}
@@ -30,13 +30,16 @@ import java.util.logging.Level;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.OperatingSystem;
public final class ConfigHolder {
private ConfigHolder() {}
public static final String CONFIG_FILENAME = "hmcl.json";
public static final Path CONFIG_PATH = Paths.get(CONFIG_FILENAME).toAbsolutePath();
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
private static Path configLocation;
private static Config configInstance;
private static boolean newlyCreated;
@@ -54,34 +57,64 @@ public final class ConfigHolder {
return newlyCreated;
}
public synchronized static void init() {
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configLocation = locateConfig();
configInstance = loadConfig();
configInstance.addListener(source -> saveConfig());
configInstance.addListener(source -> markConfigDirty());
Settings.init();
if (newlyCreated) {
saveConfig();
// hide the config file on windows
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
}
}
}
if (!Files.isWritable(configLocation)) {
// the config cannot be saved
// throw up the error now to prevent further data loss
throw new IOException("Config at " + configLocation + " is not writable");
}
}
private static Config loadConfig() {
if (Files.exists(CONFIG_PATH)) {
private static Path locateConfig() {
Path config = Paths.get(CONFIG_FILENAME);
if (Files.isRegularFile(config))
return config;
Path dotConfig = Paths.get(CONFIG_FILENAME_LINUX);
if (Files.isRegularFile(dotConfig))
return dotConfig;
// create new
return OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? config : dotConfig;
}
private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) {
try {
String content = new String(Files.readAllBytes(CONFIG_PATH), UTF_8);
String content = new String(Files.readAllBytes(configLocation), UTF_8);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
return upgradeConfig(deserialized, raw);
ConfigUpgrader.upgradeConfig(deserialized, raw);
return deserialized;
}
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Something went wrong when loading config.", e);
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
}
}
@@ -90,18 +123,22 @@ public final class ConfigHolder {
return new Config();
}
static void saveConfig() {
private static void saveConfig() throws IOException {
LOG.info("Saving config");
try {
Files.write(CONFIG_PATH, configInstance.toJson().getBytes(UTF_8));
} catch (IOException ex) {
LOG.log(Level.SEVERE, "Failed to save config", ex);
Files.write(configLocation, configInstance.toJson().getBytes(UTF_8));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
throw e;
}
}
private static Config upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
return deserialized;
static void markConfigDirty() {
// TODO: change this to async
try {
saveConfig();
} catch (IOException ignored) {
// ignore it as it has been logged
}
}
}
@@ -0,0 +1,90 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
final class ConfigUpgrader {
private ConfigUpgrader() {
}
/**
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
static void upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
upgradeV2(deserialized, rawJson);
upgradeV3(deserialized, rawJson);
}
/**
* Upgrade configuration of HMCL 2.x
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
// Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
List<OfflineAccount> accounts = new LinkedList<>();
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
OfflineAccount account = OfflineAccountFactory.INSTANCE.create((String) key, UUIDTypeAdapter.fromString((String) value));
accounts.add(account);
deserialized.getAccountStorages().add(Accounts.getAccountStorage(account));
});
});
tryCast(offline.get("IAuthenticator_UserName"), String.class).ifPresent(selected -> {
if (!rawJson.containsKey("selectedAccount"))
accounts.stream().filter(account -> selected.equals(account.getUsername())).findAny().ifPresent(account ->
deserialized.setSelectedAccount(Accounts.accountId(account)));
});
});
});
}
/**
* Upgrade configuration of HMCL earlier than 3.1.70
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
}
}
@@ -19,12 +19,5 @@ package org.jackhuang.hmcl.setting;
public enum EnumBackgroundImage {
DEFAULT,
CUSTOM;
public static EnumBackgroundImage indexOf(int index) {
if (index >= values().length || index < 0)
return DEFAULT;
else
return values()[index];
}
CUSTOM
}
@@ -66,7 +66,7 @@ public final class Profile {
return globalProperty.get();
}
public void setGlobal(VersionSetting global) {
private void setGlobal(VersionSetting global) {
if (global == null)
global = new VersionSetting();
globalProperty.set(global);
@@ -175,6 +175,7 @@ public final class Profile {
globalProperty.addListener(listener);
gameDirProperty.addListener(listener);
useRelativePathProperty.addListener(listener);
globalProperty.get().addPropertyChangedListener(listener);
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@@ -58,10 +58,8 @@ public class Settings {
for (Map.Entry<String, Profile> profileEntry : getProfileMap().entrySet()) {
profileEntry.getValue().setName(profileEntry.getKey());
profileEntry.getValue().nameProperty().setChangedListener(this::profileNameChanged);
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.saveConfig());
profileEntry.getValue().addPropertyChangedListener(e -> ConfigHolder.markConfigDirty());
}
config().addListener(source -> ConfigHolder.saveConfig());
}
public Font getFont() {
@@ -88,8 +88,6 @@ public class Theme {
}
return new String[]{
Theme.class.getResource("/css/jfoenix-fonts.css").toExternalForm(),
Theme.class.getResource("/css/jfoenix-design.css").toExternalForm(),
css,
Theme.class.getResource("/assets/css/root.css").toExternalForm()
};
@@ -104,6 +102,18 @@ public class Theme {
public static Optional<Theme> getTheme(String name) {
if (name == null)
return Optional.empty();
else if (name.equalsIgnoreCase("blue"))
return Optional.of(custom("#5C6BC0"));
else if (name.equalsIgnoreCase("darker_blue"))
return Optional.of(custom("#283593"));
else if (name.equalsIgnoreCase("green"))
return Optional.of(custom("#43A047"));
else if (name.equalsIgnoreCase("orange"))
return Optional.of(custom("#E67E22"));
else if (name.equalsIgnoreCase("purple"))
return Optional.of(custom("#9C27B0"));
else if (name.equalsIgnoreCase("red"))
return Optional.of(custom("#F44336"));
if (name.startsWith("#"))
try {
@@ -19,7 +19,8 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.LaunchOptions;
import org.jackhuang.hmcl.util.*;
@@ -503,8 +504,8 @@ public final class VersionSetting {
return new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Launcher.TITLE)
.setProfileName(Launcher.TITLE)
.setVersionName(Metadata.TITLE)
.setProfileName(Metadata.TITLE)
.setMinecraftArgs(getMinecraftArgs())
.setJavaArgs(getJavaArgs())
.setMaxMemory(getMaxMemory())
@@ -601,7 +602,9 @@ public final class VersionSetting {
}
private int parseJsonPrimitive(JsonPrimitive primitive, int defaultValue) {
if (primitive.isNumber())
if (primitive == null)
return defaultValue;
else if (primitive.isNumber())
return primitive.getAsInt();
else
return Lang.parseInt(primitive.getAsString(), defaultValue);
@@ -38,6 +38,7 @@ import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
@@ -49,7 +50,7 @@ import java.util.Optional;
public class AccountPage extends StackPane implements DecoratorPage {
private final StringProperty title;
private final ObjectProperty<Runnable> onDelete = new SimpleObjectProperty<>(this, "onDelete");
private final VersionListItem item;
private final AdvancedListItem item;
private final Account account;
@FXML
@@ -73,7 +74,7 @@ public class AccountPage extends StackPane implements DecoratorPage {
@FXML
private JFXProgressBar progressBar;
public AccountPage(Account account, VersionListItem item) {
public AccountPage(Account account, AdvancedListItem item) {
this.account = account;
this.item = item;
@@ -23,10 +23,10 @@ import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -98,7 +98,7 @@ public final class Controllers {
stage.setOnCloseRequest(e -> Launcher.stopApplication());
decorator = new Decorator(stage, getMainPage(), Launcher.TITLE, false, true);
decorator = new Decorator(stage, getMainPage(), Metadata.TITLE, false, true);
decorator.showPage(null);
leftPaneController = new LeftPaneController(decorator.getLeftPane());
@@ -115,7 +115,7 @@ public final class Controllers {
stage.setMaxHeight(521);
stage.getIcons().add(new Image("/assets/img/icon.png"));
stage.setTitle(Launcher.TITLE);
stage.setTitle(Metadata.TITLE);
}
public static void dialog(Region content) {
@@ -162,15 +162,6 @@ public final class Controllers {
return pane;
}
/**
* Use {@link DialogCloseEvent}
*/
public static void closeDialog(Region content) {
if (stage == null) // shut down
return;
decorator.closeDialog(content);
}
public static void navigate(Node node) {
if (decorator.getNowPage() == node)
decorator.showPage(null);
@@ -178,12 +169,6 @@ public final class Controllers {
decorator.showPage(node);
}
public static void showUpdate() {
if (stage == null) // shut down
return;
getLeftPaneController().showUpdate();
}
public static boolean isStopped() {
return decorator == null;
}
@@ -30,7 +30,8 @@ import javafx.stage.Stage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
/**
* @author huangyuhui
@@ -39,7 +40,7 @@ public class CrashWindow extends Stage {
public CrashWindow(String text) {
Label lblCrash = new Label();
if (Launcher.UPDATE_CHECKER.isOutOfDate())
if (UpdateChecker.isOutdated())
lblCrash.setText(i18n("launcher.crash_out_dated"));
else
lblCrash.setText(i18n("launcher.crash"));
@@ -51,7 +52,7 @@ public class CrashWindow extends Stage {
Button btnContact = new Button();
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Launcher.CONTACT));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
HBox box = new HBox();
box.setStyle("-fx-padding: 8px;");
box.getChildren().add(btnContact);
@@ -67,6 +67,8 @@ import org.jackhuang.hmcl.ui.construct.StackContainerPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogWizardDisplayer;
import org.jackhuang.hmcl.ui.wizard.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -152,6 +154,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private HBox navLeft;
@FXML
private ImageView welcomeView;
@FXML
private Rectangle separator;
public Decorator(Stage primaryStage, Node mainPage, String title) {
this(primaryStage, mainPage, title, true, true);
@@ -181,6 +185,8 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
closeNavButton.setGraphic(SVG.close(Theme.foregroundFillBinding(), 15, 15));
backNavButton.setGraphic(SVG.back(Theme.foregroundFillBinding(), 15, 15));
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
lblTitle.setText(title);
buttonsContainer.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
@@ -297,11 +303,22 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
return Optional.empty();
}
private boolean isMaximized() {
switch (OperatingSystem.CURRENT_OS) {
case OSX:
Rectangle2D bounds = Screen.getPrimary().getVisualBounds();
return primaryStage.getWidth() >= bounds.getWidth() && primaryStage.getHeight() >= bounds.getHeight();
default:
return primaryStage.isMaximized();
}
}
// ====
@FXML
private void onMouseMoved(MouseEvent mouseEvent) {
if (!primaryStage.isMaximized() && !primaryStage.isFullScreen() && !maximized) {
if (!isMaximized() && !primaryStage.isFullScreen() && !maximized) {
if (!primaryStage.isResizable())
updateInitMouseValues(mouseEvent);
else {
@@ -350,7 +367,7 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
private void onMouseDragged(MouseEvent mouseEvent) {
this.isDragging = true;
if (mouseEvent.isPrimaryButtonDown() && (this.xOffset != -1.0 || this.yOffset != -1.0)) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !this.primaryStage.isMaximized() && !this.maximized) {
if (!this.primaryStage.isFullScreen() && !mouseEvent.isStillSincePress() && !isMaximized() && !this.maximized) {
this.newX = mouseEvent.getScreenX();
this.newY = mouseEvent.getScreenY();
double deltaX = this.newX - this.initX;
@@ -598,18 +615,20 @@ public final class Decorator extends StackPane implements TaskExecutorDialogWiza
}
@SuppressWarnings("unchecked")
public void closeDialog(Node node) {
private void closeDialog(Node node) {
FXUtils.checkFxUserThread();
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) handler));
dialogPane.pop(node);
if (dialog != null) {
dialogPane.pop(node);
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
dialogPane = null;
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;
dialogPane = null;
}
}
}
@@ -21,6 +21,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.game.GameVersion;
@@ -35,6 +36,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.LinkedList;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
public class InstallerController {
private Profile profile;
@@ -43,9 +45,6 @@ public class InstallerController {
@FXML
private ScrollPane scrollPane;
@FXML private VBox contentPane;
private String forge;
private String liteLoader;
private String optiFine;
@FXML
private void initialize() {
@@ -58,32 +57,22 @@ public class InstallerController {
this.version = profile.getRepository().getResolvedVersion(versionId);
contentPane.getChildren().clear();
forge = liteLoader = optiFine = null;
for (Library library : version.getLibraries()) {
Consumer<InstallerItem> removeAction = x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction));
forge = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction));
liteLoader = library.getVersion();
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction));
optiFine = library.getVersion();
}
}
Function<Library, Consumer<InstallerItem>> removeAction = library -> x -> {
LinkedList<Library> newList = new LinkedList<>(version.getLibraries());
newList.remove(library);
new MaintainTask(version.setLibraries(newList))
.then(variables -> new VersionJsonSaveTask(profile.getRepository(), variables.get(MaintainTask.ID)))
.with(profile.getRepository().refreshVersionsAsync())
.with(Task.of(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId)))
.start();
};
analyzer.getForge().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("Forge", library.getVersion(), removeAction.apply(library))));
analyzer.getLiteLoader().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("LiteLoader", library.getVersion(), removeAction.apply(library))));
analyzer.getOptiFine().ifPresent(library -> contentPane.getChildren().add(new InstallerItem("OptiFine", library.getVersion(), removeAction.apply(library))));
}
@FXML
@@ -93,6 +82,6 @@ public class InstallerController {
if (!gameVersion.isPresent())
Controllers.dialog(i18n("version.cannot_read"));
else
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version, forge, liteLoader, optiFine));
Controllers.getDecorator().startWizard(new InstallerWizardProvider(profile, gameVersion.get(), version));
}
}
@@ -49,11 +49,8 @@ import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.MappedObservableList;
@@ -92,10 +89,20 @@ public final class LeftPaneController {
public LeftPaneController(AdvancedListBox leftPane) {
this.leftPane = leftPane;
this.launcherSettingsItem = Lang.apply(new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20), i18n("settings.launcher")), iconedItem -> {
iconedItem.prefWidthProperty().bind(leftPane.widthProperty());
iconedItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
});
launcherSettingsItem = new IconedItem(SVG.gear(Theme.blackFillBinding(), 20, 20));
launcherSettingsItem.getLabel().textProperty().bind(
new When(UpdateChecker.outdatedProperty())
.then(i18n("update.found"))
.otherwise(i18n("settings.launcher")));
launcherSettingsItem.getLabel().textFillProperty().bind(
new When(UpdateChecker.outdatedProperty())
.then(Color.RED)
.otherwise(Color.BLACK));
launcherSettingsItem.prefWidthProperty().bind(leftPane.widthProperty());
launcherSettingsItem.setOnMouseClicked(e -> Controllers.navigate(Controllers.getSettingsPage()));
leftPane
.add(new ClassTitle(i18n("account").toUpperCase(), Lang.apply(new JFXButton(), button -> {
@@ -116,7 +123,7 @@ public final class LeftPaneController {
// ==== Accounts ====
// Missing account item
VersionListItem missingAccountItem = new VersionListItem(i18n("account.missing"), i18n("message.unknown"));
AdvancedListItem missingAccountItem = new AdvancedListItem(i18n("account.missing"), i18n("message.unknown"));
RipplerContainer missingAccountRippler = new RipplerContainer(missingAccountItem);
missingAccountItem.setOnSettingsButtonClicked(e -> addNewAccount());
missingAccountRippler.setOnMouseClicked(e -> addNewAccount());
@@ -151,7 +158,7 @@ public final class LeftPaneController {
}
private RipplerContainer createAccountItem(Account account) {
VersionListItem item = new VersionListItem(account.getCharacter(), accountSubtitle(account));
AdvancedListItem item = new AdvancedListItem(account.getCharacter(), accountSubtitle(account));
RipplerContainer rippler = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> {
AccountPage accountPage = new AccountPage(account, item);
@@ -208,7 +215,7 @@ public final class LeftPaneController {
if (node instanceof RipplerContainer && node.getProperties().get("profile") instanceof String) {
boolean current = Objects.equals(node.getProperties().get("profile"), profile.getName());
((RipplerContainer) node).setSelected(current);
((VersionListItem) ((RipplerContainer) node).getContainer()).setGameVersion(current ? i18n("profile.selected") : "");
((AdvancedListItem) ((RipplerContainer) node).getContainer()).setSubtitle(current ? i18n("profile.selected") : "");
}
}
});
@@ -217,7 +224,7 @@ public final class LeftPaneController {
private void onProfilesLoading() {
LinkedList<RipplerContainer> list = new LinkedList<>();
for (Profile profile : Settings.instance().getProfiles()) {
VersionListItem item = new VersionListItem(Profiles.getProfileDisplayName(profile));
AdvancedListItem item = new AdvancedListItem(Profiles.getProfileDisplayName(profile));
RipplerContainer ripplerContainer = new RipplerContainer(item);
item.setOnSettingsButtonClicked(e -> Controllers.getDecorator().showPage(new ProfilePage(profile)));
ripplerContainer.setOnMouseClicked(e -> Settings.instance().setSelectedProfile(profile));
@@ -228,11 +235,6 @@ public final class LeftPaneController {
Platform.runLater(() -> profilePane.getChildren().setAll(list));
}
public void showUpdate() {
launcherSettingsItem.setText(i18n("update.found"));
launcherSettingsItem.setTextFill(Color.RED);
}
private boolean checkedModpack = false;
private static boolean showNewAccount = true;
@@ -30,6 +30,7 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.ProfileChangedEvent;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
@@ -119,17 +120,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
item.setGameVersion(game);
StringBuilder libraries = new StringBuilder();
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge")) {
libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader")) {
libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n");
}
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine")) {
libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n");
}
}
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
analyzer.getForge().ifPresent(library -> libraries.append(i18n("install.installer.forge")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)forge", ""))).append("\n"));
analyzer.getLiteLoader().ifPresent(library -> libraries.append(i18n("install.installer.liteloader")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)liteloader", ""))).append("\n"));
analyzer.getOptiFine().ifPresent(library -> libraries.append(i18n("install.installer.optifine")).append(": ").append(modifyVersion(game, library.getVersion().replaceAll("(?i)optifine", ""))).append("\n"));
item.setLibraries(libraries.toString());
});
@@ -236,6 +230,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void loadVersions(HMCLGameRepository repository) {
List<Node> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> buildNode(repository, version, () -> GameVersion.minecraftVersion(repository.getVersionJar(version.getId())).orElse("Unknown")))
.collect(Collectors.toList());
@@ -26,6 +26,8 @@ import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.mod.ModInfo;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.function.Consumer;
@@ -54,7 +56,14 @@ public final class ModItem extends BorderPane {
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
modItem.setTitle(info.getFileName());
modItem.setSubtitle(info.getName() + ", " + i18n("archive.version") + ": " + info.getVersion() + ", " + i18n("archive.game_version") + ": " + info.getGameVersion() + ", " + i18n("archive.author") + ": " + info.getAuthors());
StringBuilder message = new StringBuilder(info.getName());
if (StringUtils.isNotBlank(info.getVersion()))
message.append(", ").append(i18n("archive.version")).append(": ").append(info.getVersion());
if (StringUtils.isNotBlank(info.getGameVersion()))
message.append(", ").append(i18n("archive.game_version")).append(": ").append(info.getGameVersion());
if (StringUtils.isNotBlank(info.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(info.getAuthors());
modItem.setSubtitle(message.toString());
chkEnabled.setSelected(info.isActive());
chkEnabled.selectedProperty().addListener((a, b, newValue) ->
info.activeProperty().set(newValue));
@@ -20,6 +20,8 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.beans.property.ObjectProperty;
@@ -37,12 +39,17 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Launcher;
import javafx.scene.text.Text;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.FontComboBox;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.i18n.Locales;
@@ -83,6 +90,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private Label lblUpdateSub;
@FXML
private Text lblUpdateNote;
@FXML
private JFXRadioButton chkUpdateStable;
@FXML
private JFXRadioButton chkUpdateDev;
@FXML
private JFXButton btnUpdate;
@FXML
private ScrollPane scroll;
@@ -91,7 +104,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
@FXML
private StackPane themeColorPickerContainer;
@FXML
private JFXCheckBox chkEnableProxy;
private JFXCheckBox chkDisableProxy;
@FXML
private JFXRadioButton chkProxyHttp;
@FXML
@@ -105,6 +118,8 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
private ObjectProperty<Proxy.Type> selectedProxyType;
private InvalidationListener updateListener;
public SettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/setting.fxml");
@@ -146,10 +161,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
txtProxyPassword.textProperty().bindBidirectional(config().proxyPassProperty());
proxyPane.disableProperty().bind(chkEnableProxy.selectedProperty().not());
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
chkEnableProxy.selectedProperty().bindBidirectional(config().hasProxyProperty());
chkDisableProxy.setSelected(!config().hasProxy());
chkDisableProxy.selectedProperty().addListener(o -> config().setHasProxy(!chkDisableProxy.isSelected()));
config().hasProxyProperty().addListener(o -> chkDisableProxy.setSelected(!config().hasProxy()));
chkProxyAuthentication.selectedProperty().bindBidirectional(config().hasProxyAuthProperty());
selectedProxyType = new SimpleObjectProperty<Proxy.Type>(Proxy.Type.HTTP) {
@@ -194,8 +211,60 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
.orElse(i18n("launcher.common_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
// ==== Update ====
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
checkUpdate();
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
if (UpdateChecker.isOutdated()) {
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
lblUpdateNote.setWrappingWidth(470);
ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<UpdateChannel>() {
@Override
protected void invalidated() {
UpdateChannel updateChannel = Objects.requireNonNull(get());
chkUpdateDev.setSelected(updateChannel == UpdateChannel.DEVELOPMENT);
chkUpdateStable.setSelected(updateChannel == UpdateChannel.STABLE);
}
};
ToggleGroup updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
updateChannelGroup.getToggles().forEach(
toggle -> toggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
updateChannel.set((UpdateChannel) toggle.getUserData());
}
}));
updateChannel.bindBidirectional(ConfigHolder.config().updateChannelProperty());
// ====
// ==== Background ====
backgroundItem.loadChildren(Collections.singletonList(
@@ -237,26 +306,12 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
this.title.set(title);
}
public void checkUpdate() {
btnUpdate.setVisible(Launcher.UPDATE_CHECKER.isOutOfDate());
if (Launcher.UPDATE_CHECKER.isOutOfDate()) {
lblUpdateSub.setText(i18n("update.newest_version", Launcher.UPDATE_CHECKER.getNewVersion().toString()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll();
}
}
@FXML
private void onUpdate() {
Launcher.UPDATE_CHECKER.checkOutdate();
RemoteVersion target = UpdateChecker.getLatestVersion();
if (target == null) {
return;
}
UpdateHandler.updateFrom(target);
}
}
@@ -26,16 +26,17 @@ import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Tab;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.FileUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", null);
@@ -147,7 +148,7 @@ public final class VersionPage extends StackPane implements DecoratorPage {
sub = "saves";
break;
default:
throw new RuntimeException();
return;
}
FXUtils.openFolder(new File(profile.getRepository().getRunDirectory(version), sub));
}
@@ -186,7 +187,12 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void deleteVersion(Profile profile, String version) {
Controllers.confirmDialog(i18n("version.manage.remove.confirm", version), i18n("message.confirm"), () -> {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == EnumGameDirectory.VERSION_FOLDER;
boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported();
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
i18n("version.manage.remove.confirm", version);
Controllers.confirmDialog(message, i18n("message.confirm"), () -> {
if (profile.getRepository().removeVersionFromDisk(version)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.event.EventHandler;
@@ -26,27 +26,28 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
public final class VersionListItem extends StackPane {
public final class AdvancedListItem extends StackPane {
@FXML
private StackPane imageViewContainer;
@FXML
private Label lblVersionName;
private Label lblTitle;
@FXML
private Label lblGameVersion;
private Label lblSubtitle;
@FXML
private ImageView imageView;
@FXML private JFXButton btnSettings;
public VersionListItem(String versionName) {
this(versionName, "");
public AdvancedListItem(String title) {
this(title, "");
}
public VersionListItem(String versionName, String gameVersion) {
FXUtils.loadFXML(this, "/assets/fxml/version-list-item.fxml");
public AdvancedListItem(String title, String subtitle) {
FXUtils.loadFXML(this, "/assets/fxml/advanced-list-item.fxml");
lblVersionName.setText(versionName);
lblGameVersion.setText(gameVersion);
lblTitle.setText(title);
lblSubtitle.setText(subtitle);
FXUtils.limitSize(imageView, 32, 32);
}
@@ -55,12 +56,12 @@ public final class VersionListItem extends StackPane {
btnSettings.setOnMouseClicked(handler);
}
public void setVersionName(String versionName) {
lblVersionName.setText(versionName);
public void setTitle(String title) {
lblTitle.setText(title);
}
public void setGameVersion(String gameVersion) {
lblGameVersion.setText(gameVersion);
public void setSubtitle(String subtitle) {
lblSubtitle.setText(subtitle);
}
public void setImage(Image image, Rectangle2D viewport) {
@@ -50,10 +50,6 @@ public class ComponentList extends StackPane {
}
public void addChildren(Node node) {
if (node instanceof ComponentList) {
node.getProperties().put("title", ((ComponentList) node).getTitle());
node.getProperties().put("subtitle", ((ComponentList) node).getSubtitle());
}
StackPane child = new StackPane();
child.getChildren().add(new ComponentListCell(node));
if (vbox.getChildren().isEmpty())
@@ -27,6 +27,8 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
@@ -75,31 +77,47 @@ public class ComponentListCell extends StackPane {
content.getStyleClass().remove("options-list");
content.getStyleClass().add("options-sublist");
StackPane groupNode = new StackPane();
BorderPane groupNode = new BorderPane();
groupNode.getStyleClass().add("options-list-item-header");
Node expandIcon = SVG.expand(Theme.blackFillBinding(), 10, 10);
JFXButton expandButton = new JFXButton();
expandButton.setGraphic(expandIcon);
expandButton.getStyleClass().add("options-list-item-expand-button");
StackPane.setAlignment(expandButton, Pos.CENTER_RIGHT);
VBox labelVBox = new VBox();
Label label = new Label();
label.textProperty().bind(list.titleProperty());
label.setMouseTransparent(true);
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
if (list instanceof ComponentSublist) {
Node leftNode = ((ComponentSublist) list).getHeaderLeft();
if (leftNode != null)
labelVBox.getChildren().setAll(leftNode);
} else {
Label label = new Label();
label.textProperty().bind(list.titleProperty());
label.setMouseTransparent(true);
labelVBox.getChildren().add(label);
if (list.isHasSubtitle()) {
Label subtitleLabel = new Label();
subtitleLabel.textProperty().bind(list.subtitleProperty());
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle-label");
labelVBox.getChildren().add(subtitleLabel);
}
}
StackPane.setAlignment(labelVBox, Pos.CENTER_LEFT);
groupNode.getChildren().setAll(labelVBox, expandButton);
groupNode.setLeft(labelVBox);
HBox right = new HBox();
if (list instanceof ComponentSublist) {
Node rightNode = ((ComponentSublist) list).getHeaderRight();
if (rightNode != null)
right.getChildren().add(rightNode);
}
right.getChildren().add(expandButton);
groupNode.setRight(right);
labelVBox.setAlignment(Pos.CENTER_LEFT);
right.setAlignment(Pos.CENTER_RIGHT);
VBox container = new VBox();
container.setStyle("-fx-padding: 8 0 0 0;");
@@ -0,0 +1,41 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
@DefaultProperty("content")
public final class ComponentSublist extends ComponentList {
private final ObjectProperty<Node> headerLeft = new SimpleObjectProperty<>(this, "headerLeft");
private final ObjectProperty<Node> headerRight = new SimpleObjectProperty<>(this, "headerRight");
public ComponentSublist() {
super();
}
public Node getHeaderLeft() {
return headerLeft.get();
}
public ObjectProperty<Node> headerLeftProperty() {
return headerLeft;
}
public void setHeaderLeft(Node headerLeft) {
this.headerLeft.set(headerLeft);
}
public Node getHeaderRight() {
return headerRight.get();
}
public ObjectProperty<Node> headerRightProperty() {
return headerRight;
}
public void setHeaderRight(Node headerRight) {
this.headerRight.set(headerRight);
}
}
@@ -21,18 +21,25 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Paint;
public class IconedItem extends RipplerContainer {
private Label label;
public IconedItem(Node icon, String text) {
super(createHBox(icon, text));
this(icon);
label.setText(text);
}
private static HBox createHBox(Node icon, String text) {
public IconedItem(Node icon) {
super(createHBox(icon));
label = ((Label) lookup("#label"));
}
private static HBox createHBox(Node icon) {
HBox hBox = new HBox();
icon.setMouseTransparent(true);
Label textLabel = new Label(text);
Label textLabel = new Label();
textLabel.setId("label");
textLabel.setAlignment(Pos.CENTER);
textLabel.setMouseTransparent(true);
@@ -42,11 +49,7 @@ public class IconedItem extends RipplerContainer {
return hBox;
}
public void setText(String text) {
((Label) lookup("#label")).setText(text);
}
public void setTextFill(Paint paint) {
((Label) lookup("#label")).setTextFill(paint);
public Label getLabel() {
return label;
}
}
@@ -103,7 +103,7 @@ class AdditionalInstallersPage extends StackPane implements WizardPage {
}
private String getVersion(String id) {
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion<?>) it).map(RemoteVersion::getSelfVersion).orElse(null);
return Optional.ofNullable(controller.getSettings().get(id)).map(it -> (RemoteVersion) it).map(RemoteVersion::getSelfVersion).orElse(null);
}
@Override
@@ -49,16 +49,16 @@ public final class DownloadWizardProvider implements WizardProvider {
GameBuilder builder = profile.getDependency().gameBuilder();
builder.name((String) settings.get("name"));
builder.gameVersion(((RemoteVersion<?>) settings.get("game")).getGameVersion());
builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion());
if (settings.containsKey("forge"))
builder.version((RemoteVersion<?>) settings.get("forge"));
builder.version((RemoteVersion) settings.get("forge"));
if (settings.containsKey("liteloader"))
builder.version((RemoteVersion<?>) settings.get("liteloader"));
builder.version((RemoteVersion) settings.get("liteloader"));
if (settings.containsKey("optifine"))
builder.version((RemoteVersion<?>) settings.get("optifine"));
builder.version((RemoteVersion) settings.get("optifine"));
return builder.buildAsync().finalized((a, b) -> profile.getRepository().refreshVersions());
}
@@ -21,7 +21,9 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
@@ -40,16 +42,14 @@ public final class InstallerWizardProvider implements WizardProvider {
private final String optiFine;
public InstallerWizardProvider(Profile profile, String gameVersion, Version version) {
this(profile, gameVersion, version, null, null, null);
}
public InstallerWizardProvider(Profile profile, String gameVersion, Version version, String forge, String liteLoader, String optiFine) {
this.profile = profile;
this.gameVersion = gameVersion;
this.version = version;
this.forge = forge;
this.liteLoader = liteLoader;
this.optiFine = optiFine;
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
forge = analyzer.getForge().map(Library::getVersion).orElse(null);
liteLoader = analyzer.getLiteLoader().map(Library::getVersion).orElse(null);
optiFine = analyzer.getOptiFine().map(Library::getVersion).orElse(null);
}
public Profile getProfile() {
@@ -85,16 +85,16 @@ public final class InstallerWizardProvider implements WizardProvider {
settings.put("success_message", i18n("install.success"));
settings.put("failure_message", i18n("install.failed"));
Task ret = Task.empty();
Task ret = Task.ofResult("version", () -> version);
if (settings.containsKey("forge"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("forge")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("forge")));
if (settings.containsKey("liteloader"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("liteloader")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("liteloader")));
if (settings.containsKey("optifine"))
ret = ret.then(profile.getDependency().installLibraryAsync(version, (RemoteVersion<?>) settings.get("optifine")));
ret = ret.then(profile.getDependency().installLibraryAsync((RemoteVersion) settings.get("optifine")));
return ret.then(profile.getRepository().refreshVersionsAsync());
}
@@ -74,7 +74,7 @@ public class InstallersPage extends StackPane implements WizardPage {
FXUtils.loadFXML(this, "/assets/fxml/download/installers.fxml");
String gameVersion = ((RemoteVersion<?>) controller.getSettings().get("game")).getGameVersion();
String gameVersion = ((RemoteVersion) controller.getSettings().get("game")).getGameVersion();
Validator hasVersion = new Validator(s -> !repository.hasVersion(s) && StringUtils.isNotBlank(s));
hasVersion.setMessage(i18n("install.new_game.already_exists"));
txtName.getValidators().add(hasVersion);
@@ -103,7 +103,7 @@ public class InstallersPage extends StackPane implements WizardPage {
}
private String getVersion(String id) {
return ((RemoteVersion<?>) controller.getSettings().get(id)).getSelfVersion();
return ((RemoteVersion) controller.getSettings().get(id)).getSelfVersion();
}
@Override
@@ -98,6 +98,7 @@ public final class ModpackPage extends StackPane implements WizardPage {
txtModpackName.setText(manifest.getName() + (StringUtils.isBlank(manifest.getVersion()) ? "" : "-" + manifest.getVersion()));
} catch (UnsupportedModpackException e) {
txtModpackName.setText(i18n("modpack.task.install.error"));
btnInstall.setDisable(true);
}
}
}
@@ -27,9 +27,8 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.game.GameVersionList;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -48,6 +47,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private final DownloadProvider downloadProvider;
private final String libraryId;
private final String title;
private final WizardController controller;
@FXML
private JFXListView<VersionsPageItem> list;
@@ -56,6 +56,8 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
@FXML
private StackPane failedPane;
@FXML
private StackPane emptyPane;
@FXML
private JFXCheckBox chkRelease;
@FXML
private JFXCheckBox chkSnapshot;
@@ -75,11 +77,12 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
this.gameVersion = gameVersion;
this.downloadProvider = downloadProvider;
this.libraryId = libraryId;
this.controller = controller;
this.versionList = downloadProvider.getVersionListById(libraryId);
FXUtils.loadFXML(this, "/assets/fxml/download/versions.fxml");
if (versionList instanceof GameVersionList) {
if (versionList.hasType()) {
centrePane.getChildren().setAll(checkPane, list);
} else
centrePane.getChildren().setAll(list);
@@ -99,21 +102,19 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
}
private List<VersionsPageItem> loadVersions() {
boolean isGameVersionList = versionList instanceof GameVersionList;
return versionList.getVersions(gameVersion).stream()
.filter(it -> {
if (isGameVersionList)
switch (((GameRemoteVersionTag) it.getTag()).getType()) {
case RELEASE:
return chkRelease.isSelected();
case SNAPSHOT:
return chkSnapshot.isSelected();
default:
return chkOld.isSelected();
}
else return true;
switch (it.getVersionType()) {
case RELEASE:
return chkRelease.isSelected();
case SNAPSHOT:
return chkSnapshot.isSelected();
case OLD:
return chkOld.isSelected();
default:
return true;
}
})
.filter(Objects::nonNull)
.sorted()
.map(VersionsPageItem::new).collect(Collectors.toList());
}
@@ -126,8 +127,18 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
List<VersionsPageItem> items = loadVersions();
Platform.runLater(() -> {
list.getItems().setAll(items);
transitionHandler.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer());
if (versionList.getVersions(gameVersion).isEmpty()) {
transitionHandler.setContent(emptyPane, ContainerAnimations.FADE.getAnimationProducer());
} else {
if (items.isEmpty()) {
chkRelease.setSelected(true);
chkSnapshot.setSelected(true);
chkOld.setSelected(true);
} else {
list.getItems().setAll(items);
}
transitionHandler.setContent(centrePane, ContainerAnimations.FADE.getAnimationProducer());
}
});
} else {
Platform.runLater(() -> {
@@ -153,4 +164,7 @@ public final class VersionsPage extends StackPane implements WizardPage, Refresh
private void onRefresh() {
refresh();
}
@FXML
private void onBack() { controller.onPrev(true); }
}
@@ -25,8 +25,9 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersionTag;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersionTag;
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.ui.FXUtils;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -37,7 +38,7 @@ import java.util.Objects;
* @author huangyuhui
*/
public final class VersionsPageItem extends StackPane {
private final RemoteVersion<?> remoteVersion;
private final RemoteVersion remoteVersion;
@FXML
private Label lblSelfVersion;
@FXML
@@ -49,14 +50,14 @@ public final class VersionsPageItem extends StackPane {
@FXML
private StackPane imageViewContainer;
public VersionsPageItem(RemoteVersion<?> remoteVersion) {
public VersionsPageItem(RemoteVersion remoteVersion) {
this.remoteVersion = Objects.requireNonNull(remoteVersion);
FXUtils.loadFXML(this, "/assets/fxml/download/versions-list-item.fxml");
lblSelfVersion.setText(remoteVersion.getSelfVersion());
if (remoteVersion.getTag() instanceof GameRemoteVersionTag) {
switch (((GameRemoteVersionTag) remoteVersion.getTag()).getType()) {
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
lblGameVersion.setText(i18n("version.game.release"));
imageView.setImage(new Image("/assets/img/icon.png", 32, 32, false, true));
@@ -70,13 +71,13 @@ public final class VersionsPageItem extends StackPane {
imageView.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
}
} else if (remoteVersion.getTag() instanceof LiteLoaderRemoteVersionTag) {
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
imageView.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
lblGameVersion.setText(remoteVersion.getGameVersion());
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
// optifine has no icon.
lblGameVersion.setText(remoteVersion.getGameVersion());
} else { // forge
} else if (remoteVersion instanceof ForgeRemoteVersion) {
imageView.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
lblGameVersion.setText(remoteVersion.getGameVersion());
}
@@ -84,7 +85,7 @@ public final class VersionsPageItem extends StackPane {
leftPane.getChildren().remove(imageViewContainer);
}
public RemoteVersion<?> getRemoteVersion() {
public RemoteVersion getRemoteVersion() {
return remoteVersion;
}
}
@@ -28,7 +28,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.ZipEngine;
import org.jackhuang.hmcl.util.Zipper;
import java.io.File;
import java.nio.file.Files;
@@ -81,7 +81,7 @@ public final class ExportWizardProvider implements WizardProvider {
dependency = dependency.then(Task.of(() -> {
boolean flag = true;
try (ZipEngine zip = new ZipEngine(modpackFile)) {
try (Zipper zip = new Zipper(modpackFile.toPath())) {
Config exported = new Config();
exported.setBackgroundImageType(config().getBackgroundImageType());
exported.setBackgroundImage(config().getBackgroundImage());
@@ -93,7 +93,7 @@ public final class ExportWizardProvider implements WizardProvider {
File bg = new File("bg").getAbsoluteFile();
if (bg.isDirectory())
zip.putDirectory(bg);
zip.putDirectory(bg.toPath(), "bg");
File background_png = new File("background.png").getAbsoluteFile();
if (background_png.isFile())
@@ -1,253 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.upgrade;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.logging.Level;
import java.util.zip.GZIPInputStream;
/**
*
* @author huangyuhui
*/
public class AppDataUpgrader extends IUpgrader {
private void launchNewerVersion(List<String> args, File jar) throws IOException, ReflectiveOperationException {
try (JarFile jarFile = new JarFile(jar)) {
String mainClass = jarFile.getManifest().getMainAttributes().getValue("Main-Class");
if (mainClass == null)
throw new ClassNotFoundException("Main-Class not found in manifest");
ArrayList<String> al = new ArrayList<>(args);
al.add("--noupdate");
ClassLoader pre = Thread.currentThread().getContextClassLoader();
try {
Logging.stop();
ClassLoader now = new URLClassLoader(new URL[]{jar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
Thread.currentThread().setContextClassLoader(now);
now.loadClass(mainClass).getMethod("main", String[].class).invoke(null, new Object[]{al.toArray(new String[0])});
} finally {
Logging.start(Launcher.LOG_DIRECTORY);
Thread.currentThread().setContextClassLoader(pre);
}
}
}
@Override
public void parseArguments(VersionNumber nowVersion, List<String> args) {
File f = AppDataUpgraderPackGzTask.HMCL_VER_FILE;
if (!args.contains("--noupdate"))
try {
if (f.exists()) {
Map<String, String> m = Constants.GSON.fromJson(FileUtils.readText(f), new TypeToken<Map<String, String>>() {
}.getType());
String s = m.get("ver");
if (s != null && VersionNumber.asVersion(s).compareTo(nowVersion) > 0) {
String j = m.get("loc");
if (j != null) {
File jar = new File(j);
if (jar.exists()) {
launchNewerVersion(args, jar);
System.exit(0);
}
}
}
}
} catch (JsonParseException ex) {
f.delete();
} catch (IOException | ReflectiveOperationException t) {
Logging.LOG.log(Level.SEVERE, "Unable to execute newer version application", t);
AppDataUpgraderPackGzTask.HMCL_VER_FILE.delete(); // delete version json, let HMCL re-download the newer version.
}
}
@Override
public void download(UpdateChecker checker, VersionNumber ver) {
if (!(ver instanceof IntVersionNumber))
return;
IntVersionNumber version = (IntVersionNumber) ver;
checker.requestDownloadLink().then(Task.of(variables -> {
Map<String, String> map = variables.get(UpdateChecker.REQUEST_DOWNLOAD_LINK_ID);
if (map != null && map.containsKey("jar") && !StringUtils.isBlank(map.get("jar")))
try {
String hash = null;
if (map.containsKey("jarsha1"))
hash = map.get("jarsha1");
Task task = new AppDataUpgraderJarTask(NetworkUtils.toURL(map.get("jar")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderJarTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else if (map != null && map.containsKey("pack") && !StringUtils.isBlank(map.get("pack")))
try {
String hash = null;
if (map.containsKey("packsha1"))
hash = map.get("packsha1");
Task task = new AppDataUpgraderPackGzTask(NetworkUtils.toURL(map.get("pack")), version.toString(), hash);
TaskExecutor executor = task.executor();
AtomicReference<Region> region = new AtomicReference<>();
JFXUtilities.runInFX(() -> region.set(Controllers.taskDialog(executor, i18n("message.downloading"), "", null)));
if (executor.test()) {
new ProcessBuilder(JavaVersion.fromCurrentEnvironment().getBinary().getAbsolutePath(), "-jar", AppDataUpgraderPackGzTask.getSelf(version.toString()).getAbsolutePath())
.directory(new File("").getAbsoluteFile()).start();
System.exit(0);
}
JFXUtilities.runInFX(() -> region.get().fireEvent(new DialogCloseEvent()));
} catch (IOException ex) {
Logging.LOG.log(Level.SEVERE, "Failed to create upgrader", ex);
}
else {
String url = Launcher.PUBLISH;
if (map != null)
if (map.containsKey(OperatingSystem.CURRENT_OS.getCheckedName()))
url = map.get(OperatingSystem.CURRENT_OS.getCheckedName());
else if (map.containsKey(OperatingSystem.UNKNOWN.getCheckedName()))
url = map.get(OperatingSystem.UNKNOWN.getCheckedName());
try {
java.awt.Desktop.getDesktop().browse(new URI(url));
} catch (URISyntaxException | IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to browse uri: " + url, e);
OperatingSystem.setClipboard(url);
MessageBox.show(i18n("update.no_browser"));
}
}
})).start();
}
public static class AppDataUpgraderPackGzTask extends Task {
public static final File BASE_FOLDER = Launcher.HMCL_DIRECTORY;
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
public static File getSelf(String ver) {
return new File(BASE_FOLDER, "HMCL-" + ver + ".jar");
}
private final URL downloadLink;
private final String newestVersion, hash;
File tempFile;
public AppDataUpgraderPackGzTask(URL downloadLink, String newestVersion, String hash) throws IOException {
this.downloadLink = downloadLink;
this.newestVersion = newestVersion;
this.hash = hash;
tempFile = File.createTempFile("hmcl", ".pack.gz");
setName("Upgrade");
}
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash)));
}
@Override
public void execute() throws Exception {
HashMap<String, String> json = new HashMap<>();
File f = getSelf(newestVersion);
if (!FileUtils.makeDirectory(f.getParentFile()))
throw new IOException("Failed to make directories: " + f.getParent());
for (int i = 0; f.exists() && !f.delete(); i++)
f = new File(BASE_FOLDER, "HMCL-" + newestVersion + (i > 0 ? "-" + i : "") + ".jar");
if (!f.createNewFile())
throw new IOException("Failed to create new file: " + f);
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(f))) {
Pack200.newUnpacker().unpack(new GZIPInputStream(new FileInputStream(tempFile)), jos);
}
json.put("ver", newestVersion);
json.put("loc", f.getAbsolutePath());
String result = Constants.GSON.toJson(json);
FileUtils.writeText(HMCL_VER_FILE, result);
}
}
public static class AppDataUpgraderJarTask extends Task {
public static final File BASE_FOLDER = OperatingSystem.getWorkingDirectory("hmcl");
public static final File HMCL_VER_FILE = new File(BASE_FOLDER, "hmclver.json");
public static File getSelf(String ver) {
return new File(BASE_FOLDER, "HMCL-" + ver + ".jar");
}
private final URL downloadLink;
private final String newestVersion, hash;
File tempFile;
public AppDataUpgraderJarTask(URL downloadLink, String newestVersion, String hash) throws IOException {
this.downloadLink = downloadLink;
this.newestVersion = newestVersion;
this.hash = hash;
tempFile = File.createTempFile("hmcl", ".jar");
setName("Upgrade");
}
@Override
public Collection<Task> getDependents() {
return Collections.singleton(new FileDownloadTask(downloadLink, tempFile, new IntegrityCheck("SHA-1", hash)));
}
@Override
public void execute() throws Exception {
HashMap<String, String> json = new HashMap<>();
File f = getSelf(newestVersion);
FileUtils.copyFile(tempFile, f);
json.put("ver", newestVersion);
json.put("loc", f.getAbsolutePath());
String result = Constants.GSON.toJson(json);
FileUtils.writeText(HMCL_VER_FILE, result);
}
}
}
@@ -0,0 +1,124 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.upgrade;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.jackhuang.hmcl.util.IOUtils;
/**
* Helper class for adding/removing executable header from HMCL file.
*
* @author yushijinhun
*/
final class ExecutableHeaderHelper {
private ExecutableHeaderHelper() {}
private static Map<String, String> suffix2header = mapOf(
pair("exe", "assets/HMCLauncher.exe"));
private static Optional<String> getSuffix(Path file) {
String filename = file.getFileName().toString();
int idxDot = filename.lastIndexOf('.');
if (idxDot < 0) {
return Optional.empty();
} else {
return Optional.of(filename.substring(idxDot + 1));
}
}
private static Optional<byte[]> readHeader(ZipFile zip, String suffix) throws IOException {
String location = suffix2header.get(suffix);
if (location != null) {
ZipEntry entry = zip.getEntry(location);
if (entry != null && !entry.isDirectory()) {
try (InputStream in = zip.getInputStream(entry)) {
return Optional.of(IOUtils.readFullyAsByteArray(in));
}
}
}
return Optional.empty();
}
private static int detectHeaderLength(ZipFile zip, FileChannel channel) throws IOException {
ByteBuffer buf = channel.map(MapMode.READ_ONLY, 0, channel.size());
suffixLoop: for (String suffix : suffix2header.keySet()) {
Optional<byte[]> header = readHeader(zip, suffix);
if (header.isPresent()) {
buf.rewind();
for (byte b : header.get()) {
if (!buf.hasRemaining() || b != buf.get()) {
continue suffixLoop;
}
}
return header.get().length;
}
}
return 0;
}
/**
* Copies the executable and removes its header.
*/
public static void copyWithoutHeader(Path from, Path to) throws IOException {
try (
FileChannel in = FileChannel.open(from, READ);
FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING);
ZipFile zip = new ZipFile(from.toFile())
) {
in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out);
}
}
/**
* Copies the executable and appends the header according to the suffix.
*/
public static void copyWithHeader(Path from, Path to) throws IOException {
try (
FileChannel in = FileChannel.open(from, READ);
FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING);
ZipFile zip = new ZipFile(from.toFile())
) {
Optional<String> suffix = getSuffix(to);
if (suffix.isPresent()) {
Optional<byte[]> header = readHeader(zip, suffix.get());
if (header.isPresent()) {
out.write(ByteBuffer.wrap(header.get()));
}
}
in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out);
}
}
}
@@ -0,0 +1,68 @@
/*
* 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.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.tukaani.xz.XZInputStream;
class HMCLDownloadTask extends FileDownloadTask {
private RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) {
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
archiveFormat = version.getType();
}
@Override
public void execute() throws Exception {
super.execute();
try {
Path target = getFile().toPath();
switch (archiveFormat) {
case JAR:
break;
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200.newUnpacker().unpack(in, out);
}
break;
default:
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
} catch (Throwable e) {
getFile().delete();
throw e;
}
}
}
@@ -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;
import org.jackhuang.hmcl.util.JarUtils;
/**
* 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 = JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location"));
requireVerifiedJar(self);
}
}
@@ -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,29 +17,13 @@
*/
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.util.VersionNumber;
public enum UpdateChannel {
STABLE("stable"),
DEVELOPMENT("dev");
import java.util.List;
public final String channelName;
/**
*
* @author huangyuhui
*/
public abstract class IUpgrader {
/**
* Paring arguments to decide on whether the upgrade is needed.
*
* @param nowVersion now launcher version
* @param args Application CommandLine Arguments
*/
public abstract void parseArguments(VersionNumber nowVersion, List<String> args);
/**
* Just download the new app.
*
* @param checker Should be VersionChecker
* @param version the newest version
*/
public abstract void download(UpdateChecker checker, VersionNumber version);
UpdateChannel(String channelName) {
this.channelName = channelName;
}
}
@@ -1,7 +1,7 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -17,144 +17,119 @@
*/
package org.jackhuang.hmcl.upgrade;
import com.google.gson.JsonSyntaxException;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.OutOfDateEvent;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.VersionNumber;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.VersionNumber.asVersion;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
/**
*
* @author huangyuhui
*/
import javafx.beans.property.*;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.util.NetworkUtils;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.value.ObservableBooleanValue;
public final class UpdateChecker {
private UpdateChecker() {}
private volatile boolean outOfDate = false;
private final VersionNumber base;
private String versionString;
private Map<String, String> download_link = null;
public UpdateChecker(VersionNumber base) {
this.base = base;
}
private VersionNumber value;
public boolean isOutOfDate() {
return outOfDate;
}
/**
* Download the version number synchronously. When you execute this method
* first, should leave "showMessage" false.
*
* @param showMessage If it is requested to warn the user that there is a
* new version.
*
* @return the process observable.
*/
public TaskResult<VersionNumber> process(final boolean showMessage) {
return new TaskResult<VersionNumber>() {
GetTask http = new GetTask(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update.php?version=" + Launcher.VERSION));
@Override
public Collection<? extends Task> getDependents() {
return value == null ? Collections.singleton(http) : Collections.emptyList();
}
@Override
public void execute() throws Exception {
if (isDevelopmentVersion(Launcher.VERSION)) {
LOG.info("Current version is a development version, skip updating");
return;
private static ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
private static BooleanBinding outdated = Bindings.createBooleanBinding(
() -> {
RemoteVersion latest = latestVersion.get();
if (latest == null || isDevelopmentVersion(Metadata.VERSION)) {
return false;
} else {
return asVersion(latest.getVersion()).compareTo(asVersion(Metadata.VERSION)) > 0;
}
},
latestVersion);
private static ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
if (value == null) {
versionString = http.getResult();
value = VersionNumber.asVersion(versionString);
}
if (value == null) {
LOG.warning("Unable to check update...");
if (showMessage)
MessageBox.show(i18n("update.failed"));
} else if (base.compareTo(value) < 0)
outOfDate = true;
if (outOfDate)
setResult(value);
}
@Override
public String getId() {
return "update_checker.process";
}
};
public static void init() {
ConfigHolder.config().updateChannelProperty().addListener(onInvalidating(UpdateChecker::requestCheckUpdate));
requestCheckUpdate();
}
private boolean isDevelopmentVersion(String version) {
return version.contains("@") || // eg. @HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@
public static RemoteVersion getLatestVersion() {
return latestVersion.get();
}
public static ReadOnlyObjectProperty<RemoteVersion> latestVersionProperty() {
return latestVersion;
}
public static boolean isOutdated() {
return outdated.get();
}
public static ObservableBooleanValue outdatedProperty() {
return outdated;
}
public static boolean isCheckingUpdate() {
return checkingUpdate.get();
}
public static ReadOnlyBooleanProperty checkingUpdateProperty() {
return checkingUpdate.getReadOnlyProperty();
}
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Self verification failed");
}
String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf(
pair("version", Metadata.VERSION),
pair("channel", channel.channelName)));
return RemoteVersion.fetch(url);
}
private static boolean isDevelopmentVersion(String version) {
return version.contains("@") || // eg. @develop@
version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT
}
/**
* Get the <b>cached</b> newest version number, use "process" method to
* download!
*
* @return the newest version number
*
* @see #process(boolean)
*/
public VersionNumber getNewVersion() {
return value;
}
public static void requestCheckUpdate() {
Platform.runLater(() -> {
if (isCheckingUpdate())
return;
checkingUpdate.set(true);
UpdateChannel channel = config().getUpdateChannel();
/**
* Get the download links.
*
* @return a JSON, which contains the server response.
*/
public synchronized TaskResult<Map<String, String>> requestDownloadLink() {
return new TaskResult<Map<String, String>>() {
@Override
public void execute() {
if (download_link == null) {
try {
download_link = Constants.GSON.<Map<String, String>>fromJson(NetworkUtils.doGet(NetworkUtils.toURL(Launcher.UPDATE_SERVER + "/hmcl/update_link.php")), Map.class);
} catch (JsonSyntaxException | IOException e) {
LOG.log(Level.SEVERE, "Failed to get update link.", e);
}
thread(() -> {
RemoteVersion result = null;
try {
result = checkUpdate(channel);
LOG.info("Latest version (" + channel + ") is " + result);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check for update", e);
}
setResult(download_link);
}
@Override
public String getId() {
return "update_checker.request_download_link";
}
};
}
public static final String REQUEST_DOWNLOAD_LINK_ID = "update_checker.request_download_link";
public void checkOutdate() {
if (outOfDate)
if (EventBus.EVENT_BUS.fireEvent(new OutOfDateEvent(this, getNewVersion())) != Event.Result.DENY) {
Launcher.UPGRADER.download(this, getNewVersion());
}
RemoteVersion finalResult = result;
Platform.runLater(() -> {
checkingUpdate.set(false);
if (finalResult != null) {
if (channel.equals(config().getUpdateChannel())) {
latestVersion.set(finalResult);
} else {
// the channel has been changed during the period
// check update again
requestCheckUpdate();
}
}
});
});
});
}
}
@@ -0,0 +1,260 @@
/*
* 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.ui.FXUtils.checkFxUserThread;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import 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.Map;
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.JarUtils;
import org.jackhuang.hmcl.util.JavaVersion;
import org.jackhuang.hmcl.util.StringUtils;
import com.google.gson.Gson;
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) {
breakForceUpdateFeature();
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;
}
return false;
}
public static void updateFrom(RemoteVersion version) {
checkFxUserThread();
Path downloaded;
try {
downloaded = Files.createTempFile("hmcl-update-", ".jar");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create temp file", e);
return;
}
Task task = new HMCLDownloadTask(version, downloaded);
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 {
if (!IntegrityChecker.isSelfVerified()) {
throw new IOException("Current JAR is not verified");
}
requestUpdate(downloaded, getCurrentLocation());
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 void applyUpdate(Path target) throws IOException {
LOG.info("Applying update to " + target);
Path self = getCurrentLocation();
IntegrityChecker.requireVerifiedJar(self);
ExecutableHeaderHelper.copyWithHeader(self, target);
Optional<Path> newFilename = tryRename(target, Metadata.VERSION);
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);
}
}
startJava(target);
}
private static void requestUpdate(Path updateTo, Path self) throws IOException {
IntegrityChecker.requireVerifiedJar(updateTo);
startJava(updateTo, "--apply-to", self.toString());
}
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();
}
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();
}
private static Path getCurrentLocation() throws IOException {
return JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location"));
}
// ==== 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"));
requestUpdate(getCurrentLocation(), 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 = JarUtils.thisJar();
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;
}
private static void breakForceUpdateFeature() {
try {
Path hmclVersionJson = Launcher.HMCL_DIRECTORY.toPath().resolve("hmclver.json");
if (Files.isRegularFile(hmclVersionJson)) {
Map<?, ?> content = new Gson().fromJson(new String(Files.readAllBytes(hmclVersionJson), UTF_8), Map.class);
Object ver = content.get("ver");
if (ver instanceof String && ((String) ver).startsWith("3.")) {
Files.delete(hmclVersionJson);
LOG.info("Successfully broke the force update feature");
}
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to break the force update feature", e);
}
}
// ====
}
@@ -18,9 +18,13 @@
package org.jackhuang.hmcl.util;
import javafx.application.Platform;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.CrashWindow;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import static java.util.Collections.newSetFromMap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -90,7 +94,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
CAUGHT_EXCEPTIONS.add(stackTrace);
String text = "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Launcher.VERSION + "\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
" Thread: " + t.toString() + "\n" +
"\n Content: \n " +
@@ -104,8 +108,9 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
if (checkThrowable(e)) {
Platform.runLater(() -> new CrashWindow(text).show());
if (!Launcher.UPDATE_CHECKER.isOutOfDate())
if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) {
reportToServer(text);
}
}
} catch (Throwable handlingException) {
LOG.log(Level.SEVERE, "Unable to handle uncaught exception", handlingException);
@@ -116,7 +121,7 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
Thread t = new Thread(() -> {
HashMap<String, String> map = new HashMap<>();
map.put("crash_report", text);
map.put("version", Launcher.VERSION);
map.put("version", Metadata.VERSION);
map.put("log", Logging.getLogs());
try {
String response = NetworkUtils.doPost(NetworkUtils.toURL("https://hmcl.huangyuhui.net/hmcl/crash.php"), map);
Binary file not shown.
@@ -1,11 +1,4 @@
/*
* Apply google Roboto font to all UI components
* #5264AE
*/
.root {
-fx-base-font-family: "微软雅黑", "Microsoft YaHei", "Roboto";
-fx-font-family: -fx-base-font-family;
}
.disabled Label {
@@ -21,10 +21,10 @@
</StackPane>
<BorderPane style="-fx-padding: 0 0 0 10;">
<top>
<Label fx:id="lblVersionName" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
<Label fx:id="lblTitle" maxWidth="90" style="-fx-font-size: 15;" textAlignment="JUSTIFY" />
</top>
<bottom>
<Label fx:id="lblGameVersion" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
<Label fx:id="lblSubtitle" maxWidth="90" style="-fx-font-size: 10;" textAlignment="JUSTIFY" />
</bottom>
</BorderPane>
</HBox>
@@ -91,7 +91,6 @@
<HBox fx:id="navRight" alignment="CENTER_LEFT">
<JFXButton fx:id="refreshNavButton" onMouseClicked="#onRefresh"
styleClass="jfx-decorator-button" ripplerFill="white" />
<Rectangle height="${navBar.height}" width="1" fill="gray"/>
</HBox>
</right>
</BorderPane>
@@ -101,6 +100,7 @@
<padding>
<Insets topRightBottomLeft="4.0"/>
</padding>
<Rectangle fx:id="separator" height="${navBar.height}" width="1" fill="gray"/>
<JFXButton fx:id="btnMin" styleClass="jfx-decorator-button" ripplerFill="white"
onAction="#onMin">
</JFXButton>
@@ -20,4 +20,7 @@
<StackPane fx:id="failedPane">
<Label onMouseClicked="#onRefresh" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.refresh" />
</StackPane>
<StackPane fx:id="emptyPane">
<Label onMouseClicked="#onBack" style="-fx-text-fill: #0079FF; -fx-font-size: 20;" text="%download.failed.empty" />
</StackPane>
</fx:root>
@@ -6,6 +6,7 @@
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.*?>
<?import org.jackhuang.hmcl.ui.FXUtils?>
<?import javafx.scene.text.Text?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
@@ -15,21 +16,28 @@
<VBox fx:id="rootPane" style="-fx-padding: 20;">
<ComponentList fx:id="settingsPane">
<BorderPane> <!-- Update -->
<left>
<ComponentSublist fx:id="updatePane" title="%update" hasSubtitle="true">
<headerLeft>
<VBox>
<Label fx:id="lblUpdate" text="%update" BorderPane.alignment="CENTER_LEFT"/>
<Label fx:id="lblUpdateSub" styleClass="subtitle-label"/>
</VBox>
</left>
<right>
</headerLeft>
<headerRight>
<JFXButton fx:id="btnUpdate" onMouseClicked="#onUpdate" styleClass="toggle-icon4">
<graphic>
<fx:include source="/assets/svg/update.fxml" />
</graphic>
</JFXButton>
</right>
</BorderPane>
</headerRight>
<VBox spacing="8">
<JFXRadioButton fx:id="chkUpdateStable" text="%update.channel.stable" />
<JFXRadioButton fx:id="chkUpdateDev" text="%update.channel.dev" />
<VBox style="-fx-padding: 10 0 0 0;">
<Text fx:id="lblUpdateNote" text="%update.note" />
</VBox>
</VBox>
</ComponentSublist>
<MultiFileItem fx:id="fileCommonLocation" title="%launcher.common_directory" directory="true" chooserTitle="%launcher.common_directory.choose" hasSubtitle="true" customText="%settings.custom" />
@@ -55,7 +63,7 @@
<ComponentList title="%settings.launcher.proxy"> <!-- proxy -->
<VBox spacing="10">
<JFXCheckBox fx:id="chkEnableProxy" text="%settings.launcher.proxy.enable" />
<JFXCheckBox fx:id="chkDisableProxy" text="%settings.launcher.proxy.disable" />
<VBox fx:id="proxyPane" style="-fx-padding: 0 0 0 30;">
<HBox>
<JFXRadioButton fx:id="chkProxyHttp" text="%settings.launcher.proxy.http" />
@@ -83,6 +83,7 @@ crash.user_fault=Your OS or Java environment may not be properly installed resul
download=Download
download.BMCL=BMCLAPI (bangbang93, https://bmclapi2.bangbang93.com/)
download.failed=Failed to download
download.failed.empty=No candidates. Click here to return.
download.failed.refresh=Unable to load version list. Click here to retry.
download.mojang=Mojang
@@ -93,7 +94,9 @@ extension.sh=Bash shell
fatal.missing_javafx=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Java 8 or 10.\nIf you are using OpenJDK, please ensure OpenJFX is included.
fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the current Java platform.\nYou can still use HMCL, but HMCL will be unable to connect to some sites (such as sites that use certificates issued by Let's Encrypt), which may cause HMCL not to function properly.\nPlease upgrade your Java to 8u101 or later to resolve the problem.
fatal.config_access_denied=The configuration at "%s" is not readable or writable.\nPlease grant HMCL read and write access to this file and its parent directory.
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure HMCL has read and write access to "%s" and the files in it.
fatal.migration_requires_manual_reboot=The upgrade is about to be completed. Please reopen HMCL.
fatal.apply_update_failure=We're sorry that HMCL couldn't finish the upgrade because something went wrong.\nBut you can still manually finish the upgrade by downloading HMCL from %s.\nPlease consider reporting this issue to us.
folder.config=Configs
folder.coremod=Core Mod
@@ -132,6 +135,7 @@ lang.default=Use system language
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. You are supposed to install the latest version of Java 8.
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
launch.advice.newer_java=Java 8 is suggested, which can make game run more fluently. And many mods and Minecraft 1.12 and newer versions requires Java 8.
launch.advice.not_enough_space=You have allocated too much memory, because the physical memory size is %dMB, your game probably crash. The launcher will try to launch it.
@@ -179,7 +183,7 @@ message.error=Error
message.info=Info
message.success=Tasks succeeded
message.unknown=Unknown
message.warning=Warning
modpack=Mod pack
modpack.choose=Choose a modpack zip file which you want to install.
@@ -291,7 +295,7 @@ settings.launcher.language=Language
settings.launcher.log_font=Log Font
settings.launcher.proxy=Proxy
settings.launcher.proxy.authentication=Proxy Authentication
settings.launcher.proxy.enable=Enable Proxy
settings.launcher.proxy.disable=Use system proxies
settings.launcher.proxy.host=Host
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=Password
@@ -309,9 +313,13 @@ settings.type=Version setting type
settings.type.special=Specialized version settings(will not affect other versions)
update=Update
update.failed=Failed to check for updates.
update.channel.dev=Update to development version
update.channel.stable=Update to stable version
update.checking=Checking for updates
update.failed=Failed to perform upgrade
update.found=Update Available!
update.newest_version=Latest version: %s
update.note=Development version contains more functionality and bug fixes as well as more possible bugs.
update.latest=This is latest Version.
update.no_browser=Cannot open any browser. The link has been copied to the clipboard. Paste it to a browser address bar to update.
update.tooltip=Update
@@ -328,7 +336,9 @@ version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage.redownload_assets_index=Redownload Assets Index
version.manage.remove=Delete this game
version.manage.remove.confirm=Sure to remove version %s?
version.manage.remove.confirm=Sure to remove game %s? You cannot restore this game again!
version.manage.remove.confirm.trash=Sure to remove game %s? You can restore the game with the name of %s in your system trash.
version.manage.remove.confirm.independent=Since this game is in independent mode, deleting this game will result in all saved worlds belonging to this game removed simultaneously. Sure to remove game %s?
version.manage.remove_libraries=Delete library files
version.manage.rename=Rename this game
version.manage.rename.message=Please enter the new name
@@ -14,326 +14,336 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see {http://www.gnu.org/licenses/}.
#
#author: huangyuhui
# author: huangyuhui
about.copyright=版權
about.copyright.statement=Copyright (c) 2018 huangyuhui.
about.author=作者
about.author.statement=huanghongxun (hmcl@huangyuhui.net)
about.thanks_to=鳴謝
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (默認背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.thanks_to.statement=bangbang93 (BMCLAPI, https://bmclapi2.bangbang93.com/)\ngamerteam (預設背景圖)\n所有通過 Issues、Pull Requests 等方式參與本項目的貢獻者
about.dependency=依賴
# 由於篇幅限制,僅列出第一作者
about.dependency.statement=JFoenix (Shadi Shaheen, Apache 2.0)\nGson (Google, Apache 2.0)\nApache Commons Compress (ASF, Apache 2.0)\nXZ for Java (Lasse Collin, Public Domain)\nfx-gson (Joffrey Bion, MIT)
about.claim=免責聲明
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟件產生的版權問題,軟件製作方概不負責。請支持正版。
about.claim.statement=Minecraft 版權歸 Mojang AB 所有,使用本軟體產生的版權問題,軟體製作方概不負責。請支援正版。
about.open_source=開源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=賬戶
account=帳戶
account.character=角色
account.choose=選擇一個角色
account.create=新建賬戶
account.email=郵箱
account.failed.connect_authentication_server=無法連接認證服務器,可能是網絡問題
account.failed.connect_injector_server=無法連接認證服務器,可能是網絡故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的用戶名或密碼錯誤,或者登錄次數過多被暫時禁止登錄,請稍後再試
account.create=建立帳戶
account.email=電子信箱
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
account.failed.invalid_credentials=您的使用者名稱或密碼錯誤,或者登入次數過多被暫時禁止登入,請稍後再試
account.failed.invalid_password=無效的密碼
account.failed.invalid_token=請嘗試登出並重新輸入密碼登錄
account.failed.invalid_token=請嘗試登出並重新輸入密碼登入
account.failed.no_character=該帳號沒有角色
account.failed.server_response_malformed=這個服務器不是合法的登錄驗證服務器
account.injector.add=添加認證服務器
account.injector.manage=管理認證服務器
account.injector.manage.title=認證服務器
account.injector.http=警告:此服務器使用不安全的 HTTP 協議,您的密碼在登錄時會被明文傳輸。
account.injector.server=認證服務器
account.injector.server_url=服務器地址
account.injector.server_name=服務器名稱
account.methods=登錄方式
account.methods.authlib_injector=authlib-injector 登錄
account.failed.server_response_malformed=這個伺服器不是合法的登入驗證伺服器
account.injector.add=新增認證伺服器
account.injector.manage=管理認證伺服器
account.injector.manage.title=認證伺服器
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
account.injector.server=認證伺服器
account.injector.server_url=伺服器位址
account.injector.server_name=伺服器名稱
account.methods=登入方式
account.methods.authlib_injector=authlib-injector 登入
account.methods.offline=離線模式
account.methods.yggdrasil=正版登錄
account.missing=沒有賬戶
account.methods.yggdrasil=正版登入
account.missing=沒有帳戶
account.password=密碼
account.username=用戶名
account.username=使用者名稱
archive.author=作者
archive.game_version=遊戲版本
archive.version=版本
assets.download=下載資源
assets.download_all=檢查資源文件完整性
assets.download_all=驗證資源檔案完整性
button.cancel=取消
button.clear=清除
button.delete=刪除
button.edit=修改
button.edit=編輯
button.install=安裝
button.no=否
button.ok=確定
button.refresh=刷新
button.save=保存
button.refresh=重新整理
button.save=儲存
button.yes=是
color.recent=推薦
color.custom=自定義顏色
color.custom=自訂顏色
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新您的 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟件崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。
download=下載
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下載失敗
download.failed.refresh=加載版本列表失敗,點擊此處重試。
download.mojang=官方
download.failed.empty=沒有可供安裝的版本,點擊此處返回。
download.failed.refresh=載入版本列表失敗,點擊此處重試。
download.mojang=官方伺服器
extension.bat=Windows 腳本
extension.mod=模組文件
extension.png=圖片文件
extension.mod=模組檔案
extension.png=圖片檔案
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.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,但將無法連線到部分網站(如使用 Lets Encrypt 證書的站點),這可能會使 HMCL 無法正常運作。\n請將您的 Java 升級到 8u101 以上以解決此問題。
fatal.config_loading_failure=HMCL 無法載入設定檔案。\n請確保 HMCL 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
fatal.migration_requires_manual_reboot=HMCL 即將升級完成,請重新打開 HMCL。
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級程序,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。\n請考慮向我們回報該問題。
folder.config=配置文件夾
folder.coremod=核心MOD文件夾
folder.game=遊戲文件夾
folder.mod=MOD文件夾
folder.resourcepacks=資源包文件夾
folder.saves=存檔文件夾
folder.screenshots=截圖文件夾
folder.config=設定資料夾
folder.coremod=核心 MOD 模組資料夾
folder.game=遊戲資料夾
folder.mod=MOD 模組資料夾
folder.resourcepacks=資源包資料夾
folder.saves=遊戲存檔資料夾
folder.screenshots=截圖資料夾
general.prompt.empty=(無)
general.prompt.empty=(無)
input.email=用戶名必須是郵箱
input.email=「使用者名稱」必須是電子信箱格式
input.number=必須是數字
input.not_empty=必填項
input.url=必須是合法的鏈接
input.not_empty=必填
input.url=必須為有效連結
install=添加遊戲
install=新增遊戲
install.failed=安裝失敗
install.installer.choose=選擇 %s 版本
install.installer.forge=Forge
install.installer.game=遊戲
install.installer.install=安裝 %s
install.installer.liteloader=LiteLoader
install.installer.not_installed=暫不安裝 %s,可以點擊此處安裝
install.installer.not_installed=暫時不安裝 %s,可以點擊此處安裝
install.installer.optifine=OptiFine
install.installer.version=%s 版本
install.modpack=安裝整合包
install.new_game=安裝新遊戲版本
install.new_game.already_exists=此版本已經存在,請換一個名字
install.new_game.current_game_version=當前遊戲版本
install.new_game.current_game_version=目前遊戲版本
install.select=請選擇安裝方式
install.success=安裝成功
lang=繁體中文
lang=正體中文
lang.default=跟隨系統語言
launch.advice.different_platform=您的系統是 64 位的但是 Java 是 32 位的,推薦您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java9=不高於 1.13 的安裝了 Mod 的 Minecraft 不支持 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=檢測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更流暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要需要 Java 8。
launch.advice.not_enough_space=您設置的內存大小過大,由於超過了系統內存大小 %dMB,所以可能影響遊戲體驗或無法啓動遊戲,啓動器仍會嘗試啓動。
launch.advice.too_large_memory_for_32bit=您設置的內存大小過大,由於可能超過了 32 位 Java 的內存分配限制,所以可能無法啓動遊戲,請將內存調至 1024MB 或更小,啓動器仍會嘗試啓動。
launch.failed=啓動失敗
launch.failed.cannot_create_jvm=截獲到無法創建 Java 虛擬機,可能是 Java 參數有問題,可以在設置中開啓無參數模式啓動。
launch.failed.creating_process=啓動失敗,在創建新進程時發生錯誤,可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=未能解壓遊戲本地庫。
launch.failed.download_library=未能下载游戏依赖 %s.
launch.failed.executable_permission=未能爲啓動文件添加執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看日志文件,或聯繫他人尋求幫助。
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod Minecraft 版本 不支援 Java 9 或更高版本,請使用 Java 8。
launch.advice.newer_java=偵測到您未使用 Java 8 及更新版本,Java 8 能使遊戲更順暢而且 Minecraft 1.12 及更新版本和很多 Mod 強制需要 Java 8 版本。
launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。啟動器仍會嘗試啟動。
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。啟動器仍會嘗試啟動。
launch.failed=啟動失敗
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
launch.failed.creating_process=啟動失敗,在建立新執行程序時發生錯誤。可能是 Java 路徑錯誤。
launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。
launch.failed.download_library=無法下載遊戲依賴 %s.
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
launch.state.dependencies=正在處理遊戲依賴
launch.state.done=啓動完成
launch.state.logging_in=登錄中
launch.state.modpack=正在下載必要文件
launch.state.waiting_launching=等待遊戲啓動
launch.wrong_javadir=錯誤的 Java 路徑,將自動重置爲默認 Java 路徑。
launch.state.done=啟動完成
launch.state.logging_in=登入中
launch.state.modpack=正在下載必要檔案
launch.state.waiting_launching=等待遊戲啟動
launch.wrong_javadir=錯誤的 Java 路徑,將自動重設為預設 Java 路徑。
launcher=啓動器
launcher.background=背景地址
launcher=啟動器
launcher.background=背景位址
launcher.background.choose=選擇背景路徑
launcher.background.default=默認(自動檢索啓動器同目錄下的 background.png/jpg 及 bg 文件夾內的圖片)
launcher.common_directory=公共文件夾
launcher.common_directory.choose=選擇公共文件夾
launcher.common_directory.default=默認
launcher.common_directory.disabled=禁用
launcher.contact=聯繫我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並通過 MCBBS、貼吧、GitHub 或 Minecraft Forum 反饋 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已檢測到您的啓動器不是最新版本,請更新後再試!
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
launcher.common_directory=公共資料夾
launcher.common_directory.choose=選擇公共資料夾
launcher.common_directory.default=預設
launcher.common_directory.disabled=停用
launcher.contact=聯絡我們
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並透過 MCBBS、貼吧、GitHub 或 Minecraft Forum 回報 bug。
launcher.crash_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已偵測到您的啟動器不是最新版本,請更新後重試!
launcher.update_java=請更新您的 Java
login.empty_username=你還未設置用戶名!
login.empty_username=你還未設定使用者名稱!
login.enter_password=請輸入您的密碼
logwindow.show_lines=顯示行數
logwindow.terminate_game=結束遊戲進程
logwindow.title=日誌
logwindow.terminate_game=結束遊戲執行程序
logwindow.title=記錄
main_page=主頁
main_page=首頁
message.confirm=提示
message.doing=請耐心等待
message.downloading=正在下載...
message.downloading=正在下載…
message.error=錯誤
message.info=提示
message.success=已完成
message.info=資訊
message.success=完成
message.unknown=未知
message.warning=警告
modpack=整合包
modpack.choose=選擇要安裝的遊戲整合包文件
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支持 Markdown(圖片請用網絡圖)。
modpack.enter_name=給遊戲起個你喜歡的名字
modpack.export=導出整合包
modpack.choose=選擇要安裝的遊戲整合包檔案
modpack.desc=描述你要製作的整合包,比如整合包注意事項和更新記錄,支援 Markdown(圖片請上傳至網路)。
modpack.enter_name=給遊戲取個你喜歡的名字
modpack.export=匯出整合包
modpack.files.blueprints=BuildCraft 藍圖
modpack.files.config=Mod 配置文件
modpack.files.dumps=NEI 調試輸出
modpack.files.liteconfig=Mod 配置文件
modpack.files.mods=Mod
modpack.files.mods.voxelmods=VoxelMods 配置,如小地圖
modpack.files.config=Mod 模組設定檔案
modpack.files.dumps=NEI 調校輸出
modpack.files.liteconfig=Mod 模組設定檔案
modpack.files.mods=Mod 模組
modpack.files.mods.voxelmods=VoxelMods 設定,如小地圖
modpack.files.options_txt=遊戲設定
modpack.files.optionsshaders_txt=光影設定
modpack.files.resourcepacks=資源包(材質包)
modpack.files.resourcepacks=資源包 (材質包)
modpack.files.saves=遊戲存檔
modpack.files.scripts=MineTweaker 配置
modpack.files.servers_dat=多人遊戲服務器列表
modpack.install=安裝%s整合包
modpack.files.scripts=MineTweaker 設定
modpack.files.servers_dat=多人遊戲伺服器列表
modpack.install=安裝 %s 整合包
modpack.installing=正在安裝整合包
modpack.introduction=支持 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級文件,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,當前遊戲是 %s 整合包,但是提供的整合包更新文件是 %s 整合包。
modpack.introduction=支援 Curse、MultiMC、HMCL、MCBBS 整合包。
modpack.invalid=無效的整合包升級檔案,可能是下載時出現問題。
modpack.mismatched_type=整合包類型不匹配,目前遊戲是 %s 整合包,但是提供的整合包更新檔案是 %s 整合包。
modpack.name=整合包名稱
modpack.not_a_valid_name=不是一個有效的整合包名稱
modpack.not_a_valid_name=整合包名稱無效
modpack.scan=解析整合包
modpack.task.install=導入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式不正確或操作文件失敗。
modpack.task.install=匯入整合包
modpack.task.install.error=安裝失敗,可能是整合包格式錯誤或操作檔案失敗。
modpack.task.install.will=將會安裝整合包:
modpack.type.curse=Curse
modpack.type.curse.completion=下載 Curse 整合包相關文件
modpack.type.curse.error=未能完成 Curse 整合包的下载,请多次重试或设置代理
modpack.type.curse.completion=下載 Curse 整合包相關檔案
modpack.type.curse.error=無法完成 Curse 整合包的下載,請多次重試或設定代理
modpack.type.hmcl=HMCL
modpack.type.multimc=MultiMC
modpack.unsupported=該整合包不被支持。僅 HMCL、MultiMC、Curse 整合包受支持。
modpack.unsupported=不支援該整合包。僅 HMCL、MultiMC、Curse 整合包受支援。
modpack.update=正在升級整合包
modpack.wizard=導出整合包嚮導
modpack.wizard.step.1=基本設置
modpack.wizard.step.1.title=設置整合包的主要信息
modpack.wizard.step.2=文件選擇
modpack.wizard.step.2.title=選中你想加到整合包中的文件或文件夾
modpack.wizard=匯出整合包引導
modpack.wizard.step.1=基本設定
modpack.wizard.step.1.title=設定整合包的主要訊息
modpack.wizard.step.2=檔案選擇
modpack.wizard.step.2.title=選中你想加到整合包中的檔案或資料夾
modpack.wizard.step.3=整合包描述
modpack.wizard.step.initialization.exported_version=要導出的遊戲版本
modpack.wizard.step.initialization.include_launcher=包含啓動器
modpack.wizard.step.initialization.save=選擇要導出到的遊戲整合包位置
modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啓動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應當將不允許非官方途徑傳播的 Mod、材質包等納入整合包。\n整合包會保存您目前的下載源設置
modpack.wizard.step.initialization.exported_version=要匯出的遊戲版本
modpack.wizard.step.initialization.include_launcher=包含啟動器
modpack.wizard.step.initialization.save=選擇要匯出到的遊戲整合包位置
modpack.wizard.step.initialization.warning=在製作整合包前,請您確認您選擇的版本可以正常啟動,\n並保證您的 Minecraft 是正式版而非快照版,\n而且不應將不允許非官方途徑傳播的 Mod 模組、材質包等納入整合包。\n整合包會儲存您目前的下載來源設定
mods=模組管理
mods.add=添加模組
mods.add.failed=添加模組 %s 失敗。
mods.add.success=成功添加模組 %s。
mods.add=新增模組
mods.add.failed=新增模組 %s 失敗。
mods.add.success=成功新增模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
profile=配置
profile.default=當前目錄
profile.home=主文件夾
profile=設定
profile.default=目前目錄
profile.home=主資料夾
profile.instance_directory=遊戲路徑
profile.instance_directory.choose=選擇遊戲路徑
profile.new=新建配置
profile.new=建立設定
profile.title=遊戲目錄
profile.selected=已選中
profile.use_relative_path=若可能,遊戲目錄使用相對路徑
profile.selected=已選取
profile.use_relative_path=若可能,則在遊戲目錄使用相對路徑
selector.choose=選擇
selector.choose_file=選擇文件
selector.custom=自定義
selector.choose_file=選擇檔案
selector.custom=自訂
settings=普通設置
settings=普通設定
settings.advanced=高級設置
settings.advanced=進階設定
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
settings.advanced.game_dir.default=默認(.minecraft/)
settings.advanced.game_dir.default=預設(.minecraft/)
settings.advanced.game_dir.independent=各版本獨立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=啓動器默認添加的參數(請不要重複添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=內存永久保存區域(不必填寫,MB)
settings.advanced.java_args_default=啟動器預設新增的參數(請勿重複新增):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(不必填寫,格式:MB)
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.launcher_visibility.close=遊戲啓動後結束啓動器
settings.advanced.launcher_visibility.hide=遊戲啓動後隱藏啓動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啓動器並在遊戲結束後重新打開
settings.advanced.launcher_visibility.keep=保持啓動器可見
settings.advanced.launcher_visible=啓動器可見性
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
settings.advanced.launcher_visibility.hide_reopen=隱藏啟動器並在遊戲結束後重新開啟
settings.advanced.launcher_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
settings.advanced.no_common=不使用公共路徑
settings.advanced.no_jvm_args=不添加默認的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啓動前執行命令(不必填寫,將在遊戲啓動前調用)
settings.advanced.server_ip=直入服務器 IP 地址(不必填寫,啓動遊戲後直接進入對應服務器)
settings.advanced.no_jvm_args=不新增預設的 JVM 參數(使用 Java 9 時必勾)
settings.advanced.precall_command=啟動前執行命令(不必填寫,將在遊戲啟動前呼叫使用)
settings.advanced.server_ip=直接進入伺服器 IP 位址(不必填寫,啟動遊戲後直接進入對應伺服器)
settings.advanced.wrapper_launcher=前置指令(不必填寫,如 optirun)
settings.custom=自定義
settings.custom=自訂
settings.game=遊戲設置
settings.game.dimension=遊戲窗口分辨率
settings.game=遊戲設定
settings.game.dimension=遊戲介面解析度大小
settings.game.exploration=瀏覽
settings.game.fullscreen=全屏
settings.game.fullscreen=全螢幕
settings.game.game_directory=遊戲路徑(版本隔離)
settings.game.game_directory.choose=選擇遊戲路徑
settings.game.java_directory=Java 路徑
settings.game.java_directory.choose=選擇 Java 路徑
settings.game.management=管理
settings.icon=遊戲圖標
settings.icon=遊戲圖示
settings.launcher=啓動器設置
settings.launcher.common_path.tooltip=啓動器將所有遊戲資源及依賴庫文件放於此集中管理,如果遊戲文件夾內有現成的將不會使用公共庫文件
settings.launcher.download_source=下載源
settings.launcher=啟動器設定
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及依賴庫檔案放於此集中管理,如果遊戲資料夾內有現成的將不會使用公共庫檔案
settings.launcher.download_source=下載來源
settings.launcher.language=語言
settings.launcher.log_font=日誌字體
settings.launcher.log_font=記錄字體
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=身份驗證
settings.launcher.proxy.enable=启用代理
settings.launcher.proxy.disable=使用系統代理
settings.launcher.proxy.host=主機
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密碼
settings.launcher.proxy.port=端口
settings.launcher.proxy.port=埠 (Port)
settings.launcher.proxy.socks=Socks
settings.launcher.proxy.username=賬戶
settings.launcher.proxy.username=帳戶
settings.launcher.theme=主題
settings.max_memory=最大內存(MB)
settings.physical_memory=物理內存大小
settings.show_log=查看日誌
settings.max_memory=最大記憶體(MB)
settings.physical_memory=實體記憶體大小
settings.show_log=查看記錄
settings.tabs.installers=自動安裝
settings.type.global=全局版本設置(使用該設置的版本共用一套設定)
settings.type=版本設置類型
settings.type.special=單獨版本設置(不會影響到其他版本的設定)
settings.type.global=全域版本設定(使用該設定的版本共用一套設定)
settings.type=版本設定類型
settings.type.special=單獨版本設定(不會影響到其他版本的設定)
update=啓動器更新
update.failed=檢查更新失敗
update.found=發現更新
update.newest_version=最新版本爲:%s
update.latest=當前版本爲最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼板了,您可以手動粘貼網址打開頁面
update=啟動器更新
update.channel.dev=更新到開發版
update.channel.stable=更新到推薦版本
update.checking=正在檢查更新
update.failed=更新失敗
update.found=發現到更新
update.newest_version=最新版本為:%s
update.note=開發版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.latest=目前版本為最新版本
update.no_browser=無法打開瀏覽器,網址已經複製到剪貼簿了,您可以手動複製網址打開頁面
update.tooltip=更新
version.cannot_read=讀取遊戲版本失敗,無法進行自動安裝
version.forbidden_name=此版本名稱不受支持,請換一個名字
version.game.old=遠古版
version.game.release=穩定版
version.game.snapshot=測試版
version.launch=啓動遊戲
version.launch_script=生成啓動腳本
version.launch_script.failed=生成啓動腳本失敗
version.launch_script.save=保存啓動腳本
version.launch_script.success=啓動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源配置(assets_index.json)
version.forbidden_name=此版本名稱不受支援,請換一個名字
version.game.old=老舊版本
version.game.release=穩定版本
version.game.snapshot=測試版本
version.launch=啟動遊戲
version.launch_script=生成啟動腳本
version.launch_script.failed=生成啟動腳本失敗
version.launch_script.save=儲存啟動腳本
version.launch_script.success=啟動腳本已生成完畢:%s
version.manage.redownload_assets_index=重新下載資源設定(assets_index.json)
version.manage.remove=刪除該版本
version.manage.remove.confirm=真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫文件
version.manage.rename=重命名該版本
version.manage.rename.message=請輸入要改成的名字
version.manage.rename.fail=重命名版本失敗,可能文件被佔用或者名字有特殊字符
version.settings=遊戲設置
version.manage.remove.confirm=真的要刪除版本 %s 嗎?你將無法找回被刪除的檔案!
version.manage.remove.confirm.trash=真的要刪除版本 %s 嗎?你可以在系統的資源回收桶 (或垃圾桶) 中還原資料夾 %s 來找回該版本。
version.manage.remove.confirm.independent=由於該遊戲使用了版本隔離,所以刪除該版本將導致該遊戲的存檔等資料一同被刪除,真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫檔案
version.manage.rename=重新命名該版本
version.manage.rename.message=請輸入新名稱
version.manage.rename.fail=重新命名版本失敗,可能檔案被佔用或者名字有特殊字元
version.settings=遊戲設定
version.update=更新
wizard.prev=< 上一步
@@ -83,6 +83,7 @@ crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩
download=下载
download.BMCL=BMCLAPI(bangbang93,https://bmclapi2.bangbang93.com/)
download.failed=下载失败
download.failed.empty=没有可供安装的版本,点击此处返回。
download.failed.refresh=加载版本列表失败,点击此处重试。
download.mojang=官方
@@ -92,11 +93,13 @@ extension.png=图片文件
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.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 HMCL,但将无法连接到部分站点(如使用 Lets Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
fatal.config_loading_failure=HMCL 无法加载配置文件。\n请确保 HMCL 对 "%s" 目录及该目录下的文件拥有读写权限。
fatal.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。
fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 HMCL 来完成升级。\n请考虑向我们反馈该问题。
folder.config=配置文件夹
folder.coremod=核心MOD文件夹
folder.coremod=核心 MOD 文件夹
folder.game=游戏文件夹
folder.mod=MOD文件夹
folder.resourcepacks=资源包文件夹
@@ -130,10 +133,11 @@ install.success=安装成功
lang=简体中文
lang.default=跟随系统语言
launch.advice.different_platform=您的系统是 64 位的但是 Java 是 32 位的,推荐您安装 64 位 Java。
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
launch.advice.java9=不高于 1.13 的安装了 Mod 的 Minecraft 不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要需要 Java 8。
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8 能使游戏更流畅,而且 Minecraft 1.12 及更新版本和很多 Mod 强制需要 Java 8。
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏,启动器仍会尝试启动。
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小,启动器仍会尝试启动。
launch.failed=启动失败
@@ -179,7 +183,7 @@ message.error=错误
message.info=提示
message.success=已完成
message.unknown=未知
message.warning=警告
modpack=整合包
modpack.choose=选择要安装的游戏整合包文件
@@ -256,7 +260,7 @@ settings.advanced.dont_check_game_completeness=不检查游戏完整性
settings.advanced.game_dir.default=默认(.minecraft/)
settings.advanced.game_dir.independent=各版本独立(.minecraft/versions/<版本名>/,除 assets、libraries)
settings.advanced.java_args_default=启动器默认添加的参数(请不要重复添加):-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:-UseAdaptiveSizePolicy -XX:MaxPermSize=???m -Xmx???m -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,MB)
settings.advanced.java_permanent_generation_space=内存永久保存区域(不必填写,单位 MB)
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
@@ -291,7 +295,7 @@ settings.launcher.language=语言
settings.launcher.log_font=日志字体
settings.launcher.proxy=代理
settings.launcher.proxy.authentication=身份验证
settings.launcher.proxy.enable=启用代理
settings.launcher.proxy.disable=使用系统代理
settings.launcher.proxy.host=主机
settings.launcher.proxy.http=HTTP
settings.launcher.proxy.password=密码
@@ -309,9 +313,13 @@ settings.type=版本设置类型
settings.type.special=单独版本设置(不会影响到其他版本的设定)
update=启动器更新
update.failed=检查更新失败
update.channel.dev=更新到开发版
update.channel.stable=更新到推荐版本
update.checking=正在检查更新
update.failed=更新失败
update.found=发现更新
update.newest_version=最新版本为:%s
update.note=开发版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器,网址已经复制到剪贴板了,您可以手动粘贴网址打开页面
update.tooltip=更新
@@ -328,7 +336,9 @@ version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕:%s
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
version.manage.remove=删除该版本
version.manage.remove.confirm=真的要删除版本 %s 吗?
version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件!
version.manage.remove.confirm.trash=真的要删除版本 %s 吗?你可以在系统的回收站中恢复文件夹 %s 来找回该版本。
version.manage.remove.confirm.independent=由于该游戏使用了版本隔离,所以删除该版本将导致该游戏的存档等数据一同被删除,真的要删除版本 %s 吗?
version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该版本
version.manage.rename.message=请输入要改成的名字
@@ -37,6 +37,10 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
private OfflineAccountFactory() {
}
public OfflineAccount create(String username, UUID uuid) {
return new OfflineAccount(username, uuid);
}
@Override
public OfflineAccount create(CharacterSelector selector, String username, String password, Object additionalData) {
return new OfflineAccount(username, getUUIDFromUserName(username));
@@ -64,8 +64,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
return baseURL
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven")
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven")
.replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");
@@ -28,6 +28,8 @@ import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.ParallelTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.ExceptionalFunction;
/**
* Note: This class has no state.
@@ -63,7 +65,6 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
public Task checkGameCompletionAsync(Version version) {
return new ParallelTask(
new GameAssetDownloadTask(this, version),
new GameLoggingDownloadTask(this, version),
new GameLibrariesTask(this, version)
);
}
@@ -77,7 +78,7 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
}
@Override
public Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion) {
public Task installLibraryAsync(Version version, RemoteVersion libraryVersion) {
if (libraryVersion instanceof ForgeRemoteVersion)
return new ForgeInstallTask(this, version, (ForgeRemoteVersion) libraryVersion)
.then(variables -> new LibrariesUniqueTask(variables.get("version")))
@@ -97,4 +98,8 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
throw new IllegalArgumentException("Remote library " + libraryVersion + " is unrecognized.");
}
public ExceptionalFunction<AutoTypingMap<String>, Task, ?> installLibraryAsync(RemoteVersion libraryVersion) {
return var -> installLibraryAsync(var.get("version"), libraryVersion);
}
}
@@ -55,7 +55,6 @@ public class DefaultGameBuilder extends GameBuilder {
variables.set("version", version);
Task result = new ParallelTask(
new GameAssetDownloadTask(dependencyManager, version),
new GameLoggingDownloadTask(dependencyManager, version),
downloadGameAsync(gameVersion, version),
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version)); // using [with] because download failure here are tolerant.
@@ -67,8 +66,8 @@ public class DefaultGameBuilder extends GameBuilder {
if (toolVersions.containsKey("optifine"))
result = result.then(libraryTaskHelper(gameVersion, "optifine"));
for (RemoteVersion<?> remoteVersion : remoteVersions)
result = result.then(var -> dependencyManager.installLibraryAsync(var.get("version"), remoteVersion));
for (RemoteVersion remoteVersion : remoteVersions)
result = result.then(dependencyManager.installLibraryAsync(remoteVersion));
return result;
}).finalized((variables, isDependentsSucceeded) -> {
@@ -67,7 +67,7 @@ public interface DependencyManager {
* @param libraryVersion the remote version of being installed library.
* @return the task to install the specific library.
*/
Task installLibraryAsync(Version version, RemoteVersion<?> libraryVersion);
Task installLibraryAsync(Version version, RemoteVersion libraryVersion);
/**
* Get registered version list.
@@ -31,7 +31,7 @@ public abstract class GameBuilder {
protected String name = "";
protected String gameVersion = "";
protected final Map<String, String> toolVersions = new HashMap<>();
protected final Set<RemoteVersion<?>> remoteVersions = new HashSet<>();
protected final Set<RemoteVersion> remoteVersions = new HashSet<>();
public String getName() {
return name;
@@ -64,7 +64,7 @@ public abstract class GameBuilder {
return this;
}
public GameBuilder version(RemoteVersion<?> remoteVersion) {
public GameBuilder version(RemoteVersion remoteVersion) {
remoteVersions.add(remoteVersion);
return this;
}
@@ -0,0 +1,78 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import java.util.Optional;
public final class LibraryAnalyzer {
private final Library forge;
private final Library liteLoader;
private final Library optiFine;
public LibraryAnalyzer(Library forge, Library liteLoader, Library optiFine) {
this.forge = forge;
this.liteLoader = liteLoader;
this.optiFine = optiFine;
}
public Optional<Library> getForge() {
return Optional.ofNullable(forge);
}
public boolean hasForge() {
return forge != null;
}
public Optional<Library> getLiteLoader() {
return Optional.ofNullable(liteLoader);
}
public boolean hasLiteLoader() {
return liteLoader != null;
}
public Optional<Library> getOptiFine() {
return Optional.ofNullable(optiFine);
}
public boolean hasOptiFine() {
return optiFine != null;
}
public static LibraryAnalyzer analyze(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
for (Library library : version.getLibraries()) {
String groupId = library.getGroupId();
String artifactId = library.getArtifactId();
if (groupId.equalsIgnoreCase("net.minecraftforge") && artifactId.equalsIgnoreCase("forge"))
forge = library;
if (groupId.equalsIgnoreCase("com.mumfrey") && artifactId.equalsIgnoreCase("liteloader"))
liteLoader = library;
if ((groupId.equalsIgnoreCase("optifine") || groupId.equalsIgnoreCase("net.optifine")) && artifactId.equalsIgnoreCase("optifine"))
optiFine = library;
}
return new LibraryAnalyzer(forge, liteLoader, optiFine);
}
}
@@ -17,14 +17,12 @@
*/
package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.CommandBuilder;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
public class MaintainTask extends TaskResult<Version> {
@@ -46,55 +44,41 @@ public class MaintainTask extends TaskResult<Version> {
}
public static Version maintain(Version version) {
Library forge = null, liteLoader = null, optiFine = null;
List<String> args = new ArrayList<>(StringUtils.tokenize(version.getMinecraftArguments().orElse("")));
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
List<String> mcArgs = version.getMinecraftArguments().map(StringUtils::tokenize).map(ArrayList::new).orElse(null);
List<Argument> game = version.getArguments().map(Arguments::getGame).map(ArrayList::new).orElseGet(ArrayList::new);
boolean useMcArgs = mcArgs != null;
for (Library library : version.getLibraries()) {
if (library.getGroupId().equalsIgnoreCase("net.minecraftforge") && library.getArtifactId().equalsIgnoreCase("forge"))
forge = library;
if (library.getGroupId().equalsIgnoreCase("com.mumfrey") && library.getArtifactId().equalsIgnoreCase("liteloader"))
liteLoader = library;
if (library.getGroupId().equalsIgnoreCase("net.optifine") && library.getArtifactId().equalsIgnoreCase("optifine"))
optiFine = library;
}
if (forge == null) {
removeTweakClass(args, "forge");
if (!libraryAnalyzer.hasForge()) {
removeTweakClass(useMcArgs, mcArgs, game, "forge");
}
// Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added.
if (liteLoader == null) {
removeTweakClass(args, "liteloader");
removeTweakClass(useMcArgs, mcArgs, game, "liteloader");
if (libraryAnalyzer.hasLiteLoader()) {
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
removeTweakClass(useMcArgs, mcArgs, game, "optifine");
if (libraryAnalyzer.hasOptiFine()) {
if (!libraryAnalyzer.hasLiteLoader() && !libraryAnalyzer.hasForge()) {
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineTweaker");
} else {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineForgeTweaker");
}
}
Version result;
if (useMcArgs) {
// Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping,
// so we regenerate the minecraftArgument without escaping.
result = version.setMinecraftArguments(new CommandBuilder().addAllWithoutParsing(mcArgs).toString());
} else {
if (!StringUtils.containsOne(args, "LiteLoaderTweaker")) {
args.add("--tweakClass");
args.add("com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
result = version.setArguments(version.getArguments().map(args -> args.withGame(game)).orElse(new Arguments(game, Collections.emptyList())));
}
if (optiFine == null) {
removeTweakClass(args, "optifine");
}
if (liteLoader == null && forge == null && optiFine != null) {
if (!StringUtils.containsOne(args, "optifine.OptiFineTweaker")) {
args.add("--tweakClass");
args.add("optifine.OptiFineTweaker");
}
}
if ((liteLoader != null || forge != null) && optiFine != null) {
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
removeTweakClass(args, "optifine");
args.add("--tweakClass");
args.add("optifine.OptiFineForgeTweaker");
}
return version.setMinecraftArguments(new CommandBuilder().addAll(args).toString());
return result;
}
@Override
@@ -102,17 +86,43 @@ public class MaintainTask extends TaskResult<Version> {
return id;
}
private static void removeTweakClass(List<String> args, String target) {
for (int i = 0; i < args.size(); ++i) {
if (args.get(i).toLowerCase().contains(target)) {
if (i > 0 && args.get(i - 1).equals("--tweakClass")) {
args.remove(i - 1);
args.remove(i - 1);
i -= 2;
private static void removeTweakClass(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String target) {
if (useMcArgs) {
for (int i = 0; i + 1 < mcArgs.size(); ++i) {
String arg0Str = mcArgs.get(i);
String arg1Str = mcArgs.get(i + 1);
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
mcArgs.remove(i);
mcArgs.remove(i);
--i;
}
}
} else {
for (int i = 0; i + 1 < game.size(); ++i) {
Argument arg0 = game.get(i);
Argument arg1 = game.get(i + 1);
if (arg0 instanceof StringArgument && arg1 instanceof StringArgument) {
// We need to preserve the tokens
String arg0Str = arg0.toString();
String arg1Str = arg1.toString();
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
game.remove(i);
game.remove(i);
--i;
}
}
}
}
}
private static void addArgument(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String... args) {
if (useMcArgs) {
mcArgs.addAll(Arrays.asList(args));
} else {
for (String arg : args)
game.add(new StringArgument(arg));
}
}
public static final String ID = "version";
}
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.download.forge.ForgeVersionList;
import org.jackhuang.hmcl.download.game.GameVersionList;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineVersionList;
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList;
/**
* @see <a href="http://wiki.vg">http://wiki,vg</a>
@@ -48,7 +48,7 @@ public class MojangDownloadProvider implements DownloadProvider {
case "liteloader":
return LiteLoaderVersionList.INSTANCE;
case "optifine":
return OptiFineVersionList.INSTANCE;
return OptiFineBMCLVersionList.INSTANCE;
default:
throw new IllegalArgumentException("Unrecognized version list id: " + id);
}
@@ -27,26 +27,36 @@ import java.util.Objects;
*
* @author huangyuhui
*/
public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
public class RemoteVersion implements Comparable<RemoteVersion> {
private final String gameVersion;
private final String selfVersion;
private final String url;
private final T tag;
private final Type type;
/**
* Constructor.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param tag some necessary information for Installer Task.
* @param url the installer or universal jar URL.
*/
public RemoteVersion(String gameVersion, String selfVersion, String url, T tag) {
public RemoteVersion(String gameVersion, String selfVersion, String url) {
this(gameVersion, selfVersion, url, Type.UNCATEGORIZED);
}
/**
* Constructor.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
*/
public RemoteVersion(String gameVersion, String selfVersion, String url, Type type) {
this.gameVersion = Objects.requireNonNull(gameVersion);
this.selfVersion = Objects.requireNonNull(selfVersion);
this.url = Objects.requireNonNull(url);
this.tag = tag;
this.type = Objects.requireNonNull(type);
}
public String getGameVersion() {
@@ -57,14 +67,14 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
return selfVersion;
}
public T getTag() {
return tag;
}
public String getUrl() {
return url;
}
public Type getVersionType() {
return type;
}
@Override
public boolean equals(Object obj) {
return obj instanceof RemoteVersion && Objects.equals(selfVersion, ((RemoteVersion) obj).selfVersion);
@@ -80,13 +90,19 @@ public class RemoteVersion<T> implements Comparable<RemoteVersion<T>> {
return new ToStringBuilder(this)
.append("selfVersion", selfVersion)
.append("gameVersion", gameVersion)
.append("tag", tag)
.toString();
}
@Override
public int compareTo(RemoteVersion<T> o) {
public int compareTo(RemoteVersion o) {
// newer versions are smaller than older versions
return VersionNumber.asVersion(o.selfVersion).compareTo(VersionNumber.asVersion(selfVersion));
}
public enum Type {
UNCATEGORIZED,
RELEASE,
SNAPSHOT,
OLD
}
}
@@ -26,18 +26,18 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* The remote version list.
*
* @param <T> The type of {@code RemoteVersion<T>}, the type of tags.
* @param <T> The subclass of {@code RemoteVersion}, the type of RemoteVersion.
*
* @author huangyuhui
*/
public abstract class VersionList<T> {
public abstract class VersionList<T extends RemoteVersion> {
/**
* the remote version list.
* key: game version.
* values: corresponding remote versions.
*/
protected final SimpleMultimap<String, RemoteVersion<T>> versions = new SimpleMultimap<String, RemoteVersion<T>>(HashMap::new, TreeSet::new);
protected final SimpleMultimap<String, T> versions = new SimpleMultimap<String, T>(HashMap::new, TreeSet::new);
/**
* True if the version list has been loaded.
@@ -46,6 +46,8 @@ public abstract class VersionList<T> {
return !versions.isEmpty();
}
public abstract boolean hasType();
protected final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
@@ -68,11 +70,10 @@ public abstract class VersionList<T> {
});
}
private Collection<RemoteVersion<T>> getVersionsImpl(String gameVersion) {
protected Collection<T> getVersionsImpl(String gameVersion) {
lock.readLock().lock();
try {
Collection<RemoteVersion<T>> ans = versions.get(gameVersion);
return ans.isEmpty() ? versions.values() : ans;
return versions.get(gameVersion);
} finally {
lock.readLock().unlock();
}
@@ -84,7 +85,7 @@ public abstract class VersionList<T> {
* @param gameVersion the Minecraft version that remote versions belong to
* @return the collection of specific remote versions
*/
public final Collection<RemoteVersion<T>> getVersions(String gameVersion) {
public final Collection<T> getVersions(String gameVersion) {
return Collections.unmodifiableCollection(getVersionsImpl(gameVersion));
}
@@ -95,11 +96,11 @@ public abstract class VersionList<T> {
* @param remoteVersion the version of the remote version.
* @return the specific remote version, null if it is not found.
*/
public final Optional<RemoteVersion<T>> getVersion(String gameVersion, String remoteVersion) {
public final Optional<T> getVersion(String gameVersion, String remoteVersion) {
lock.readLock().lock();
try {
RemoteVersion<T> result = null;
for (RemoteVersion<T> it : versions.get(gameVersion))
T result = null;
for (T it : versions.get(gameVersion))
if (remoteVersion.equals(it.getSelfVersion()))
result = it;
return Optional.ofNullable(result);
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.RemoteVersion;
public class ForgeRemoteVersion extends RemoteVersion<Void> {
public class ForgeRemoteVersion extends RemoteVersion {
/**
* Constructor.
*
@@ -28,6 +28,6 @@ public class ForgeRemoteVersion extends RemoteVersion<Void> {
* @param url the installer or universal jar URL.
*/
public ForgeRemoteVersion(String gameVersion, String selfVersion, String url) {
super(gameVersion, selfVersion, url, null);
super(gameVersion, selfVersion, url);
}
}
@@ -32,13 +32,18 @@ import java.util.*;
*
* @author huangyuhui
*/
public final class ForgeVersionList extends VersionList<Void> {
public final class ForgeVersionList extends VersionList<ForgeRemoteVersion> {
public static final ForgeVersionList INSTANCE = new ForgeVersionList();
private ForgeVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
final GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(FORGE_LIST)));
@@ -75,8 +75,6 @@ public final class GameAssetDownloadTask extends Task {
@Override
public void execute() throws Exception {
int size = refreshTask.getResult().size();
int downloaded = 0;
for (Map.Entry<File, AssetObject> entry : refreshTask.getResult()) {
if (Thread.interrupted())
throw new InterruptedException();
@@ -88,29 +86,11 @@ public final class GameAssetDownloadTask extends Task {
Logging.LOG.log(Level.SEVERE, "Unable to create new file " + file + ", because parent directory cannot be created");
continue;
}
if (file.isDirectory())
if (file.isDirectory() || file.isFile() && file.exists())
continue;
boolean flag = true;
try {
// check the checksum of file to ensure that the file is not need to re-download.
if (file.exists()) {
String sha1 = encodeHex(digest("SHA-1", FileUtils.readBytes(file)));
if (sha1.equals(assetObject.getHash())) {
++downloaded;
Logging.LOG.finest("File $file has been downloaded successfully, skipped downloading");
updateProgress(downloaded, size);
continue;
}
}
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to get hash code of file " + file, e);
flag = !file.exists();
}
if (flag) {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
dependencies.add(task);
}
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(url), file, new IntegrityCheck("SHA-1", assetObject.getHash()));
task.setName(assetObject.getHash());
dependencies.add(task);
}
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.download.DependencyManager;
import org.jackhuang.hmcl.game.DownloadType;
import org.jackhuang.hmcl.game.LoggingInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.NetworkUtils;
import java.io.File;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
/**
* This task is to download log4j configuration file provided in minecraft.json.
*
* @author huangyuhui
*/
public final class GameLoggingDownloadTask extends Task {
private final DependencyManager dependencyManager;
private final Version version;
private final List<Task> dependencies = new LinkedList<>();
/**
* Constructor.
*
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version
*/
public GameLoggingDownloadTask(DependencyManager dependencyManager, Version version) {
this.dependencyManager = dependencyManager;
this.version = version;
setSignificance(TaskSignificance.MODERATE);
}
@Override
public Collection<Task> getDependencies() {
return dependencies;
}
@Override
public void execute() {
if (version.getLogging() == null || !version.getLogging().containsKey(DownloadType.CLIENT))
return;
LoggingInfo logging = version.getLogging().get(DownloadType.CLIENT);
File file = dependencyManager.getGameRepository().getLoggingObject(version.getId(), version.getAssetIndex().getId(), logging);
if (!file.exists())
dependencies.add(new FileDownloadTask(NetworkUtils.toURL(logging.getFile().getUrl()), file));
}
}
@@ -17,12 +17,9 @@
*/
package org.jackhuang.hmcl.download.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.Validation;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Date;
@@ -30,64 +27,45 @@ import java.util.Date;
*
* @author huangyuhui
*/
public final class GameRemoteVersion implements Validation {
@Immutable
public final class GameRemoteVersion extends RemoteVersion {
@SerializedName("id")
private final String gameVersion;
@SerializedName("time")
private final ReleaseType type;
private final Date time;
@SerializedName("releaseTime")
private final Date releaseTime;
@SerializedName("type")
private final ReleaseType type;
@SerializedName("url")
private final String url;
public GameRemoteVersion() {
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
}
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
}
public GameRemoteVersion(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
this.gameVersion = gameVersion;
this.time = time;
this.releaseTime = releaseTime;
public GameRemoteVersion(String gameVersion, String selfVersion, String url, ReleaseType type, Date time) {
super(gameVersion, selfVersion, url, getReleaseType(type));
this.type = type;
this.url = url;
}
public String getGameVersion() {
return gameVersion;
this.time = time;
}
public Date getTime() {
return time;
}
public Date getReleaseTime() {
return releaseTime;
}
public ReleaseType getType() {
return type;
}
public String getUrl() {
return url;
}
@Override
public void validate() throws JsonParseException {
if (StringUtils.isBlank(gameVersion))
throw new JsonParseException("GameRemoteVersion id cannot be blank");
if (StringUtils.isBlank(url))
throw new JsonParseException("GameRemoteVersion url cannot be blank");
public int compareTo(RemoteVersion o) {
if (!(o instanceof GameRemoteVersion))
return 0;
return ((GameRemoteVersion) o).getTime().compareTo(getTime());
}
private static Type getReleaseType(ReleaseType type) {
if (type == null) return Type.UNCATEGORIZED;
switch (type) {
case RELEASE:
return Type.RELEASE;
case SNAPSHOT:
return Type.SNAPSHOT;
case UNKNOWN:
return Type.UNCATEGORIZED;
default:
return Type.OLD;
}
}
}
@@ -0,0 +1,93 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.Validation;
import java.util.Date;
/**
*
* @author huangyuhui
*/
public final class GameRemoteVersionInfo implements Validation {
@SerializedName("id")
private final String gameVersion;
@SerializedName("time")
private final Date time;
@SerializedName("releaseTime")
private final Date releaseTime;
@SerializedName("type")
private final ReleaseType type;
@SerializedName("url")
private final String url;
public GameRemoteVersionInfo() {
this("", new Date(), new Date(), ReleaseType.UNKNOWN);
}
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type) {
this(gameVersion, time, releaseTime, type, Constants.DEFAULT_LIBRARY_URL + gameVersion + "/" + gameVersion + ".json");
}
public GameRemoteVersionInfo(String gameVersion, Date time, Date releaseTime, ReleaseType type, String url) {
this.gameVersion = gameVersion;
this.time = time;
this.releaseTime = releaseTime;
this.type = type;
this.url = url;
}
public String getGameVersion() {
return gameVersion;
}
public Date getTime() {
return time;
}
public Date getReleaseTime() {
return releaseTime;
}
public ReleaseType getType() {
return type;
}
public String getUrl() {
return url;
}
@Override
public void validate() throws JsonParseException {
if (StringUtils.isBlank(gameVersion))
throw new JsonParseException("GameRemoteVersion id cannot be blank");
if (StringUtils.isBlank(url))
throw new JsonParseException("GameRemoteVersion url cannot be blank");
}
}
@@ -1,55 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.game;
import org.jackhuang.hmcl.game.ReleaseType;
import org.jackhuang.hmcl.util.Immutable;
import java.util.Date;
/**
*
* @author huangyuhui
*/
@Immutable
public final class GameRemoteVersionTag {
private final ReleaseType type;
private final Date time;
/**
* No-arg constructor for Gson.
*/
@SuppressWarnings("unused")
public GameRemoteVersionTag() {
this(ReleaseType.UNKNOWN, new Date());
}
public GameRemoteVersionTag(ReleaseType type, Date time) {
this.type = type;
this.time = time;
}
public Date getTime() {
return time;
}
public ReleaseType getType() {
return type;
}
}
@@ -31,7 +31,7 @@ import java.util.List;
public final class GameRemoteVersions {
@SerializedName("versions")
private final List<GameRemoteVersion> versions;
private final List<GameRemoteVersionInfo> versions;
@SerializedName("latest")
private final GameRemoteLatestVersions latest;
@@ -44,7 +44,7 @@ public final class GameRemoteVersions {
this(Collections.emptyList(), null);
}
public GameRemoteVersions(List<GameRemoteVersion> versions, GameRemoteLatestVersions latest) {
public GameRemoteVersions(List<GameRemoteVersionInfo> versions, GameRemoteLatestVersions latest) {
this.versions = versions;
this.latest = latest;
}
@@ -53,7 +53,7 @@ public final class GameRemoteVersions {
return latest;
}
public List<GameRemoteVersion> getVersions() {
public List<GameRemoteVersionInfo> getVersions() {
return versions;
}
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Collection;
import java.util.Collections;
@@ -32,13 +33,28 @@ import java.util.Collections;
*
* @author huangyuhui
*/
public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
public final class GameVersionList extends VersionList<GameRemoteVersion> {
public static final GameVersionList INSTANCE = new GameVersionList();
private GameVersionList() {
}
@Override
public boolean hasType() {
return true;
}
@Override
protected Collection<GameRemoteVersion> getVersionsImpl(String gameVersion) {
lock.readLock().lock();
try {
return StringUtils.isBlank(gameVersion) ? versions.values() : versions.get(gameVersion);
} finally {
lock.readLock().unlock();
}
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.getVersionListURL()));
@@ -56,12 +72,12 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
versions.clear();
GameRemoteVersions root = Constants.GSON.fromJson(task.getResult(), GameRemoteVersions.class);
for (GameRemoteVersion remoteVersion : root.getVersions()) {
versions.put(remoteVersion.getGameVersion(), new RemoteVersionGame(
for (GameRemoteVersionInfo remoteVersion : root.getVersions()) {
versions.put(remoteVersion.getGameVersion(), new GameRemoteVersion(
remoteVersion.getGameVersion(),
remoteVersion.getGameVersion(),
remoteVersion.getUrl(),
new GameRemoteVersionTag(remoteVersion.getType(), remoteVersion.getReleaseTime()))
remoteVersion.getType(), remoteVersion.getReleaseTime())
);
}
} finally {
@@ -70,15 +86,4 @@ public final class GameVersionList extends VersionList<GameRemoteVersionTag> {
}
};
}
private static class RemoteVersionGame extends RemoteVersion<GameRemoteVersionTag> {
public RemoteVersionGame(String gameVersion, String selfVersion, String url, GameRemoteVersionTag tag) {
super(gameVersion, selfVersion, url, tag);
}
@Override
public int compareTo(RemoteVersion<GameRemoteVersionTag> o) {
return o.getTag().getTime().compareTo(getTag().getTime());
}
}
}
@@ -56,11 +56,11 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
String serialized = Constants.GSON.toJson(library);
if (multimap.containsKey(id)) {
boolean hasEqualRule = false;
boolean duplicate = false;
for (Library otherLibrary : multimap.get(id)) {
VersionNumber otherNumber = VersionNumber.asVersion(otherLibrary.getVersion());
if (CompatibilityRule.equals(library.getRules(), otherLibrary.getRules())) { // rules equal, ignore older version.
hasEqualRule = true;
boolean flag = true;
if (number.compareTo(otherNumber) > 0) { // if this library is newer
multimap.removeValue(otherLibrary);
multimap.put(id, library);
@@ -76,12 +76,20 @@ public class LibrariesUniqueTask extends TaskResult<Version> {
multimap.put(id, library);
break;
}
} else {
// for text2speech, which have same library id as well as version number,
// but its library and native library does not equal
flag = false;
}
}
if (flag) {
duplicate = true;
break;
}
}
}
if (!hasEqualRule) {
if (!duplicate) {
multimap.put(id, library);
}
} else {
@@ -1,6 +1,5 @@
package org.jackhuang.hmcl.download.game;
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import org.jackhuang.hmcl.download.AbstractDependencyManager;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.task.FileDownloadTask;
@@ -10,9 +9,13 @@ import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.IOUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
@@ -23,15 +26,15 @@ import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
public final class LibraryDownloadTask extends Task {
private final FileDownloadTask xzTask;
private final FileDownloadTask task;
private final LibraryDownloadTaskHelper helperTask;
private final File jar;
private final File xzFile;
private final Library library;
private boolean downloaded = false;
private boolean xz;
public LibraryDownloadTask(AbstractDependencyManager dependencyManager, File file, Library library) {
setSignificance(TaskSignificance.MODERATE);
if (library.is("net.minecraftforge", "forge"))
library = library.setClassifier("universal");
@@ -42,20 +45,12 @@ public final class LibraryDownloadTask extends Task {
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
xzTask = new FileDownloadTask(NetworkUtils.toURL(url + ".pack.xz"),
xzFile, null, 1);
xzTask.setSignificance(TaskSignificance.MINOR);
setSignificance(TaskSignificance.MODERATE);
task = new FileDownloadTask(NetworkUtils.toURL(url),
file,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
helperTask = new LibraryDownloadTaskHelper(file, url);
}
@Override
public Collection<? extends Task> getDependents() {
return library.getChecksums() != null ? Collections.singleton(xzTask) : Collections.singleton(task);
return Collections.singleton(helperTask);
}
@Override
@@ -66,32 +61,75 @@ public final class LibraryDownloadTask extends Task {
@Override
public void execute() throws Exception {
if (!isDependentsSucceeded()) {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = library.getChecksums() != null ? xzTask.getLastException() : task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
if (helperTask.task == null) {
// NetworkUtils.URLExists failed.
throw new LibraryDownloadException(library, helperTask.getLastException());
} else {
// Since FileDownloadTask wraps the actual exception with another IOException.
// We should extract it letting the error message clearer.
Throwable t = helperTask.task.getLastException();
if (t.getCause() != null && t.getCause() != t)
throw new LibraryDownloadException(library, t.getCause());
else
throw new LibraryDownloadException(library, t);
}
} else {
if (xz) unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums())) {
jar.delete();
throw new IOException("Checksum failed for " + library);
}
}
}
private class LibraryDownloadTaskHelper extends Task {
private FileDownloadTask task;
private final File file;
private final String url;
public LibraryDownloadTaskHelper(File file, String url) {
this.file = file;
this.url = url;
setName(library.getName());
}
if (library.getChecksums() != null) {
unpackLibrary(jar, FileUtils.readBytes(xzFile));
if (!checksumValid(jar, library.getChecksums()))
throw new IOException("Checksum failed for " + library);
@Override
public boolean isRelyingOnDependencies() {
return true;
}
@Override
public Collection<? extends Task> getDependencies() {
return Collections.singleton(task);
}
@Override
public void execute() throws Exception {
URL packXz = NetworkUtils.toURL(url + ".pack.xz");
if (NetworkUtils.URLExists(packXz)) {
task = new FileDownloadTask(packXz, xzFile, null);
xz = true;
} else {
task = new FileDownloadTask(NetworkUtils.toURL(url),
file,
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
xz = false;
}
}
}
private static boolean checksumValid(File libPath, List<String> checksums) {
try {
if ((checksums == null) || (checksums.isEmpty())) {
if (checksums == null || checksums.isEmpty()) {
return true;
}
byte[] fileData = FileUtils.readBytes(libPath);
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
if ((!valid) && (libPath.getName().endsWith(".jar"))) {
if (!valid && libPath.getName().endsWith(".jar")) {
valid = validateJar(fileData, checksums);
}
return validateJar(fileData, checksums);
return valid;
} catch (IOException e) {
e.printStackTrace();
}
@@ -104,7 +142,7 @@ public final class LibraryDownloadTask extends Task {
JarInputStream jar = new JarInputStream(new ByteArrayInputStream(data));
JarEntry entry = jar.getNextJarEntry();
while (entry != null) {
byte[] eData = IOUtils.readFullyAsByteArray(jar);
byte[] eData = IOUtils.readFullyWithoutClosing(jar);
if (entry.getName().equals("checksums.sha1")) {
hashes = new String(eData, Charset.forName("UTF-8")).split("\n");
}
@@ -115,8 +153,8 @@ public final class LibraryDownloadTask extends Task {
}
jar.close();
if (hashes != null) {
boolean passed = !checksums.contains(files.get("checksums.sha1"));
if (passed) {
boolean failed = !checksums.contains(files.get("checksums.sha1"));
if (!failed) {
for (String hash : hashes) {
if ((!hash.trim().equals("")) && (hash.contains(" "))) {
String[] e = hash.split(" ");
@@ -125,17 +163,17 @@ public final class LibraryDownloadTask extends Task {
String checksum = files.get(target);
if ((!files.containsKey(target)) || (checksum == null)) {
Logging.LOG.warning(" " + target + " : missing");
passed = false;
failed = true;
break;
} else if (!checksum.equals(validChecksum)) {
Logging.LOG.warning(" " + target + " : failed (" + checksum + ", " + validChecksum + ")");
passed = false;
failed = true;
break;
}
}
}
}
return passed;
return !failed;
}
return false;
}
@@ -145,7 +183,7 @@ public final class LibraryDownloadTask extends Task {
if (!dest.delete())
throw new IOException("Unable to delete file " + dest);
byte[] decompressed = IOUtils.readFullyAsByteArray(new XZCompressorInputStream(new ByteArrayInputStream(src)));
byte[] decompressed = IOUtils.readFullyAsByteArray(new XZInputStream(new ByteArrayInputStream(src)));
String end = new String(decompressed, decompressed.length - 4, 4);
if (!end.equals("SIGN"))
@@ -62,7 +62,7 @@ public final class VersionJsonDownloadTask extends Task {
@Override
public void execute() {
RemoteVersion<?> remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
RemoteVersion remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
.orElseThrow(() -> new IllegalStateException("Cannot find specific version " + gameVersion + " in remote repository"));
String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl());
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), ID));
@@ -39,13 +39,18 @@ import java.util.Optional;
*
* @author huangyuhui
*/
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
public final class LiteLoaderBMCLVersionList extends VersionList<LiteLoaderRemoteVersion> {
public static final LiteLoaderBMCLVersionList INSTANCE = new LiteLoaderBMCLVersionList();
private LiteLoaderBMCLVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.LibrariesDownloadInfo;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Lang;
@@ -74,18 +71,12 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()))
);
Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Collections.singleton(library)));
String mcArg = version.getMinecraftArguments().orElse("");
if (mcArg.contains("--tweakClass optifine.OptiFineTweaker"))
mcArg = mcArg.replace("--tweakClass optifine.OptiFineTweaker", "");
Version tempVersion = version.setLibraries(Lang.merge(remote.getLibraries(), Collections.singleton(library)));
setResult(version
.setMainClass("net.minecraft.launchwrapper.Launch")
.setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries()))
.setLogging(Collections.emptyMap())
.setMinecraftArguments(mcArg + " --tweakClass " + remote.getTag().getTweakClass())
//.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass()))
);
dependencies.add(new GameLibrariesTask(dependencyManager, tempVersion));
@@ -22,7 +22,9 @@ import org.jackhuang.hmcl.game.Library;
import java.util.Collection;
public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersionTag> {
public class LiteLoaderRemoteVersion extends RemoteVersion {
private final String tweakClass;
private final Collection<Library> libraries;
/**
* Constructor.
*
@@ -31,6 +33,18 @@ public class LiteLoaderRemoteVersion extends RemoteVersion<LiteLoaderRemoteVersi
* @param url the installer or universal jar URL.
*/
LiteLoaderRemoteVersion(String gameVersion, String selfVersion, String url, String tweakClass, Collection<Library> libraries) {
super(gameVersion, selfVersion, url, new LiteLoaderRemoteVersionTag(tweakClass, libraries));
super(gameVersion, selfVersion, url);
this.tweakClass = tweakClass;
this.libraries = libraries;
}
public Collection<Library> getLibraries() {
return libraries;
}
public String getTweakClass() {
return tweakClass;
}
}
@@ -1,54 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.liteloader;
import org.jackhuang.hmcl.game.Library;
import java.util.Collection;
import java.util.Collections;
/**
*
* @author huangyuhui
*/
public final class LiteLoaderRemoteVersionTag {
private final String tweakClass;
private final Collection<Library> libraries;
/**
* No-arg constructor for Gson.
*/
@SuppressWarnings("unused")
public LiteLoaderRemoteVersionTag() {
this("", Collections.emptySet());
}
public LiteLoaderRemoteVersionTag(String tweakClass, Collection<Library> libraries) {
this.tweakClass = tweakClass;
this.libraries = libraries;
}
public Collection<Library> getLibraries() {
return libraries;
}
public String getTweakClass() {
return tweakClass;
}
}
@@ -39,13 +39,18 @@ import java.util.Optional;
*
* @author huangyuhui
*/
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersionTag> {
public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVersion> {
public static final LiteLoaderVersionList INSTANCE = new LiteLoaderVersionList();
private LiteLoaderVersionList() {
}
@Override
public boolean hasType() {
return false;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL(downloadProvider.injectURL(LITELOADER_LIST)));
@@ -33,13 +33,18 @@ import java.util.*;
*
* @author huangyuhui
*/
public final class OptiFineBMCLVersionList extends VersionList<Void> {
public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVersion> {
public static final OptiFineBMCLVersionList INSTANCE = new OptiFineBMCLVersionList();
private OptiFineBMCLVersionList() {
}
@Override
public boolean hasType() {
return true;
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("http://bmclapi2.bangbang93.com/optifine/versionlist"));
@@ -57,16 +62,16 @@ public final class OptiFineBMCLVersionList extends VersionList<Void> {
}.getType());
for (OptiFineVersion element : root) {
String version = element.getType() + "_" + element.getPatch();
if (element.getType() == null || "pre".equals(element.getPatch()))
continue;
String mirror = "http://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
if (!duplicates.add(mirror))
continue;
boolean isPre = element.getPatch() != null && (element.getPatch().equals("pre") || element.getPatch().startsWith("alpha"));
if (StringUtils.isBlank(element.getGameVersion()))
continue;
VersionNumber.parseVersion(element.getGameVersion())
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror)));
.ifPresent(gameVersion -> versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, () -> mirror, isPre)));
}
}
};
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.Lang;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -70,46 +69,24 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
@Override
public void execute() {
String remoteVersion = remote.getGameVersion() + "-" + remote.getSelfVersion();
String remoteVersion = remote.getGameVersion() + "_" + remote.getSelfVersion();
Library library = new Library(
"net.optifine", "optifine", remoteVersion, null, null,
"optifine", "OptiFine", remoteVersion, null, null,
new LibrariesDownloadInfo(new LibraryDownloadInfo(
"net/optifine/optifine/" + remoteVersion + "/optifine-" + remoteVersion + ".jar",
"optifine/OptiFine/" + remoteVersion + "/OptiFine-" + remoteVersion + ".jar",
remote.getUrl()))
);
List<Library> libraries = new LinkedList<>();
libraries.add(library);
boolean hasFMLTweaker = false;
if (version.getMinecraftArguments().isPresent() && version.getMinecraftArguments().get().contains("FMLTweaker"))
hasFMLTweaker = true;
if (version.getArguments().isPresent()) {
List<Argument> game = version.getArguments().get().getGame();
if (game.stream().anyMatch(arg -> arg.toString(Collections.emptyMap(), Collections.emptyMap()).contains("FMLTweaker")))
hasFMLTweaker = true;
}
/*Arguments arguments = Lang.get(version.getArguments());
if (!hasFMLTweaker)
arguments = Arguments.addGameArguments(arguments, "--tweakClass", "optifine.OptiFineTweaker");
*/
String minecraftArguments = version.getMinecraftArguments().orElse("");
if (!hasFMLTweaker)
minecraftArguments = minecraftArguments + " --tweakClass optifine.OptiFineTweaker";
else
minecraftArguments = minecraftArguments + " --tweakClass optifine.OptiFineForgeTweaker";
if (version.getMainClass() == null || !version.getMainClass().startsWith("net.minecraft.launchwrapper."))
libraries.add(0, new Library("net.minecraft", "launchwrapper", "1.12"));
setResult(version
.setLibraries(Lang.merge(version.getLibraries(), libraries))
.setMainClass("net.minecraft.launchwrapper.Launch")
.setMinecraftArguments(minecraftArguments)
//.setArguments(arguments)
.setLibraries(Lang.merge(version.getLibraries(), libraries))
.setMainClass("net.minecraft.launchwrapper.Launch")
);
dependencies.add(new GameLibrariesTask(dependencyManager, version.setLibraries(libraries)));
@@ -21,11 +21,11 @@ import org.jackhuang.hmcl.download.RemoteVersion;
import java.util.function.Supplier;
public class OptiFineRemoteVersion extends RemoteVersion<Void> {
public class OptiFineRemoteVersion extends RemoteVersion {
private final Supplier<String> url;
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url) {
super(gameVersion, selfVersion, "", null);
public OptiFineRemoteVersion(String gameVersion, String selfVersion, Supplier<String> url, boolean snapshot) {
super(gameVersion, selfVersion, "", snapshot ? Type.SNAPSHOT : Type.RELEASE);
this.url = url;
}
@@ -1,121 +0,0 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2018 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download.optifine;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.NetworkUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author huangyuhui
*/
public final class OptiFineVersionList extends VersionList<Void> {
private static final Pattern PATTERN = Pattern.compile("OptiFine (.*?) ");
private static final Pattern LINK_PATTERN = Pattern.compile("\"downloadx\\?f=OptiFine(.*)\"");
public static final OptiFineVersionList INSTANCE = new OptiFineVersionList();
private OptiFineVersionList() {
}
@Override
public Task refreshAsync(DownloadProvider downloadProvider) {
GetTask task = new GetTask(NetworkUtils.toURL("https://optifine.net/downloads"));
return new Task() {
@Override
public Collection<Task> getDependents() {
return Collections.singleton(task);
}
@Override
public void execute() throws Exception {
lock.writeLock().lock();
try {
versions.clear();
String html = task.getResult().replace("&nbsp;", " ").replace("&gt;", ">").replace("&lt;", "<").replace("<br>", "<br />");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder db = factory.newDocumentBuilder();
Document doc = db.parse(new ByteArrayInputStream(html.getBytes("UTF-8")));
Element r = doc.getDocumentElement();
NodeList tables = r.getElementsByTagName("table");
for (int i = 0; i < tables.getLength(); i++) {
Element e = (Element) tables.item(i);
if ("downloadTable".equals(e.getAttribute("class"))) {
NodeList tr = e.getElementsByTagName("tr");
for (int k = 0; k < tr.getLength(); k++) {
NodeList downloadLine = ((Element) tr.item(k)).getElementsByTagName("td");
String url = null, version = null, gameVersion = null;
for (int j = 0; j < downloadLine.getLength(); j++) {
Element td = (Element) downloadLine.item(j);
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineMirror"))
url = ((Element) td.getElementsByTagName("a").item(0)).getAttribute("href");
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile"))
version = td.getTextContent();
}
if (version == null || url == null)
continue;
Matcher matcher = PATTERN.matcher(version);
while (matcher.find())
gameVersion = matcher.group(1);
if (gameVersion == null)
continue;
String finalURL = url;
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, Lang.hideException(() -> getLink(finalURL))));
}
}
}
} finally {
lock.writeLock().unlock();
}
}
};
}
private static String getLink(String url) throws IOException {
String result = null;
String content = NetworkUtils.doGet(NetworkUtils.toURL(url));
Matcher m = LINK_PATTERN.matcher(content);
while (m.find())
result = m.group(1);
if (result == null)
throw new IllegalStateException("Cannot find version in " + content);
return "https://optifine.net/downloadx?f=OptiFine" + result;
}
}
@@ -45,12 +45,4 @@ public class ProcessStoppedEvent extends Event {
public ManagedProcess getProcess() {
return process;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("source", source)
.append("process", process)
.toString();
}
}
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more