Launching 1.13

This commit is contained in:
huangyuhui 2017-11-03 13:38:38 +08:00
parent 4bde509708
commit d9225b9529
9 changed files with 252 additions and 40 deletions

View File

@ -0,0 +1,48 @@
/*
* 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.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<String, String>, features: Map<String, Boolean>): List<String>
companion object Serializer : JsonDeserializer<Argument>, JsonSerializer<Argument> {
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")
}
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.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<Argument>? = null,
@SerializedName("jvm")
val jvm: List<Argument>? = null
) {
companion object {
fun parseStringArguments(arguments: List<String>, keys: Map<String, String>, features: Map<String, Boolean> = emptyMap()): List<String> {
return arguments.flatMap { StringArgument(it).toString(keys, features) }
}
fun parseArguments(arguments: List<Argument>, keys: Map<String, String>, features: Map<String, Boolean> = emptyMap()): List<String> {
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}"))
)
}
}

View File

@ -26,18 +26,28 @@ import java.util.regex.Pattern
@Immutable @Immutable
data class CompatibilityRule( data class CompatibilityRule(
val action: Action = CompatibilityRule.Action.ALLOW, val action: Action = CompatibilityRule.Action.ALLOW,
val os: OSRestriction? = null val os: OSRestriction? = null,
val features: Map<String, Boolean>? = null
) { ) {
val appliedAction: Action? get() = if (os != null && !os.allow()) null else action fun getAppliedAction(supportedFeatures: Map<String, Boolean>): 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 { companion object {
fun appliesToCurrentEnvironment(rules: Collection<CompatibilityRule>?): Boolean { fun appliesToCurrentEnvironment(rules: Collection<CompatibilityRule>?, features: Map<String, Boolean> = emptyMap()): Boolean {
if (rules == null) if (rules == null)
return true return true
var action = CompatibilityRule.Action.DISALLOW var action = CompatibilityRule.Action.DISALLOW
for (rule in rules) { for (rule in rules) {
val thisAction = rule.appliedAction val thisAction = rule.getAppliedAction(features)
if (thisAction != null) action = thisAction if (thisAction != null) action = thisAction
} }
return action == CompatibilityRule.Action.ALLOW return action == CompatibilityRule.Action.ALLOW

View File

@ -0,0 +1,54 @@
/*
* 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.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<CompatibilityRule>? = null,
val value: List<String>? = null) : Argument {
override fun toString(keys: Map<String, String>, features: Map<String, Boolean>): List<String> =
if (CompatibilityRule.appliesToCurrentEnvironment(rules))
value?.map { StringArgument(it).toString(keys, features).single() } ?: emptyList()
else
emptyList()
companion object Serializer : JsonSerializer<RuledArgument>, JsonDeserializer<RuledArgument> {
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<List<CompatibilityRule>>()),
value = if (obj["value"].isJsonPrimitive)
listOf(obj["value"].asString)
else
context.deserialize(obj["value"], typeOf<List<String>>())
)
}
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.game
import java.util.*
import java.util.regex.Pattern
class StringArgument(var argument: String) : Argument {
override fun toString(keys: Map<String, String>, features: Map<String, Boolean>): List<String> {
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)
}
}

View File

@ -26,6 +26,8 @@ import java.util.*
open class Version( open class Version(
@SerializedName("minecraftArguments") @SerializedName("minecraftArguments")
val minecraftArguments: String? = null, val minecraftArguments: String? = null,
@SerializedName("arguments")
val arguments: Arguments? = null,
@SerializedName("mainClass") @SerializedName("mainClass")
val mainClass: String? = null, val mainClass: String? = null,
@SerializedName("time") @SerializedName("time")
@ -138,6 +140,7 @@ open class Version(
fun copy( fun copy(
minecraftArguments: String? = this.minecraftArguments, minecraftArguments: String? = this.minecraftArguments,
arguments: Arguments? = this.arguments,
mainClass: String? = this.mainClass, mainClass: String? = this.mainClass,
time: Date = this.time, time: Date = this.time,
releaseTime: Date = this.releaseTime, releaseTime: Date = this.releaseTime,
@ -153,6 +156,7 @@ open class Version(
downloads: Map<DownloadType, DownloadInfo>? = this.downloads, downloads: Map<DownloadType, DownloadInfo>? = this.downloads,
logging: Map<DownloadType, LoggingInfo>? = this.logging) = logging: Map<DownloadType, LoggingInfo>? = this.logging) =
Version(minecraftArguments, Version(minecraftArguments,
arguments,
mainClass, mainClass,
time, time,
id, id,

View File

@ -18,10 +18,7 @@
package org.jackhuang.hmcl.launch package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.auth.AuthInfo import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.DownloadType import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.game.GameException
import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.task.TaskResult import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.util.* import org.jackhuang.hmcl.util.*
import java.io.File 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") 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 * 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) if (OS.CURRENT_OS != OS.WINDOWS)
res.add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump")
else
res.add("-Duser.home=${options.gameDir.parent}") res.add("-Duser.home=${options.gameDir.parent}")
if (options.java.version >= JavaVersion.JAVA_7) if (options.java.version >= JavaVersion.JAVA_7)
@ -114,9 +112,6 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
res.add("-Dfml.ignorePatchDiscrepancies=true") res.add("-Dfml.ignorePatchDiscrepancies=true")
} }
// Classpath
res.add("-Djava.library.path=${native.absolutePath}")
val lateload = LinkedList<File>() val lateload = LinkedList<File>()
val classpath = StringBuilder() val classpath = StringBuilder()
for (library in version.libraries) for (library in version.libraries)
@ -135,32 +130,21 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
if (!jar.exists() || !jar.isFile) if (!jar.exists() || !jar.isFile)
throw GameException("Minecraft jar does not exist") throw GameException("Minecraft jar does not exist")
classpath.append(jar.absolutePath) classpath.append(jar.absolutePath)
res.add("-cp")
res.add(classpath.toString())
// Main Class
res.add(version.mainClass!!)
// Provided Minecraft arguments // Provided Minecraft arguments
val gameAssets = repository.getActualAssetDirectory(version.id, version.actualAssetIndex.id) 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.addAll(Arguments.parseArguments(version.arguments?.jvm ?: defaultJVMArguments, configuration))
res.add(line res.add(version.mainClass!!)
.replace("\${auth_player_name}", account.username)
.replace("\${auth_session}", account.authToken) val features = getFeatures()
.replace("\${auth_access_token}", account.authToken) res.addAll(Arguments.parseArguments(version.arguments?.game ?: defaultGameArguments, configuration, features))
.replace("\${auth_uuid}", account.userId) res.addAll(Arguments.parseStringArguments(version.minecraftArguments?.tokenize()?.toList() ?: emptyList(), configuration))
.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)
)
}
// Optional Minecraft arguments // Optional Minecraft arguments
if (options.height != null && options.height != 0 && options.width != null && options.width != 0) { 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<String, String> = 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<String, Boolean> = mutableMapOf(
"has_custom_resolution" to (options.height != null && options.height != 0 && options.width != null && options.width != 0)
)
override fun launch(): ManagedProcess { override fun launch(): ManagedProcess {
// To guarantee that when failed to generate code, we will not call precalled command // 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()) { if (options.precalledCommand != null && options.precalledCommand.isNotBlank()) {
try { try {
val process = Runtime.getRuntime().exec(options.precalledCommand) val process = Runtime.getRuntime().exec(options.precalledCommand)
if (process.isAlive) if (process.isAlive)
process.waitFor() process.waitFor()
} catch (e: IOException) { } catch (e: IOException) {
// TODO: alert precalledCommand is wrong. // TODO: alert precalledCommand is wrong.
// rethrow InterruptedException // rethrow InterruptedException
@ -301,7 +303,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
processListener.setProcess(managedProcess) processListener.setProcess(managedProcess)
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); managedProcess.lines += line }.apply { start() } val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); managedProcess.lines += line }.apply { start() }
managedProcess.relatedThreads += logHandler 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 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) 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 managedProcess.relatedThreads += stderr

View File

@ -22,7 +22,9 @@ import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import org.jackhuang.hmcl.game.Argument
import org.jackhuang.hmcl.game.Library import org.jackhuang.hmcl.game.Library
import org.jackhuang.hmcl.game.RuledArgument
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.lang.reflect.Type import java.lang.reflect.Type
@ -36,6 +38,8 @@ val GSON: Gson = GsonBuilder()
.enableComplexMapKeySerialization() .enableComplexMapKeySerialization()
.setPrettyPrinting() .setPrettyPrinting()
.registerTypeAdapter(Library::class.java, Library) .registerTypeAdapter(Library::class.java, Library)
.registerTypeAdapter(Argument::class.java, Argument)
.registerTypeAdapter(RuledArgument::class.java, RuledArgument)
.registerTypeAdapter(Date::class.java, DateTypeAdapter) .registerTypeAdapter(Date::class.java, DateTypeAdapter)
.registerTypeAdapter(UUID::class.java, UUIDTypeAdapter) .registerTypeAdapter(UUID::class.java, UUIDTypeAdapter)
.registerTypeAdapter(Platform::class.java, Platform) .registerTypeAdapter(Platform::class.java, Platform)

View File

@ -20,7 +20,7 @@ group 'org.jackhuang'
version '3.0' version '3.0'
buildscript { buildscript {
ext.kotlin_version = '1.1.4-2' ext.kotlin_version = '1.1.4-4'
repositories { repositories {
mavenCentral() mavenCentral()