CurseForge modpack support. Random UserAgent for OptiFine. Fixed mis-repositioning of maronry pane
This commit is contained in:
@@ -18,9 +18,10 @@
|
||||
package org.jackhuang.hmcl.download
|
||||
|
||||
import org.jackhuang.hmcl.game.*
|
||||
import java.net.Proxy
|
||||
|
||||
abstract class AbstractDependencyManager(repository: GameRepository)
|
||||
: DependencyManager(repository) {
|
||||
abstract class AbstractDependencyManager(repository: GameRepository, proxy: Proxy)
|
||||
: DependencyManager(repository, proxy) {
|
||||
abstract val downloadProvider: DownloadProvider
|
||||
|
||||
fun getVersions(id: String, selfVersion: String) =
|
||||
|
||||
@@ -27,8 +27,8 @@ import java.net.Proxy
|
||||
/**
|
||||
* This class has no state.
|
||||
*/
|
||||
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY)
|
||||
: AbstractDependencyManager(repository) {
|
||||
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, proxy: Proxy = Proxy.NO_PROXY)
|
||||
: AbstractDependencyManager(repository, proxy) {
|
||||
|
||||
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameB
|
||||
val gameVersion = gameVersion
|
||||
return VersionJSONDownloadTask(gameVersion = gameVersion) then a@{ task ->
|
||||
var version = GSON.fromJson<Version>(task.result!!) ?: return@a null
|
||||
version = version.copy(id = name)
|
||||
version = version.copy(id = name, jar = null)
|
||||
var result = ParallelTask(
|
||||
GameAssetDownloadTask(dependencyManager, version),
|
||||
GameLoggingDownloadTask(dependencyManager, version),
|
||||
|
||||
@@ -20,8 +20,9 @@ package org.jackhuang.hmcl.download
|
||||
import org.jackhuang.hmcl.game.GameRepository
|
||||
import org.jackhuang.hmcl.game.Version
|
||||
import org.jackhuang.hmcl.task.Task
|
||||
import java.net.Proxy
|
||||
|
||||
abstract class DependencyManager(open val repository: GameRepository) {
|
||||
abstract class DependencyManager(open val repository: GameRepository, open val proxy: Proxy) {
|
||||
|
||||
/**
|
||||
* Check if the game is complete.
|
||||
|
||||
@@ -53,7 +53,7 @@ class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyMa
|
||||
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)
|
||||
val file = dependencyManager.repository.getLoggingObject(version.id, version.actualAssetIndex.id, logging)
|
||||
if (!file.exists())
|
||||
dependencies += FileDownloadTask(logging.file.url.toURL(), file, proxy = dependencyManager.proxy)
|
||||
}
|
||||
@@ -63,11 +63,11 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
|
||||
override val dependencies: MutableCollection<Task> = LinkedList()
|
||||
override fun execute() {
|
||||
val assetIndexInfo = version.actualAssetIndex
|
||||
val assetDir = dependencyManager.repository.getAssetDirectory(assetIndexInfo.id)
|
||||
val assetDir = dependencyManager.repository.getAssetDirectory(version.id, assetIndexInfo.id)
|
||||
if (!assetDir.makeDirectory())
|
||||
throw IOException("Cannot create directory: $assetDir")
|
||||
|
||||
val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id)
|
||||
val assetIndexFile = dependencyManager.repository.getIndexFile(version.id, assetIndexInfo.id)
|
||||
dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(assetIndexInfo.url).toURL(), assetIndexFile, proxy = dependencyManager.proxy)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
|
||||
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)
|
||||
private val assetIndexFile = dependencyManager.repository.getIndexFile(version.id, assetIndexInfo.id)
|
||||
override val dependents: MutableCollection<Task> = LinkedList()
|
||||
|
||||
init {
|
||||
@@ -88,7 +88,7 @@ class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManag
|
||||
val res = LinkedList<Pair<File, AssetObject>>()
|
||||
var progress = 0
|
||||
index?.objects?.entries?.forEach { (_, assetObject) ->
|
||||
res += Pair(dependencyManager.repository.getAssetObject(assetIndexInfo.id, assetObject), assetObject)
|
||||
res += Pair(dependencyManager.repository.getAssetObject(version.id, assetIndexInfo.id, assetObject), assetObject)
|
||||
updateProgress(++progress, index.objects.size)
|
||||
}
|
||||
result = res
|
||||
|
||||
@@ -37,7 +37,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
}
|
||||
override fun getVersionCount() = versions.size
|
||||
override fun getVersions() = versions.values
|
||||
override fun getLibraryFile(id: Version, lib: Library) = File(baseDirectory, "libraries/${lib.path}")
|
||||
override fun getLibraryFile(version: Version, lib: Library) = baseDirectory.resolve("libraries/${lib.path}")
|
||||
override fun getRunDirectory(id: String) = baseDirectory
|
||||
override fun getVersionJar(version: Version): File {
|
||||
val v = version.resolve(this)
|
||||
@@ -45,7 +45,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
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")
|
||||
override 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)
|
||||
@@ -135,49 +135,49 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
EVENT_BUS.fireEvent(RefreshedVersionsEvent(this))
|
||||
}
|
||||
|
||||
override fun getAssetIndex(assetId: String): AssetIndex {
|
||||
return GSON.fromJson(getIndexFile(assetId).readText())!!
|
||||
override fun getAssetIndex(version: String, assetId: String): AssetIndex {
|
||||
return GSON.fromJson(getIndexFile(version, assetId).readText())!!
|
||||
}
|
||||
|
||||
override fun getActualAssetDirectory(assetId: String): File {
|
||||
override fun getActualAssetDirectory(version: String, assetId: String): File {
|
||||
try {
|
||||
return reconstructAssets(assetId)
|
||||
return reconstructAssets(version, assetId)
|
||||
} catch (e: IOException) {
|
||||
LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e)
|
||||
return getAssetDirectory(assetId)
|
||||
return getAssetDirectory(version, assetId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAssetDirectory(assetId: String): File =
|
||||
override fun getAssetDirectory(version: String, assetId: String): File =
|
||||
baseDirectory.resolve("assets")
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getAssetObject(assetId: String, name: String): File {
|
||||
override fun getAssetObject(version: String, assetId: String, name: String): File {
|
||||
try {
|
||||
return getAssetObject(assetId, getAssetIndex(assetId).objects["name"]!!)
|
||||
return getAssetObject(version, assetId, getAssetIndex(version, 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)
|
||||
override fun getAssetObject(version: String, assetId: String, obj: AssetObject): File =
|
||||
getAssetObject(version, getAssetDirectory(version, assetId), obj)
|
||||
|
||||
open fun getAssetObject(assetDir: File, obj: AssetObject): File {
|
||||
open fun getAssetObject(version: String, assetDir: File, obj: AssetObject): File {
|
||||
return assetDir.resolve("objects/${obj.location}")
|
||||
}
|
||||
|
||||
open fun getIndexFile(assetId: String): File =
|
||||
getAssetDirectory(assetId).resolve("indexes/$assetId.json")
|
||||
open fun getIndexFile(version: String, assetId: String): File =
|
||||
getAssetDirectory(version, assetId).resolve("indexes/$assetId.json")
|
||||
|
||||
override fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File =
|
||||
getAssetDirectory(assetId).resolve("log_configs/${loggingInfo.file.id}")
|
||||
override fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File =
|
||||
getAssetDirectory(version, assetId).resolve("log_configs/${loggingInfo.file.id}")
|
||||
|
||||
@Throws(IOException::class, JsonSyntaxException::class)
|
||||
protected open fun reconstructAssets(assetId: String): File {
|
||||
val assetsDir = getAssetDirectory(assetId)
|
||||
protected open fun reconstructAssets(version: String, assetId: String): File {
|
||||
val assetsDir = getAssetDirectory(version, assetId)
|
||||
val assetVersion = assetId
|
||||
val indexFile: File = getIndexFile(assetVersion)
|
||||
val indexFile: File = getIndexFile(version, assetVersion)
|
||||
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
|
||||
|
||||
if (!indexFile.isFile) {
|
||||
@@ -193,7 +193,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
val tot = index.objects.entries.size
|
||||
for ((location, assetObject) in index.objects.entries) {
|
||||
val target = File(virtualRoot, location)
|
||||
val original = getAssetObject(assetsDir, assetObject)
|
||||
val original = getAssetObject(version, assetsDir, assetObject)
|
||||
if (original.exists()) {
|
||||
cnt++
|
||||
if (!target.isFile)
|
||||
|
||||
@@ -59,6 +59,13 @@ interface GameRepository : VersionProvider {
|
||||
*/
|
||||
fun refreshVersions()
|
||||
|
||||
/**
|
||||
* Gets the root folder of specific version.
|
||||
* The root folders the versions must be unique.
|
||||
* For example, .minecraft/versions/<version name>/.
|
||||
*/
|
||||
fun getVersionRoot(id: String): File
|
||||
|
||||
/**
|
||||
* Gets the current running directory of the given version for game.
|
||||
* @param id the version id
|
||||
@@ -69,11 +76,11 @@ interface GameRepository : VersionProvider {
|
||||
* 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 version versionversion
|
||||
* @param lib the library, [Version.libraries]
|
||||
* @return the library file
|
||||
*/
|
||||
fun getLibraryFile(id: Version, lib: Library): File
|
||||
fun getLibraryFile(version: Version, lib: Library): File
|
||||
|
||||
/**
|
||||
* Get the directory that native libraries will be unzipped to.
|
||||
@@ -123,12 +130,12 @@ interface GameRepository : VersionProvider {
|
||||
* @throws java.io.IOException if I/O operation fails.
|
||||
* @return the actual asset directory
|
||||
*/
|
||||
fun getActualAssetDirectory(assetId: String): File
|
||||
fun getActualAssetDirectory(version: String, assetId: String): File
|
||||
|
||||
/**
|
||||
* Get the asset directory according to the asset id.
|
||||
*/
|
||||
fun getAssetDirectory(assetId: String): File
|
||||
fun getAssetDirectory(version: String, assetId: String): File
|
||||
|
||||
/**
|
||||
* Get the file that given asset object refers to
|
||||
@@ -138,7 +145,7 @@ interface GameRepository : VersionProvider {
|
||||
* @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
|
||||
fun getAssetObject(version: String, assetId: String, name: String): File
|
||||
|
||||
/**
|
||||
* Get the file that given asset object refers to
|
||||
@@ -147,7 +154,7 @@ interface GameRepository : VersionProvider {
|
||||
* @param obj the asset object, [AssetIndex.objects]
|
||||
* @return the file that given asset object refers to
|
||||
*/
|
||||
fun getAssetObject(assetId: String, obj: AssetObject): File
|
||||
fun getAssetObject(version: String, assetId: String, obj: AssetObject): File
|
||||
|
||||
/**
|
||||
* Get asset index that assetId represents
|
||||
@@ -155,7 +162,7 @@ interface GameRepository : VersionProvider {
|
||||
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
|
||||
* @return the asset index
|
||||
*/
|
||||
fun getAssetIndex(assetId: String): AssetIndex
|
||||
fun getAssetIndex(version: String, assetId: String): AssetIndex
|
||||
|
||||
/**
|
||||
* Get logging object
|
||||
@@ -164,5 +171,5 @@ interface GameRepository : VersionProvider {
|
||||
* @param loggingInfo the logging info
|
||||
* @return the file that loggingInfo refers to
|
||||
*/
|
||||
fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File
|
||||
fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File
|
||||
}
|
||||
@@ -68,14 +68,14 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
|
||||
|
||||
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);
|
||||
res.add("-Xdock:icon=" + repository.getAssetObject(version.id, 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)
|
||||
val loggingFile = repository.getLoggingObject(version.id, version.actualAssetIndex.id, loggingInfo)
|
||||
if (loggingFile.exists())
|
||||
res.add(loggingInfo.argument.replace("\${path}", loggingFile.absolutePath))
|
||||
}
|
||||
@@ -139,7 +139,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
|
||||
res.add(version.mainClass!!)
|
||||
|
||||
// Provided Minecraft arguments
|
||||
val gameAssets = repository.getActualAssetDirectory(version.actualAssetIndex.id)
|
||||
val gameAssets = repository.getActualAssetDirectory(version.id, version.actualAssetIndex.id)
|
||||
|
||||
version.minecraftArguments!!.tokenize().forEach { line ->
|
||||
res.add(line
|
||||
|
||||
@@ -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.mod
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager
|
||||
import org.jackhuang.hmcl.download.DependencyManager
|
||||
import org.jackhuang.hmcl.game.GameException
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask
|
||||
import org.jackhuang.hmcl.task.Task
|
||||
import org.jackhuang.hmcl.task.task
|
||||
import org.jackhuang.hmcl.util.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class CurseForgeModpackManifest @JvmOverloads constructor(
|
||||
@SerializedName("manifestType")
|
||||
val manifestType: String = MINECRAFT_MODPACK,
|
||||
@SerializedName("manifestVersion")
|
||||
val manifestVersion: Int = 1,
|
||||
@SerializedName("name")
|
||||
val name: String = "",
|
||||
@SerializedName("version")
|
||||
val version: String = "1.0",
|
||||
@SerializedName("author")
|
||||
val author: String = "",
|
||||
@SerializedName("overrides")
|
||||
val overrides: String = "overrides",
|
||||
@SerializedName("minecraft")
|
||||
val minecraft: CurseForgeModpackManifestMinecraft = CurseForgeModpackManifestMinecraft(),
|
||||
@SerializedName("files")
|
||||
val files: List<CurseForgeModpackManifestFile> = emptyList()
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(manifestType == MINECRAFT_MODPACK, { "Only support Minecraft modpack" })
|
||||
}
|
||||
|
||||
companion object {
|
||||
val MINECRAFT_MODPACK = "minecraftModpack"
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackManifestMinecraft (
|
||||
@SerializedName("version")
|
||||
val gameVersion: String = "",
|
||||
@SerializedName("modLoaders")
|
||||
val modLoaders: List<CurseForgeModpackManifestModLoader> = emptyList()
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(gameVersion.isNotBlank())
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackManifestModLoader (
|
||||
@SerializedName("id")
|
||||
val id: String = "",
|
||||
@SerializedName("primary")
|
||||
val primary: Boolean = false
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(id.isNotBlank(), { "Curse Forge modpack manifest Mod loader id cannot be blank." })
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackManifestFile (
|
||||
@SerializedName("projectID")
|
||||
val projectID: Int = 0,
|
||||
@SerializedName("fileID")
|
||||
val fileID: Int = 0,
|
||||
@SerializedName("fileName")
|
||||
var fileName: String = "",
|
||||
@SerializedName("required")
|
||||
val required: Boolean = true
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(projectID != 0)
|
||||
check(fileID != 0)
|
||||
}
|
||||
|
||||
val url: URL get() = "https://minecraft.curseforge.com/projects/$projectID/files/$fileID/download".toURL()
|
||||
}
|
||||
|
||||
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 json = zipFile.getInputStream(entry).readFullyAsString()
|
||||
return GSON.fromJson<CurseForgeModpackManifest>(json) ?: throw JsonParseException("Manifest.json not found. Not a valid CurseForge modpack.")
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDependencyManager, private val zipFile: File, private val manifest: CurseForgeModpackManifest, private val name: String): Task() {
|
||||
val repository = dependencyManager.repository
|
||||
init {
|
||||
if (repository.hasVersion(name))
|
||||
throw IllegalStateException("Version $name already exists.")
|
||||
}
|
||||
|
||||
val root = repository.getVersionRoot(name)
|
||||
val run = repository.getRunDirectory(name)
|
||||
override val dependents = mutableListOf<Task>()
|
||||
override val dependencies = mutableListOf<Task>()
|
||||
init {
|
||||
val builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.minecraft.gameVersion)
|
||||
manifest.minecraft.modLoaders.forEach {
|
||||
if (it.id.startsWith("forge-"))
|
||||
builder.version("forge", it.id.substring("forge-".length))
|
||||
}
|
||||
dependents += builder.buildAsync()
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
unzipSubDirectory(zipFile, run, manifest.overrides)
|
||||
|
||||
var finished = 0
|
||||
for (f in manifest.files) {
|
||||
try {
|
||||
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.
|
||||
}
|
||||
++finished
|
||||
updateProgress(1.0 * finished / manifest.files.size)
|
||||
}
|
||||
|
||||
root.resolve("manifest.json").writeText(GSON.toJson(manifest))
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackCompletionTask(dependencyManager: DependencyManager, version: String): Task() {
|
||||
val repository = dependencyManager.repository
|
||||
val run = repository.getRunDirectory(version)
|
||||
private var manifest: CurseForgeModpackManifest?
|
||||
private val proxy = dependencyManager.proxy
|
||||
override val dependents = mutableListOf<Task>()
|
||||
override val dependencies = mutableListOf<Task>()
|
||||
|
||||
init {
|
||||
try {
|
||||
val manifestFile = repository.getVersionRoot(version).resolve("manifest.json")
|
||||
if (!manifestFile.exists()) manifest = null
|
||||
else {
|
||||
manifest = GSON.fromJson<CurseForgeModpackManifest>(repository.getVersionRoot(version).resolve("manifest.json").readText())!!
|
||||
|
||||
for (f in manifest!!.files) {
|
||||
if (f.fileName.isBlank())
|
||||
dependents += task { f.fileName = f.url.detectFileName(proxy) }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.log(Level.WARNING, "Unable to read CurseForge modpack manifest.json", e)
|
||||
manifest = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
if (manifest == null) return
|
||||
for (f in manifest!!.files) {
|
||||
if (f.fileName.isBlank())
|
||||
throw GameException("Unable to download mod, cannot continue")
|
||||
val file = run.resolve("mods").resolve(f.fileName)
|
||||
if (!file.exists())
|
||||
dependencies += FileDownloadTask(f.url, file, proxy = proxy)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,12 +61,12 @@ abstract class Task {
|
||||
infix fun with(b: Task): Task = CoupleTask(this, { b }, false)
|
||||
|
||||
/**
|
||||
* The collection of sub-tasks that should execute before this task running.
|
||||
* 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.
|
||||
* The collection of sub-tasks that should execute **after** this task running.
|
||||
*/
|
||||
open val dependencies: Collection<Task> = emptySet()
|
||||
|
||||
@@ -119,14 +119,12 @@ abstract class Task {
|
||||
submit(subscriber).start()
|
||||
}
|
||||
|
||||
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(scheduler, closure))
|
||||
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(task(scheduler, closure))
|
||||
|
||||
override fun toString(): String {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun of(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
|
||||
fun <V> of(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
|
||||
}
|
||||
}
|
||||
fun task(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
|
||||
fun <V> task(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
|
||||
@@ -30,6 +30,8 @@ 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 {
|
||||
@@ -56,7 +58,7 @@ fun initHttps() {
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(HNV);
|
||||
}
|
||||
|
||||
var DEFAULT_USER_AGENT = "JMCCC"
|
||||
var DEFAULT_USER_AGENT: () -> String = { RandomUserAgent.randomUserAgent }
|
||||
|
||||
fun String.toURL() = URL(this)
|
||||
|
||||
@@ -67,7 +69,7 @@ fun URL.createConnection(proxy: Proxy): HttpURLConnection {
|
||||
useCaches = false
|
||||
connectTimeout = 15000
|
||||
readTimeout = 15000
|
||||
addRequestProperty("User-Agent", DEFAULT_USER_AGENT)
|
||||
addRequestProperty("User-Agent", DEFAULT_USER_AGENT())
|
||||
} as HttpURLConnection
|
||||
}
|
||||
|
||||
@@ -118,4 +120,34 @@ fun HttpURLConnection.readData(): String {
|
||||
} finally {
|
||||
input?.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
fun URL.detectFileNameQuietly(proxy: Proxy = Proxy.NO_PROXY): String {
|
||||
try {
|
||||
val conn = createConnection(proxy)
|
||||
conn.connect()
|
||||
if (conn.responseCode / 100 != 2)
|
||||
throw IOException("Response code ${conn.responseCode}")
|
||||
return conn.detectFileName()
|
||||
} catch (e: IOException) {
|
||||
LOG.log(Level.WARNING, "Cannot detect the file name of URL $this", e)
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID())
|
||||
}
|
||||
}
|
||||
|
||||
fun URL.detectFileName(proxy: Proxy = Proxy.NO_PROXY): String {
|
||||
val conn = createConnection(proxy)
|
||||
conn.connect()
|
||||
if (conn.responseCode / 100 != 2)
|
||||
throw IOException("Response code ${conn.responseCode}")
|
||||
return conn.detectFileName()
|
||||
}
|
||||
|
||||
fun HttpURLConnection.detectFileName(): String {
|
||||
val disposition = getHeaderField("Content-Disposition")
|
||||
if (disposition == null || disposition.indexOf("filename=") == -1) {
|
||||
val u = url.toString()
|
||||
return u.substringAfterLast('/')
|
||||
} else
|
||||
return disposition.substringAfter("filename=").removeSurrounding("\"")
|
||||
}
|
||||
1683
HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/RandomUserAgent.kt
Normal file
1683
HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/RandomUserAgent.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -155,4 +155,62 @@ 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 解压失败或无法写入
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Throws(IOException::class)
|
||||
fun unzipSubDirectory(zip: File, dest: File, subDirectory: String, ignoreExistsFile: Boolean = true) {
|
||||
val buf = ByteArray(1024)
|
||||
dest.mkdirs()
|
||||
ZipInputStream(zip.inputStream()).use { zipFile ->
|
||||
if (zip.exists()) {
|
||||
var gbkPath: String
|
||||
var strtemp: String
|
||||
val strPath = dest.absolutePath
|
||||
var zipEnt: ZipEntry?
|
||||
while (true) {
|
||||
zipEnt = zipFile.nextEntry
|
||||
if (zipEnt == null)
|
||||
break
|
||||
gbkPath = zipEnt.name
|
||||
if (!gbkPath.startsWith(subDirectory))
|
||||
continue
|
||||
gbkPath = gbkPath.substring(subDirectory.length)
|
||||
if (gbkPath.startsWith("/") || gbkPath.startsWith("\\")) gbkPath = gbkPath.substring(1)
|
||||
strtemp = strPath + File.separator + gbkPath
|
||||
if (zipEnt.isDirectory) {
|
||||
val dir = File(strtemp)
|
||||
dir.mkdirs()
|
||||
} else {
|
||||
//建目录
|
||||
val strsubdir = gbkPath
|
||||
for (i in 0..strsubdir.length - 1)
|
||||
if (strsubdir.substring(i, i + 1).equals("/", ignoreCase = true)) {
|
||||
val temp = strPath + File.separator + strsubdir.substring(0, i)
|
||||
val subdir = File(temp)
|
||||
if (!subdir.exists())
|
||||
subdir.mkdir()
|
||||
}
|
||||
if (ignoreExistsFile && File(strtemp).exists())
|
||||
continue
|
||||
File(strtemp).outputStream().use({ fos ->
|
||||
zipFile.copyTo(fos, buf)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user