This commit is contained in:
huangyuhui
2017-08-01 18:10:36 +08:00
parent 6dc2b36d14
commit 1410b3b5b1
236 changed files with 18466 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
/*
* 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.auth
import java.net.Proxy
abstract class Account() {
abstract val username: String
@Throws(AuthenticationException::class)
abstract fun logIn(proxy: Proxy = Proxy.NO_PROXY): AuthInfo
abstract fun logOut()
abstract fun toStorage(): Map<out Any, Any>
}

View File

@@ -0,0 +1,23 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.auth
interface AccountFactory<T : Account> {
fun fromUsername(username: String, password: String = ""): T
fun fromStorage(storage: Map<Any, Any>): T
}

View File

@@ -0,0 +1,30 @@
/*
* 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.auth
import org.jackhuang.hmcl.util.Immutable
@Immutable
data class AuthInfo(
val username: String,
val userId: String,
val authToken: String,
val userType: UserType = UserType.LEGACY,
val userProperties: String = "{}",
val userPropertyMap: String = "{}"
)

View File

@@ -0,0 +1,24 @@
/*
* 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.auth
class AuthenticationException : Exception {
constructor() : super() {}
constructor(message: String) : super(message) {}
constructor(message: String, cause: Throwable) : super(message, cause) {}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.auth
import org.jackhuang.hmcl.util.DigestUtils
import java.net.Proxy
class OfflineAccount private constructor(val uuid: String, override val username: String): Account() {
override fun logIn(proxy: Proxy): AuthInfo {
if (username.isBlank() || uuid.isBlank())
throw AuthenticationException("Username cannot be empty")
return AuthInfo(
username = username,
userId = uuid,
authToken = uuid
)
}
override fun logOut() {
// Offline account need not log out.
}
override fun toStorage(): Map<Any, Any> {
return mapOf(
"uuid" to uuid,
"username" to username
)
}
companion object OfflineAccountFactory : AccountFactory<OfflineAccount> {
override fun fromUsername(username: String, password: String): OfflineAccount {
return OfflineAccount(
username = username,
uuid = getUUIDFromUserName(username)
)
}
override fun fromStorage(storage: Map<Any, Any>): OfflineAccount {
val username = storage["username"] as? String
?: throw IllegalStateException("Configuration is malformed.")
val obj = storage["uuid"]
return OfflineAccount(
username = username,
uuid = obj as? String ?: getUUIDFromUserName(username)
)
}
private fun getUUIDFromUserName(username: String) = DigestUtils.md5Hex(username)
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.auth
enum class UserType() {
LEGACY,
MOJANG;
companion object {
fun fromName(name: String) = BY_NAME[name.toLowerCase()]
fun fromLegacy(isLegacy: Boolean) = if (isLegacy) LEGACY else MOJANG
private val BY_NAME = HashMap<String, UserType>().apply {
for (type in values())
this[type.name.toLowerCase()] = type
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.auth.yggdrasil
data class AuthenticationRequest(
val username: String,
val password: String,
val clientToken: String,
val agent: Map<String, Any> = mapOf(
"name" to "Minecraft",
"version" to 1
),
val requestUser: Boolean = true
) {
}

View File

@@ -0,0 +1,51 @@
/*
* 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.auth.yggdrasil
import com.google.gson.*
import java.lang.reflect.Type
import com.google.gson.JsonObject
import java.util.UUID
import com.google.gson.JsonParseException
data class GameProfile(
val id: UUID? = null,
val name: String? = null,
val properties: PropertyMap = PropertyMap(),
val legacy: Boolean = false
) {
companion object GameProfileSerializer: JsonSerializer<GameProfile>, JsonDeserializer<GameProfile> {
override fun serialize(src: GameProfile, typeOfSrc: Type?, context: JsonSerializationContext): JsonElement {
val result = JsonObject()
if (src.id != null)
result.add("id", context.serialize(src.id))
if (src.name != null)
result.addProperty("name", src.name)
return result
}
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext): GameProfile {
if (json !is JsonObject)
throw JsonParseException("The json element is not a JsonObject.")
val id = if (json.has("id")) context.deserialize<UUID>(json.get("id"), UUID::class.java) else null
val name = if (json.has("name")) json.getAsJsonPrimitive("name").asString else null
return GameProfile(id, name)
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.auth.yggdrasil
data class Property(val name: String, val value: String)

View File

@@ -0,0 +1,90 @@
/*
* 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.auth.yggdrasil
import com.google.gson.*
import java.lang.reflect.Type
import java.util.*
import com.google.gson.JsonObject
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import kotlin.collections.HashMap
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
class PropertyMap: HashMap<String, Property>() {
fun toList(): List<Map<String, String>> {
val properties = LinkedList<Map<String, String>>()
values.forEach { (name, value) ->
properties += mapOf(
"name" to name,
"value" to value
)
}
return properties
}
/**
* Load property map from list.
* @param list Right type is List<Map<String, String>>. Using List<*> here because of fault tolerance
*/
fun fromList(list: List<*>) {
list.forEach { propertyMap ->
if (propertyMap is Map<*, *>) {
val name = propertyMap["name"] as? String
val value = propertyMap["value"] as? String
if (name != null && value != null)
put(name, Property(name, value))
}
}
}
companion object Serializer : JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> {
override fun serialize(src: PropertyMap, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
val result = JsonArray()
for ((name, value) in src.values)
result.add(JsonObject().apply {
addProperty("name", name)
addProperty("value", value)
})
return result
}
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): PropertyMap {
val result = PropertyMap()
if (json is JsonObject) {
for ((key, value) in json.entrySet())
if (value is JsonArray)
for (element in value)
result.put(key, Property(key, element.asString))
} else if (json is JsonArray)
for (element in json)
if (element is JsonObject) {
val name = element.getAsJsonPrimitive("name").asString
val value = element.getAsJsonPrimitive("value").asString
result.put(name, Property(name, value))
}
return result
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.auth.yggdrasil
data class RefreshRequest(
val clientToken: String,
val accessToken: String,
val selectedProfile: GameProfile?,
val requestUser: Boolean = true
)

View File

@@ -0,0 +1,29 @@
/*
* 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.auth.yggdrasil
class Response (
val accessToken: String? = null,
val clientToken: String? = null,
val selectedProfile: GameProfile? = null,
val availableProfiles: Array<GameProfile>? = null,
val user: User? = null,
val error: String? = null,
val errorMessage: String? = null,
val cause: String? = null
)

View File

@@ -0,0 +1,31 @@
/*
* 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.auth.yggdrasil
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.util.Validation
class User (
val id: String = "",
val properties: PropertyMap? = null
) : Validation {
override fun validate() {
if (id.isBlank())
throw JsonParseException("User id cannot be empty")
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.auth.yggdrasil
data class ValidateRequest (
val clientToken: String = "",
val accessToken: String = ""
) {
}

View File

@@ -0,0 +1,242 @@
/*
* 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.auth.yggdrasil
import com.google.gson.GsonBuilder
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.auth.*
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.doGet
import org.jackhuang.hmcl.util.doPost
import org.jackhuang.hmcl.util.toURL
import java.io.IOException
import java.net.Proxy
import java.net.URL
import java.util.*
class YggdrasilAccount private constructor(override val username: String): Account() {
private var password: String? = null
private var userId: String? = null
private var accessToken: String? = null
private var clientToken: String = UUID.randomUUID().toString()
private var isOnline: Boolean = false
private var userProperties = PropertyMap()
private var selectedProfile: GameProfile? = null
private var profiles: Array<GameProfile>? = null
private var userType: UserType = UserType.LEGACY
init {
if (username.isBlank())
throw IllegalArgumentException("Username cannot be blank")
if (!username.contains("@"))
throw IllegalArgumentException("Yggdrasil account user name must be email")
}
val isLoggedIn: Boolean
get() = isNotBlank(accessToken)
val canPlayOnline: Boolean
get() = isLoggedIn && selectedProfile != null && isOnline
val canLogIn: Boolean
get() = !canPlayOnline && username.isNotBlank() && (isNotBlank(password) || isNotBlank((accessToken)))
override fun logIn(proxy: Proxy): AuthInfo {
if (canPlayOnline)
return AuthInfo(
username = selectedProfile!!.name!!,
userId = UUIDTypeAdapter.fromUUID(selectedProfile!!.id!!),
authToken = accessToken!!,
userType = userType,
userProperties = GSON.toJson(userProperties)
)
else {
logIn0(proxy)
if (!isLoggedIn)
throw AuthenticationException("Wrong password for account $username")
if (selectedProfile == null) {
// TODO: multi-available-profiles support
throw UnsupportedOperationException("Do not support multi-available-profiles account yet.")
} else {
return AuthInfo(
username = selectedProfile!!.name!!,
userId = UUIDTypeAdapter.fromUUID(selectedProfile!!.id!!),
authToken = accessToken!!,
userType = userType,
userProperties = GSON.toJson(userProperties)
)
}
}
}
private fun logIn0(proxy: Proxy) {
if (isNotBlank(accessToken)) {
if (isBlank(userId))
if (isNotBlank(username))
userId = username
else
throw AuthenticationException("Invalid uuid and username")
if (checkTokenValidity(proxy)) {
isOnline = true
return
}
logIn1(ROUTE_REFRESH, RefreshRequest(accessToken!!, clientToken, selectedProfile), proxy)
} else if (isNotBlank(password)) {
logIn1(ROUTE_AUTHENTICATE, AuthenticationRequest(username, password!!, clientToken), proxy)
} else
throw AuthenticationException("Password cannot be blank")
}
private fun logIn1(url: URL, input: Any, proxy: Proxy) {
val response = makeRequest(url, input, proxy)
if (clientToken != response?.clientToken)
throw AuthenticationException("Client token changed")
if (response.selectedProfile != null)
userType = UserType.fromLegacy(response.selectedProfile.legacy)
else if (response.availableProfiles?.getOrNull(0) != null)
userType = UserType.fromLegacy(response.availableProfiles[0].legacy)
val user = response.user
userId = user?.id ?: username
isOnline = true
profiles = response.availableProfiles
selectedProfile = response.selectedProfile
userProperties.clear()
accessToken = response.accessToken
if (user != null && user.properties != null)
userProperties.putAll(user.properties)
}
override fun logOut() {
password = null
userId = null
accessToken = null
isOnline = false
userProperties.clear()
profiles = null
selectedProfile = null
}
override fun toStorage(): Map<out Any, Any> {
val result = HashMap<String, Any>()
result[STORAGE_KEY_USER_NAME] = username
if (userId != null)
result[STORAGE_KEY_USER_ID] = userId!!
if (!userProperties.isEmpty())
result[STORAGE_KEY_USER_PROPERTIES] = userProperties.toList()
val profile = selectedProfile
if (profile != null && profile.name != null && profile.id != null) {
result[STORAGE_KEY_PROFILE_NAME] = profile.name
result[STORAGE_KEY_PROFILE_ID] = profile.id
if (!profile.properties.isEmpty())
result[STORAGE_KEY_PROFILE_PROPERTIES] = profile.properties.toList()
}
if (accessToken?.isNotBlank() ?: false)
result[STORAGE_KEY_ACCESS_TOKEN] = accessToken!!
return result
}
private fun makeRequest(url: URL, input: Any?, proxy: Proxy): Response? {
try {
val jsonResult =
if (input == null) url.doGet(proxy)
else url.doPost(GSON.toJson(input), "application/json", proxy)
val response = GSON.fromJson<Response>(jsonResult) ?: return null
if (response.error?.isNotBlank() ?: false) {
LOG.severe("Failed to log in, the auth server returned an error: " + response.error + ", message: " + response.errorMessage + ", cause: " + response.cause)
throw AuthenticationException("Request error: ${response.errorMessage}")
}
return response
} catch (e: IOException) {
throw AuthenticationException("Unable to connect to authentication server", e)
} catch (e: JsonParseException) {
throw AuthenticationException("Unable to parse server response", e)
}
}
private fun checkTokenValidity(proxy: Proxy): Boolean {
val access = accessToken
try {
if (access == null)
return false
makeRequest(ROUTE_VALIDATE, ValidateRequest(clientToken, access), proxy)
return true
} catch (e: AuthenticationException) {
return false
}
}
companion object YggdrasilAccountFactory : AccountFactory<YggdrasilAccount> {
private val GSON = GsonBuilder()
.registerTypeAdapter(GameProfile::class.java, GameProfile)
.registerTypeAdapter(PropertyMap::class.java, PropertyMap)
.registerTypeAdapter(UUID::class.java, UUIDTypeAdapter)
.create()
private val BASE_URL = "https://authserver.mojang.com/"
private val ROUTE_AUTHENTICATE = (BASE_URL + "authenticate").toURL()
private val ROUTE_REFRESH = (BASE_URL + "refresh").toURL()
private val ROUTE_VALIDATE = (BASE_URL + "validate").toURL()
private val STORAGE_KEY_ACCESS_TOKEN = "accessToken"
private val STORAGE_KEY_PROFILE_NAME = "displayName"
private val STORAGE_KEY_PROFILE_ID = "uuid"
private val STORAGE_KEY_PROFILE_PROPERTIES = "profileProperties"
private val STORAGE_KEY_USER_NAME = "username"
private val STORAGE_KEY_USER_ID = "userid"
private val STORAGE_KEY_USER_PROPERTIES = "userProperties"
override fun fromUsername(username: String, password: String): YggdrasilAccount {
val account = YggdrasilAccount(username)
account.password = password
return account
}
override fun fromStorage(storage: Map<Any, Any>): YggdrasilAccount {
val username = storage[STORAGE_KEY_USER_NAME] as? String ?: throw IllegalArgumentException("storage does not have key $STORAGE_KEY_USER_NAME")
val account = YggdrasilAccount(username)
account.userId = storage[STORAGE_KEY_USER_ID] as? String ?: username
account.accessToken = storage[STORAGE_KEY_ACCESS_TOKEN] as? String
val userProperties = storage[STORAGE_KEY_USER_PROPERTIES] as? List<*>
if (userProperties != null)
account.userProperties.fromList(userProperties)
val profileId = storage[STORAGE_KEY_PROFILE_ID] as? String
val profileName = storage[STORAGE_KEY_PROFILE_NAME] as? String
val profile: GameProfile?
if (profileId != null && profileName != null) {
profile = GameProfile(UUIDTypeAdapter.fromString(profileId), profileName)
val profileProperties = storage[STORAGE_KEY_PROFILE_PROPERTIES] as? List<*>
if (profileProperties != null)
profile.properties.fromList(profileProperties)
} else
profile = null
account.selectedProfile = profile
return account
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.*
abstract class AbstractDependencyManager(repository: GameRepository)
: DependencyManager(repository) {
abstract val downloadProvider: DownloadProvider
fun getVersions(id: String, selfVersion: String) =
downloadProvider.getVersionListById(id).getVersions(selfVersion)
override fun getVersionList(id: String): VersionList<*> {
return downloadProvider.getVersionListById(id)
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.download.forge.ForgeVersionList
import org.jackhuang.hmcl.download.game.GameVersionList
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import org.jackhuang.hmcl.download.optifine.OptiFineBMCLVersionList
object BMCLAPIDownloadProvider : DownloadProvider() {
override val libraryBaseURL: String = "http://bmclapi2.bangbang93.com/libraries/"
override val versionListURL: String = "http://bmclapi2.bangbang93.com/mc/game/version_manifest.json"
override val versionBaseURL: String = "http://bmclapi2.bangbang93.com/versions/"
override val assetIndexBaseURL: String = "http://bmclapi2.bangbang93.com/indexes/"
override val assetBaseURL: String = "http://bmclapi2.bangbang93.com/assets/"
override fun getVersionListById(id: String): VersionList<*> {
return when(id) {
"game" -> GameVersionList
"forge" -> ForgeVersionList
"liteloader" -> LiteLoaderVersionList
"optifine" -> OptiFineBMCLVersionList
else -> throw IllegalArgumentException("Unrecognized version list id: $id")
}
}
override fun injectURL(baseURL: String): String = baseURL
.replace("https://launchermeta.mojang.com", "http://bmclapi2.bangbang93.com")
.replace("https://launcher.mojang.com", "http://bmclapi2.bangbang93.com")
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries")
.replace("http://files.minecraftforge.net/maven", "http://bmclapi2.bangbang93.com/maven")
.replace("http://dl.liteloader.com/versions/versions.json", "http://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
.replace("http://dl.liteloader.com/versions", "http://bmclapi2.bangbang93.com/maven")
}

View File

@@ -0,0 +1,65 @@
/*
* 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.download.game.GameAssetDownloadTask
import org.jackhuang.hmcl.download.game.GameLibrariesTask
import org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
import org.jackhuang.hmcl.download.game.VersionJSONSaveTask
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask
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.task.then
import java.net.Proxy
class DefaultDependencyManager(override val repository: DefaultGameRepository, override val downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY)
: AbstractDependencyManager(repository) {
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)
override fun checkGameCompletionAsync(version: Version): Task {
val tasks: Array<Task> = arrayOf(
GameAssetDownloadTask(this, version),
GameLoggingDownloadTask(this, version),
GameLibrariesTask(this, version)
)
return ParallelTask(*tasks)
}
override fun installLibraryAsync(version: Version, libraryId: String, libraryVersion: String): Task {
if (libraryId == "forge")
return ForgeInstallTask(this, version, libraryVersion) then { task ->
val newVersion = task.result!!
VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
}
else if (libraryId == "liteloader")
return LiteLoaderInstallTask(this, version, libraryVersion) then { task ->
val newVersion = task.result!!
VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
}
else if (libraryId == "optifine")
return OptiFineInstallTask(this, version, libraryVersion) then { task ->
val newVersion = task.result!!
VersionJSONSaveTask(this@DefaultDependencyManager, newVersion)
}
else
throw IllegalArgumentException("Library id $libraryId is unrecognized.")
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.download.game.GameAssetDownloadTask
import org.jackhuang.hmcl.download.game.GameLibrariesTask
import org.jackhuang.hmcl.download.game.GameLoggingDownloadTask
import org.jackhuang.hmcl.download.game.VersionJSONSaveTask
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.task.*
import org.jackhuang.hmcl.util.*
import java.util.*
class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameBuilder() {
val repository = dependencyManager.repository
val downloadProvider = dependencyManager.downloadProvider
override fun buildAsync(): Task {
return VersionJSONDownloadTask(gameVersion = gameVersion) then { task ->
var version = GSON.fromJson<Version>(task.result!!)
version = version.copy(jar = version.id, id = name)
var result = ParallelTask(
GameAssetDownloadTask(dependencyManager, version),
GameLoggingDownloadTask(dependencyManager, version),
GameDownloadTask(version),
GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
) then VersionJSONSaveTask(dependencyManager, version)
if (toolVersions.containsKey("forge"))
result = result then libraryTaskHelper(version, "forge")
if (toolVersions.containsKey("liteloader"))
result = result then libraryTaskHelper(version, "liteloader")
if (toolVersions.containsKey("optifine"))
result = result then libraryTaskHelper(version, "optifine")
result
}
}
private fun libraryTaskHelper(version: Version, libraryId: String): Task.(Task) -> Task = { prev ->
var thisVersion = version
if (prev is TaskResult<*> && prev.result is Version) {
thisVersion = prev.result as Version
}
dependencyManager.installLibraryAsync(thisVersion, libraryId, toolVersions[libraryId]!!)
}
inner class VersionJSONDownloadTask(val gameVersion: String): Task() {
override val dependents: MutableCollection<Task> = LinkedList()
override val dependencies: MutableCollection<Task> = LinkedList()
var httpTask: GetTask? = null
val result: String? get() = httpTask?.result
val gameVersionList: VersionList<*> = dependencyManager.getVersionList("game")
init {
if (!gameVersionList.loaded)
dependents += gameVersionList.refreshAsync(downloadProvider)
}
override fun execute() {
val remoteVersion = gameVersionList.getVersions(gameVersion).firstOrNull()
?: throw Error("Cannot find specific version $gameVersion in remote repository")
val jsonURL = downloadProvider.injectURL(remoteVersion.url)
httpTask = GetTask(jsonURL.toURL(), proxy = dependencyManager.proxy)
dependencies += httpTask!!
}
}
inner class GameDownloadTask(var version: Version) : Task() {
override val dependencies: MutableCollection<Task> = LinkedList()
override fun execute() {
val jar = repository.getVersionJar(version)
dependencies += FileDownloadTask(
url = downloadProvider.injectURL(version.download.url).toURL(),
file = jar,
hash = version.download.sha1,
proxy = dependencyManager.proxy)
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.GameRepository
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.Task
abstract class DependencyManager(open val repository: GameRepository) {
/**
* Check if the game is complete.
* Check libraries, assets, logging files and so on.
*
* @return
*/
abstract fun checkGameCompletionAsync(version: Version): Task
/**
* The builder to build a brand new game then libraries such as Forge, LiteLoader and OptiFine.
*/
abstract fun gameBuilder(): GameBuilder
abstract fun installLibraryAsync(version: Version, libraryId: String, libraryVersion: String): Task
/**
* Get registered version list.
* @param id the id of version list. i.e. game, forge, liteloader, optifine
* @throws IllegalArgumentException if the version list of specific id is not found.
*/
abstract fun getVersionList(id: String): VersionList<*>
}

