This commit is contained in:
huangyuhui
2017-08-22 14:31:35 +08:00
parent b40b0ec47c
commit 0cc7d621bc
108 changed files with 752 additions and 371 deletions

View File

@@ -19,9 +19,7 @@ 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
import java.util.*
data class GameProfile(
val id: UUID? = null,

View File

@@ -20,13 +20,6 @@ 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>() {

View File

@@ -21,9 +21,6 @@ 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

View File

@@ -17,11 +17,11 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.game.GameRepository
import java.net.Proxy
abstract class AbstractDependencyManager(repository: GameRepository, proxy: Proxy)
: DependencyManager(repository, proxy) {
abstract class AbstractDependencyManager
: DependencyManager {
abstract val downloadProvider: DownloadProvider
fun getVersions(id: String, selfVersion: String) =

View File

@@ -17,6 +17,14 @@
*/
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
/**
* @see {@link http://bmclapi2.bangbang93.com}
*/
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"

View File

@@ -17,6 +17,13 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.download.forge.ForgeInstallTask
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.download.optifine.OptiFineInstallTask
import org.jackhuang.hmcl.game.DefaultGameRepository
import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.ParallelTask
@@ -27,8 +34,8 @@ import java.net.Proxy
/**
* This class has no state.
*/
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, proxy: Proxy = Proxy.NO_PROXY)
: AbstractDependencyManager(repository, proxy) {
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, override val proxy: Proxy = Proxy.NO_PROXY)
: AbstractDependencyManager() {
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)

View File

@@ -17,9 +17,16 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.game.*
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.Version
import org.jackhuang.hmcl.task.*
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.AutoTypingMap
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.util.toURL
import java.util.*
class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameBuilder() {

View File

@@ -22,27 +22,51 @@ import org.jackhuang.hmcl.game.Version
import org.jackhuang.hmcl.task.Task
import java.net.Proxy
abstract class DependencyManager(open val repository: GameRepository, open val proxy: Proxy) {
/**
* Do everything that will connect to Internet.
* Downloading Minecraft files.
*/
interface DependencyManager {
/**
* The relied game repository.
*/
val repository: GameRepository
/**
* The proxy that all network operations should go through.
*/
val proxy: Proxy
/**
* Check if the game is complete.
* Check libraries, assets, logging files and so on.
*
* @return
* @return the task to check game completion.
*/
abstract fun checkGameCompletionAsync(version: Version): Task
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
fun gameBuilder(): GameBuilder
abstract fun installLibraryAsync(gameVersion: String, version: Version, libraryId: String, libraryVersion: String): Task
/**
* Install a library to a version.
* **Note**: Installing a library may change the version.json.
*
* @param gameVersion the Minecraft version that the library relies on.
* @param version the version.json.
* @param libraryId the type of being installed library. i.e. "forge", "liteloader", "optifine"
* @param libraryVersion the version of being installed library.
* @return the task to install the specific library.
*/
fun installLibraryAsync(gameVersion: String, 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<*>
fun getVersionList(id: String): VersionList<*>
}

View File

@@ -17,14 +17,31 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.download.VersionList
/**
* The service provider that provides Minecraft online file downloads.
*/
abstract class DownloadProvider {
abstract val libraryBaseURL: String
abstract val versionListURL: String
abstract val versionBaseURL: String
abstract val assetIndexBaseURL: String
abstract val assetBaseURL: String
/**
* Inject into original URL provided by Mojang and Forge.
*
* Since there are many provided URLs that are written in JSONs and are unmodifiable,
* this method provides a way to change them.
*
* @param baseURL original URL provided by Mojang and Forge.
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/
abstract fun injectURL(baseURL: String): String
/**
* the specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
* @param id the id of specific version list that this download provider provides. i.e. "forge", "liteloader", "game", "optifine"
* @return the version list
*/
abstract fun getVersionListById(id: String): VersionList<*>
}

View File

@@ -19,12 +19,20 @@ package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.task.Task
/**
* The builder which provide a task to build Minecraft environment.
*
* @author huangyuhui
*/
abstract class GameBuilder {
var name: String = ""
protected var gameVersion: String = ""
protected var toolVersions = HashMap<String, String>()
/**
* The new game version name, for .minecraft/<version name>.
* @param name the name of new game version.
*/
fun name(name: String): GameBuilder {
this.name = name
return this
@@ -35,10 +43,17 @@ abstract class GameBuilder {
return this
}
/**
* @param id the core library id. i.e. "forge", "liteloader", "optifine"
* @param version the version of the core library. For documents, you can first try [VersionList.versions]
*/
fun version(id: String, version: String): GameBuilder {
toolVersions[id] = version
return this
}
/**
* @return the task that can build thw whole Minecraft environment
*/
abstract fun buildAsync(): Task
}

View File

@@ -17,8 +17,15 @@
*/
package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.download.forge.ForgeVersionList
import org.jackhuang.hmcl.download.game.GameVersionList
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import org.jackhuang.hmcl.download.optifine.OptiFineVersionList
import java.util.*
/**
* @see {@link http://wiki.vg}
*/
object MojangDownloadProvider : DownloadProvider() {
override val libraryBaseURL: String = "https://libraries.minecraft.net/"
override val versionBaseURL: String = "http://s3.amazonaws.com/Minecraft.Download/versions/"
@@ -37,15 +44,6 @@ object MojangDownloadProvider : DownloadProvider() {
}
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)

View File

@@ -19,14 +19,18 @@ package org.jackhuang.hmcl.download
import org.jackhuang.hmcl.util.VersionNumber
import java.util.*
import kotlin.Comparator
/**
* The remote version.
*
* @param gameVersion the Minecraft version that this remote version suits.
* @param selfVersion the version string of the remote version.
* @param url the installer or universal jar URL.
* @param tag some necessary information for Installer Task.
*/
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>> {
@@ -39,6 +43,7 @@ data class RemoteVersion<T> (
}
override fun compareTo(other: RemoteVersion<T>): Int {
// newer versions are smaller than older versions
return -selfVersion.compareTo(other.selfVersion)
}

View File

@@ -22,20 +22,37 @@ import org.jackhuang.hmcl.util.SimpleMultimap
import java.util.*
import kotlin.collections.HashMap
/**
* The remote version list.
* @param T The type of RemoteVersion<T>, the type of tags.
*/
abstract class VersionList<T> {
@JvmField
/**
* the remote version list.
* key: game version.
* values: corresponding remote versions.
*/
protected val versions = SimpleMultimap<String, RemoteVersion<T>>(::HashMap, ::TreeSet)
/**
* True if the version list has been loaded.
*/
val loaded = versions.isNotEmpty
/**
* @param downloadProvider DownloadProvider
* @return the task to reload the remote version list.
*/
abstract fun refreshAsync(downloadProvider: DownloadProvider): Task
protected open fun getVersionsImpl(gameVersion: String): Collection<RemoteVersion<T>> {
private fun getVersionsImpl(gameVersion: String): Collection<RemoteVersion<T>> {
val ans = versions[gameVersion]
return if (ans.isEmpty()) versions.values else ans
}
/**
* Get the remote versions that specifics Minecraft version.
*
* @param gameVersion the Minecraft version that remote versions belong to
* @return the collection of specific remote versions
*/
@@ -43,6 +60,13 @@ abstract class VersionList<T> {
return Collections.unmodifiableCollection(getVersionsImpl(gameVersion))
}
/**
* Get the specific remote version.
*
* @param gameVersion the Minecraft version that remote versions belong to
* @param remoteVersion the version of the remote version.
* @return the specific remote version, null if it is not found.
*/
fun getVersion(gameVersion: String, remoteVersion: String): RemoteVersion<T>? {
var result : RemoteVersion<T>? = null
versions[gameVersion].forEach {

View File

@@ -15,8 +15,11 @@
* 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
package org.jackhuang.hmcl.download.forge
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.RemoteVersion
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
@@ -30,9 +33,9 @@ import java.io.IOException
import java.util.zip.ZipFile
class ForgeInstallTask(private val dependencyManager: DefaultDependencyManager,
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String) : TaskResult<Version>() {
private val gameVersion: String,
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<*>

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download
package org.jackhuang.hmcl.download.forge
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.Version

View File

@@ -15,8 +15,11 @@
* 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
package org.jackhuang.hmcl.download.forge
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.*

View File

@@ -15,8 +15,11 @@
* 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
package org.jackhuang.hmcl.download.game
import org.jackhuang.hmcl.download.AbstractDependencyManager
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.DependencyManager
import org.jackhuang.hmcl.game.AssetIndex
import org.jackhuang.hmcl.game.AssetObject
import org.jackhuang.hmcl.game.DownloadType
@@ -30,13 +33,12 @@ 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() {
class GameLibrariesTask(private val dependencyManager: AbstractDependencyManager, private val resolvedVersion: Version): Task() {
override val dependencies = LinkedList<Task>()
override fun execute() {
for (library in resolvedVersion.libraries)
@@ -49,7 +51,12 @@ class GameLibrariesTask(private val dependencyManager: DefaultDependencyManager,
}
class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
/**
* This task is to download log4j configuration file provided in minecraft.json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class GameLoggingDownloadTask(private val dependencyManager: DependencyManager, private val version: Version) : Task() {
override val dependencies = LinkedList<Task>()
override fun execute() {
val logging = version.logging?.get(DownloadType.CLIENT) ?: return
@@ -59,6 +66,11 @@ class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyMa
}
}
/**
* This task is to download asset index file provided in minecraft.json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
override val dependencies = LinkedList<Task>()
override fun execute() {
@@ -72,6 +84,11 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
}
}
/**
* This task is to extract all asset objects described in asset index json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
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
@@ -100,6 +117,11 @@ class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManag
}
}
/**
* This task is to download all asset objects provided in asset index json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class GameAssetDownloadTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : Task() {
private val refreshTask = GameAssetRefreshTask(dependencyManager, version)
override val dependents = listOf(refreshTask)
@@ -140,6 +162,11 @@ class GameAssetDownloadTask(private val dependencyManager: DefaultDependencyMana
}
}
/**
* This task is to save the version json.
* @param dependencyManager the dependency manager that can provides proxy settings and [GameRepository]
* @param version the **resolved** version
*/
class VersionJSONSaveTask(private val dependencyManager: DefaultDependencyManager, private val version: Version): Task() {
override fun execute() {
val json = dependencyManager.repository.getVersionJson(version.id).absoluteFile

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download
package org.jackhuang.hmcl.download.game
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.ReleaseType

View File

@@ -15,13 +15,16 @@
* 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
package org.jackhuang.hmcl.download.game
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
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.fromJson
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL

View File

@@ -15,8 +15,11 @@
* 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
package org.jackhuang.hmcl.download.liteloader
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.RemoteVersion
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
@@ -27,12 +30,12 @@ import org.jackhuang.hmcl.task.then
import org.jackhuang.hmcl.util.merge
/**
* LiteLoader must be installed after Forge.
* Note: LiteLoader must be installed after Forge.
*/
class LiteLoaderInstallTask(private val dependencyManager: DefaultDependencyManager,
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val gameVersion: String,
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 = mutableListOf<Task>()

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download
package org.jackhuang.hmcl.download.liteloader
import com.google.gson.annotations.SerializedName
import org.jackhuang.hmcl.game.Library

View File

@@ -15,13 +15,16 @@
* 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
package org.jackhuang.hmcl.download.liteloader
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
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.fromJson
import org.jackhuang.hmcl.task.GetTask
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.util.toURL
object LiteLoaderVersionList : VersionList<LiteLoaderRemoteVersionTag>() {

View File

@@ -15,15 +15,17 @@
* 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
package org.jackhuang.hmcl.download.optifine
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
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 {

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download
package org.jackhuang.hmcl.download.optifine
object Opt

View File

@@ -15,8 +15,11 @@
* 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
package org.jackhuang.hmcl.download.optifine
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.RemoteVersion
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
@@ -26,10 +29,13 @@ import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.task.then
import org.jackhuang.hmcl.util.merge
/**
* **Note**: OptiFine should be installed in the end.
*/
class OptiFineInstallTask(private val dependencyManager: DefaultDependencyManager,
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val gameVersion: String,
private val version: Version,
private val remoteVersion: String): TaskResult<Version>() {
private val optiFineVersionList = dependencyManager.getVersionList("optifine")
lateinit var remote: RemoteVersion<*>
override val dependents = mutableListOf<Task>()

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.download
package org.jackhuang.hmcl.download.optifine
import com.google.gson.annotations.SerializedName

View File

@@ -15,15 +15,18 @@
* 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
package org.jackhuang.hmcl.download.optifine
import org.jackhuang.hmcl.download.DownloadProvider
import org.jackhuang.hmcl.download.RemoteVersion
import org.jackhuang.hmcl.download.VersionList
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
import javax.xml.parsers.DocumentBuilderFactory
object OptiFineVersionList : VersionList<Unit>() {
private val pattern = Pattern.compile("OptiFine (.*?) ")

View File

@@ -17,8 +17,8 @@
*/
package org.jackhuang.hmcl.event
import org.jackhuang.hmcl.util.JavaProcess
import java.util.EventObject
import org.jackhuang.hmcl.util.ManagedProcess
import java.util.*
/**
* This event gets fired when loading versions in a .minecraft folder.
@@ -59,7 +59,7 @@ class LoadedOneVersionEvent(source: Any, val version: String) : EventObject(sour
* @param JavaProcess The process that exited abnormally.
* @author huangyuhui
*/
class JavaProcessExitedAbnormallyEvent(source: Any, val value: JavaProcess) : EventObject(source)
class JavaProcessExitedAbnormallyEvent(source: Any, val value: ManagedProcess) : EventObject(source)
/**
* This event gets fired when minecraft process exited successfully and the exit code is 0.
@@ -69,7 +69,7 @@ class JavaProcessExitedAbnormallyEvent(source: Any, val value: JavaProcess) : Ev
* @param JavaProcess minecraft process
* @author huangyuhui
*/
class JavaProcessStoppedEvent(source: Any, val value: JavaProcess) : EventObject(source)
class JavaProcessStoppedEvent(source: Any, val value: ManagedProcess) : EventObject(source)
/**
* This event gets fired when we launch the JVM and it got crashed.
@@ -79,4 +79,4 @@ class JavaProcessStoppedEvent(source: Any, val value: JavaProcess) : EventObject
* @param JavaProcess the crashed process.
* @author huangyuhui
*/
class JVMLaunchFailedEvent(source: Any, val value: JavaProcess) : EventObject(source)
class JVMLaunchFailedEvent(source: Any, val value: ManagedProcess) : EventObject(source)

View File

@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game
import jdk.nashorn.internal.ir.annotations.Immutable
import java.net.URL
@Immutable
class AssetIndexInfo(

View File

@@ -20,6 +20,9 @@ package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.util.Immutable
import java.io.File
/**
* The Minecraft version for 1.5.x and earlier.
*/
@Immutable
class ClassicVersion : Version(
mainClass = "net.minecraft.client.Minecraft",
@@ -41,6 +44,9 @@ class ClassicVersion : Version(
)
companion object {
/**
* Check if this directory is an old style Minecraft repository.
*/
fun hasClassicVersion(baseDirectory: File): Boolean {
val file = File(baseDirectory, "bin")
if (!file.exists()) return false

View File

@@ -18,7 +18,10 @@
package org.jackhuang.hmcl.game
import com.google.gson.JsonSyntaxException
import org.jackhuang.hmcl.event.*
import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.event.LoadedOneVersionEvent
import org.jackhuang.hmcl.event.RefreshedVersionsEvent
import org.jackhuang.hmcl.event.RefreshingVersionsEvent
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.fromJson
@@ -28,6 +31,9 @@ import java.io.IOException
import java.util.*
import java.util.logging.Level
/**
* An implementation of classic Minecraft game repository.
*/
open class DefaultGameRepository(var baseDirectory: File): GameRepository {
protected val versions: MutableMap<String, Version> = TreeMap<String, Version>()
@@ -44,6 +50,11 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
val id = v.jar ?: v.id
return getVersionRoot(id).resolve("$id.jar")
}
/**
* {@inheritsDoc}
* @return something like ".minecraft/versions/<version name>/<version name>-natives"
*/
override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives")
override fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id")
open fun getVersionJson(id: String) = File(getVersionRoot(id), "$id.json")
@@ -83,7 +94,6 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
}
protected open fun refreshVersionsImpl() {
versions.clear()
if (ClassicVersion.hasClassicVersion(baseDirectory)) {
@@ -167,7 +177,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
return assetDir.resolve("objects/${obj.location}")
}
open fun getIndexFile(version: String, assetId: String): File =
override fun getIndexFile(version: String, assetId: String): File =
getAssetDirectory(version, assetId).resolve("indexes/$assetId.json")
override fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File =
@@ -176,9 +186,8 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
@Throws(IOException::class, JsonSyntaxException::class)
protected open fun reconstructAssets(version: String, assetId: String): File {
val assetsDir = getAssetDirectory(version, assetId)
val assetVersion = assetId
val indexFile: File = getIndexFile(version, assetVersion)
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
val indexFile: File = getIndexFile(version, assetId)
val virtualRoot = assetsDir.resolve("virtual").resolve(assetId)
if (!indexFile.isFile) {
return assetsDir

View File

@@ -18,13 +18,15 @@
package org.jackhuang.hmcl.game
import java.io.File
import java.io.IOException
/**
* Supports operations on versioning.
*
* Note that game repository will not do any tasks that connect to Internet.
* Note that game repository will not do any tasks that connect to Internet, if do, see [org.jackhuang.hmcl.download.DependencyManager]
*/
interface GameRepository : VersionProvider {
/**
* Does the version of id exist?
* @param id the id of version
@@ -126,7 +128,8 @@ interface GameRepository : VersionProvider {
* 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]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @throws java.io.IOException if I/O operation fails.
* @return the actual asset directory
*/
@@ -134,24 +137,31 @@ interface GameRepository : VersionProvider {
/**
* Get the asset directory according to the asset id.
*
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @return the asset directory
*/
fun getAssetDirectory(version: String, 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]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @param name the asset object name, you can find it in [AssetIndex.objects.keys]
* @throws java.io.IOException if I/O operation fails.
* @return the file that given asset object refers to
*/
@Throws(IOException::class)
fun getAssetObject(version: String, 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]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @param obj the asset object, you can find it in [AssetIndex.objects]
* @return the file that given asset object refers to
*/
fun getAssetObject(version: String, assetId: String, obj: AssetObject): File
@@ -159,17 +169,28 @@ interface GameRepository : VersionProvider {
/**
* Get asset index that assetId represents
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
* @return the asset index
*/
fun getAssetIndex(version: String, assetId: String): AssetIndex
/**
* Get the asset_index.json which includes asset objects information.
*
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.actualAssetIndex.id]
*/
fun getIndexFile(version: String, assetId: String): File
/**
* Get logging object
*
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
* @param version the id of specific version that is relevant to [assetId]
* @param assetId the asset id, you can find it in [AssetIndexInfo.id] [Version.assets]
* @param loggingInfo the logging info
* @return the file that loggingInfo refers to
*/
fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File
}

View File

@@ -19,10 +19,10 @@ package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.util.closeQuietly
import org.jackhuang.hmcl.util.readFullyAsByteArray
import java.io.File
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.io.File
private fun lessThan32(b: ByteArray, startIndex: Int): Int {
for (i in startIndex until b.size)

View File

@@ -17,7 +17,6 @@
*/
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

View File

@@ -23,6 +23,7 @@ package org.jackhuang.hmcl.game
* @see Version.resolve
*/
interface VersionProvider {
/**
* Does the version of id exist?
* @param id the id of version
@@ -36,4 +37,5 @@ interface VersionProvider {
* @return the version you want
*/
fun getVersion(id: String): Version
}

View File

@@ -18,14 +18,16 @@
package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.game.*
import org.jackhuang.hmcl.game.DownloadType
import org.jackhuang.hmcl.game.GameException
import org.jackhuang.hmcl.game.GameRepository
import org.jackhuang.hmcl.game.LaunchOptions
import org.jackhuang.hmcl.task.TaskResult
import org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.concurrent.thread
import kotlin.coroutines.experimental.EmptyCoroutineContext.plus
/**
* @param versionId The version to be launched.
@@ -220,7 +222,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
}
}
override fun launch(): JavaProcess {
override fun launch(): ManagedProcess {
// To guarantee that when failed to generate code, we will not call precalled command
val builder = ProcessBuilder(rawCommandLine)
@@ -237,7 +239,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
builder.directory(repository.getRunDirectory(version.id))
.environment().put("APPDATA", options.gameDir.absoluteFile.parent)
val p = JavaProcess(builder.start(), rawCommandLine)
val p = ManagedProcess(builder.start(), rawCommandLine)
if (listener == null)
startMonitors(p)
else
@@ -245,8 +247,8 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
return p
}
fun launchAsync(): TaskResult<JavaProcess> {
return object : TaskResult<JavaProcess>() {
fun launchAsync(): TaskResult<ManagedProcess> {
return object : TaskResult<ManagedProcess>() {
override val id = LAUNCH_ASYNC_ID
override fun execute() {
result = launch()
@@ -279,20 +281,20 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
return scriptFile
}
private fun startMonitors(javaProcess: JavaProcess) {
javaProcess.relatedThreads += thread(name = "stdout-pump", isDaemon = true, block = StreamPump(javaProcess.process.inputStream)::run)
javaProcess.relatedThreads += thread(name = "stderr-pump", isDaemon = true, block = StreamPump(javaProcess.process.errorStream)::run)
private fun startMonitors(managedProcess: ManagedProcess) {
managedProcess.relatedThreads += thread(name = "stdout-pump", isDaemon = true, block = StreamPump(managedProcess.process.inputStream)::run)
managedProcess.relatedThreads += thread(name = "stderr-pump", isDaemon = true, block = StreamPump(managedProcess.process.errorStream)::run)
}
private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
processListener.setProcess(javaProcess)
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); javaProcess.lines += line }.apply { start() }
javaProcess.relatedThreads += logHandler
val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, { logHandler.newLine(it) } )::run)
javaProcess.relatedThreads += stdout
val stderr = thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.errorStream, { processListener.onLog(it + OS.LINE_SEPARATOR, Log4jLevel.ERROR); javaProcess.lines += it })::run)
javaProcess.relatedThreads += stderr
javaProcess.relatedThreads += thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess, listOf(stdout, stderr), { exitCode, exitType -> logHandler.onStopped(); processListener.onExit(exitCode, exitType) })::run)
private fun startMonitors(managedProcess: ManagedProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
processListener.setProcess(managedProcess)
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); managedProcess.lines += line }.apply { start() }
managedProcess.relatedThreads += logHandler
val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(managedProcess.process.inputStream, { logHandler.newLine(it) } )::run)
managedProcess.relatedThreads += stdout
val stderr = thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(managedProcess.process.errorStream, { processListener.onLog(it + OS.LINE_SEPARATOR, Log4jLevel.ERROR); managedProcess.lines += it })::run)
managedProcess.relatedThreads += stderr
managedProcess.relatedThreads += thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(managedProcess, listOf(stdout, stderr), { exitCode, exitType -> logHandler.onStopped(); processListener.onExit(exitCode, exitType) })::run)
}
companion object {

View File

@@ -21,16 +21,15 @@ import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.event.JVMLaunchFailedEvent
import org.jackhuang.hmcl.event.JavaProcessExitedAbnormallyEvent
import org.jackhuang.hmcl.event.JavaProcessStoppedEvent
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.ManagedProcess
import org.jackhuang.hmcl.util.containsOne
import org.jackhuang.hmcl.util.guessLogLineError
import java.util.*
/**
* @param process the process to wait for
* @param watcher the callback that will be called after process stops.
*/
internal class ExitWaiter(val process: JavaProcess, val joins: Collection<Thread>, val watcher: (Int, ProcessListener.ExitType) -> Unit) : Runnable {
internal class ExitWaiter(val process: ManagedProcess, val joins: Collection<Thread>, val watcher: (Int, ProcessListener.ExitType) -> Unit) : Runnable {
override fun run() {
try {
process.process.waitFor()

View File

@@ -21,7 +21,7 @@ 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 org.jackhuang.hmcl.util.ManagedProcess
import java.io.File
abstract class Launcher(
@@ -34,7 +34,7 @@ abstract class Launcher(
val version: Version = repository.getVersion(versionId).resolve(repository)
abstract val rawCommandLine: List<String>
abstract fun launch(): JavaProcess
abstract fun launch(): ManagedProcess
/**
* @param file The file path without extension

View File

@@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicBoolean
/**
* This class is to parse log4j classic XML layout logging, since only vanilla Minecraft will enable this layout.
* Also supports plain logs.
*/
internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread() {
val reader = XMLReaderFactory.createXMLReader().apply {
@@ -53,6 +54,9 @@ internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread
}
}
/**
* Should be called to stop [Log4jHandler] manually.
*/
fun onStopped() {
if (interrupted.get())
return
@@ -66,7 +70,7 @@ internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread
}
/**
* Should be called in [ProcessListener.onErrorLog] and [ProcessListener.onLog] manually.
* New XML line.
*/
fun newLine(content: String) =
Scheduler.COMPUTATION.schedule {
@@ -102,6 +106,7 @@ internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread
l = Log4jLevel.ERROR
}
"log4j_Message" -> readingMessage = true
"log4j_Throwable" -> {}
}
}

View File

@@ -17,15 +17,15 @@
*/
package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.Log4jLevel
import org.jackhuang.hmcl.util.ManagedProcess
interface ProcessListener {
/**
* When a game launched, this method will be called to get the new process.
* You should not override this method when your ProcessListener is shared with all processes.
*/
fun setProcess(process: JavaProcess) {}
fun setProcess(process: ManagedProcess) {}
/**
* Called when receiving a log from stdout/stderr.

View File

@@ -23,6 +23,12 @@ import java.io.IOException
import java.io.InputStream
import java.util.logging.Level
/**
* Pump the given input stream.
* @param inputStream the input stream to pump
* @param callback receives each line
*
*/
internal class StreamPump @JvmOverloads constructor(
private val inputStream: InputStream,
private val callback: (String) -> Unit = {}

View File

@@ -99,14 +99,27 @@ class CurseForgeModpackManifestFile (
val url: URL get() = "https://minecraft.curseforge.com/projects/$projectID/files/$fileID/download".toURL()
}
/**
* @param f the CurseForge modpack file.
* @return the manifest.
*/
fun readCurseForgeModpackManifest(f: File): CurseForgeModpackManifest {
ZipFile(f).use { zipFile ->
val entry = zipFile.getEntry("manifest.json") ?: throw IOException("Manifest.json not found. Not a valid CurseForge modpack.")
val entry = zipFile.getEntry("manifest.json") ?: throw IOException("`manifest.json` not found. Not a valid CurseForge modpack.")
val json = zipFile.getInputStream(entry).readFullyAsString()
return GSON.fromJson<CurseForgeModpackManifest>(json) ?: throw JsonParseException("Manifest.json not found. Not a valid CurseForge modpack.")
return GSON.fromJson<CurseForgeModpackManifest>(json) ?: throw JsonParseException("`manifest.json` not found. Not a valid CurseForge modpack.")
}
}
/**
* Install a downloaded CurseForge modpack.
*
* @param dependencyManager the dependency manager.
* @param zipFile the CurseForge modpack file.
* @param manifest The manifest content of given CurseForge modpack.
* @param name the new version name
* @see readCurseForgeModpackManifest
*/
class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDependencyManager, private val zipFile: File, private val manifest: CurseForgeModpackManifest, private val name: String): Task() {
val repository = dependencyManager.repository
init {
@@ -136,7 +149,8 @@ class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDepende
f.fileName = f.url.detectFileName(dependencyManager.proxy)
dependencies += FileDownloadTask(f.url, run.resolve("mods").resolve(f.fileName), proxy = dependencyManager.proxy)
} catch (e: IOException) {
// ignore it and retry next time.
// Because in China, the CurseForge is too difficult to visit.
// So if failed, ignore it and retry next time.
}
++finished
updateProgress(1.0 * finished / manifest.files.size)
@@ -146,6 +160,12 @@ class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDepende
}
}
/**
* Complete the CurseForge version.
*
* @param dependencyManager the dependency manager.
* @param version the existent and physical version.
*/
class CurseForgeModpackCompletionTask(dependencyManager: DependencyManager, version: String): Task() {
val repository = dependencyManager.repository
val run = repository.getRunDirectory(version)
@@ -161,6 +181,8 @@ class CurseForgeModpackCompletionTask(dependencyManager: DependencyManager, vers
else {
manifest = GSON.fromJson<CurseForgeModpackManifest>(repository.getVersionRoot(version).resolve("manifest.json").readText())!!
// Because in China, the CurseForge is too difficult to visit.
// So caching the file name is necessary.
for (f in manifest!!.files) {
if (f.fileName.isBlank())
dependents += task { f.fileName = f.url.detectFileName(proxy) }

View File

@@ -26,7 +26,7 @@ import org.jackhuang.hmcl.util.typeOf
import java.io.File
import java.util.zip.ZipFile
class ForgeModMetadata(
class ForgeModMetadata @JvmOverloads internal constructor(
@SerializedName("modid")
val modId: String = "",
val name: String = "",
@@ -42,6 +42,9 @@ class ForgeModMetadata(
) {
companion object {
/**
* Read Forge mod ModInfo.
*/
fun fromFile(modFile: File): ModInfo {
ZipFile(modFile).use {
val entry = it.getEntry("mcmod.info") ?: throw JsonParseException("File $modFile is not a Forge mod.")

View File

@@ -18,11 +18,13 @@
package org.jackhuang.hmcl.mod
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.fromJson
import org.jackhuang.hmcl.util.readFullyAsString
import java.io.File
import java.util.zip.ZipFile
class LiteModMetadata (
class LiteModMetadata @JvmOverloads internal constructor(
val name: String = "",
val version: String = "",
val mcversion: String = "",
@@ -37,6 +39,9 @@ class LiteModMetadata (
) {
companion object {
/**
* Read LiteLoader mod ModInfo.
*/
fun fromFile(modFile: File): ModInfo {
ZipFile(modFile).use {
val entry = it.getEntry("litemod.json")

View File

@@ -17,9 +17,9 @@
*/
package org.jackhuang.hmcl.mod
import com.google.gson.JsonParseException
import org.jackhuang.hmcl.util.property.ImmediateBooleanProperty
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.ImmediateBooleanProperty
import org.jackhuang.hmcl.util.getValue
import org.jackhuang.hmcl.util.setValue
import java.io.File
class ModInfo (

View File

@@ -17,8 +17,6 @@
*/
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

@@ -19,6 +19,13 @@ package org.jackhuang.hmcl.task
import org.jackhuang.hmcl.util.AutoTypingMap
/**
* A task that combines two tasks and make sure [pred] runs before [succ].
*
* @param pred the task that runs before [succ]
* @param succ a callback that returns the task runs after [pred], [succ] will be executed asynchronously. You can do something that relies on the result of [pred].
* @param reliant true if this task chain will be broken when task [pred] fails.
*/
internal class CoupleTask<P: Task>(pred: P, private val succ: (AutoTypingMap<String>) -> Task?, override val reliant: Boolean) : Task() {
override val hidden: Boolean = true

View File

@@ -19,28 +19,39 @@ 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 org.jackhuang.hmcl.util.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.RandomAccessFile
import java.math.BigInteger
import java.net.Proxy
import java.net.URL
import java.math.BigInteger
import java.util.logging.Level
/**
* A task that can download a file online.
*
* @param url the URL of remote file.
* @param file the location that download to.
* @param hash the SHA-1 hash code of remote file, null if the hash is unknown or it is no need to check the hash code.
* @param retry the times for retrying if downloading fails.
* @param proxy the proxy.
*
* @author huangyuhui
*/
class FileDownloadTask @JvmOverloads constructor(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
/**
* Once downloading fails, this event will be fired to gain the substitute URL.
*/
var onFailed = EventManager<FailedEvent<URL>>()
private var rFile: RandomAccessFile? = null
private var stream: InputStream? = null
fun closeFiles() {
private fun closeFiles() {
rFile?.closeQuietly()
rFile = null
stream?.closeQuietly()

View File

@@ -25,6 +25,17 @@ import java.net.Proxy
import java.net.URL
import java.nio.charset.Charset
/**
* A task that can read the content of a remote text file.
*
* @param url the URL of remote text file.
* @param encoding the encoding/charset of the remote text file.
* @param retry the times for retrying if downloading fails.
* @param proxy the proxy.
* @param id the result variable id, see [Task.variables]
*
* @author huangyuhui
*/
class GetTask @JvmOverloads constructor(val url: URL, val encoding: Charset = Charsets.UTF_8, private val retry: Int = 5, private val proxy: Proxy = Proxy.NO_PROXY, override val id: String = ID): TaskResult<String>() {
override val scheduler: Scheduler = Scheduler.IO
@@ -65,6 +76,9 @@ class GetTask @JvmOverloads constructor(val url: URL, val encoding: Charset = Ch
}
companion object {
/**
* The default task result ID.
*/
const val ID = "http_get"
}
}

View File

@@ -17,6 +17,11 @@
*/
package org.jackhuang.hmcl.task
/**
* The tasks that provides a way to execute tasks parallelly.
*
* @param tasks the tasks that can be executed parallelly.
*/
class ParallelTask(vararg tasks: Task): Task() {
override val hidden: Boolean = true
override val dependents: Collection<Task> = listOf(*tasks)

View File

@@ -17,13 +17,25 @@
*/
package org.jackhuang.hmcl.task
import javafx.application.Platform
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicReference
import javax.swing.SwingUtilities
/**
* Determines how a task is executed.
*
* @see [Task.scheduler]
*/
interface Scheduler {
/**
* Schedules the given task.
* @return the future, null if future is not supported.
*/
fun schedule(block: () -> Unit): Future<*>? = schedule(Callable { block() })
/**
* Schedules the given task.
* @return the future, null if future is not supported.
*/
fun schedule(block: Callable<Unit>): Future<*>?
private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler {
@@ -58,7 +70,7 @@ interface Scheduler {
}
private class SchedulerExecutorService(val executorService: ExecutorService) : Scheduler {
override fun schedule(block: Callable<Unit>) = executorService.submit(block)
override fun schedule(block: Callable<Unit>): Future<*> = executorService.submit(block)
}
companion object Schedulers {
@@ -90,13 +102,44 @@ interface Scheduler {
return null
}
}
val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater)
val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater)
/**
* A scheduler for JavaFX UI operations.
*/
val JAVAFX: Scheduler = SchedulerImpl(javafx.application.Platform::runLater)
/**
* A scheduler for Swing UI operations.
*/
val SWING: Scheduler = SchedulerImpl(javax.swing.SwingUtilities::invokeLater)
/**
* A scheduler that always create new threads to execute tasks.
* For tasks that do not do heavy operations.
*/
val NEW_THREAD: Scheduler = SchedulerExecutorService(CACHED_EXECUTOR)
/**
* A scheduler that exclusively executes tasks that do I/O operations.
* The number tasks that do I/O operations in the meantime cannot be larger then 6.
*/
val IO: Scheduler = SchedulerExecutorService(IO_EXECUTOR)
/**
* A scheduler that exclusively executes tasks that do computations.
* The best way to do computations is an event queue.
*/
val COMPUTATION: Scheduler = SchedulerExecutorService(SINGLE_EXECUTOR)
/**
* The default scheduler for tasks to be executed.
* @see [Task.scheduler]
*/
val DEFAULT = NEW_THREAD
/**
* Shut down all executor services to guarantee that the application can stop implicitly.
*/
fun shutdown() {
CACHED_EXECUTOR.shutdown()
IO_EXECUTOR.shutdown()

View File

@@ -17,8 +17,7 @@
*/
package org.jackhuang.hmcl.task
class SilentException : Exception {
constructor() : super() {}
constructor(message: String) : super(message) {}
constructor(message: String, cause: Throwable) : super(message, cause) {}
}
/**
* If a task throws [SilentException], the task will be marked as failure but do not log the stacktrace.
*/
class SilentException : Exception()

View File

@@ -22,12 +22,15 @@ import javafx.beans.property.ReadOnlyDoubleWrapper
import javafx.beans.property.ReadOnlyStringProperty
import javafx.beans.property.ReadOnlyStringWrapper
import org.jackhuang.hmcl.event.EventManager
import org.jackhuang.hmcl.util.*
import org.jackhuang.hmcl.util.AutoTypingMap
import org.jackhuang.hmcl.util.updateAsync
import java.util.concurrent.Callable
import java.util.concurrent.atomic.AtomicReference
/**
* Disposable task.
*
* @see [TaskExecutor]
*/
abstract class Task {
/**

View File

@@ -19,11 +19,13 @@ package org.jackhuang.hmcl.task
import org.jackhuang.hmcl.util.AutoTypingMap
import org.jackhuang.hmcl.util.LOG
import java.util.concurrent.*
import java.util.concurrent.Callable
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Future
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

View File

@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.task
/**
* A task that has a result.
*/
abstract class TaskResult<V> : Task() {
open var result: V? = null

View File

@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.util
/**
* Mark if the model is immutable.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Immutable

View File

@@ -17,6 +17,9 @@
*/
package org.jackhuang.hmcl.util
/**
* A map that support auto casting.
*/
class AutoTypingMap<K>(private val impl: MutableMap<K, Any>) {
fun clear() = impl.clear()

View File

@@ -18,10 +18,6 @@
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()

View File

@@ -20,15 +20,12 @@ 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 com.google.gson.stream.JsonWriter
import org.jackhuang.hmcl.game.Library
import java.io.File
import java.io.IOException
import java.lang.reflect.Type
import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat

View File

@@ -64,6 +64,10 @@ fun InputStream.copyToAndClose(dest: OutputStream) {
}
}
/**
* @param cmd the command line
* @return the final command line
*/
fun makeCommand(cmd: List<String>): String {
val cmdbuf = StringBuilder(120)
for (i in cmd.indices) {

View File

@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util.property
package org.jackhuang.hmcl.util
import javafx.beans.property.*
import javafx.beans.value.ChangeListener

View File

@@ -24,12 +24,26 @@ import java.io.Serializable
import java.util.*
import java.util.regex.Pattern
/**
* Represents a Java installation.
*/
data class JavaVersion internal constructor(
@SerializedName("location")
val binary: File,
val longVersion: String,
val platform: Platform) : Serializable
{
/**
* The major version of Java installation.
*
* @see JAVA_X
* @see JAVA_9
* @see JAVA_8
* @see JAVA_7
* @see JAVA_6
* @see JAVA_5
* @see UNKNOWN
*/
val version = parseVersion(longVersion)
companion object {
@@ -110,10 +124,12 @@ data class JavaVersion internal constructor(
return path.resolve("java")
}
private val currentJava: JavaVersion = JavaVersion(
binary = getJavaFile(File(System.getProperty("java.home"))),
longVersion = System.getProperty("java.version"),
platform = Platform.PLATFORM)
private val currentJava: JavaVersion by lazy {
JavaVersion(
binary = getJavaFile(File(System.getProperty("java.home"))),
longVersion = System.getProperty("java.version"),
platform = Platform.PLATFORM)
}
fun fromCurrentEnvironment() = currentJava
init {

View File

@@ -18,12 +18,9 @@
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 {

View File

@@ -18,13 +18,13 @@
@file:JvmName("HMCLog")
package org.jackhuang.hmcl.util
import javafx.scene.paint.Color
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.*
import java.util.logging.Formatter
import javafx.scene.paint.Color
import java.util.regex.Pattern
val LOG = Logger.getLogger("HMCL").apply {

View File

@@ -20,25 +20,58 @@ package org.jackhuang.hmcl.util
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class JavaProcess(
/**
* The managed process.
*
* @param process the raw system process that this instance manages.
* @param commands the command line of [process].
* @see [org.jackhuang.hmcl.launch.ExitWaiter]
* @see [org.jackhuang.hmcl.launch.StreamPump]
*/
class ManagedProcess(
val process: Process,
val commands: List<String>
) {
/**
* To save some information you need.
*/
val properties = mutableMapOf<String, Any>()
/**
* The standard output/error lines.
*/
val lines: Queue<String> = ConcurrentLinkedQueue<String>()
/**
* The related threads.
*
* If a thread is monitoring this raw process,
* you are required to add the instance to [relatedThreads].
*/
val relatedThreads = mutableListOf<Thread>()
/**
* True if the managed process is running.
*/
val isRunning: Boolean = try {
process.exitValue()
true
} catch (ex: IllegalThreadStateException) {
false
}
/**
* The exit code of raw process.
*/
val exitCode: Int get() = process.exitValue()
override fun toString() = "JavaProcess[commands=$commands, isRunning=$isRunning]"
/**
* Destroys the raw process and other related threads that are monitoring this raw process.
*/
fun stop() {
process.destroy()
relatedThreads.forEach(Thread::interrupt)
}
override fun toString() = "ManagedProcess[commands=$commands, isRunning=$isRunning]"
}

View File

@@ -19,20 +19,19 @@ package org.jackhuang.hmcl.util
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.Proxy
import java.net.URL
import java.security.GeneralSecurityException
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.*
import java.util.logging.Level
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import java.io.OutputStream
import java.util.*
import java.util.logging.Level
import kotlin.text.Charsets
private val XTM = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}

View File

@@ -25,17 +25,31 @@ import java.lang.management.ManagementFactory
import java.nio.charset.Charset
import java.util.*
/**
* Represents the operating system.
*/
enum class OS {
@SerializedName("windows")
/**
* Microsoft Windows.
*/
WINDOWS,
@SerializedName("linux")
/**
* Linux and Unix like OS, including Solaris.
*/
LINUX,
@SerializedName("osx")
/**
* Mac OS X.
*/
OSX,
@SerializedName("unknown")
/**
* Unknown operating system.
*/
UNKNOWN;
companion object {
/**
* The current operating system.
*/
val CURRENT_OS: OS by lazy {
System.getProperty("os.name").toLowerCase(Locale.US).run {
when {
@@ -47,12 +61,18 @@ enum class OS {
}
}
val TOTAL_MEMORY: Long by lazy {
/**
* The total memory/MB this computer have.
*/
val TOTAL_MEMORY: Int by lazy {
val bytes = ReflectionHelper.invoke<Long>(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize")
if (bytes == null) 1024
else bytes / 1024 / 1024
else (bytes / 1024 / 1024).toInt()
}
/**
* The suggested memory size/MB for Minecraft to allocate.
*/
val SUGGESTED_MEMORY: Int by lazy {
val memory = TOTAL_MEMORY / 4
(Math.round(1.0 * memory / 128.0) * 128).toInt()
@@ -61,13 +81,27 @@ enum class OS {
val PATH_SEPARATOR: String = File.pathSeparator
val FILE_SEPARATOR: String = File.separator
val LINE_SEPARATOR: String by lazy(System::lineSeparator)
/**
* The system default encoding.
*/
val ENCODING: String by lazy {
System.getProperty("sun.jnu.encoding", Charset.defaultCharset().name())
}
/**
* The version of current operating system.
*/
val SYSTEM_VERSION: String by lazy { System.getProperty("os.version") }
/**
* The arch of current operating system.
*/
val SYSTEM_ARCH: String by lazy { System.getProperty("os.arch") }
/**
* Set the content of clipboard.
*/
fun setClipboard(string: String) {
val clipboard = Clipboard.getSystemClipboard()
clipboard.setContent(ClipboardContent().apply {

View File

@@ -20,11 +20,18 @@ package org.jackhuang.hmcl.util
import com.google.gson.*
import java.lang.reflect.Type
/**
* The platform that indicates which the platform of operating system is, 64-bit or 32-bit.
* Of course, 128-bit and 16-bit is not supported.
*/
enum class Platform(val bit: String) {
BIT_32("32"),
BIT_64("64"),
UNKNOWN("unknown");
/**
* The json serializer to [Platform]
*/
companion object Serializer: JsonSerializer<Platform>, JsonDeserializer<Platform> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Platform? {
@@ -45,10 +52,16 @@ enum class Platform(val bit: String) {
}
}
/**
* The platform of current Java Environment.
*/
val PLATFORM: Platform by lazy {
if (IS_64_BIT) BIT_64 else BIT_32
}
/**
* True if current Java Environment is 64-bit.
*/
val IS_64_BIT: Boolean by lazy {
val arch = System.getProperty("sun.arch.data.model") ?: System.getProperty("os.arch")
arch.contains("64")

View File

@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.util
import java.util.HashMap
import java.util.*
object RandomUserAgent {

View File

@@ -17,11 +17,11 @@
*/
package org.jackhuang.hmcl.util
import java.lang.reflect.AccessibleObject
import sun.misc.Unsafe
import java.lang.reflect.AccessibleObject
import java.lang.reflect.Method
import java.security.PrivilegedExceptionAction
import java.security.AccessController
import java.security.PrivilegedExceptionAction
object ReflectionHelper {

View File

@@ -19,6 +19,10 @@ package org.jackhuang.hmcl.util
import java.util.*
/**
* A simple implementation of Multimap.
* Just a combination of map and set.
*/
class SimpleMultimap<K, V>(val maper: () -> MutableMap<K, Collection<V>>, val valuer: () -> MutableCollection<V>) {
private val map = HashMap<K, MutableCollection<V>>()
private val valuesImpl: MutableCollection<V> = valuer()

View File

@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.util
import java.util.*
/**
* The formatted version number represents a version string.
*/
abstract class VersionNumber: Comparable<VersionNumber> {
companion object {
@JvmStatic
@@ -47,6 +50,9 @@ abstract class VersionNumber: Comparable<VersionNumber> {
}
}
/**
* If a version string contains alphabets, a [StringVersionNumber] will be constructed.
*/
class StringVersionNumber internal constructor(val version: String): VersionNumber() {
override fun compareTo(other: VersionNumber): Int {
if (other !is StringVersionNumber) return 0
@@ -64,6 +70,9 @@ class StringVersionNumber internal constructor(val version: String): VersionNumb
}
}
/**
* If a version string formats x.x.x.x, a [IntVersionNumber] will be generated.
*/
class IntVersionNumber internal constructor(val version: List<Int>): VersionNumber() {
override fun compareTo(other: VersionNumber): Int {

View File

@@ -17,24 +17,19 @@
*/
package org.jackhuang.hmcl.util
import java.util.zip.ZipEntry
import java.io.File
import java.io.IOException
import java.util.zip.ZipOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
/**
* 功能:把 src 目录下的所有文件进行 zip 格式的压缩,保存为指定 zip 文件
* @param src 源文件夹
* *
* @param destZip 压缩生成的zip文件路径。
* *
* @param pathNameCallback callback(pathName, isDirectory) returns your
* * modified pathName
* *
* *
* @throws java.io.IOException 压缩失败或无法读取
* Compress the given directory to a zip file.
*
* @param src the source directory or a file.
* @param destZip the location of dest zip file.
* @param pathNameCallback callback(pathName, isDirectory) returns your modified pathName
* @throws IOException
*/
@JvmOverloads
@Throws(IOException::class)
@@ -53,14 +48,11 @@ fun zip(src: File, destZip: File, pathNameCallback: ((String, Boolean) -> String
/**
* Zip file.
* @param src 源文件夹
* @param basePath file directory that will be compressed
* *
* @param zos zip文件的os
* *
* @param pathNameCallback callback(pathName, isDirectory) returns your
* * modified pathName, null if you dont want this file zipped
* @param src source directory to be compressed.
* @param basePath the file directory to be compressed, if [src] is a file, this is the parent directory of [src]
* @param zos the [ZipOutputStream] of dest zip file.
* @param pathNameCallback callback(pathName, isDirectory) returns your modified pathName, null if you dont want this file zipped
* @throws IOException
*/
@Throws(IOException::class)
private fun zipFile(src: File,
@@ -98,17 +90,11 @@ private fun zipFile(src: File,
}
/**
* 将文件压缩成zip文件
* @param zip zip文件路径
* *
* @param dest 待压缩文件根目录
* *
* @param callback will be called for every entry in the zip file,
* * returns false if you dont want this file unzipped.
* *
* *
* @throws java.io.IOException 解压失败或无法写入
* Decompress the given zip file to a directory.
* @param zip the source zip file.
* @param dest the dest directory.
* @param callback will be called for every entry in the zip file, returns false if you dont want this file unzipped.
* @throws IOException
*/
@JvmOverloads
@Throws(IOException::class)
@@ -158,21 +144,16 @@ fun unzip(zip: File, dest: File, callback: ((String) -> Boolean)? = null, ignore
}
/**
* 将文件压缩成zip文件
* @param zip zip文件路径
* *
* @param dest 待压缩文件根目录
* *
* @param callback will be called for every entry in the zip file,
* * returns false if you dont want this file unzipped.
* *
* *
* @throws java.io.IOException 解压失败或无法写入
* Decompress the subdirectory of given zip file.
* @param zip the source zip file.
* @param dest the dest directory.
* @param subDirectory the subdirectory of the zip file to be decompressed.
* @param ignoreExistentFile true if skip all existent files.
* @throws IOException
*/
@JvmOverloads
@Throws(IOException::class)
fun unzipSubDirectory(zip: File, dest: File, subDirectory: String, ignoreExistsFile: Boolean = true) {
fun unzipSubDirectory(zip: File, dest: File, subDirectory: String, ignoreExistentFile: Boolean = true) {
val buf = ByteArray(1024)
dest.mkdirs()
ZipInputStream(zip.inputStream()).use { zipFile ->
@@ -204,7 +185,7 @@ fun unzipSubDirectory(zip: File, dest: File, subDirectory: String, ignoreExistsF
if (!subdir.exists())
subdir.mkdir()
}
if (ignoreExistsFile && File(strtemp).exists())
if (ignoreExistentFile && File(strtemp).exists())
continue
File(strtemp).outputStream().use({ fos ->
zipFile.copyTo(fos, buf)

View File

@@ -19,7 +19,7 @@ package org.jackhuang.hmcl
import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.download.DefaultDependencyManager
import org.jackhuang.hmcl.download.LiteLoaderVersionList
import org.jackhuang.hmcl.download.liteloader.LiteLoaderVersionList
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.download.MojangDownloadProvider
import org.jackhuang.hmcl.game.DefaultGameRepository
@@ -30,9 +30,6 @@ import org.jackhuang.hmcl.launch.ProcessListener
import org.jackhuang.hmcl.util.makeCommand
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.TaskListener
import org.jackhuang.hmcl.util.detectFileName
import org.jackhuang.hmcl.util.toURL
import org.junit.Test
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy