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

@ -26,6 +26,7 @@ import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.OS
import java.io.File
import java.util.concurrent.Callable
import java.util.logging.Level
fun i18n(key: String): String {
@ -51,12 +52,13 @@ class Main : Application() {
companion object {
val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
val TITLE = "HMCL $VERSION"
val NAME = "HMCL"
val TITLE = "$NAME $VERSION"
lateinit var PRIMARY_STAGE: Stage
@JvmStatic
fun main(args: Array<String>) {
DEFAULT_USER_AGENT = "Hello Minecraft! Launcher"
DEFAULT_USER_AGENT = { "Hello Minecraft! Launcher" }
launch(Main::class.java, *args)
}

View File

@ -0,0 +1,34 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.launch.DefaultLauncher
import org.jackhuang.hmcl.launch.ProcessListener
class HMCLGameLauncher(repository: GameRepository, versionId: String, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
: DefaultLauncher(repository, versionId, account, options, listener, isDaemon) {
override fun appendJvmArgs(res: MutableList<String>) {
super.appendJvmArgs(res)
res.add("-Dminecraft.launcher.version=" + Main.VERSION);
res.add("-Dminecraft.launcher.brand=" + Main.NAME);
}
}

View File

@ -19,6 +19,9 @@ package org.jackhuang.hmcl.game
import com.google.gson.GsonBuilder
import javafx.beans.InvalidationListener
import org.jackhuang.hmcl.setting.EnumGameDirectory
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.setting.VersionSetting
import org.jackhuang.hmcl.util.GSON
import org.jackhuang.hmcl.util.LOG
@ -27,10 +30,44 @@ import java.io.File
import java.io.IOException
import java.util.logging.Level
class HMCLGameRepository(baseDirectory: File)
class HMCLGameRepository(val profile: Profile, baseDirectory: File)
: DefaultGameRepository(baseDirectory) {
private val versionSettings = HashMap<String, VersionSetting>()
private val versionSettings = mutableMapOf<String, VersionSetting>()
private val beingModpackVersions = mutableSetOf<String>()
private fun useSelf(version: String, assetId: String): Boolean {
val vs = profile.getVersionSetting(version)
return File(baseDirectory, "assets/indexes/$assetId.json").exists() || vs.noCommon
}
override fun getAssetDirectory(version: String, assetId: String): File {
if (useSelf(version, assetId))
return super.getAssetDirectory(version, assetId)
else
return File(Settings.commonPath).resolve("assets")
}
override fun getRunDirectory(id: String): File {
if (beingModpackVersions.contains(id))
return getVersionRoot(id)
else {
val vs = profile.getVersionSetting(id)
return when (vs.gameDirType) {
EnumGameDirectory.VERSION_FOLDER -> getVersionRoot(id)
EnumGameDirectory.ROOT_FOLDER -> super.getRunDirectory(id)
}
}
}
override fun getLibraryFile(version: Version, lib: Library): File {
val vs = profile.getVersionSetting(version.id)
val self = super.getLibraryFile(version, lib);
if (self.exists() || vs.noCommon)
return self;
else
return File(Settings.commonPath).resolve("libraries/${lib.path}")
}
override fun refreshVersionsImpl() {
versionSettings.clear()
@ -91,6 +128,14 @@ class HMCLGameRepository(baseDirectory: File)
return versionSettings[id]
}
fun markVersionAsModpack(id: String) {
beingModpackVersions += id
}
fun undoMark(id: String) {
beingModpackVersions -= id
}
companion object {
val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"
val GSON = GsonBuilder().registerTypeAdapter(VersionSetting::class.java, VersionSetting).setPrettyPrinting().create()

View File

@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.launch.DefaultLauncher
import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
@ -25,16 +25,18 @@ object LauncherHelper {
fun launch() {
val profile = Settings.selectedProfile
val repository = profile.repository
val dependency = profile.dependency
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
val version = repository.getVersion(profile.selectedVersion)
val launcher = DefaultLauncher(
val launcher = HMCLGameLauncher(
repository = repository,
versionId = profile.selectedVersion,
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
account = account.logIn(Settings.proxy)
)
profile.dependency.checkGameCompletionAsync(version)
dependency.checkGameCompletionAsync(version)
.then(CurseForgeModpackCompletionTask(dependency, profile.selectedVersion))
.then(launcher.launchAsync())
.subscribe(Scheduler.JAVAFX) { println("lalala") }
}

View File

@ -38,7 +38,7 @@ class Profile(var name: String = "Default", initialGameDir: File = File(".minecr
val gameDirProperty = ImmediateObjectProperty<File>(this, "gameDir", initialGameDir)
var gameDir: File by gameDirProperty
var repository = HMCLGameRepository(initialGameDir)
var repository = HMCLGameRepository(this, initialGameDir)
val dependency: DefaultDependencyManager get() = DefaultDependencyManager(repository, Settings.downloadProvider, Settings.proxy)
var modManager = ModManager(repository)
@ -57,11 +57,15 @@ class Profile(var name: String = "Default", initialGameDir: File = File(".minecr
}
}
fun specializeVersionSetting(id: String) {
/**
* @return null if the given version id does not exist.
*/
fun specializeVersionSetting(id: String): VersionSetting? {
var vs = repository.getVersionSetting(id)
if (vs == null)
vs = repository.createVersionSetting(id) ?: return
vs = repository.createVersionSetting(id) ?: return null
vs.usesGlobal = false
return vs
}
fun globalizeVersionSetting(id: String) {

View File

@ -28,7 +28,7 @@ import java.net.Proxy
import java.util.*
object Proxies {
val PROXIES = listOf(null, Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS)
val PROXIES = listOf(Proxy.Type.DIRECT, Proxy.Type.HTTP, Proxy.Type.SOCKS)
fun getProxyType(index: Int): Proxy.Type? = PROXIES.getOrNull(index)
}

View File

@ -35,6 +35,7 @@ import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.task
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import java.util.concurrent.Callable
@ -104,7 +105,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
if (newValue != null)
Settings.selectedAccount = newValue.properties["account"] as Account
}
masonryPane.children.setAll(children)
masonryPane.resetChildren(children)
Platform.runLater {
masonryPane.requestLayout()
scrollPane.requestLayout()
@ -135,7 +136,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
val username = txtUsername.text
val password = txtPassword.text
progressBar.isVisible = true
val task = Task.of(Callable {
val task = task(Callable {
try {
val account = when (type) {
0 -> OfflineAccount.fromUsername(username)

View File

@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.adapters.ReflectionHelper
import com.jfoenix.concurrency.JFXUtilities
import com.jfoenix.controls.*
import javafx.animation.Animation
@ -183,3 +184,9 @@ val SINE: Interpolator = object : Interpolator() {
return "Interpolator.DISCRETE"
}
}
fun JFXMasonryPane.resetChildren(children: List<Node>) {
// Fixes mis-repositioning.
ReflectionHelper.setFieldContent(JFXMasonryPane::class.java, this, "oldBoxes", null)
this.children.setAll(children)
}

View File

@ -36,6 +36,7 @@ import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.construct.RipplerContainer
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
/**
@ -45,6 +46,8 @@ class MainPage : StackPane(), DecoratorPage {
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main"))
@FXML lateinit var btnLaunch: JFXButton
@FXML lateinit var btnRefresh: JFXButton
@FXML lateinit var btnAdd: JFXButton
@FXML lateinit var masonryPane: JFXMasonryPane
init {
@ -60,8 +63,8 @@ class MainPage : StackPane(), DecoratorPage {
Settings.onProfileLoading()
// Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game")
// Settings.selectedProfile.repository.refreshVersions()
btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") }
btnRefresh.setOnMouseClicked { Settings.selectedProfile.repository.refreshVersions() }
btnLaunch.setOnMouseClicked { LauncherHelper.launch() }
}
@ -106,7 +109,7 @@ class MainPage : StackPane(), DecoratorPage {
if (newValue != null)
profile.selectedVersion = newValue.properties["version"] as String
}
masonryPane.children.setAll(children)
masonryPane.resetChildren(children)
}
@Suppress("UNCHECKED_CAST")

View File

@ -25,6 +25,7 @@ import javafx.scene.layout.VBox
import org.jackhuang.hmcl.mod.ModManager
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.task
import java.util.concurrent.Callable
class ModController {
@ -40,9 +41,9 @@ class ModController {
fun loadMods(modManager: ModManager, versionId: String) {
this.modManager = modManager
this.versionId = versionId
Task.of(Callable {
task {
modManager.refreshMods(versionId)
}).subscribe(Scheduler.JAVAFX) {
}.subscribe(Scheduler.JAVAFX) {
rootPane.children.clear()
for (modInfo in modManager.getMods(versionId)) {
rootPane.children += ModItem(modInfo) {

View File

@ -0,0 +1,33 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui.construct
import com.jfoenix.validation.base.ValidatorBase
import javafx.scene.control.TextInputControl
/**
* @param validator return true if the input string is valid.
*/
class Validator(val validator: (String) -> Boolean) : ValidatorBase() {
override fun eval() {
if (this.srcControl.get() is TextInputControl) {
val text = (srcControl.get() as TextInputControl).text
hasErrors.set(!validator(text))
}
}
}

View File

@ -20,14 +20,29 @@ package org.jackhuang.hmcl.ui.download
import javafx.scene.Node
import javafx.scene.layout.Pane
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask
import org.jackhuang.hmcl.mod.CurseForgeModpackInstallTask
import org.jackhuang.hmcl.mod.CurseForgeModpackManifest
import org.jackhuang.hmcl.setting.EnumGameDirectory
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Task
import org.jackhuang.hmcl.task.task
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardProvider
import java.io.File
class DownloadWizardProvider(): WizardProvider() {
lateinit var profile: Profile
override fun finish(settings: Map<String, Any>): Any? {
val builder = Settings.selectedProfile.dependency.gameBuilder()
override fun start(settings: MutableMap<String, Any>) {
profile = Settings.selectedProfile
settings[PROFILE] = profile
}
private fun finishVersionDownloading(settings: MutableMap<String, Any>): Task {
val builder = profile.dependency.gameBuilder()
builder.name(settings["name"] as String)
builder.gameVersion(settings["game"] as String)
@ -44,12 +59,42 @@ class DownloadWizardProvider(): WizardProvider() {
return builder.buildAsync()
}
override fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node {
private fun finishModpackInstalling(settings: MutableMap<String, Any>): Task? {
if (!settings.containsKey(ModpackPage.MODPACK_FILE))
return null
val selectedFile = settings[ModpackPage.MODPACK_FILE] as? File? ?: return null
val manifest = settings[ModpackPage.MODPACK_CURSEFORGE_MANIFEST] as? CurseForgeModpackManifest? ?: return null
val name = settings[ModpackPage.MODPACK_NAME] as? String? ?: return null
profile.repository.markVersionAsModpack(name)
return CurseForgeModpackInstallTask(profile.dependency, selectedFile, manifest, name) with task {
profile.repository.refreshVersions()
val vs = profile.specializeVersionSetting(name)
profile.repository.undoMark(name)
if (vs != null) {
vs.gameDirType = EnumGameDirectory.VERSION_FOLDER
}
}
}
override fun finish(settings: MutableMap<String, Any>): Any? {
return when (settings[InstallTypePage.INSTALL_TYPE]) {
0 -> finishVersionDownloading(settings)
1 -> finishModpackInstalling(settings)
else -> null
}
}
override fun createPage(controller: WizardController, step: Int, settings: MutableMap<String, Any>): Node {
return when (step) {
0 -> InstallTypePage(controller)
1 -> when (settings[InstallTypePage.INSTALL_TYPE]) {
0 -> VersionsPage(controller, "", BMCLAPIDownloadProvider, "game", { controller.onNext(InstallersPage(controller, BMCLAPIDownloadProvider)) })
else -> Pane()
1 -> ModpackPage(controller)
else -> throw Error()
}
else -> throw IllegalStateException()
}
@ -59,4 +104,8 @@ class DownloadWizardProvider(): WizardProvider() {
return true
}
companion object {
const val PROFILE = "PROFILE"
}
}

View File

@ -0,0 +1,97 @@
/*
* 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.ui.download
import com.google.gson.JsonParseException
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXTextField
import javafx.application.Platform
import javafx.fxml.FXML
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.stage.FileChooser
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.mod.readCurseForgeModpackManifest
import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.construct.Validator
import org.jackhuang.hmcl.ui.loadFXML
import org.jackhuang.hmcl.ui.wizard.WizardController
import org.jackhuang.hmcl.ui.wizard.WizardPage
import java.io.IOException
class ModpackPage(private val controller: WizardController): StackPane(), WizardPage {
override val title: String = "Install a modpack"
@FXML lateinit var lblName: Label
@FXML lateinit var lblVersion: Label
@FXML lateinit var lblAuthor: Label
@FXML lateinit var lblModpackLocation: Label
@FXML lateinit var txtModpackName: JFXTextField
@FXML lateinit var btnInstall: JFXButton
init {
loadFXML("/assets/fxml/download/modpack.fxml")
val profile = controller.settings["PROFILE"] as Profile
val chooser = FileChooser()
chooser.title = i18n("modpack.choose")
val selectedFile = chooser.showOpenDialog(Controllers.stage)
if (selectedFile == null) Platform.runLater { controller.onFinish() }
else {
// TODO: original HMCL modpack support.
controller.settings[MODPACK_FILE] = selectedFile
lblModpackLocation.text = selectedFile.absolutePath
txtModpackName.text = selectedFile.nameWithoutExtension
txtModpackName.validators += Validator { !profile.repository.hasVersion(it) }
txtModpackName.textProperty().addListener { _ ->
btnInstall.isDisabled = !txtModpackName.validate()
}
try {
val manifest = readCurseForgeModpackManifest(selectedFile)
controller.settings[MODPACK_CURSEFORGE_MANIFEST] = manifest
lblName.text = manifest.name
lblVersion.text = manifest.version
lblAuthor.text = manifest.author
} catch (e: IOException) {
// TODO
} catch (e: JsonParseException) {
// TODO
}
}
}
override fun cleanup(settings: MutableMap<String, Any>) {
settings.remove(MODPACK_FILE)
}
fun onInstall() {
if (!txtModpackName.validate()) return
controller.settings[MODPACK_NAME] = txtModpackName.text
controller.onFinish()
}
companion object {
val MODPACK_FILE = "MODPACK_FILE"
val MODPACK_NAME = "MODPACK_NAME"
val MODPACK_CURSEFORGE_MANIFEST = "CURSEFORGE_MANIFEST"
}
}

View File

@ -126,7 +126,7 @@ interface AbstractWizardDisplayer : WizardDisplayer {
cancelQueue.add(executor)
executor.submit(Task.of(Scheduler.JAVAFX) {
executor.submit(org.jackhuang.hmcl.task.task(Scheduler.JAVAFX) {
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
})
}.start()

View File

@ -31,6 +31,7 @@ class WizardController(protected val displayer: WizardDisplayer) : Navigation {
pages.clear()
val page = navigatingTo(0)
pages.push(page)
provider.start(settings)
displayer.onStart()
displayer.navigateTo(page, Navigation.NavigationDirection.START)
}

View File

@ -20,8 +20,8 @@ package org.jackhuang.hmcl.ui.wizard
import javafx.scene.Node
abstract class WizardProvider {
abstract fun finish(settings: Map<String, Any>): Any?
abstract fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node
abstract fun start(settings: MutableMap<String, Any>)
abstract fun finish(settings: MutableMap<String, Any>): Any?
abstract fun createPage(controller: WizardController, step: Int, settings: MutableMap<String, Any>): Node
abstract fun cancel(): Boolean
}

View File

@ -414,6 +414,12 @@
-jfx-mask-type: CIRCLE;
}
.jfx-button-raised {
-fx-text-fill: white;
-fx-background-color: #5264AE;
-fx-font-size:14px;
}
/*******************************************************************************
* *
* JFX Check Box *

View File

@ -15,7 +15,7 @@
<top>
<VBox alignment="CENTER" style="-fx-padding: 40px;" spacing="20">
<Label fx:id="lblGameVersion" alignment="CENTER" />
<JFXTextField fx:id="txtName" labelFloat="true" promptText="Enter the name of this new version" maxWidth="300" />
<JFXTextField fx:id="txtName" labelFloat="true" promptText="%modpack.enter_name" maxWidth="300" />
</VBox>
</top>
<center>
@ -48,8 +48,7 @@
</center>
<bottom>
<HBox alignment="CENTER">
<JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="Install"
style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;"/>
<JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="%ui.button.install" styleClass="jfx-button-raised" />
</HBox>
</bottom>
</BorderPane>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXTextField?>
<?import com.jfoenix.controls.JFXButton?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<VBox maxWidth="500" maxHeight="500" spacing="50">
<Label text="Installing modpack" />
<Label fx:id="lblModpackLocation" />
<JFXTextField fx:id="txtModpackName" labelFloat="true" promptText="%modpack.enter_name" />
<GridPane>
<columnConstraints>
<ColumnConstraints />
<ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
<Label text="Name" GridPane.rowIndex="0" GridPane.columnIndex="0" />
<Label text="Version" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<Label text="Author" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<Label fx:id="lblName" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<Label fx:id="lblVersion" GridPane.rowIndex="1" GridPane.columnIndex="1" />
<Label fx:id="lblAuthor" GridPane.rowIndex="2" GridPane.columnIndex="1" />
</GridPane>
<JFXButton buttonType="RAISED" fx:id="btnInstall" onMouseClicked="#onInstall" text="%ui.button.install" styleClass="jfx-button-raised" />
</VBox>
</fx:root>

View File

@ -27,7 +27,7 @@
</items>
</JFXComboBox></right></BorderPane>
<BorderPane><left><Label text="%launcher.lang" /></left><right><JFXComboBox fx:id="cboLanguage" /></right></BorderPane>
<BorderPane><left><Label text="%launcher.proxy" /></left><right><HBox alignment="CENTER_LEFT">
<BorderPane><left><Label text="%launcher.proxy" /></left><right><HBox alignment="CENTER_LEFT" spacing="5">
<JFXComboBox fx:id="cboProxyType">
<items>
<FXCollections fx:factory="observableArrayList">

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
}
@ -119,3 +121,33 @@ fun HttpURLConnection.readData(): String {
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

@ -156,3 +156,61 @@ 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

View File

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