diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Argument.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Argument.kt new file mode 100644 index 000000000..f761adb9c --- /dev/null +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Argument.kt @@ -0,0 +1,48 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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.game + +import com.google.gson.* +import java.lang.reflect.Type + +interface Argument { + + /** + * Parse this argument in form: ${key name} or simply a string. + * + * @param keys the parse map + * @param features the map that contains some features such as 'is_demo_user', 'has_custom_resolution' + * @return parsed argument element, empty if this argument is ignored and will not be added. + */ + fun toString(keys: Map, features: Map): List + + companion object Serializer : JsonDeserializer, JsonSerializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Argument = + if (json.isJsonPrimitive) + StringArgument(json.asString) + else + context.deserialize(json, RuledArgument::class.java) + + override fun serialize(src: Argument, typeOfSrc: Type, context: JsonSerializationContext): JsonElement = + when (src) { + is StringArgument -> JsonPrimitive(src.argument) + is RuledArgument -> context.serialize(src, RuledArgument::class.java) + else -> throw AssertionError("Unrecognized argument type: $src") + } + } +} \ No newline at end of file diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Arguments.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Arguments.kt new file mode 100644 index 000000000..b8d726ded --- /dev/null +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Arguments.kt @@ -0,0 +1,54 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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.game + +import com.google.gson.annotations.SerializedName +import org.jackhuang.hmcl.util.OS +import org.jackhuang.hmcl.game.CompatibilityRule.* + +class Arguments @JvmOverloads constructor( + @SerializedName("game") + val game: List? = null, + @SerializedName("jvm") + val jvm: List? = null +) { + companion object { + fun parseStringArguments(arguments: List, keys: Map, features: Map = emptyMap()): List { + return arguments.flatMap { StringArgument(it).toString(keys, features) } + } + + fun parseArguments(arguments: List, keys: Map, features: Map = emptyMap()): List { + return arguments.flatMap { it.toString(keys, features) } + } + + val DEFAULT_JVM_ARGUMENTS = listOf( + RuledArgument(listOf(CompatibilityRule(Action.ALLOW, OSRestriction(OS.OSX))), listOf("-XstartOnFirstThread")), + RuledArgument(listOf(CompatibilityRule(Action.ALLOW, OSRestriction(OS.WINDOWS))), listOf("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump")), + RuledArgument(listOf(CompatibilityRule(Action.ALLOW, OSRestriction(OS.WINDOWS, "^10\\."))), listOf("-Dos.name=Windows 10", "-Dos.version=10.0")), + StringArgument("-Djava.library.path=\${natives_directory}"), + StringArgument("-Dminecraft.launcher.brand=\${launcher_name}"), + StringArgument("-Dminecraft.launcher.version=\${launcher_version}"), + StringArgument("-cp"), + StringArgument("\${classpath}") + ) + + val DEFAULT_GAME_ARGUMENTS = listOf( + RuledArgument(listOf(CompatibilityRule(Action.ALLOW, features = mapOf("has_custom_resolution" to true))), listOf("--width", "\${resolution_width}", "--height", "\${resolution_height}")) + ) + } +} \ No newline at end of file diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/CompatibilityRule.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/CompatibilityRule.kt index 46f930496..0362f2cde 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/CompatibilityRule.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/CompatibilityRule.kt @@ -26,18 +26,28 @@ import java.util.regex.Pattern @Immutable data class CompatibilityRule( val action: Action = CompatibilityRule.Action.ALLOW, - val os: OSRestriction? = null + val os: OSRestriction? = null, + val features: Map? = null ) { - val appliedAction: Action? get() = if (os != null && !os.allow()) null else action + fun getAppliedAction(supportedFeatures: Map): Action? { + if (os != null && !os.allow()) return null + if (features != null) { + features.entries.forEach { + if (supportedFeatures[it.key] != it.value) + return null + } + } + return action + } companion object { - fun appliesToCurrentEnvironment(rules: Collection?): Boolean { + fun appliesToCurrentEnvironment(rules: Collection?, features: Map = emptyMap()): Boolean { if (rules == null) return true var action = CompatibilityRule.Action.DISALLOW for (rule in rules) { - val thisAction = rule.appliedAction + val thisAction = rule.getAppliedAction(features) if (thisAction != null) action = thisAction } return action == CompatibilityRule.Action.ALLOW diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/RuledArgument.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/RuledArgument.kt new file mode 100644 index 000000000..bb3b12806 --- /dev/null +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/RuledArgument.kt @@ -0,0 +1,54 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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.game + +import com.google.gson.* +import com.google.gson.reflect.TypeToken +import org.jackhuang.hmcl.util.typeOf +import java.lang.reflect.Type + + +class RuledArgument @JvmOverloads constructor( + val rules: List? = null, + val value: List? = null) : Argument { + + override fun toString(keys: Map, features: Map): List = + if (CompatibilityRule.appliesToCurrentEnvironment(rules)) + value?.map { StringArgument(it).toString(keys, features).single() } ?: emptyList() + else + emptyList() + + companion object Serializer : JsonSerializer, JsonDeserializer { + override fun serialize(src: RuledArgument, typeOfSrc: Type, context: JsonSerializationContext) = + JsonObject().apply { + add("rules", context.serialize(src.rules)) + add("value", context.serialize(src.value)) + } + + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): RuledArgument { + val obj = json.asJsonObject + return RuledArgument( + rules = context.deserialize(obj["rules"], typeOf>()), + value = if (obj["value"].isJsonPrimitive) + listOf(obj["value"].asString) + else + context.deserialize(obj["value"], typeOf>()) + ) + } + } +} \ No newline at end of file diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/StringArgument.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/StringArgument.kt new file mode 100644 index 000000000..c2a7c0a3d --- /dev/null +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/StringArgument.kt @@ -0,0 +1,36 @@ +/* + * Hello Minecraft! Launcher. + * Copyright (C) 2017 huangyuhui + * + * 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.game + +import java.util.* +import java.util.regex.Pattern + +class StringArgument(var argument: String) : Argument { + + override fun toString(keys: Map, features: Map): List { + var res = argument + val pattern = Pattern.compile("\\$\\{(.*?)\\}") + val m = pattern.matcher(argument) + while (m.find()) { + val entry = m.group() + res = res.replace(entry, keys.getOrDefault(entry, entry)) + } + return Collections.singletonList(res) + } + +} \ No newline at end of file diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Version.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Version.kt index 427d10d5b..66a1578ca 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Version.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/game/Version.kt @@ -26,6 +26,8 @@ import java.util.* open class Version( @SerializedName("minecraftArguments") val minecraftArguments: String? = null, + @SerializedName("arguments") + val arguments: Arguments? = null, @SerializedName("mainClass") val mainClass: String? = null, @SerializedName("time") @@ -138,6 +140,7 @@ open class Version( fun copy( minecraftArguments: String? = this.minecraftArguments, + arguments: Arguments? = this.arguments, mainClass: String? = this.mainClass, time: Date = this.time, releaseTime: Date = this.releaseTime, @@ -153,6 +156,7 @@ open class Version( downloads: Map? = this.downloads, logging: Map? = this.logging) = Version(minecraftArguments, + arguments, mainClass, time, id, diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/launch/DefaultLauncher.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/launch/DefaultLauncher.kt index 0e10f8200..a84137f16 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/launch/DefaultLauncher.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/launch/DefaultLauncher.kt @@ -18,10 +18,7 @@ package org.jackhuang.hmcl.launch import org.jackhuang.hmcl.auth.AuthInfo -import org.jackhuang.hmcl.game.DownloadType -import org.jackhuang.hmcl.game.GameException -import org.jackhuang.hmcl.game.GameRepository -import org.jackhuang.hmcl.game.LaunchOptions +import org.jackhuang.hmcl.game.* import org.jackhuang.hmcl.task.TaskResult import org.jackhuang.hmcl.util.* import java.io.File @@ -48,6 +45,9 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun throw NullPointerException("Version main class can not be null") } + protected open val defaultJVMArguments = Arguments.DEFAULT_JVM_ARGUMENTS + protected open val defaultGameArguments = Arguments.DEFAULT_GAME_ARGUMENTS + /** * Note: the [account] must have logged in when calling this property */ @@ -84,9 +84,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun } } - if (OS.CURRENT_OS == OS.WINDOWS) - res.add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump") - else + if (OS.CURRENT_OS != OS.WINDOWS) res.add("-Duser.home=${options.gameDir.parent}") if (options.java.version >= JavaVersion.JAVA_7) @@ -114,9 +112,6 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun res.add("-Dfml.ignorePatchDiscrepancies=true") } - // Classpath - res.add("-Djava.library.path=${native.absolutePath}") - val lateload = LinkedList() val classpath = StringBuilder() for (library in version.libraries) @@ -135,32 +130,21 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun if (!jar.exists() || !jar.isFile) throw GameException("Minecraft jar does not exist") classpath.append(jar.absolutePath) - res.add("-cp") - res.add(classpath.toString()) - - // Main Class - res.add(version.mainClass!!) // Provided Minecraft arguments val gameAssets = repository.getActualAssetDirectory(version.id, version.actualAssetIndex.id) + val configuration = getConfigurations() + configuration["\${classpath}"] = classpath.toString() + configuration["\${natives_directory}"] = native.absolutePath + configuration["\${game_assets}"] = gameAssets.absolutePath + configuration["\${assets_root}"] = gameAssets.absolutePath - version.minecraftArguments!!.tokenize().forEach { line -> - res.add(line - .replace("\${auth_player_name}", account.username) - .replace("\${auth_session}", account.authToken) - .replace("\${auth_access_token}", account.authToken) - .replace("\${auth_uuid}", account.userId) - .replace("\${version_name}", options.versionName ?: version.id) - .replace("\${profile_name}", options.profileName ?: "Minecraft") - .replace("\${version_type}", version.type.id) - .replace("\${game_directory}", repository.getRunDirectory(version.id).absolutePath) - .replace("\${game_assets}", gameAssets.absolutePath) - .replace("\${assets_root}", gameAssets.absolutePath) - .replace("\${user_type}", account.userType.toString().toLowerCase()) - .replace("\${assets_index_name}", version.actualAssetIndex.id) - .replace("\${user_properties}", account.userProperties) - ) - } + res.addAll(Arguments.parseArguments(version.arguments?.jvm ?: defaultJVMArguments, configuration)) + res.add(version.mainClass!!) + + val features = getFeatures() + res.addAll(Arguments.parseArguments(version.arguments?.game ?: defaultGameArguments, configuration, features)) + res.addAll(Arguments.parseStringArguments(version.minecraftArguments?.tokenize()?.toList() ?: emptyList(), configuration)) // Optional Minecraft arguments if (options.height != null && options.height != 0 && options.width != null && options.width != 0) { @@ -222,6 +206,24 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun } } + open fun getConfigurations(): MutableMap = mutableMapOf( + "\${auth_player_name}" to account.username, + "\${auth_session}" to account.authToken, + "\${auth_access_token}" to account.authToken, + "\${auth_uuid}" to account.userId, + "\${version_name}" to (options.versionName ?: version.id), + "\${profile_name}" to (options.profileName ?: "Minecraft"), + "\${version_type}" to version.type.id, + "\${game_directory}" to repository.getRunDirectory(version.id).absolutePath, + "\${user_type}" to account.userType.toString().toLowerCase(), + "\${assets_index_name}" to version.actualAssetIndex.id, + "\${user_properties}" to account.userProperties + ) + + open fun getFeatures(): MutableMap = mutableMapOf( + "has_custom_resolution" to (options.height != null && options.height != 0 && options.width != null && options.width != 0) + ) + override fun launch(): ManagedProcess { // To guarantee that when failed to generate code, we will not call precalled command @@ -232,8 +234,8 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun if (options.precalledCommand != null && options.precalledCommand.isNotBlank()) { try { val process = Runtime.getRuntime().exec(options.precalledCommand) - if (process.isAlive) - process.waitFor() + if (process.isAlive) + process.waitFor() } catch (e: IOException) { // TODO: alert precalledCommand is wrong. // rethrow InterruptedException @@ -301,7 +303,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun processListener.setProcess(managedProcess) val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); managedProcess.lines += line }.apply { start() } managedProcess.relatedThreads += logHandler - val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(managedProcess.process.inputStream, { logHandler.newLine(it) } )::run) + val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(managedProcess.process.inputStream, { logHandler.newLine(it) })::run) managedProcess.relatedThreads += stdout val stderr = thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(managedProcess.process.errorStream, { processListener.onLog(it + OS.LINE_SEPARATOR, Log4jLevel.ERROR); managedProcess.lines += it })::run) managedProcess.relatedThreads += stderr diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/Gson.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/Gson.kt index 826a5102c..d4b6838ad 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/Gson.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/Gson.kt @@ -22,7 +22,9 @@ import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import org.jackhuang.hmcl.game.Argument import org.jackhuang.hmcl.game.Library +import org.jackhuang.hmcl.game.RuledArgument import java.io.File import java.io.IOException import java.lang.reflect.Type @@ -36,6 +38,8 @@ val GSON: Gson = GsonBuilder() .enableComplexMapKeySerialization() .setPrettyPrinting() .registerTypeAdapter(Library::class.java, Library) + .registerTypeAdapter(Argument::class.java, Argument) + .registerTypeAdapter(RuledArgument::class.java, RuledArgument) .registerTypeAdapter(Date::class.java, DateTypeAdapter) .registerTypeAdapter(UUID::class.java, UUIDTypeAdapter) .registerTypeAdapter(Platform::class.java, Platform) diff --git a/build.gradle b/build.gradle index 972878c78..c9bf4070d 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ group 'org.jackhuang' version '3.0' buildscript { - ext.kotlin_version = '1.1.4-2' + ext.kotlin_version = '1.1.4-4' repositories { mavenCentral()