View File

@@ -0,0 +1,30 @@
/*
* 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.download.VersionList
abstract class DownloadProvider {
abstract val libraryBaseURL: String
abstract val versionListURL: String
abstract val versionBaseURL: String
abstract val assetIndexBaseURL: String
abstract val assetBaseURL: String
abstract fun injectURL(baseURL: String): String
abstract fun getVersionListById(id: String): VersionList<*>
}

View File

@@ -0,0 +1,82 @@
/*
* 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.download.game.GameLibrariesTask
import org.jackhuang.hmcl.game.Library
import org.jackhuang.hmcl.game.SimpleVersionProvider
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.FileDownloadTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.task.then
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.util.zip.ZipFile
class ForgeInstallTask(private val dependencyManager: DefaultDependencyManager,
private val version: Version,
private val remoteVersion: String) : TaskResult<Version>() {
private val forgeVersionList = dependencyManager.getVersionList("forge")
private val installer: File = File("forge-installer.jar").absoluteFile
lateinit var remote: RemoteVersion<*>
override val dependents: MutableCollection<Task> = mutableListOf()
override val dependencies: MutableCollection<Task> = mutableListOf()
init {
if (version.jar == null)
throw IllegalArgumentException()
if (!forgeVersionList.loaded)
dependents += forgeVersionList.refreshAsync(dependencyManager.downloadProvider) then {
remote = forgeVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote forge version ${version.jar}, $remoteVersion not found")
FileDownloadTask(remote.url.toURL(), installer)
}
else {
remote = forgeVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote forge version ${version.jar}, $remoteVersion not found")
dependents += FileDownloadTask(remote.url.toURL(), installer)
}
}
override fun execute() {
ZipFile(installer).use { zipFile ->
val installProfile = GSON.fromJson<InstallProfile>(zipFile.getInputStream(zipFile.getEntry("install_profile.json")).readFullyAsString())
// unpack the universal jar in the installer file.
val forgeLibrary = Library.fromName(installProfile.install!!.path!!)
val forgeFile = dependencyManager.repository.getLibraryFile(version, forgeLibrary)
if (!forgeFile.makeFile())
throw IOException("Cannot make directory ${forgeFile.parentFile}")
val forgeEntry = zipFile.getEntry(installProfile.install.filePath)
zipFile.getInputStream(forgeEntry).copyToAndClose(forgeFile.outputStream())
// resolve the version
val versionProvider = SimpleVersionProvider()
versionProvider.addVersion(version)
result = installProfile.versionInfo!!.copy(inheritsFrom = version.id).resolve(versionProvider).copy(id = version.id)
dependencies += GameLibrariesTask(dependencyManager, installProfile.versionInfo)
}
check(installer.delete(), { "Unable to delete installer file $installer" })
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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 com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.util.Immutable
import org.jackhuang.hmcl.util.Validation
@Immutable
internal class ForgeVersion (
val branch: String? = null,
val mcversion: String? = null,
val jobver: String? = null,
val version: String? = null,
val build: Int = 0,
val modified: Double = 0.0,
val files: Array<Array<String>>? = null
) : Validation {
override fun validate() {
check(files != null, { "ForgeVersion files cannot be null" })
check(version != null, { "ForgeVersion version cannot be null" })
check(mcversion != null, { "ForgeVersion mcversion cannot be null" })
}
}
@Immutable
internal class ForgeVersionRoot (
val artifact: String? = null,
val webpath: String? = null,
val adfly: String? = null,
val homepage: String? = null,
val name: String? = null,
val branches: Map<String, Array<Int>>? = null,
val mcversion: Map<String, Array<Int>>? = null,
val promos: Map<String, Int>? = null,
val number: Map<Int, ForgeVersion>? = null
) : Validation {
override fun validate() {
check(number != null, { "ForgeVersionRoot number cannot be null" })
check(mcversion != null, { "ForgeVersionRoot mcversion cannot be null" })
}
}
@Immutable
internal data class Install (
val profileName: String? = null,
val target: String? = null,
val path: String? = null,
val version: String? = null,
val filePath: String? = null,
val welcome: String? = null,
val minecraft: String? = null,
val mirrorList: String? = null,
val logo: String? = null
)
@Immutable
internal data class InstallProfile (
@SerializedName("install")
val install: Install? = null,
@SerializedName("versionInfo")
val versionInfo: Version? = null
) : Validation {
override fun validate() {
check(install != null, { "InstallProfile install cannot be null" })
check(versionInfo != null, { "InstallProfile versionInfo cannot be null" })
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.*
object ForgeVersionList : VersionList<Unit>() {
@JvmField
val FORGE_LIST = "http://files.minecraftforge.net/maven/net/minecraftforge/forge/json"
override fun refreshAsync(downloadProvider: DownloadProvider): Task {
return RefreshTask(downloadProvider)
}
private class RefreshTask(val downloadProvider: DownloadProvider): Task() {
val task = GetTask(downloadProvider.injectURL(FORGE_LIST).toURL())
override val dependents: Collection<Task> = listOf(task)
override fun execute() {
val root = GSON.fromJson<ForgeVersionRoot>(task.result!!) ?: return
versions.clear()
for ((x, versions) in root.mcversion!!.entries) {
val gameVersion = x.asVersion() ?: continue
for (v in versions) {
val version = root.number!!.get(v) ?: continue
var jar: String? = null
for (file in version.files!!)
if (file.getOrNull(1) == "installer") {
val classifier = "${version.mcversion}-${version.version}" + (
if (isNotBlank(version.branch))
"-${version.branch}"
else
""
)
val fileName = "${root.artifact}-$classifier-${file[1]}.${file[0]}"
jar = downloadProvider.injectURL("${root.webpath}$classifier/$fileName")
}
if (jar == null) continue
val remoteVersion = RemoteVersion<Unit>(
gameVersion = version.mcversion!!,
selfVersion = version.version!!,
url = jar,
tag = Unit
)
ForgeVersionList.versions.put(gameVersion, remoteVersion)
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.task.Task
abstract class GameBuilder {
var name: String = ""
protected var gameVersion: String = ""
protected var toolVersions = HashMap<String, String>()
fun name(name: String): GameBuilder {
this.name = name
return this
}
fun gameVersion(version: String): GameBuilder {
gameVersion = version
return this
}
fun version(id: String, version: String): GameBuilder {
toolVersions[id] = version
return this
}
abstract fun buildAsync(): Task
}

View File

@@ -0,0 +1,145 @@
/*
* 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.AssetIndex
import org.jackhuang.hmcl.game.AssetObject
import org.jackhuang.hmcl.game.DownloadType
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.FileDownloadTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.util.*
import java.util.logging.Level
/**
* This task is to download game libraries.
* This task should be executed last(especially after game downloading, Forge, LiteLoader and OptiFine install task)
* @param resolvedVersion the <b>resolved</b> version
*/
class GameLibrariesTask(private val dependencyManager: DefaultDependencyManager, private val resolvedVersion: Version): Task() {
override val dependencies: MutableCollection<Task> = LinkedList()
override fun execute() {
for (library in resolvedVersion.libraries)
if (library.appliesToCurrentEnvironment) {
val file = dependencyManager.repository.getLibraryFile(resolvedVersion, library)
if (!file.exists())
dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(library.download.url).toURL(), file, library.download.sha1, proxy = dependencyManager.proxy)
}
}
}
class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
override val dependencies: MutableCollection<Task> = LinkedList()
override fun execute() {
val logging = version.logging?.get(DownloadType.CLIENT) ?: return
val file = dependencyManager.repository.getLoggingObject(version.actualAssetIndex.id, logging)
if (!file.exists())
dependencies += FileDownloadTask(logging.file.url.toURL(), file, proxy = dependencyManager.proxy)
}
}
class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
override val dependencies: MutableCollection<Task> = LinkedList()
override fun execute() {
val assetIndexInfo = version.actualAssetIndex
val assetDir = dependencyManager.repository.getAssetDirectory(assetIndexInfo.id)
if (!assetDir.makeDirectory())
throw IOException("Cannot create directory: $assetDir")
val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id)
dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(assetIndexInfo.url).toURL(), assetIndexFile, proxy = dependencyManager.proxy)
}
}
class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : TaskResult<Collection<Pair<File, AssetObject>>>() {
private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version)
private val assetIndexInfo = version.actualAssetIndex
private val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id)
override val dependents: MutableCollection<Task> = LinkedList()
init {
if (!assetIndexFile.exists())
dependents += assetIndexTask
}
override fun execute() {
val index = GSON.fromJson<AssetIndex>(assetIndexFile.readText())
val res = LinkedList<Pair<File, AssetObject>>()
var progress = 0
index?.objects?.entries?.forEach { (_, assetObject) ->
res += Pair(dependencyManager.repository.getAssetObject(assetIndexInfo.id, assetObject), assetObject)
updateProgress(++progress, index.objects.size)
}
result = res
}
}
class GameAssetDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
private val refreshTask = GameAssetRefreshTask(dependencyManager, version)
override val dependents: Collection<Task> = listOf(refreshTask)
override val dependencies: MutableCollection<Task> = LinkedList()
override fun execute() {
val size = refreshTask.result?.size ?: 0
refreshTask.result?.forEach single@{ (file, assetObject) ->
val url = dependencyManager.downloadProvider.assetBaseURL + assetObject.location
if (!file.absoluteFile.parentFile.makeDirectory()) {
LOG.severe("Unable to create new file $file, because parent directory cannot be created")
return@single
}
if (file.isDirectory)
return@single
var flag = true
var downloaded = 0
try {
// check the checksum of file to ensure that the file is not need to re-download.
if (file.exists()) {
val sha1 = DigestUtils.sha1Hex(file.readBytes())
if (assetObject.hash == sha1) {
++downloaded
LOG.finest("File $file has been downloaded successfully, skipped downloading")
updateProgress(downloaded, size)
return@single
}
}
} catch (e: IOException) {
LOG.log(Level.WARNING, "Unable to get hash code of file $file", e)
flag = !file.exists()
}
if (flag)
dependencies += FileDownloadTask(url.toURL(), file, assetObject.hash, proxy = dependencyManager.proxy).apply {
title = assetObject.hash
}
}
}
}
class VersionJSONSaveTask(private val dependencyManager: DefaultDependencyManager, private val version: Version): Task() {
override fun execute() {
val json = dependencyManager.repository.getVersionJson(version.id).absoluteFile
if (!json.makeFile())
throw IOException("Cannot create file $json")
json.writeText(GSON.toJson(version))
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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 com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.ReleaseType
import org.jackhuang.hmcl.util.DEFAULT_LIBRARY_URL
import org.jackhuang.hmcl.util.Immutable
import org.jackhuang.hmcl.util.Validation
import java.util.*
@Immutable
internal class GameRemoteLatestVersions(
@SerializedName("snapshot")
val snapshot: String,
@SerializedName("release")
val release: String
)
@Immutable
internal class GameRemoteVersion(
@SerializedName("id")
val gameVersion: String = "",
@SerializedName("time")
val time: Date = Date(),
@SerializedName("releaseTime")
val releaseTime: Date = Date(),
@SerializedName("type")
val type: ReleaseType = ReleaseType.UNKNOWN,
@SerializedName("url")
val url: String = "$DEFAULT_LIBRARY_URL$gameVersion/$gameVersion.json"
) : Validation {
override fun validate() {
if (gameVersion.isBlank())
throw IllegalArgumentException("GameRemoteVersion id cannot be blank")
if (url.isBlank())
throw IllegalArgumentException("GameRemoteVersion url cannot be blank")
}
}
@Immutable
internal class GameRemoteVersions(
@SerializedName("versions")
val versions: List<GameRemoteVersion> = emptyList(),
@SerializedName("latest")
val latest: GameRemoteLatestVersions? = null
)
@Immutable
data class GameRemoteVersionTag (
val type: ReleaseType,
val time: Date
)

View File

@@ -0,0 +1,58 @@
/*
* 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.util.GSON
import org.jackhuang.hmcl.util.asVersion
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL
object GameVersionList : VersionList<GameRemoteVersionTag>() {
override fun refreshAsync(downloadProvider: DownloadProvider): Task {
return RefreshTask(downloadProvider)
}
private class RefreshTask(provider: DownloadProvider) : Task() {
val task = GetTask(provider.versionListURL.toURL())
override val dependents: Collection<Task> = listOf(task)
override fun execute() {
versionMap.clear()
versions.clear()
val root = GSON.fromJson<GameRemoteVersions>(task.result!!) ?: return
for (remoteVersion in root.versions) {
val gg = remoteVersion.gameVersion.asVersion() ?: continue
val x = RemoteVersion(
gameVersion = remoteVersion.gameVersion,
selfVersion = remoteVersion.gameVersion,
url = remoteVersion.url,
tag = GameRemoteVersionTag(
type = remoteVersion.type,
time = remoteVersion.releaseTime
)
)
versions.add(x)
versionMap[gg] = listOf(x)
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.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.task.Task
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.task.then
import org.jackhuang.hmcl.util.merge
/**
* LiteLoader must be installed after Forge.
*/
class LiteLoaderInstallTask(private val dependencyManager: DefaultDependencyManager,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val liteLoaderVersionList = dependencyManager.getVersionList("liteloader") as LiteLoaderVersionList
lateinit var remote: RemoteVersion<LiteLoaderRemoteVersionTag>
override val dependents: MutableCollection<Task> = mutableListOf()
override val dependencies: MutableCollection<Task> = mutableListOf()
init {
if (version.jar == null)
throw IllegalArgumentException()
if (!liteLoaderVersionList.loaded)
dependents += LiteLoaderVersionList.refreshAsync(dependencyManager.downloadProvider) then {
remote = liteLoaderVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version ${version.jar}, $remoteVersion not found")
null
}
else {
remote = liteLoaderVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version ${version.jar}, $remoteVersion not found")
}
}
override fun execute() {
val library = Library(
groupId = "com.mumfrey",
artifactId = "liteloader",
version = remote.selfVersion,
url = "http://dl.liteloader.com/versions/",
downloads = LibrariesDownloadInfo(
artifact = LibraryDownloadInfo(
url = remote.url
)
)
)
val tempVersion = version.copy(libraries = merge(remote.tag.libraries, listOf(library)))
result = version.copy(
mainClass = "net.minecraft.launchwrapper.Launch",
minecraftArguments = version.minecraftArguments + " --tweakClass " + remote.tag.tweakClass,
libraries = merge(tempVersion.libraries, version.libraries)
)
dependencies += GameLibrariesTask(dependencyManager, tempVersion)
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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 com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.Library
import org.jackhuang.hmcl.util.Immutable
@Immutable
internal data class LiteLoaderVersionsMeta (
@SerializedName("description")
val description: String = "",
@SerializedName("authors")
val authors: String = "",
@SerializedName("url")
val url: String = ""
)
@Immutable
internal data class LiteLoaderRepository (
@SerializedName("stream")
val stream: String = "",
@SerializedName("type")
val type: String = "",
@SerializedName("url")
val url: String = "",
@SerializedName("classifier")
val classifier: String = ""
)
@Immutable
internal class LiteLoaderVersion (
@SerializedName("tweakClass")
val tweakClass: String = "",
@SerializedName("file")
val file: String = "",
@SerializedName("version")
val version: String = "",
@SerializedName("md5")
val md5: String = "",
@SerializedName("timestamp")
val timestamp: String = "",
@SerializedName("lastSuccessfulBuild")
val lastSuccessfulBuild: Int = 0,
@SerializedName("libraries")
val libraries: Collection<Library> = emptyList()
)
@Immutable
internal class LiteLoaderBranch (
@SerializedName("libraries")
val libraries: Collection<Library> = emptyList(),
@SerializedName("com.mumfrey:liteloader")
val liteLoader: Map<String, LiteLoaderVersion> = emptyMap()
)
@Immutable
internal class LiteLoaderGameVersions (
@SerializedName("repo")
val repo: LiteLoaderRepository? = null,
@SerializedName("artefacts")
val artifacts: LiteLoaderBranch? = null,
@SerializedName("snapshots")
val snapshots: LiteLoaderBranch? = null
)
@Immutable
internal class LiteLoaderVersionsRoot (
@SerializedName("versions")
val versions: Map<String, LiteLoaderGameVersions> = emptyMap(),
@SerializedName("meta")
val meta: LiteLoaderVersionsMeta? = null
)
@Immutable
data class LiteLoaderRemoteVersionTag (
val tweakClass: String,
val libraries: Collection<Library>
)

View File

@@ -0,0 +1,70 @@
/*
* 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.util.GSON
import org.jackhuang.hmcl.util.asVersion
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL
object LiteLoaderVersionList : VersionList<LiteLoaderRemoteVersionTag>() {
@JvmField
val LITELOADER_LIST = "http://dl.liteloader.com/versions/versions.json"
override fun refreshAsync(downloadProvider: DownloadProvider): Task {
return RefreshTask(downloadProvider)
}
internal class RefreshTask(val downloadProvider: DownloadProvider) : Task() {
val task = GetTask(downloadProvider.injectURL(LITELOADER_LIST).toURL())
override val dependents: Collection<Task> = listOf(task)
override fun execute() {
val root = GSON.fromJson<LiteLoaderVersionsRoot>(task.result!!) ?: return
versions.clear()
for ((gameVersion, liteLoader) in root.versions.entries) {
val gg = gameVersion.asVersion() ?: continue
doBranch(gg, gameVersion, liteLoader.repo, liteLoader.artifacts, false)
doBranch(gg, gameVersion, liteLoader.repo, liteLoader.snapshots, true)
}
}
private fun doBranch(key: String, gameVersion: String, repository: LiteLoaderRepository?, branch: LiteLoaderBranch?, snapshot: Boolean) {
if (branch == null || repository == null)
return
for ((branchName, v) in branch.liteLoader.entries) {
if ("latest" == branchName)
continue
val iv = RemoteVersion<LiteLoaderRemoteVersionTag>(
selfVersion = v.version.replace("SNAPSHOT", "SNAPSHOT-" + v.lastSuccessfulBuild),
gameVersion = gameVersion,
url = if (snapshot)
"http://jenkins.liteloader.com/view/$gameVersion/job/LiteLoader $gameVersion/lastSuccessfulBuild/artifact/build/libs/liteloader-${v.version}-release.jar"
else
downloadProvider.injectURL(repository.url + "com/mumfrey/liteloader/" + gameVersion + "/" + v.file),
tag = LiteLoaderRemoteVersionTag(tweakClass = v.tweakClass, libraries = v.libraries)
)
versions.put(key, iv)
}
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.download.forge.ForgeVersionList
import org.jackhuang.hmcl.download.game.GameVersionList
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import java.util.*
object MojangDownloadProvider : DownloadProvider() {
override val libraryBaseURL: String = "https://libraries.minecraft.net/"
override val versionBaseURL: String = "http://s3.amazonaws.com/Minecraft.Download/versions/"
override val versionListURL: String = "https://launchermeta.mojang.com/mc/game/version_manifest.json"
override val assetIndexBaseURL: String = "http://s3.amazonaws.com/Minecraft.Download/indexes/"
override val assetBaseURL: String = "http://resources.download.minecraft.net/"
override fun getVersionListById(id: String): VersionList<*> {
return when(id) {
"game" -> GameVersionList
"forge" -> ForgeVersionList
"liteloader" -> LiteLoaderVersionList
"optifine" -> OptiFineVersionList
else -> throw IllegalArgumentException("Unrecognized version list id: $id")
}
}
override fun injectURL(baseURL: String): String {
/**if (baseURL.contains("scala-swing") || baseURL.contains("scala-xml") || baseURL.contains("scala-parser-combinators"))
return baseURL.replace("http://files.minecraftforge.net/maven", "http://ftb.cursecdn.com/FTB2/maven/");
else if (baseURL.contains("typesafe") || baseURL.contains("scala"))
if (Locale.getDefault() == Locale.CHINA)
return baseURL.replace("http://files.minecraftforge.net/maven", "http://maven.aliyun.com/nexus/content/groups/public");
else
return baseURL.replace("http://files.minecraftforge.net/maven", "http://repo1.maven.org/maven2");
else
return baseURL; */
if (baseURL.endsWith("net/minecraftforge/forge/json"))
return baseURL
else if (Locale.getDefault() == Locale.CHINA)
return baseURL.replace("http://files.minecraftforge.net/maven", "http://maven.aliyun.com/nexus/content/groups/public");
else
return baseURL.replace("http://files.minecraftforge.net/maven", "http://ftb.cursecdn.com/FTB2/maven")
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.asVersion
import org.jackhuang.hmcl.util.toURL
import org.jackhuang.hmcl.util.typeOf
import java.util.TreeSet
object OptiFineBMCLVersionList : VersionList<Unit>() {
override fun refreshAsync(downloadProvider: DownloadProvider): Task {
return RefreshTask(downloadProvider)
}
private class RefreshTask(val downloadProvider: DownloadProvider): Task() {
val task = GetTask("http://bmclapi.bangbang93.com/optifine/versionlist".toURL())
override val dependents: Collection<Task> = listOf(task)
override fun execute() {
versionMap.clear()
versions.clear()
val duplicates = mutableSetOf<String>()
val root = GSON.fromJson<List<OptiFineVersion>>(task.result!!, typeOf<List<OptiFineVersion>>())
for (element in root) {
val version = element.type ?: continue
val mirror = "http://bmclapi2.bangbang93.com/optifine/${element.gameVersion}/${element.type}/${element.patch}"
if (duplicates.contains(mirror))
continue
else
duplicates += mirror
val gameVersion = element.gameVersion?.asVersion() ?: continue
val remoteVersion = RemoteVersion<Unit>(
gameVersion = gameVersion,
selfVersion = version,
url = mirror,
tag = Unit
)
val set = versionMap.getOrPut(gameVersion, { TreeSet<RemoteVersion<Unit>>() }) as MutableCollection<RemoteVersion<Unit>>
set.add(remoteVersion)
versions.add(remoteVersion)
}
}
}
}

View File

@@ -0,0 +1,21 @@
/*
* 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
object Opt

View File

@@ -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.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.task.Task
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.task.then
import org.jackhuang.hmcl.util.merge
class OptiFineInstallTask(private val dependencyManager: DefaultDependencyManager,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val optiFineVersionList = dependencyManager.getVersionList("optifine")
lateinit var remote: RemoteVersion<*>
override val dependents: MutableCollection<Task> = mutableListOf()
override val dependencies: MutableCollection<Task> = mutableListOf()
init {
if (version.jar == null)
throw IllegalArgumentException()
if (!optiFineVersionList.loaded)
dependents += optiFineVersionList.refreshAsync(dependencyManager.downloadProvider) then {
remote = optiFineVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version $remoteVersion not found")
null
}
else {
remote = optiFineVersionList.getVersion(version.jar, remoteVersion) ?: throw IllegalArgumentException("Remote LiteLoader version $remoteVersion not found")
}
}
override fun execute() {
val library = Library(
groupId = "net.optifine",
artifactId = "optifine",
version = remoteVersion,
lateload = true,
downloads = LibrariesDownloadInfo(
artifact = LibraryDownloadInfo(
path = "net/optifine/optifine/$remoteVersion/optifine-$remoteVersion.jar",
url = remote.url
)
))
val libraries = mutableListOf(library)
var arg = version.minecraftArguments!!
if (!arg.contains("FMLTweaker"))
arg += " --tweakClass optifine.OptiFineTweaker"
var mainClass = version.mainClass
if (mainClass == null || !mainClass.startsWith("net.minecraft.launchwrapper.")) {
mainClass = "net.minecraft.launchwrapper.Launch"
libraries.add(0, Library(
groupId = "net.minecraft",
artifactId = "launchwrapper",
version = "1.12"
))
}
result = version.copy(libraries = merge(version.libraries, libraries), mainClass = mainClass, minecraftArguments = arg)
dependencies += GameLibrariesTask(dependencyManager, version.copy(libraries = libraries))
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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 com.google.gson.annotations.SerializedName
data class OptiFineVersion (
@SerializedName("dl")
val downloadLink: String? = null,
@SerializedName("ver")
val version: String? = null,
@SerializedName("date")
val date: String? = null,
@SerializedName("type")
val type: String? = null,
@SerializedName("patch")
val patch: String? = null,
@SerializedName("mirror")
val mirror: String? = null,
@SerializedName("mcversion")
val gameVersion: String? = null
)

View File

@@ -0,0 +1,82 @@
/*
* 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.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL
import org.w3c.dom.Element
import java.io.ByteArrayInputStream
import javax.xml.parsers.DocumentBuilderFactory
import java.util.regex.Pattern
object OptiFineVersionList : VersionList<Unit>() {
private val pattern = Pattern.compile("OptiFine (.*?) ")
override fun refreshAsync(downloadProvider: DownloadProvider): Task {
return RefreshTask(downloadProvider)
}
private class RefreshTask(val downloadProvider: DownloadProvider) : Task() {
val task = GetTask("http://optifine.net/downloads".toURL())
override val dependents: Collection<Task> = listOf(task)
override fun execute() {
versions.clear()
val html = task.result!!.replace("&nbsp;", " ").replace("&gt;", ">").replace("&lt;", "<").replace("<br>", "<br />")
val factory = DocumentBuilderFactory.newInstance()
val db = factory.newDocumentBuilder()
val doc = db.parse(ByteArrayInputStream(html.toByteArray()))
val r = doc.documentElement
val tables = r.getElementsByTagName("table")
for (i in 0..tables.length - 1) {
val e = tables.item(i) as Element
if ("downloadTable" == e.getAttribute("class")) {
val tr = e.getElementsByTagName("tr")
for (k in 0..tr.length - 1) {
val downloadLine = (tr.item(k) as Element).getElementsByTagName("td")
var url: String? = null
var version: String? = null
for (j in 0..downloadLine.length - 1) {
val td = downloadLine.item(j) as Element
if (td.getAttribute("class")?.startsWith("downloadLineMirror") ?: false)
url = (td.getElementsByTagName("a").item(0) as Element).getAttribute("href")
if (td.getAttribute("class")?.startsWith("downloadLineFile") ?: false)
version = td.textContent
}
val matcher = pattern.matcher(version)
var gameVersion: String? = null
while (matcher.find())
gameVersion = matcher.group(1)
if (gameVersion == null || version == null || url == null) continue
val remoteVersion = RemoteVersion(
gameVersion = gameVersion,
selfVersion = version,
url = url,
tag = Unit
)
versions.put(gameVersion, remoteVersion)
}
}
}
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.util.VersionNumber
import java.util.*
import kotlin.Comparator
data class RemoteVersion<T> (
val gameVersion: String,
val selfVersion: String,
/**
* The file of remote version, may be an installer or an universal jar.
*/
val url: String,
val tag: T
): Comparable<RemoteVersion<T>> {
override fun hashCode(): Int {
return selfVersion.hashCode()
}
override fun equals(other: Any?): Boolean {
return other is RemoteVersion<*> && Objects.equals(this.selfVersion, other.selfVersion)
}
override fun compareTo(other: RemoteVersion<T>): Int {
return -selfVersion.compareTo(other.selfVersion)
}
companion object RemoteVersionComparator: Comparator<RemoteVersion<*>> {
override fun compare(o1: RemoteVersion<*>, o2: RemoteVersion<*>): Int {
return -VersionNumber.asVersion(o1.selfVersion).compareTo(VersionNumber.asVersion(o2.selfVersion))
}
}
}

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.download
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.SimpleMultimap
import java.util.*
import kotlin.collections.HashMap
abstract class VersionList<T> {
@JvmField
protected val versions = SimpleMultimap<String, RemoteVersion<T>>(::HashMap, ::TreeSet)
val loaded = versions.isNotEmpty
abstract fun refreshAsync(downloadProvider: DownloadProvider): Task
protected open fun getVersionsImpl(gameVersion: String): Collection<RemoteVersion<T>> {
return versions[gameVersion] ?: versions.values
}
/**
* @param gameVersion the Minecraft version that remote versions belong to
* @return the collection of specific remote versions
*/
fun getVersions(gameVersion: String): Collection<RemoteVersion<T>> {
return Collections.unmodifiableCollection(getVersionsImpl(gameVersion))
}
fun getVersion(gameVersion: String, remoteVersion: String): RemoteVersion<T>? {
var result : RemoteVersion<T>? = null
versions[gameVersion]?.forEach {
if (it.selfVersion == remoteVersion)
result = it
}
return result
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.event
import java.util.*
open class Event(source: Any) : EventObject(source) {
/**
* true if this event is canceled.
*
* @throws UnsupportedOperationException if trying to cancel a non-cancelable event.
*/
var isCanceled = false
set(value) {
if (!isCancelable)
throw UnsupportedOperationException("Attempted to cancel a non-cancelable event: ${this.javaClass}")
field = value
}
/**
* Retutns the value set as the result of this event
*/
var result = Result.DEFAULT
set(value) {
if (!hasResult)
throw UnsupportedOperationException("Attempted to set result on a no result event: ${this.javaClass} of type.")
field = value
}
/**
* true if this Event this cancelable.
*/
open val isCancelable = false
/**
* true if this event has a significant result.
*/
open val hasResult = false
enum class Result {
DENY,
DEFAULT,
ALLOW
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.event
import java.util.*
class EventBus {
val events = HashMap<Class<*>, EventManager<*>>()
@Suppress("UNCHECKED_CAST")
fun <T : EventObject> channel(classOfT: Class<T>): EventManager<T> {
if (!events.containsKey(classOfT))
events.put(classOfT, EventManager<T>())
return events[classOfT] as EventManager<T>
}
inline fun <reified T: EventObject> channel() = channel(T::class.java)
fun fireEvent(obj: EventObject) {
channel(obj.javaClass).fireEvent(obj)
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.event
import java.util.*
class EventManager<T : EventObject> {
private val handlers = EnumMap<EventPriority, MutableList<(T) -> Unit>>(EventPriority::class.java).apply {
for (value in EventPriority.values())
put(value, LinkedList<(T) -> Unit>())
}
fun register(func: (T) -> Unit, priority: EventPriority = EventPriority.NORMAL) {
if (!handlers[priority]!!.contains(func))
handlers[priority]!!.add(func)
}
fun unregister(func: (T) -> Unit) {
EventPriority.values().forEach { handlers[it]!!.remove(func) }
}
fun fireEvent(event: T) {
for (priority in EventPriority.values())
for (handler in handlers[priority]!!)
handler(event)
}
operator fun plusAssign(func: (T) -> Unit) = register(func)
operator fun minusAssign(func: (T) -> Unit) = unregister(func)
operator fun invoke(event: T) = fireEvent(event)
}

View File

@@ -0,0 +1,26 @@
/*
* 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.event
enum class EventPriority {
HIGHEST,
HIGH,
NORMAL,
LOW,
LOWEST
}

View File

@@ -0,0 +1,21 @@
/*
* 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.event
open class FailedEvent<T>(source: Any, val failedTime: Int, var newResult: T) : Event(source) {
}

View File

@@ -0,0 +1,33 @@
/*
* 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 java.util.*
class AssetIndex(
@SerializedName("virtual")
val virtual: Boolean = false,
objects: Map<String, AssetObject> = emptyMap()
) {
val objects: Map<String, AssetObject>
get() = Collections.unmodifiableMap(objectsImpl)
@SerializedName("objects")
private val objectsImpl: MutableMap<String, AssetObject> = HashMap(objects)
}

View File

@@ -0,0 +1,30 @@
/*
* 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 jdk.nashorn.internal.ir.annotations.Immutable
import java.net.URL
@Immutable
class AssetIndexInfo(
url: String = "",
sha1: String? = null,
size: Int = 0,
id: String = "",
val totalSize: Long = 0
) : IdDownloadInfo(url, sha1, size, id)

View File

@@ -0,0 +1,32 @@
/*
* 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 org.jackhuang.hmcl.util.Validation
data class AssetObject(
val hash: String = "",
val size: Long = 0
): Validation {
val location: String
get() = hash.substring(0, 2) + "/" + hash
override fun validate() {
check(hash.isNotBlank(), { "AssetObject hash cannot be blank." })
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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
/**
* What's circle dependency?
* When C inherits from B, and B inherits from something else, and finally inherits from C again.
*/
class CircleDependencyException : Exception {
constructor() : super() {}
constructor(message: String) : super(message) {}
constructor(message: String, cause: Throwable) : super(message, cause) {}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 org.jackhuang.hmcl.util.Immutable
import java.io.File
@Immutable
class ClassicVersion : Version(
mainClass = "net.minecraft.client.Minecraft",
id = "Classic",
type = ReleaseType.UNKNOWN,
minecraftArguments = "\${auth_player_name} \${auth_session} --workDir \${game_directory}",
libraries = listOf(
ClassicLibrary("lwjgl"),
ClassicLibrary("jinput"),
ClassicLibrary("lwjgl_util")
)
) {
@Immutable
private class ClassicLibrary(name: String) :
Library(groupId = "", artifactId = "", version = "",
downloads = LibrariesDownloadInfo(
artifact = LibraryDownloadInfo(path = "bin/$name.jar")
)
)
companion object {
fun hasClassicVersion(baseDirectory: File): Boolean {
val file = File(baseDirectory, "bin")
if (!file.exists()) return false
if (!File(file, "lwjgl.jar").exists()) return false
if (!File(file, "jinput.jar").exists()) return false
if (!File(file, "lwjgl_util.jar").exists()) return false
return true
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.Immutable
import org.jackhuang.hmcl.util.OS
import org.jackhuang.hmcl.util.ignoreThrowable
import java.util.regex.Pattern
@Immutable
data class CompatibilityRule(
val action: Action = CompatibilityRule.Action.ALLOW,
val os: OSRestriction? = null
) {
val appliedAction: Action? get() = if (os != null && !os.allow()) null else action
companion object {
fun appliesToCurrentEnvironment(rules: Collection<CompatibilityRule>?): Boolean {
if (rules == null)
return true
var action = CompatibilityRule.Action.DISALLOW
for (rule in rules) {
val thisAction = rule.appliedAction
if (thisAction != null) action = thisAction
}
return action == CompatibilityRule.Action.ALLOW
}
}
enum class Action {
@SerializedName("allow")
ALLOW,
@SerializedName("disallow")
DISALLOW
}
@Immutable
data class OSRestriction(
val name: OS = OS.UNKNOWN,
val version: String? = null,
val arch: String? = null
) {
fun allow(): Boolean {
if (name != OS.UNKNOWN && name != OS.CURRENT_OS)
return false
if (version != null) {
ignoreThrowable {
if (!Pattern.compile(version).matcher(OS.SYSTEM_VERSION).matches())
return false
}
}
if (arch != null)
ignoreThrowable {
if (!Pattern.compile(arch).matcher(OS.SYSTEM_ARCH).matches())
return false
}
return true
}
}
}

View File

@@ -0,0 +1,203 @@
/*
* 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.JsonSyntaxException
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.util.listFilesByExtension
import java.io.File
import java.io.IOException
import java.util.*
import java.util.logging.Level
open class DefaultGameRepository(val baseDirectory: File): GameRepository {
protected val versions: MutableMap<String, Version> = TreeMap<String, Version>()
override fun hasVersion(id: String) = versions.containsKey(id)
override fun getVersion(id: String): Version {
return versions[id] ?: throw VersionNotFoundException("Version '$id' does not exist.")
}
override fun getVersionCount() = versions.size
override fun getVersions() = versions.values
override fun getLibraryFile(id: Version, lib: Library) = File(baseDirectory, "libraries/${lib.path}")
override fun getRunDirectory(id: String) = baseDirectory
override fun getVersionJar(version: Version): File {
val v = version.resolve(this)
val id = v.id
return getVersionRoot(id).resolve("$id.jar")
}
override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives")
open fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id")
open fun getVersionJson(id: String) = File(getVersionRoot(id), "$id.json")
open fun readVersionJson(id: String): Version? = readVersionJson(getVersionJson(id))
@Throws(IOException::class, JsonSyntaxException::class, VersionNotFoundException::class)
open fun readVersionJson(file: File): Version? = GSON.fromJson<Version>(file.readText())
override fun renameVersion(from: String, to: String): Boolean {
try {
val fromVersion = getVersion(from)
val fromDir = getVersionRoot(from)
val toDir = getVersionRoot(to)
if (!fromDir.renameTo(toDir))
return false
val toJson = File(toDir, "$to.json")
val toJar = File(toDir, "$to.jar")
if (!File(toDir, "$from.json").renameTo(toJson) ||
!File(toDir, "$from.jar").renameTo(toJar)) {
toDir.renameTo(fromDir) // simple recovery
return false
}
toJson.writeText(GSON.toJson(fromVersion.copy(id = to)))
return true
} catch (e: IOException) {
return false
} catch (e2: JsonSyntaxException) {
return false
} catch (e: VersionNotFoundException) {
return false
}
}
open fun removeVersionFromDisk(name: String): Boolean {
val file = getVersionRoot(name)
if (!file.exists())
return true
versions.remove(name)
return file.deleteRecursively()
}
@Synchronized
override fun refreshVersions() {
versions.clear()
if (ClassicVersion.hasClassicVersion(baseDirectory)) {
val version = ClassicVersion()
versions[version.id] = version
}
baseDirectory.resolve("versions").listFiles()?.filter { it.isDirectory }?.forEach tryVersion@{ dir ->
val id = dir.name
val json = dir.resolve("$id.json")
// If user renamed the json file by mistake or created the json file in a wrong name,
// we will find the only json and rename it to correct name.
if (!json.exists()) {
val jsons = dir.listFilesByExtension("json")
if (jsons.size == 1)
jsons.single().renameTo(json)
}
var version: Version
try {
version = readVersionJson(json)!!
} catch(e: Exception) { // JsonSyntaxException or IOException or NullPointerException(!!)
// TODO: auto making up for the missing json
// TODO: and even asking for removing the redundant version folder.
return@tryVersion
}
if (id != version.id) {
version = version.copy(id = id)
try {
json.writeText(GSON.toJson(version))
} catch(e: Exception) {
LOG.warning("Ignoring version $id because wrong id ${version.id} is set and cannot correct it.")
return@tryVersion
}
}
versions[id] = version
}
}
override fun getAssetIndex(assetId: String): AssetIndex {
return GSON.fromJson(getIndexFile(assetId).readText())
}
override fun getActualAssetDirectory(assetId: String): File {
try {
return reconstructAssets(assetId)
} catch (e: IOException) {
LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e)
return getAssetDirectory(assetId)
}
}
override fun getAssetDirectory(assetId: String): File =
baseDirectory.resolve("assets")
@Throws(IOException::class)
override fun getAssetObject(assetId: String, name: String): File {
try {
return getAssetObject(assetId, getAssetIndex(assetId).objects["name"]!!)
} catch (e: Exception) {
throw IOException("Asset index file malformed", e)
}
}
override fun getAssetObject(assetId: String, obj: AssetObject): File =
getAssetObject(getAssetDirectory(assetId), obj)
open fun getAssetObject(assetDir: File, obj: AssetObject): File {
return assetDir.resolve("objects/${obj.location}")
}
open fun getIndexFile(assetId: String): File =
getAssetDirectory(assetId).resolve("indexes/$assetId.json")
override fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File =
getAssetDirectory(assetId).resolve("log_configs/${loggingInfo.file.id}")
@Throws(IOException::class, JsonSyntaxException::class)
protected open fun reconstructAssets(assetId: String): File {
val assetsDir = getAssetDirectory(assetId)
val assetVersion = assetId
val indexFile: File = getIndexFile(assetVersion)
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
if (!indexFile.isFile) {
return assetsDir
}
val assetIndexContent = indexFile.readText()
val index = GSON.fromJson(assetIndexContent, AssetIndex::class.java) ?: return assetsDir
if (index.virtual) {
var cnt = 0
LOG.info("Reconstructing virtual assets folder at " + virtualRoot)
val tot = index.objects.entries.size
for ((location, assetObject) in index.objects.entries) {
val target = File(virtualRoot, location)
val original = getAssetObject(assetsDir, assetObject)
if (original.exists()) {
cnt++
if (!target.isFile)
original.copyTo(target)
}
}
// If the scale new format existent file is lower then 0.1, use the old format.
if (cnt * 10 < tot)
return assetsDir
else
return virtualRoot
}
return assetsDir
}
}

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.JsonSyntaxException
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.util.Immutable
import org.jackhuang.hmcl.util.Validation
@Immutable
open class DownloadInfo(
@SerializedName("url")
val url: String = "",
@SerializedName("sha1")
val sha1: String? = null,
@SerializedName("size")
val size: Int = 0
): Validation {
override fun validate() {
if (url.isBlank())
throw JsonSyntaxException("DownloadInfo url can not be null")
}
}
@Immutable
open class IdDownloadInfo(
url: String = "",
sha1: String? = null,
size: Int = 0,
@SerializedName("id")
val id: String = ""
): DownloadInfo(url, sha1, size) {
override fun validate() {
super.validate()
if (id.isBlank())
throw JsonSyntaxException("IdDownloadInfo id can not be null")
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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
enum class DownloadType {
@SerializedName("client")
CLIENT,
@SerializedName("server")
SERVER,
@SerializedName("windows-server")
WINDOWS_SERVER
}

View File

@@ -0,0 +1,32 @@
/*
* 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 java.util.*
class ExtractRules(exclude: List<String> = emptyList()) {
val exclude: List<String> get() = Collections.unmodifiableList(excludeImpl)
@SerializedName("exclude")
private val excludeImpl: MutableList<String> = LinkedList(exclude)
fun shouldExtract(path: String): Boolean =
exclude.filter { path.startsWith(it) }.isEmpty()
}

View File

@@ -0,0 +1,24 @@
/*
* 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
class GameException : Exception {
constructor() : super() {}
constructor(message: String) : super(message) {}
constructor(message: String, cause: Throwable) : super(message, cause) {}
}

View File

@@ -0,0 +1,160 @@
/*
* 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.io.File
/**
* Supports operations on versioning.
*
* Note that game repository will not do any tasks that connect to Internet.
*/
interface GameRepository : VersionProvider {
/**
* Does the version of id exist?
* @param id the id of version
* @return true if the version exists
*/
override fun hasVersion(id: String): Boolean
/**
* Get the version
* @param id the id of version
* @return the version you want
*/
override fun getVersion(id: String): Version
/**
* How many version are there?
*/
fun getVersionCount(): Int
/**
* Gets the collection of versions
* @return the collection of versions
*/
fun getVersions(): Collection<Version>
/**
* Load version list.
*
* This method should be called before launching a version.
* A time-costly operation.
* You'd better execute this method in a new thread.
*/
fun refreshVersions()
/**
* Gets the current running directory of the given version for game.
* @param id the version id
*/
fun getRunDirectory(id: String): File
/**
* Get the library file in disk.
* This method allows versions and libraries that are not loaded by this game repository.
*
* @param id version id
* @param lib the library, [Version.libraries]
* @return the library file
*/
fun getLibraryFile(id: Version, lib: Library): File
/**
* Get the directory that native libraries will be unzipped to.
*
* You'd better return a unique directory.
* Or if it returns a temporary directory, [org.jackhuang.hmcl.launch.Launcher.makeLaunchScript] will fail.
* If you do want to return a temporary directory, make [org.jackhuang.hmcl.launch.Launcher.makeLaunchScript] always fail([UnsupportedOperationException]) and not to use it.
*
* @param id version id
* @return the native directory
*/
fun getNativeDirectory(id: String): File
/**
* Get minecraft jar
*
* @param version resolvedVersion
* @return the minecraft jar
*/
fun getVersionJar(version: Version): File
/**
* Rename given version to new name.
*
* @param from The id of original version
* @param to The new id of the version
* @throws UnsupportedOperationException if this game repository does not support renaming a version
* @throws java.io.IOException if I/O operation fails.
* @return true if the operation is done successfully.
*/
fun renameVersion(from: String, to: String): Boolean
/**
* Get actual asset directory.
* Will reconstruct assets or do some blocking tasks if necessary.
* You'd better create a new thread to invoke this method.
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @throws java.io.IOException if I/O operation fails.
* @return the actual asset directory
*/
fun getActualAssetDirectory(assetId: String): File
/**
* Get the asset directory according to the asset id.
*/
fun getAssetDirectory(assetId: String): File
/**
* Get the file that given asset object refers to
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param name the asset object name, [AssetIndex.objects.key]
* @throws java.io.IOException if I/O operation fails.
* @return the file that given asset object refers to
*/
fun getAssetObject(assetId: String, name: String): File
/**
* Get the file that given asset object refers to
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param obj the asset object, [AssetIndex.objects]
* @return the file that given asset object refers to
*/
fun getAssetObject(assetId: String, obj: AssetObject): File
/**
* Get asset index that assetId represents
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @return the asset index
*/
fun getAssetIndex(assetId: String): AssetIndex
/**
* Get logging object
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param loggingInfo the logging info
* @return the file that loggingInfo refers to
*/
fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File
}

View File

@@ -0,0 +1,128 @@
/*
* 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 org.jackhuang.hmcl.util.JavaVersion
import java.io.File
import java.io.Serializable
data class LaunchOptions(
/**
* The game directory
*/
val gameDir: File,
/**
* The Java Environment that Minecraft runs on.
*/
val java: JavaVersion = JavaVersion.fromCurrentEnvironment(),
/**
* Will shown in the left bottom corner of the main menu of Minecraft.
* null if use the id of launch version.
*/
val versionName: String? = null,
/**
* Don't know what the hell this is.
*/
var profileName: String? = null,
/**
* User custom additional minecraft command line arguments.
*/
val minecraftArgs: String? = null,
/**
* User custom additional java virtual machine command line arguments.
*/
val javaArgs: String? = null,
/**
* The minimum memory that the JVM can allocate.
*/
val minMemory: Int? = null,
/**
* The maximum memory that the JVM can allocate.
*/
val maxMemory: Int? = null,
/**
* The maximum metaspace memory that the JVM can allocate.
* For Java 7 -XX:PermSize and Java 8 -XX:MetaspaceSize
* Containing class instances.
*/
val metaspace: Int? = null,
/**
* The initial game window width
*/
val width: Int? = null,
/**
* The initial game window height
*/
val height: Int? = null,
/**
* Is inital game window fullscreen.
*/
val fullscreen: Boolean = false,
/**
* The server ip that will connect to when enter game main menu.
*/
val serverIp: String? = null,
/**
* i.e. optirun
*/
val wrapper: String? = null,
/**
* The host of the proxy address
*/
val proxyHost: String? = null,
/**
* the port of the proxy address.
*/
val proxyPort: String? = null,
/**
* The user name of the proxy, optional.
*/
val proxyUser: String? = null,
/**
* The password of the proxy, optional
*/
val proxyPass: String? = null,
/**
* Prevent game launcher from generating default JVM arguments like max memory.
*/
val noGeneratedJVMArgs: Boolean = false,
/**
* Called command line before launching the game.
*/
val precalledCommand: String? = null
) : Serializable

View File

@@ -0,0 +1,38 @@
/*
* 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.Immutable
import java.util.*
import kotlin.collections.HashMap
@Immutable
class LibrariesDownloadInfo(
@SerializedName("artifact")
val artifact: LibraryDownloadInfo? = null,
classifier_: Map<String, LibraryDownloadInfo>? = null
) {
val classifiers: Map<String, LibraryDownloadInfo>?
get() = Collections.unmodifiableMap(classifiersImpl)
@SerializedName("classifiers")
private var classifiersImpl: MutableMap<String, LibraryDownloadInfo>? =
if (classifier_ == null || classifier_.isEmpty()) null
else HashMap(classifier_)
}

View File

@@ -0,0 +1,114 @@
/*
* 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.annotations.SerializedName
import org.jackhuang.hmcl.util.*
import java.lang.reflect.Type
/**
* A class that describes a Minecraft dependency.
*
* @see LibraryDeserializer
*/
@Immutable
open class Library(
val groupId: String,
val artifactId: String,
val version: String,
classifier_: String? = null,
@SerializedName("url")
private val url: String? = null,
@SerializedName("downloads")
private val downloads: LibrariesDownloadInfo? = null,
@SerializedName("extract")
val extract: ExtractRules? = null,
@SerializedName("lateload")
val lateload: Boolean = false,
private val natives: Map<OS, String>? = null,
private val rules: List<CompatibilityRule>? = null
) {
val appliesToCurrentEnvironment: Boolean = CompatibilityRule.appliesToCurrentEnvironment(rules)
val isNative: Boolean = natives != null && appliesToCurrentEnvironment
val download: LibraryDownloadInfo
val classifier: String? = classifier_ ?: natives?.get(OS.CURRENT_OS)?.replace("\${arch}", Platform.PLATFORM.bit)
val path: String
init {
var temp: LibraryDownloadInfo? = null
if (downloads != null) {
if (isNative)
temp = downloads.classifiers?.get(classifier)
else
temp = downloads.artifact
}
path = temp?.path ?: "${groupId.replace(".", "/")}/$artifactId/$version/$artifactId-$version" + (if (classifier == null) "" else "-$classifier") + ".jar"
download = LibraryDownloadInfo(
url = temp?.url ?: (url ?: DEFAULT_LIBRARY_URL) + path,
path = path,
size = temp?.size ?: 0,
sha1 = temp?.sha1
)
}
override fun toString() = "Library[$groupId:$artifactId:$version]"
companion object LibrarySerializer : JsonDeserializer<Library>, JsonSerializer<Library> {
fun fromName(name: String, url: String? = null, downloads: LibrariesDownloadInfo? = null, extract: ExtractRules? = null, natives: Map<OS, String>? = null, rules: List<CompatibilityRule>? = null): Library {
val arr = name.split(":".toRegex(), 3)
if (arr.size != 3)
throw IllegalArgumentException("Library name is malformed. Correct example: group:artifact:version.")
return Library(
groupId = arr[0].replace("\\", "/"),
artifactId = arr[1],
version = arr[2],
url = url,
downloads = downloads,
extract = extract,
natives = natives,
rules = rules
)
}
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext): Library? {
if (json == null || json == JsonNull.INSTANCE)
return null
val jsonObject = json.asJsonObject
if (jsonObject["name"] == null)
throw JsonParseException("Library name not found.")
return fromName(jsonObject["name"].asString, jsonObject["url"]?.asString, context.deserialize(jsonObject["downloads"], LibrariesDownloadInfo::class.java),
context.deserialize(jsonObject["extract"], ExtractRules::class.java),
context.deserialize(jsonObject["natives"], typeOf<Map<OS, String>>()),
context.deserialize(jsonObject["rules"], typeOf<List<CompatibilityRule>>()))
}
override fun serialize(src: Library?, typeOfSrc: Type?, context: JsonSerializationContext): JsonElement {
if (src == null) return JsonNull.INSTANCE
val obj = JsonObject()
obj.addProperty("name", "${src.groupId}:${src.artifactId}:${src.version}")
obj.addProperty("url", src.url)
obj.add("downloads", context.serialize(src.downloads))
obj.add("extract", context.serialize(src.extract))
obj.add("natives", context.serialize(src.natives, typeOf<Map<OS, String>>()))
obj.add("rules", context.serialize(src.rules, typeOf<List<CompatibilityRule>>()))
return obj
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.Immutable
@Immutable
class LibraryDownloadInfo(
url: String = "",
sha1: String? = null,
size: Int = 0,
@SerializedName("path")
val path: String? = null
): DownloadInfo(url, sha1, size)

View File

@@ -0,0 +1,42 @@
/*
* 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.JsonSyntaxException
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.util.Immutable
import org.jackhuang.hmcl.util.Validation
@Immutable
class LoggingInfo(
@SerializedName("file")
val file: IdDownloadInfo = IdDownloadInfo(""),
@SerializedName("argument")
val argument: String = "",
@SerializedName("type")
val type: String = ""
): Validation {
override fun validate() {
file.validate()
require(argument.isNotBlank(), { "LoggingInfo" })
require(type.isNotBlank())
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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
enum class ReleaseType(val id: String) {
@SerializedName("release")
RELEASE("release"),
@SerializedName("snapshot")
SNAPSHOT("snapshot"),
@SerializedName("modified")
MODIFIED("modified"),
@SerializedName("old-beta")
OLD_BETA("old-beta"),
@SerializedName("old-alpha")
OLD_ALPHA("old-alpha"),
@SerializedName("unknown")
UNKNOWN("unknown")
}

View File

@@ -0,0 +1,34 @@
/*
* 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
open class SimpleVersionProvider() : VersionProvider {
protected val versionMap = HashMap<String, Version>()
override fun getVersion(id: String): Version {
return versionMap[id] ?: throw VersionNotFoundException("Version id $id not found")
}
override fun hasVersion(id: String): Boolean {
return versionMap.containsKey(id)
}
fun addVersion(version: Version) {
versionMap[version.id] = version
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.JsonParseException
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.util.*
import java.util.*
@Immutable
open class Version(
@SerializedName("minecraftArguments")
val minecraftArguments: String? = null,
@SerializedName("mainClass")
val mainClass: String? = null,
@SerializedName("time")
val time: Date = Date(),
@SerializedName("id")
val id: String = "",
@SerializedName("type")
val type: ReleaseType = ReleaseType.UNKNOWN,
@SerializedName("releaseTime")
val releaseTime: Date = Date(),
@SerializedName("jar")
val jar: String? = null,
@SerializedName("inheritsFrom")
val inheritsFrom: String? = null,
@SerializedName("assets")
private val assets: String? = null,
@SerializedName("minimumLauncherVersion")
val minimumLauncherVersion: Int = 0,
@SerializedName("assetIndex")
private val assetIndex: AssetIndexInfo? = null,
libraries: List<Library> = emptyList(),
compatibilityRules: List<CompatibilityRule>? = null,
downloads: Map<DownloadType, DownloadInfo>? = null,
logging: Map<DownloadType, LoggingInfo>? = null
): Comparable<Version>, Validation {
val downloads: Map<DownloadType, DownloadInfo>? get() = unmodifiableMap(downloadsImpl)
@SerializedName("downloads")
private val downloadsImpl: MutableMap<DownloadType, DownloadInfo>? = copyMap(downloads)
val logging: Map<DownloadType, LoggingInfo>? get() = unmodifiableMap(loggingImpl)
@SerializedName("logging")
private val loggingImpl: MutableMap<DownloadType, LoggingInfo>? = copyMap(logging)
val libraries: List<Library> get() = Collections.unmodifiableList(librariesImpl)
@SerializedName("libraries")
private val librariesImpl: MutableList<Library> = LinkedList(libraries)
val compatibilityRules: List<CompatibilityRule>? get() = unmodifiableList(compatibilityRulesImpl)
@SerializedName("compatibilityRules")
private val compatibilityRulesImpl: MutableList<CompatibilityRule>? = copyList(compatibilityRules)
val download: DownloadInfo
get() {
val client = downloads?.get(DownloadType.CLIENT)
val jar = this.jar ?: this.id
if (client == null) {
return DownloadInfo("$DEFAULT_VERSION_DOWNLOAD_URL$jar/$jar.jar")
} else {
return client
}
}
val actualAssetIndex: AssetIndexInfo
get() {
val id = assets ?: "legacy"
return assetIndex ?: AssetIndexInfo(id = id, url = "$DEFAULT_VERSION_DOWNLOAD_URL$id.json")
}
fun appliesToCurrentEnvironment() = CompatibilityRule.appliesToCurrentEnvironment(compatibilityRules)
override fun hashCode(): Int = id.hashCode()
override fun equals(other: Any?): Boolean =
if (other is Version) Objects.equals(this.id, other.id)
else false
override fun compareTo(other: Version) = id.compareTo(other.id)
open fun install(id: String): Boolean {
return false
}
/**
* Resolve given version
*
* @throws CircleDependencyException
*/
fun resolve(provider: VersionProvider): Version =
resolve(provider, HashSet<String>())
protected open fun resolve(provider: VersionProvider, resolvedSoFar: MutableSet<String>): Version {
if (this.inheritsFrom == null)
return this
if (!resolvedSoFar.add(this.id))
throw CircleDependencyException(resolvedSoFar.toString())
// It is supposed to auto install an version in getVersion.
val parent = provider.getVersion(this.inheritsFrom).resolve(provider, resolvedSoFar)
return Version(
id = this.id,
jar = this.jar ?: parent.jar,
time = this.time,
type = this.type,
assets = this.assets ?: parent.assets,
logging = this.logging ?: parent.logging,
mainClass = this.mainClass ?: parent.mainClass,
libraries = merge(this.libraries, parent.libraries),
downloads = this.downloads ?: parent.downloads,
assetIndex = this.assetIndex ?: parent.assetIndex,
releaseTime = this.releaseTime,
inheritsFrom = null,
minecraftArguments = this.minecraftArguments ?: parent.minecraftArguments,
minimumLauncherVersion = parent.minimumLauncherVersion
)
}
fun copy(
minecraftArguments: String? = this.minecraftArguments,
mainClass: String? = this.mainClass,
time: Date = this.time,
releaseTime: Date = this.releaseTime,
id: String = this.id,
type: ReleaseType = this.type,
jar: String? = this.jar,
inheritsFrom: String? = this.inheritsFrom,
assets: String? = this.assets,
minimumLauncherVersion: Int = this.minimumLauncherVersion,
assetIndex: AssetIndexInfo? = this.assetIndex,
libraries: List<Library> = this.librariesImpl,
compatibilityRules: List<CompatibilityRule>? = this.compatibilityRulesImpl,
downloads: Map<DownloadType, DownloadInfo>? = this.downloads,
logging: Map<DownloadType, LoggingInfo>? = this.logging) =
Version(minecraftArguments,
mainClass,
time,
id,
type,
releaseTime,
jar,
inheritsFrom,
assets,
minimumLauncherVersion,
assetIndex,
libraries,
compatibilityRules,
downloads,
logging
)
override fun validate() {
if (id.isBlank())
throw JsonParseException("Version ID cannot be blank")
(downloadsImpl as Map<*, *>?)?.forEach { (key, value) ->
if (key !is DownloadType)
throw JsonParseException("Version downloads key must be DownloadType")
if (value !is DownloadInfo)
throw JsonParseException("Version downloads value must be DownloadInfo")
}
(loggingImpl as Map<*, *>?)?.forEach { (key, value) ->
if (key !is DownloadType)
throw JsonParseException("Version logging key must be DownloadType")
if (value !is LoggingInfo)
throw JsonParseException("Version logging value must be DownloadInfo")
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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
class VersionNotFoundException : Exception {
constructor() : super() {}
constructor(message: String) : super(message) {}
constructor(message: String, cause: Throwable) : super(message, cause) {}
}

View File

@@ -0,0 +1,39 @@
/*
* 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
/**
* Supports version accessing.
*
* @see Version.resolve
*/
interface VersionProvider {
/**
* Does the version of id exist?
* @param id the id of version
* @return true if the version exists
*/
fun hasVersion(id: String): Boolean
/**
* Get the version
* @param id the id of version
* @return the version you want
*/
fun getVersion(id: String): Version
}

View File

@@ -0,0 +1,281 @@
/*
* 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.launch
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.concurrent.thread
/**
* @param version A resolved version(calling [Version.resolve])
* @param account The user account
* @param options The launching configuration
*/
open class DefaultLauncher(repository: GameRepository, version: Version, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
: Launcher(repository, version, account, options, listener, isDaemon) {
protected val native: File by lazy { repository.getNativeDirectory(version.id) }
init {
if (version.inheritsFrom != null)
throw IllegalArgumentException("Version must be resolved")
if (version.minecraftArguments == null)
throw NullPointerException("Version minecraft argument can not be null")
if (version.mainClass == null || version.mainClass.isBlank())
throw NullPointerException("Version main class can not be null")
}
/**
* Note: the [account] must have logged in when calling this property
*/
override val rawCommandLine: List<String> by lazy {
val res = LinkedList<String>()
// Executable
if (options.wrapper != null && options.wrapper.isNotBlank())
res.add(options.wrapper)
res.add(options.java.binary.toString())
if (options.javaArgs != null && options.javaArgs.isNotBlank())
res.addAll(options.javaArgs.tokenize())
// JVM Args
if (!options.noGeneratedJVMArgs) {
appendJvmArgs(res)
res.add("-Dminecraft.client.jar=${repository.getVersionJar(version)}")
if (OS.CURRENT_OS == OS.OSX) {
res.add("-Xdock:name=Minecraft ${version.id}")
res.add("-Xdock:icon=" + repository.getAssetObject(version.actualAssetIndex.id, "icons/minecraft.icns").absolutePath);
}
val logging = version.logging
if (logging != null) {
val loggingInfo = logging[DownloadType.CLIENT]
if (loggingInfo != null) {
val loggingFile = repository.getLoggingObject(version.actualAssetIndex.id, loggingInfo)
if (loggingFile.exists())
res.add(loggingInfo.argument.replace("\${path}", loggingFile.absolutePath))
}
}
if (OS.CURRENT_OS == OS.WINDOWS)
res.add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump")
else
res.add("-Duser.home=${options.gameDir.parent}")
if (options.java.version >= JavaVersion.JAVA_7)
res.add("-XX:+UseG1GC")
else
res.add("-Xincgc")
if (options.metaspace != null && options.metaspace > 0) {
if (options.java.version < JavaVersion.JAVA_8)
res.add("-XX:PermSize=${options.metaspace}m")
else
res.add("-XX:MetaspaceSize=${options.metaspace}m")
}
res.add("-XX:-UseAdaptiveSizePolicy")
res.add("-XX:-OmitStackTraceInFastThrow")
res.add("-Xmn128m")
if (options.maxMemory != null && options.maxMemory > 0)
res.add("-Xmx${options.maxMemory}m")
if (options.minMemory != null && options.minMemory > 0)
res.add("-Xms${options.minMemory}m")
res.add("-Dfml.ignoreInvalidMinecraftCertificates=true");
res.add("-Dfml.ignorePatchDiscrepancies=true");
}
// Classpath
res.add("-Djava.library.path=${native.absolutePath}")
val lateload = LinkedList<File>()
val classpath = StringBuilder()
for (library in version.libraries)
if (library.appliesToCurrentEnvironment && !library.isNative) {
val f = repository.getLibraryFile(version, library)
if (f.exists() && f.isFile) {
if (library.lateload)
lateload += f
else classpath.append(f.absolutePath).append(OS.PATH_SEPARATOR)
}
}
for (library in lateload)
classpath.append(library.absolutePath).append(OS.PATH_SEPARATOR)
val jar = repository.getVersionJar(version)
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.actualAssetIndex.id)
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)
)
}
// Optional Minecraft arguments
if (options.height != null && options.width != null) {
res.add("--height");
res.add(options.height.toString())
res.add("--width");
res.add(options.width.toString())
}
if (options.serverIp != null) {
val args = options.serverIp.split(":")
res.add("--server")
res.add(args[0])
res.add("--port")
res.add(if (args.size > 1) args[1] else "25565")
}
if (options.fullscreen)
res.add("--fullscreen")
if (options.proxyHost != null && options.proxyHost.isNotBlank() &&
options.proxyPort != null && options.proxyPort.isNotBlank()) {
res.add("--proxyHost");
res.add(options.proxyHost)
res.add("--proxyPort");
res.add(options.proxyPort)
if (options.proxyUser != null && options.proxyUser.isNotBlank() &&
options.proxyPass != null && options.proxyPass.isNotBlank()) {
res.add("--proxyUser");
res.add(options.proxyUser);
res.add("--proxyPass");
res.add(options.proxyPass);
}
}
if (options.minecraftArgs != null && options.minecraftArgs.isNotBlank())
res.addAll(options.minecraftArgs.tokenize())
res
}
/**
* Do something here.
* i.e.
* -Dminecraft.launcher.version=<Your launcher name>
* -Dminecraft.launcher.brand=<Your launcher version>
* -Dlog4j.configurationFile=<Your custom log4j configuration
*/
protected open fun appendJvmArgs(res: MutableList<String>) {}
open fun decompressNatives() {
version.libraries.filter { it.isNative }.forEach { library ->
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
unzip(zip = repository.getLibraryFile(version, library),
dest = native,
callback = if (library.extract == null) null
else library.extract!!::shouldExtract,
ignoreExistsFile = false)
}
}
override fun launch(): JavaProcess {
// To guarantee that when failed to generate code, we will not call precalled command
val builder = ProcessBuilder(rawCommandLine)
decompressNatives()
if (options.precalledCommand != null && options.precalledCommand.isNotBlank()) {
val process = Runtime.getRuntime().exec(options.precalledCommand)
ignoreException {
if (process.isAlive)
process.waitFor()
}
}
builder.directory(repository.getRunDirectory(version.id))
.environment().put("APPDATA", options.gameDir.parent)
val p = JavaProcess(builder.start(), rawCommandLine)
if (listener == null)
startMonitors(p)
else
startMonitors(p, listener, isDaemon)
return p
}
override fun makeLaunchScript(file: String): File {
val isWindows = OS.WINDOWS == OS.CURRENT_OS
val scriptFile = File(file + (if (isWindows) ".bat" else ".sh"))
if (!scriptFile.makeFile())
throw IOException("Script file: $scriptFile cannot be created.")
scriptFile.bufferedWriter().use { writer ->
if (isWindows) {
writer.write("@echo off");
writer.newLine()
writer.write("set APPDATA=" + options.gameDir.parent);
writer.newLine()
writer.write("cd /D %APPDATA%");
writer.newLine()
}
if (options.precalledCommand != null && options.precalledCommand.isNotBlank()) {
writer.write(options.precalledCommand)
writer.newLine()
}
writer.write(makeCommand(rawCommandLine))
}
if (!scriptFile.setExecutable(true))
throw IOException("Cannot make script file '$scriptFile' executable.")
return scriptFile
}
private fun startMonitors(javaProcess: JavaProcess) {
thread(name = "stdout-pump", isDaemon = true, block = StreamPump(javaProcess.process.inputStream)::run)
thread(name = "stderr-pump", isDaemon = true, block = StreamPump(javaProcess.process.errorStream)::run)
}
private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, processListener::onLog)::run)
thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.errorStream, processListener::onErrorLog)::run)
thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess.process, processListener::onExit)::run)
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.launch
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.util.JavaProcess
import java.io.File
abstract class Launcher(
protected val repository: GameRepository,
protected val version: Version,
protected val account: AuthInfo,
protected val options: LaunchOptions,
protected val listener: ProcessListener? = null,
protected val isDaemon: Boolean = true) {
abstract val rawCommandLine: List<String>
abstract fun launch(): JavaProcess
/**
* @param file The file path without extension
* @return the actual file
*/
abstract fun makeLaunchScript(file: String): File
}

View File

@@ -0,0 +1,41 @@
/*
* 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.launch
interface ProcessListener {
/**
* Called when receiving a log from stdout
*
* @param log the log
*/
fun onLog(log: String)
/**
* Called when receiving a log from stderr.
*
* @param log the log
*/
fun onErrorLog(log: String)
/**
* Called when the game process stops.
*
* @param exitCode the exit code
*/
fun onExit(exitCode: Int)
}

View File

@@ -0,0 +1,71 @@
/*
* 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.mod
import com.google.gson.JsonParseException
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.parseParams
import org.jackhuang.hmcl.util.readFullyAsString
import org.jackhuang.hmcl.util.typeOf
import java.io.File
import java.util.zip.ZipFile
class ForgeModMetadata(
@SerializedName("modid")
val modId: String = "",
val name: String = "",
val description: String = "",
val author: String = "",
val version: String = "",
val mcversion: String = "",
val url: String = "",
val updateUrl: String = "",
val credits: String = "",
val authorList: Array<String> = emptyArray(),
val authors: Array<String> = emptyArray()
) {
companion object {
fun fromFile(modFile: File): ModInfo {
ZipFile(modFile).use {
val entry = it.getEntry("mcmod.info") ?: throw JsonParseException("File $modFile is not a Forge mod.")
val modList: List<ForgeModMetadata>? = GSON.fromJson(it.getInputStream(entry).readFullyAsString(), typeOf<List<ForgeModMetadata>>())
val metadata = modList?.firstOrNull() ?: throw JsonParseException("Mod $modFile 'mcmod.info' is malformed")
var authors: String = metadata.author
if (authors.isBlank() && metadata.authors.isNotEmpty()) {
authors = parseParams("", metadata.authors, ", ")
}
if (authors.isBlank() && metadata.authorList.isNotEmpty()) {
authors = parseParams("", metadata.authorList, ", ")
}
if (authors.isBlank())
authors = metadata.credits
return ModInfo(
file = modFile,
name = metadata.name,
description = metadata.description,
authors = authors,
version = metadata.version,
mcversion = metadata.mcversion,
url = if (metadata.url.isBlank()) metadata.updateUrl else metadata.url
)
}
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.mod
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.util.*
import java.io.File
import java.util.zip.ZipFile
class LiteModMetadata (
val name: String = "",
val version: String = "",
val mcversion: String = "",
val revision: String = "",
val author: String = "",
val classTransformerClasses: String = "",
val description: String = "",
val modpackName: String = "",
val modpackVersion: String = "",
val checkUpdateUrl: String = "",
val updateURI: String = ""
) {
companion object {
fun fromFile(modFile: File): ModInfo {
ZipFile(modFile).use {
val entry = it.getEntry("litemod.json")
requireNotNull(entry, { "File $modFile is not a LiteLoader mod." })
val modList: LiteModMetadata? = GSON.fromJson<LiteModMetadata>(it.getInputStream(entry).readFullyAsString())
val metadata = modList ?: throw JsonParseException("Mod $modFile 'litemod.json' is malformed")
return ModInfo(
file = modFile,
name = metadata.name,
description = metadata.description,
authors = metadata.author,
version = metadata.version,
mcversion = metadata.mcversion,
url = metadata.updateURI
)
}
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.mod
import com.google.gson.JsonParseException
import java.io.File
class ModInfo (
val file: File,
val name: String,
val description: String = "",
val authors: String = "",
val version: String = "",
val mcversion: String = "",
val url: String = ""
): Comparable<ModInfo> {
val isActive: Boolean
get() = file.extension != DISABLED_EXTENSION
val fileName: String = (if (isActive) file.name else file.nameWithoutExtension).substringBeforeLast(".")
override fun compareTo(other: ModInfo): Int {
return fileName.compareTo(other.fileName)
}
companion object {
val DISABLED_EXTENSION = "disabled"
fun isFileMod(file: File): Boolean {
var name = file.name
val disabled = name.endsWith(".disabled")
if (disabled)
name = name.substringBeforeLast(".disabled")
return name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith("litemod")
}
fun fromFile(modFile: File): ModInfo {
val file = if (modFile.extension == DISABLED_EXTENSION)
modFile.absoluteFile.parentFile.resolve(modFile.nameWithoutExtension)
else modFile
if (file.extension == "zip" || file.extension == "jar")
try {
return ForgeModMetadata.fromFile(modFile)
} catch (e: JsonParseException) {
throw e
} catch (ignore: Exception) {}
else if (file.extension == "litemod")
try {
return LiteModMetadata.fromFile(modFile)
} catch (e: JsonParseException) {
throw e
} catch (ignore: Exception) {}
else throw IllegalArgumentException("File $modFile is not mod")
return ModInfo(file = modFile, name = modFile.nameWithoutExtension, description = "Unrecognized mod file")
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.mod
import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.util.SimpleMultimap
import org.jackhuang.hmcl.util.asVersion
import org.jackhuang.hmcl.util.ignoreException
import org.jackhuang.hmcl.util.makeDirectory
import java.io.File
import java.io.IOException
import java.util.*
class ModManager(private val repository: GameRepository) {
private val modCache = SimpleMultimap<String, ModInfo>(::HashMap, ::TreeSet)
fun refreshMods(id: String): Collection<ModInfo> {
val modsDirectory = repository.getRunDirectory(id).resolve("mods")
val puter = { modFile: File -> ignoreException { modCache.put(id, ModInfo.fromFile(modFile)) } }
modsDirectory.listFiles()?.forEach { modFile ->
if (modFile.isDirectory && modFile.name.asVersion() != null)
modFile.listFiles()?.forEach(puter)
puter(modFile)
}
return modCache[id]
}
fun getMods(id: String) : Collection<ModInfo> {
if (!modCache.containsKey(id))
refreshMods(id)
return modCache[id] ?: emptyList()
}
fun addMod(id: String, file: File) {
if (!ModInfo.isFileMod(file))
throw IllegalArgumentException("File $file is not a valid mod file.")
if (!modCache.containsKey(id))
refreshMods(id)
val modsDirectory = repository.getRunDirectory(id).resolve("mods")
if (!modsDirectory.makeDirectory())
throw IOException("Cannot make directory $modsDirectory")
val newFile = modsDirectory.resolve(file.name)
file.copyTo(newFile)
modCache.put(id, ModInfo.fromFile(newFile))
}
fun removeMods(id: String, vararg modInfos: ModInfo): Boolean =
modInfos.fold(true, { acc, modInfo -> acc && modInfo.file.delete() }).apply { refreshMods(id) }
}

View File

@@ -0,0 +1,26 @@
/*
* 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.mod
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.task.Task
import java.io.File
class Modpack(val file: File) {
}

View File

@@ -0,0 +1,22 @@
/*
* 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.mod
object ModpackManager {
}

View File

@@ -0,0 +1,40 @@
/*
* 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.task
private class CoupleTask<P: Task>(private val pred: P, private val succ: Task.(P) -> Task?, override val reliant: Boolean) : Task() {
override val hidden: Boolean = true
override val dependents: Collection<Task> = listOf(pred)
override val dependencies: MutableCollection<Task> = mutableListOf()
override fun execute() {
val task = this.succ(pred)
if (task != null)
dependencies += task
}
}
infix fun <T: Task> T.then(b: Task): Task = CoupleTask(this, { b }, true)
/**
* @param b A runnable that decides what to do next, You can also do something here.
*/
infix fun <T: Task> T.then(b: Task.(T) -> Task?): Task = CoupleTask(this, b, true)
infix fun Task.with(b: Task): Task = CoupleTask(this, { b }, false)

View File

@@ -0,0 +1,156 @@
/*
* 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.task
import org.jackhuang.hmcl.event.EventManager
import org.jackhuang.hmcl.event.FailedEvent
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.closeQuietly
import org.jackhuang.hmcl.util.DigestUtils
import org.jackhuang.hmcl.util.makeDirectory
import org.jackhuang.hmcl.util.createConnection
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.RandomAccessFile
import java.net.Proxy
import java.net.URL
import java.math.BigInteger
import java.util.logging.Level
class FileDownloadTask(val url: URL, val file: File, val hash: String? = null, val retry: Int = 5, val proxy: Proxy = Proxy.NO_PROXY): Task() {
override val scheduler: Scheduler = Scheduler.IO_THREAD
var onFailed = EventManager<FailedEvent<URL>>()
private var rFile: RandomAccessFile? = null
private var stream: InputStream? = null
fun closeFiles() {
rFile?.closeQuietly()
rFile = null
stream?.closeQuietly()
stream = null
}
override fun execute() {
var currentURL = url
LOG.finer("Downloading: $currentURL, to: $file")
var exception: Exception? = null
for (repeat in 0 until retry) {
if (repeat > 0) {
val event = FailedEvent(this, repeat, currentURL)
onFailed(event)
if (currentURL != event.newResult) {
LOG.fine("Switch from: $currentURL to: ${event.newResult}")
currentURL = event.newResult
}
}
if (Thread.interrupted()) {
Thread.currentThread().interrupt()
break
}
var temp: File? = null
try {
updateProgress(0.0)
val conn = url.createConnection(proxy)
conn.connect()
if (conn.responseCode / 100 != 2)
throw IOException("Server error, response code: ${conn.responseCode}")
val contentLength = conn.contentLength
if (contentLength < 1)
throw IOException("The content length is invalid")
if (!file.absoluteFile.parentFile.makeDirectory())
throw IOException("Could not make directory: ${file.absoluteFile.parent}")
temp = createTempFile("HMCLCore")
rFile = RandomAccessFile(temp, "rw")
val digest = DigestUtils.sha1Digest
stream = conn.inputStream
var lastDownloaded = 0
var downloaded = 0
var lastTime = System.currentTimeMillis()
val buf = ByteArray(4096)
while (true) {
if (Thread.interrupted()) {
Thread.currentThread().interrupt()
break
}
val read = stream!!.read(buf)
if (read == -1)
break
if (hash != null)
digest.update(buf, 0, read)
rFile!!.write(buf, 0, read)
downloaded += read
updateProgress(downloaded, contentLength)
val now = System.currentTimeMillis()
if (now - lastTime >= 1000L) {
updateMessage(((downloaded - lastDownloaded) / 1024).toString() + "KB/s")
lastDownloaded = downloaded
lastTime = now
}
}
closeFiles()
if (Thread.interrupted()) {
temp.delete()
Thread.currentThread().interrupt()
break
} else {
if (file.exists())
file.delete()
if (!file.absoluteFile.parentFile.makeDirectory())
throw IOException("Cannot make parent directory $file")
if (!temp.renameTo(file))
throw IOException("Cannot move temp file to $file")
}
check(downloaded == contentLength, { "Unexpected file size: $downloaded, expected: $contentLength" })
if (hash != null) {
val hashCode = String.format("%1$040x", BigInteger(1, digest.digest()))
check(hash.equals(hashCode, ignoreCase = true), { "Unexpected hash code: $hashCode, expected: $hash" })
}
return
} catch(e: Exception) {
temp?.delete()
exception = e
LOG.log(Level.WARNING, "Unable to download file $currentURL", e)
} finally {
closeFiles()
}
}
if (exception != null)
throw exception
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.task
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.createConnection
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.net.Proxy
import java.net.URL
import java.nio.charset.Charset
class GetTask(val url: URL, val encoding: Charset = Charsets.UTF_8, private val retry: Int = 5, private val proxy: Proxy = Proxy.NO_PROXY): TaskResult<String>() {
override val scheduler: Scheduler = Scheduler.IO_THREAD
override fun execute() {
var exception: IOException? = null
for (time in 0 until retry) {
if (time > 0)
LOG.warning("Unable to finish downloading $url, retrying time: $time")
try {
updateProgress(0.0)
val conn = url.createConnection(proxy)
val input = conn.inputStream
val baos = ByteArrayOutputStream()
val buf = ByteArray(4096)
val size = conn.contentLength
var read = 0
while (true) {
val len = input.read(buf)
if (len == -1)
break
read += len
baos.write(buf, 0, len)
updateProgress(read, size)
if (Thread.currentThread().isInterrupted)
return
}
result = baos.toString(encoding.name())
return
} catch (e: IOException) {
exception = e
}
}
if (exception != null)
throw exception
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.task
class ParallelTask(vararg tasks: Task): Task() {
override val hidden: Boolean = true
override val dependents: Collection<Task> = listOf(*tasks)
override fun execute() {}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.task
import javafx.application.Platform
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
import javax.swing.SwingUtilities
interface Scheduler {
fun schedule(block: Runnable): Future<*>?
companion object Schedulers {
val IMMEDIATE = object : Scheduler {
override fun schedule(block: Runnable): Future<*>? {
block.run()
return null
}
}
val JAVAFX: Scheduler = object : Scheduler {
override fun schedule(block: Runnable): Future<*>? {
Platform.runLater(block)
return null
}
}
val SWING: Scheduler = object : Scheduler {
override fun schedule(block: Runnable): Future<*>? {
SwingUtilities.invokeLater(block)
return null
}
}
val NEW_THREAD: Scheduler = object : Scheduler {
override fun schedule(block: Runnable) = CACHED_EXECUTOR.submit(block)
}
val IO_THREAD: Scheduler = object : Scheduler {
override fun schedule(block: Runnable) = IO_EXECUTOR.submit(block)
}
val DEFAULT = NEW_THREAD
private val CACHED_EXECUTOR = Executors.newCachedThreadPool()
private val IO_EXECUTOR: ExecutorService by lazy {
Executors.newFixedThreadPool(6, { r: Runnable ->
val thread: Thread = Executors.defaultThreadFactory().newThread(r)
thread.isDaemon = true
thread
})
}
fun shutdown() {
CACHED_EXECUTOR.shutdown()
IO_EXECUTOR.shutdown()
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.task
class SilentException : Exception {
constructor() : super() {}
constructor(message: String) : super(message) {}
constructor(message: String, cause: Throwable) : super(message, cause) {}
}

View File

@@ -0,0 +1,24 @@
/*
* 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.task
internal class SimpleTask(private val runnable: () -> Unit, override val scheduler: Scheduler = Scheduler.DEFAULT) : Task() {
override fun execute() {
runnable()
}
}

View File

@@ -0,0 +1,123 @@
/*
* 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.task
import javafx.beans.property.ReadOnlyDoubleWrapper
import javafx.beans.property.ReadOnlyStringWrapper
import org.jackhuang.hmcl.event.EventManager
import org.jackhuang.hmcl.util.*
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicReference
/**
* Disposable task.
*/
abstract class Task {
/**
* True if not logging when executing this task.
*/
open val hidden: Boolean = false
/**
* The scheduler that decides how this task runs.
*/
open val scheduler: Scheduler = Scheduler.DEFAULT
/**
* True if requires all dependent tasks finishing successfully.
*
* **Note** if this field is set false, you are not supposed to invoke [run]
*/
open val reliant: Boolean = true
var title: String = this.javaClass.toString()
/**
* @see Thread.isInterrupted
* @throws InterruptedException if current thread is interrupted
*/
@Throws(Exception::class)
abstract fun execute()
infix fun parallel(couple: Task): Task = ParallelTask(this, couple)
/**
* The collection of sub-tasks that should execute before this task running.
*/
open val dependents: Collection<Task> = emptySet()
/**
* The collection of sub-tasks that should execute after this task running.
*/
open val dependencies: Collection<Task> = emptySet()
protected open val progressInterval = 1000L
private var lastTime = Long.MIN_VALUE
private val progressUpdate = AtomicReference<Double>()
val progressProperty = ReadOnlyDoubleWrapper(this, "progress", 0.0)
protected fun updateProgress(progress: Int, total: Int) = updateProgress(1.0 * progress / total)
protected fun updateProgress(progress: Double) {
val now = System.currentTimeMillis()
if (now - lastTime >= progressInterval) {
progressProperty.updateAsync(progress, progressUpdate)
lastTime = now
}
}
private val messageUpdate = AtomicReference<String>()
val messageProperty = ReadOnlyStringWrapper(this, "message", null)
protected fun updateMessage(newMessage: String) = messageProperty.updateAsync(newMessage, messageUpdate)
val onDone = EventManager<TaskEvent>()
/**
* **Note** reliant does not work here, which is always treated as true here.
*/
@Throws(Exception::class)
fun run() {
dependents.forEach(subTaskRunnable)
execute()
dependencies.forEach(subTaskRunnable)
onDone(TaskEvent(this, this, false))
}
private val subTaskRunnable = { task: Task ->
this.messageProperty.bind(task.messageProperty)
this.progressProperty.bind(task.progressProperty)
task.run()
this.messageProperty.unbind()
this.progressProperty.unbind()
}
fun executor() = TaskExecutor().submit(this)
fun subscribe(subscriber: Task) {
executor().submit(subscriber).start()
}
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(closure, scheduler))
override fun toString(): String {
return title
}
companion object {
fun of(closure: () -> Unit, scheduler: Scheduler = Scheduler.DEFAULT): Task = SimpleTask(closure, scheduler)
fun <V> of(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.task
import java.util.concurrent.Callable
internal class TaskCallable<V>(private val callable: Callable<V>) : TaskResult<V>() {
override fun execute() {
result = callable.call()
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.task
import java.util.*
class TaskEvent(source: Any, val task: Task, val failed: Boolean) : EventObject(source)

View File

@@ -0,0 +1,156 @@
/*
* 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.task
import org.jackhuang.hmcl.util.LOG
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import kotlin.concurrent.thread
class TaskExecutor() {
var taskListener: TaskListener? = null
var canceled = false
private set
private val totTask = AtomicInteger(0)
private val taskQueue = ConcurrentLinkedQueue<Task>()
private val workerQueue = ConcurrentLinkedQueue<Future<*>>()
/**
* Submit a task to subscription to run.
* You can submit a task even when started this subscription.
* Thread-safe function.
*/
fun submit(task: Task): TaskExecutor {
taskQueue.add(task)
return this
}
/**
* Start the subscription and run all registered tasks asynchronously.
*/
fun start() {
thread {
totTask.addAndGet(taskQueue.size)
while (!taskQueue.isEmpty() && !canceled) {
val task = taskQueue.poll()
if (task != null) {
val future = task.scheduler.schedule(Runnable { executeTask(task) })
try {
future?.get()
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
break
}
}
}
if (!canceled)
taskListener?.onTerminate()
}
}
/**
* Cancel the subscription ant interrupt all tasks.
*/
fun cancel() {
canceled = true
while (!workerQueue.isEmpty())
workerQueue.poll()?.cancel(true)
}
private fun executeTasks(tasks: Collection<Task>): Boolean {
if (tasks.isEmpty())
return true
totTask.addAndGet(tasks.size)
val success = AtomicBoolean(true)
val counter = CountDownLatch(tasks.size)
for (task in tasks) {
if (canceled)
return false
val invoker = Invoker(task, counter, success)
val future = task.scheduler.schedule(invoker)
if (future != null)
workerQueue.add(future)
}
try {
counter.await()
return success.get()
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
// Once interrupted, we are aborting the subscription.
// and operations fail.
return false
}
}
private fun executeTask(t: Task): Boolean {
if (canceled)
return false
if (!t.hidden)
LOG.fine("Executing task: ${t.title}")
taskListener?.onReady(t)
val doDependentsSucceeded = executeTasks(t.dependents)
var flag = false
try {
if (!doDependentsSucceeded && t.reliant)
throw SilentException()
t.execute()
flag = true
if (!t.hidden)
LOG.finer("Task finished: ${t.title}")
executeTasks(t.dependencies)
if (!t.hidden) {
t.onDone(TaskEvent(source = this, task = t, failed = false))
taskListener?.onFinished(t)
}
} catch (e: InterruptedException) {
if (!t.hidden) {
LOG.log(Level.FINE, "Task aborted: ${t.title}", e)
t.onDone(TaskEvent(source = this, task = t, failed = true))
taskListener?.onFailed(t)
}
} catch (e: SilentException) {
// nothing here
} catch (e: Exception) {
if (!t.hidden) {
LOG.log(Level.SEVERE, "Task failed: ${t.title}", e)
t.onDone(TaskEvent(source = this, task = t, failed = true))
taskListener?.onFailed(t)
}
}
return flag
}
private inner class Invoker(val task: Task, val latch: CountDownLatch, val boolean: AtomicBoolean): Runnable {
override fun run() {
try {
Thread.currentThread().name = task.title
if (!executeTask(task))
boolean.set(false)
} finally {
latch.countDown()
}
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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.task
import java.util.*
interface TaskListener : EventListener {
fun onReady(task: Task)
fun onFinished(task: Task)
fun onFailed(task: Task)
fun onTerminate()
}

View File

@@ -0,0 +1,22 @@
/*
* 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.task
abstract class TaskResult<V> : Task() {
open var result: V? = null
}

View File

@@ -0,0 +1,22 @@
/*
* 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.util
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Immutable

View File

@@ -0,0 +1,47 @@
/*
* 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.util
import javafx.application.Platform
import java.awt.EventQueue
import java.nio.charset.Charset
val DEFAULT_LIBRARY_URL = "https://libraries.minecraft.net/"
val DEFAULT_VERSION_DOWNLOAD_URL = "http://s3.amazonaws.com/Minecraft.Download/versions/"
val DEFAULT_INDEX_URL = "http://s3.amazonaws.com/Minecraft.Download/indexes/"
val SWING_UI_THREAD_SCHEDULER = { runnable: () -> Unit ->
if (EventQueue.isDispatchThread())
runnable()
else
EventQueue.invokeLater(runnable)
}
val JAVAFX_UI_THREAD_SCHEDULER = { runnable: () -> Unit ->
if (Platform.isFxApplicationThread())
runnable()
else
Platform.runLater(runnable)
}
val UI_THREAD_SCHEDULER: (() -> Unit) -> Unit = { }
val DEFAULT_ENCODING = "UTF-8"
val DEFAULT_CHARSET = Charsets.UTF_8
val SYSTEM_CHARSET: Charset by lazy { charset(OS.ENCODING) }

View File

@@ -0,0 +1,242 @@
/*
* 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.util
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* @author huangyuhui
*/
object DigestUtils {
private val STREAM_BUFFER_LENGTH = 1024
@Throws(IOException::class)
private fun digest(digest: MessageDigest, data: InputStream): ByteArray {
return updateDigest(digest, data).digest()
}
fun getDigest(algorithm: String): MessageDigest {
try {
return MessageDigest.getInstance(algorithm)
} catch (e: NoSuchAlgorithmException) {
throw IllegalArgumentException(e)
}
}
val md2Digest: MessageDigest
get() = getDigest("MD2")
val md5Digest: MessageDigest
get() = getDigest("MD5")
val sha1Digest: MessageDigest
get() = getDigest("SHA-1")
val sha256Digest: MessageDigest
get() = getDigest("SHA-256")
val sha384Digest: MessageDigest
get() = getDigest("SHA-384")
val sha512Digest: MessageDigest
get() = getDigest("SHA-512")
fun md2(data: ByteArray): ByteArray {
return md2Digest.digest(data)
}
@Throws(IOException::class)
fun md2(data: InputStream): ByteArray {
return digest(md2Digest, data)
}
fun md2(data: String): ByteArray {
return md2(data.toByteArray(Charsets.UTF_8))
}
fun md2Hex(data: ByteArray): String {
return Hex.encodeHexString(md2(data))
}
@Throws(IOException::class)
fun md2Hex(data: InputStream): String {
return Hex.encodeHexString(md2(data))
}
fun md2Hex(data: String): String {
return Hex.encodeHexString(md2(data))
}
fun md5(data: ByteArray): ByteArray {
return md5Digest.digest(data)
}
@Throws(IOException::class)
fun md5(data: InputStream): ByteArray {
return digest(md5Digest, data)
}
fun md5(data: String): ByteArray {
return md5(data.toByteArray(Charsets.UTF_8))
}
fun md5Hex(data: ByteArray): String {
return Hex.encodeHexString(md5(data))
}
@Throws(IOException::class)
fun md5Hex(data: InputStream): String {
return Hex.encodeHexString(md5(data))
}
fun md5Hex(data: String): String {
return Hex.encodeHexString(md5(data))
}
fun sha1(data: ByteArray): ByteArray {
return sha1Digest.digest(data)
}
@Throws(IOException::class)
fun sha1(data: InputStream): ByteArray {
return digest(sha1Digest, data)
}
fun sha1(data: String): ByteArray {
return sha1(data.toByteArray(Charsets.UTF_8))
}
fun sha1Hex(data: ByteArray): String {
return Hex.encodeHexString(sha1(data))
}
@Throws(IOException::class)
fun sha1Hex(data: InputStream): String {
return Hex.encodeHexString(sha1(data))
}
fun sha1Hex(data: String): String {
return Hex.encodeHexString(sha1(data))
}
fun sha256(data: ByteArray): ByteArray {
return sha256Digest.digest(data)
}
@Throws(IOException::class)
fun sha256(data: InputStream): ByteArray {
return digest(sha256Digest, data)
}
fun sha256(data: String): ByteArray {
return sha256(data.toByteArray(Charsets.UTF_8))
}
fun sha256Hex(data: ByteArray): String {
return Hex.encodeHexString(sha256(data))
}
@Throws(IOException::class)
fun sha256Hex(data: InputStream): String {
return Hex.encodeHexString(sha256(data))
}
fun sha256Hex(data: String): String {
return Hex.encodeHexString(sha256(data))
}
fun sha384(data: ByteArray): ByteArray {
return sha384Digest.digest(data)
}
@Throws(IOException::class)
fun sha384(data: InputStream): ByteArray {
return digest(sha384Digest, data)
}
fun sha384(data: String): ByteArray {
return sha384(data.toByteArray(Charsets.UTF_8))
}
fun sha384Hex(data: ByteArray): String {
return Hex.encodeHexString(sha384(data))
}
@Throws(IOException::class)
fun sha384Hex(data: InputStream): String {
return Hex.encodeHexString(sha384(data))
}
fun sha384Hex(data: String): String {
return Hex.encodeHexString(sha384(data))
}
fun sha512(data: ByteArray): ByteArray {
return sha512Digest.digest(data)
}
@Throws(IOException::class)
fun sha512(data: InputStream): ByteArray {
return digest(sha512Digest, data)
}
fun sha512(data: String): ByteArray {
return sha512(data.toByteArray(Charsets.UTF_8))
}
fun sha512Hex(data: ByteArray): String {
return Hex.encodeHexString(sha512(data))
}
@Throws(IOException::class)
fun sha512Hex(data: InputStream): String {
return Hex.encodeHexString(sha512(data))
}
fun sha512Hex(data: String): String {
return Hex.encodeHexString(sha512(data))
}
fun updateDigest(messageDigest: MessageDigest, valueToDigest: ByteArray): MessageDigest {
messageDigest.update(valueToDigest)
return messageDigest
}
@Throws(IOException::class)
fun updateDigest(digest: MessageDigest, data: InputStream): MessageDigest {
val buffer = ByteArray(STREAM_BUFFER_LENGTH)
var read = data.read(buffer, 0, STREAM_BUFFER_LENGTH)
while (read > -1) {
digest.update(buffer, 0, read)
read = data.read(buffer, 0, STREAM_BUFFER_LENGTH)
}
return digest
}
fun updateDigest(messageDigest: MessageDigest, valueToDigest: String): MessageDigest {
messageDigest.update(valueToDigest.toByteArray(Charsets.UTF_8))
return messageDigest
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.util
/**
* @param process the process to wait for
* @param watcher the callback that will be called after process stops.
*/
internal class ExitWaiter(val process: Process, val watcher: (Int) -> Unit) : Runnable {
override fun run() {
try {
process.waitFor()
watcher(process.exitValue())
} catch (e: InterruptedException) {
watcher(1)
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.util
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
fun File.makeDirectory(): Boolean = isDirectory || mkdirs()
fun File.makeFile(): Boolean {
if (!absoluteFile.parentFile.makeDirectory())
return false
if (!exists() && !createNewFile())
return false
return true
}
fun File.isSymlink(): Boolean {
if (File.separatorChar == '\\')
return false
val fileInCanonicalDir: File =
if (parent == null) this
else File(parentFile.canonicalFile, name)
return fileInCanonicalDir.canonicalFile != fileInCanonicalDir.absoluteFile
}
fun File.listFilesByExtension(ext: String): List<File> {
val list = mutableListOf<File>()
this.listFiles()?.filter { it.extension == ext }?.forEach { list.add(it) }
return list
}

View File

@@ -0,0 +1,192 @@
/*
* 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.util
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import java.lang.reflect.Type
import com.google.gson.stream.JsonToken
import java.io.IOException
import com.google.gson.TypeAdapter
import com.google.gson.Gson
import com.google.gson.TypeAdapterFactory
import org.jackhuang.hmcl.game.Library
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.UUID
val GSON: Gson = GsonBuilder()
.enableComplexMapKeySerialization()
.setPrettyPrinting()
.registerTypeAdapter(Library::class.java, Library)
.registerTypeAdapter(Date::class.java, DateTypeAdapter)
.registerTypeAdapter(UUID::class.java, UUIDTypeAdapter)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory)
.registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory)
.create()
inline fun <reified T> typeOf(): Type = object : TypeToken<T>() {}.type
inline fun <reified T> Gson.fromJson(json: String): T? = fromJson<T>(json, T::class.java)
/**
* Check if the json object's fields automatically filled by Gson are in right format.
*/
interface Validation {
/**
* 1. Check some non-null fields and;
* 2. Check strings and;
* 3. Check generic type of lists <T> and maps <K, V> are correct.
*
* Will be called immediately after initialization.
* Throw an exception when values are malformed.
* @throws JsonParseException if fields are filled in wrong format or wrong type.
*/
fun validate()
}
object ValidationTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T?>?): TypeAdapter<T?> {
val delgate = gson.getDelegateAdapter(this, type)
return object : TypeAdapter<T?>() {
override fun write(out: JsonWriter?, value: T?) {
if (value is Validation)
value.validate()
delgate.write(out, value)
}
override fun read(reader: JsonReader?): T? {
val value = delgate.read(reader)
if (value is Validation)
value.validate()
return value
}
}
}
}
object LowerCaseEnumTypeAdapterFactory : TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T?>): TypeAdapter<T?>? {
val rawType = type.rawType
if (!rawType.isEnum) {
return null
}
val lowercaseToConstant = HashMap<String, T>()
for (constant in rawType.enumConstants) {
@Suppress("UNCHECKED_CAST")
lowercaseToConstant.put(toLowercase(constant!!), constant as T)
}
return object : TypeAdapter<T?>() {
@Throws(IOException::class)
override fun write(out: JsonWriter, value: T?) {
if (value == null) {
out.nullValue()
} else {
out.value(toLowercase(value))
}
}
@Throws(IOException::class)
override fun read(reader: JsonReader): T? {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return null
}
return lowercaseToConstant[reader.nextString().toLowerCase()]
}
}
}
private fun toLowercase(o: Any): String {
return o.toString().toLowerCase(Locale.US)
}
}
object UUIDTypeAdapter : TypeAdapter<UUID>() {
override fun read(reader: JsonReader): UUID {
return fromString(reader.nextString())
}
override fun write(writer: JsonWriter, value: UUID?) {
writer.value(if (value == null) null else fromUUID(value))
}
fun fromUUID(value: UUID): String {
return value.toString().replace("-", "")
}
fun fromString(input: String): UUID {
return UUID.fromString(input.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})".toRegex(), "$1-$2-$3-$4-$5"))
}
}
object DateTypeAdapter : JsonSerializer<Date>, JsonDeserializer<Date> {
private val enUsFormat: DateFormat = DateFormat.getDateTimeInstance(2, 2, Locale.US)
private val iso8601Format: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Date {
if (json !is JsonPrimitive) {
throw JsonParseException("The date should be a string value")
} else {
val date = this.deserializeToDate(json.getAsString())
if (typeOfT === Date::class.java) {
return date
} else {
throw IllegalArgumentException(this.javaClass.toString() + " cannot deserialize to " + typeOfT)
}
}
}
override fun serialize(src: Date, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
synchronized(this.enUsFormat) {
return JsonPrimitive(this.serializeToString(src))
}
}
fun deserializeToDate(string: String): Date {
synchronized(this.enUsFormat) {
try {
return this.enUsFormat.parse(string)
} catch (ex1: ParseException) {
try {
return this.iso8601Format.parse(string)
} catch (ex2: ParseException) {
try {
var cleaned = string.replace("Z", "+00:00")
cleaned = cleaned.substring(0, 22) + cleaned.substring(23)
return this.iso8601Format.parse(cleaned)
} catch (e: Exception) {
throw JsonSyntaxException("Invalid date: " + string, e)
}
}
}
}
}
fun serializeToString(date: Date): String {
synchronized(this.enUsFormat) {
val result = this.iso8601Format.format(date)
return result.substring(0, 22) + ":" + result.substring(22)
}
}
}

View File

@@ -0,0 +1,121 @@
/*
* 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.util
import java.nio.charset.Charset
class Hex @JvmOverloads constructor(val charset: Charset = DEFAULT_CHARSET) {
@Throws(Exception::class)
fun decode(array: ByteArray): ByteArray {
return decodeHex(String(array, charset).toCharArray())
}
@Throws(Exception::class)
fun decode(`object`: Any): Any {
try {
val charArray = (`object` as? String)?.toCharArray() ?: `object` as CharArray
return decodeHex(charArray)
} catch (e: ClassCastException) {
throw Exception(e.message, e)
}
}
fun encode(array: ByteArray): ByteArray {
return encodeHexString(array).toByteArray(charset)
}
@Throws(Exception::class)
fun encode(`object`: Any): Any {
try {
val byteArray = (`object` as? String)?.toByteArray(charset) ?: `object` as ByteArray
return encodeHex(byteArray)
} catch (e: ClassCastException) {
throw Exception(e.message, e)
}
}
val charsetName: String
get() = this.charset.name()
override fun toString(): String {
return super.toString() + "[charsetName=$charset]"
}
companion object {
private val DIGITS_LOWER = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
private val DIGITS_UPPER = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
@Throws(Exception::class)
fun decodeHex(data: CharArray): ByteArray {
val len = data.size
if (len and 0x1 != 0)
throw Exception("Odd number of characters.")
val out = ByteArray(len shr 1)
var i = 0
var j = 0
while (j < len) {
var f = toDigit(data[j], j) shl 4
j++
f = f or toDigit(data[j], j)
j++
out[i] = (f and 0xFF).toByte()
i++
}
return out
}
@JvmOverloads fun encodeHex(data: ByteArray, toLowerCase: Boolean = true): CharArray {
return encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER)
}
protected fun encodeHex(data: ByteArray, toDigits: CharArray): CharArray {
val l = data.size
val out = CharArray(l shl 1)
var i = 0
var j = 0
while (i < l) {
out[j++] = toDigits[(0xF0 and data[i].toInt()).ushr(4)]
out[j++] = toDigits[0xF and data[i].toInt()]
i++
}
return out
}
fun encodeHexString(data: ByteArray): String {
return String(encodeHex(data))
}
protected fun toDigit(ch: Char, index: Int): Int {
val digit = Character.digit(ch, 16)
if (digit == -1)
throw IllegalArgumentException("Illegal hexadecimal character $ch at index $index")
return digit
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* 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.util
import java.io.*
import java.nio.charset.Charset
const val MAX_BUFFER_SIZE = 4096
fun Closeable.closeQuietly() {
try {
this.close()
} catch (ex: IOException) {}
}
fun InputStream.readFully(): ByteArrayOutputStream {
try {
val ans = ByteArrayOutputStream()
copyTo(ans)
return ans
} finally {
this.closeQuietly()
}
}
fun InputStream.readFullyAsByteArray(): ByteArray =
readFully().toByteArray()
fun InputStream.readFullyAsString(): String =
readFully().toString()
fun InputStream.readFullyAsString(charset: Charset): String =
readFully().toString(charset.name())
fun InputStream.copyTo(dest: OutputStream, buf: ByteArray) {
while (true) {
val len = read(buf)
if (len == -1)
break
dest.write(buf, 0, len)
}
}
fun InputStream.copyToAndClose(dest: OutputStream) {
this.use { input ->
dest.use { output ->
input.copyTo(output)
}
}
}
fun makeCommand(cmd: List<String>): String {
val cmdbuf = StringBuilder(120)
for (i in cmd.indices) {
if (i > 0)
cmdbuf.append(' ')
val s = cmd[i]
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0)
if (s[0] != '"') {
cmdbuf.append('"')
cmdbuf.append(s)
if (s.endsWith("\\"))
cmdbuf.append("\\")
cmdbuf.append('"')
} else if (s.endsWith("\""))
// The argument has already been quoted.
cmdbuf.append(s)
else
// Unmatched quote for the argument.
throw IllegalArgumentException()
else
cmdbuf.append(s)
}
return cmdbuf.toString()
}

View File

@@ -0,0 +1,42 @@
/*
* 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.util
import java.util.*
class JavaProcess(
val process: Process,
val commands: List<String>
) {
val stdOutLines: List<String> = Collections.synchronizedList(LinkedList<String>())
val isRunning: Boolean = try {
process.exitValue()
true
} catch (ex: IllegalThreadStateException) {
false
}
val exitCode: Int get() = process.exitValue()
override fun toString(): String {
return "JavaProcess[commands=$commands, isRunning=$isRunning]"
}
fun stop() {
process.destroy()
}
}

View File

@@ -0,0 +1,92 @@
/*
* 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.util
import java.io.File
import java.io.IOException
import java.io.Serializable
import java.util.regex.Pattern
data class JavaVersion internal constructor(
val binary: File,
val version: Int,
val platform: Platform) : Serializable
{
companion object {
private val regex = Pattern.compile("java version \"(?<version>[1-9]*\\.[1-9]*\\.[0-9]*(.*?))\"")
val UNKNOWN: Int = -1
val JAVA_5: Int = 50
val JAVA_6: Int = 60
val JAVA_7: Int = 70
val JAVA_8: Int = 80
val JAVA_9: Int = 90
val JAVA_X: Int = 100
private fun parseVersion(version: String): Int {
with(version) {
if (startsWith("10") || startsWith("X")) return JAVA_X
else if (contains("1.9.") || startsWith("9")) return JAVA_9
else if (contains("1.8.")) return JAVA_8
else if (contains("1.7.")) return JAVA_7
else if (contains("1.6.")) return JAVA_6
else if (contains("1.5.")) return JAVA_5
else return UNKNOWN
}
}
fun fromExecutable(file: File): JavaVersion {
var platform = Platform.BIT_32
var version: String? = null
try {
val process = ProcessBuilder(file.absolutePath, "-version").start()
process.waitFor()
process.inputStream.bufferedReader().forEachLine { line ->
val m = regex.matcher(line)
if (m.find())
version = m.group("version")
if (line.contains("64-Bit"))
platform = Platform.BIT_64
}
} catch (e: InterruptedException) {
throw IOException("Java process is interrupted", e)
}
val thisVersion = version ?: throw IOException("Java version not matched")
val parsedVersion = parseVersion(thisVersion)
if (parsedVersion == UNKNOWN)
throw IOException("Java version '$thisVersion' can not be recognized")
return JavaVersion(file.parentFile, parsedVersion, platform)
}
fun getJavaFile(home: File): File {
var path = home.resolve("bin")
var javaw = path.resolve("javaw.exe")
if (OS.CURRENT_OS === OS.WINDOWS && javaw.isFile)
return javaw
else
return path.resolve("java")
}
fun fromCurrentEnvironment(): JavaVersion {
return JavaVersion(
binary = getJavaFile(File(System.getProperty("java.home"))),
version = parseVersion(System.getProperty("java.version")),
platform = Platform.PLATFORM)
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* 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.util
import javafx.beans.property.Property
import javafx.beans.value.ObservableValue
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.HashMap
import sun.text.normalizer.UTF16.append
import java.lang.reflect.Array.getLength
inline fun ignoreException(func: () -> Unit) {
try {
func()
} catch(ignore: Exception) {}
}
inline fun ignoreThrowable(func: () -> Unit) {
try {
func()
} catch (ignore: Throwable) {}
}
fun <K, V> unmodifiableMap(map: Map<K, V>?): Map<K, V>? =
if (map == null) null
else Collections.unmodifiableMap(map)
fun <K, V> copyMap(map: Map<K, V>?): MutableMap<K, V>? =
if (map == null) null
else HashMap(map)
fun <T> unmodifiableList(list: List<T>?): List<T>? =
if (list == null) null
else Collections.unmodifiableList(list)
fun <T> copyList(list: List<T>?): MutableList<T>? =
if (list == null) null
else LinkedList(list)
fun <T> merge(vararg c: Collection<T>): List<T> = LinkedList<T>().apply {
for (a in c)
addAll(a)
}
fun isBlank(str: String?) = str?.isBlank() ?: true
fun isNotBlank(str: String?) = !isBlank(str)
fun String.tokenize(delim: String = " \t\n\r"): List<String> {
val list = mutableListOf<String>()
val tokenizer = StringTokenizer(this, delim)
while (tokenizer.hasMoreTokens())
list.add(tokenizer.nextToken())
return list
}
fun String.asVersion(): String? {
if (count { it != '.' && (it < '0' || it > '9') } > 0 || isBlank())
return null
val s = split(".")
for (i in s) if (i.isBlank()) return null
val builder = StringBuilder()
var last = s.size - 1
for (i in s.size - 1 downTo 0)
if (s[i].toInt() == 0)
last = i
for (i in 0 .. last)
builder.append(s[i]).append('.')
return builder.deleteCharAt(builder.length - 1).toString()
}
fun parseParams(addBefore: String, objects: Collection<*>, addAfter: String): String {
return parseParams(addBefore, objects.toTypedArray(), addAfter)
}
fun parseParams(addBefore: String, objects: Array<*>, addAfter: String): String {
return parseParams({ addBefore }, objects, { addAfter })
}
fun parseParams(beforeFunc: (Any?) -> String, params: Array<*>?, afterFunc: (Any?) -> String): String {
if (params == null)
return ""
val sb = StringBuilder()
for (i in params.indices) {
val param = params[i]
val addBefore = beforeFunc(param)
val addAfter = afterFunc(param)
if (i > 0)
sb.append(addAfter).append(addBefore)
if (param == null)
sb.append("null")
else if (param.javaClass.isArray) {
sb.append("[")
if (param is Array<*>) {
sb.append(parseParams(beforeFunc, param, afterFunc))
} else
for (j in 0..java.lang.reflect.Array.getLength(param) - 1) {
if (j > 0)
sb.append(addAfter)
sb.append(addBefore).append(java.lang.reflect.Array.get(param, j))
}
sb.append("]")
} else
sb.append(addBefore).append(params[i])
}
return sb.toString()
}
fun <T> Property<in T>.updateAsync(newValue: T, update: AtomicReference<T>) {
if (update.getAndSet(newValue) == null) {
UI_THREAD_SCHEDULER {
val current = update.getAndSet(null)
this.value = current
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.util
import javafx.beans.value.*
import javafx.collections.ListChangeListener
import javafx.collections.ObservableList
fun <T> ObservableValue<T>.onChange(op: (T?) -> Unit) = apply { addListener { _, _, new -> op(new) } }
fun ObservableBooleanValue.onChange(op: (Boolean) -> Unit) = apply { addListener { _, _, new -> op(new ?: false) } }
fun ObservableIntegerValue.onChange(op: (Int) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0).toInt()) } }
fun ObservableLongValue.onChange(op: (Long) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0L).toLong()) } }
fun ObservableFloatValue.onChange(op: (Float) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0f).toFloat()) } }
fun ObservableDoubleValue.onChange(op: (Double) -> Unit) = apply { addListener { _, _, new -> op((new ?: 0.0).toDouble()) } }
fun <T> ObservableList<T>.onChange(op: (ListChangeListener.Change<out T>) -> Unit) = apply {
addListener(ListChangeListener { op(it) })
}

Some files were not shown because too many files have changed in this diff Show More