CurseForge modpack support. Random UserAgent for OptiFine. Fixed mis-repositioning of maronry pane

This commit is contained in:
huangyuhui
2017-08-17 13:14:34 +08:00
parent 1394034160
commit 99f60ea6e5
35 changed files with 2367 additions and 84 deletions

View File

@@ -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) =

View File

@@ -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)

View File

@@ -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),

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View 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

View File

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

View File

@@ -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)

View File

@@ -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("\"")
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)
})
}
}
}
}
}

View File

@@ -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