mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-09 11:55:52 -04:00
CurseForge modpack support. Random UserAgent for OptiFine. Fixed mis-repositioning of maronry pane
This commit is contained in:
parent
1394034160
commit
99f60ea6e5
@ -26,6 +26,7 @@ import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
|
|||||||
import org.jackhuang.hmcl.util.LOG
|
import org.jackhuang.hmcl.util.LOG
|
||||||
import org.jackhuang.hmcl.util.OS
|
import org.jackhuang.hmcl.util.OS
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.Callable
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
fun i18n(key: String): String {
|
fun i18n(key: String): String {
|
||||||
@ -51,12 +52,13 @@ class Main : Application() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
|
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
|
lateinit var PRIMARY_STAGE: Stage
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
DEFAULT_USER_AGENT = "Hello Minecraft! Launcher"
|
DEFAULT_USER_AGENT = { "Hello Minecraft! Launcher" }
|
||||||
|
|
||||||
launch(Main::class.java, *args)
|
launch(Main::class.java, *args)
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,9 @@ package org.jackhuang.hmcl.game
|
|||||||
|
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import javafx.beans.InvalidationListener
|
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.setting.VersionSetting
|
||||||
import org.jackhuang.hmcl.util.GSON
|
import org.jackhuang.hmcl.util.GSON
|
||||||
import org.jackhuang.hmcl.util.LOG
|
import org.jackhuang.hmcl.util.LOG
|
||||||
@ -27,10 +30,44 @@ import java.io.File
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
|
|
||||||
class HMCLGameRepository(baseDirectory: File)
|
class HMCLGameRepository(val profile: Profile, baseDirectory: File)
|
||||||
: DefaultGameRepository(baseDirectory) {
|
: 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() {
|
override fun refreshVersionsImpl() {
|
||||||
versionSettings.clear()
|
versionSettings.clear()
|
||||||
@ -91,6 +128,14 @@ class HMCLGameRepository(baseDirectory: File)
|
|||||||
return versionSettings[id]
|
return versionSettings[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun markVersionAsModpack(id: String) {
|
||||||
|
beingModpackVersions += id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun undoMark(id: String) {
|
||||||
|
beingModpackVersions -= id
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"
|
val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"
|
||||||
val GSON = GsonBuilder().registerTypeAdapter(VersionSetting::class.java, VersionSetting).setPrettyPrinting().create()
|
val GSON = GsonBuilder().registerTypeAdapter(VersionSetting::class.java, VersionSetting).setPrettyPrinting().create()
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.game
|
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.setting.Settings
|
||||||
import org.jackhuang.hmcl.task.Scheduler
|
import org.jackhuang.hmcl.task.Scheduler
|
||||||
|
|
||||||
@ -25,16 +25,18 @@ object LauncherHelper {
|
|||||||
fun launch() {
|
fun launch() {
|
||||||
val profile = Settings.selectedProfile
|
val profile = Settings.selectedProfile
|
||||||
val repository = profile.repository
|
val repository = profile.repository
|
||||||
|
val dependency = profile.dependency
|
||||||
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
|
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
|
||||||
val version = repository.getVersion(profile.selectedVersion)
|
val version = repository.getVersion(profile.selectedVersion)
|
||||||
val launcher = DefaultLauncher(
|
val launcher = HMCLGameLauncher(
|
||||||
repository = repository,
|
repository = repository,
|
||||||
versionId = profile.selectedVersion,
|
versionId = profile.selectedVersion,
|
||||||
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
|
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir),
|
||||||
account = account.logIn(Settings.proxy)
|
account = account.logIn(Settings.proxy)
|
||||||
)
|
)
|
||||||
|
|
||||||
profile.dependency.checkGameCompletionAsync(version)
|
dependency.checkGameCompletionAsync(version)
|
||||||
|
.then(CurseForgeModpackCompletionTask(dependency, profile.selectedVersion))
|
||||||
.then(launcher.launchAsync())
|
.then(launcher.launchAsync())
|
||||||
.subscribe(Scheduler.JAVAFX) { println("lalala") }
|
.subscribe(Scheduler.JAVAFX) { println("lalala") }
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ class Profile(var name: String = "Default", initialGameDir: File = File(".minecr
|
|||||||
val gameDirProperty = ImmediateObjectProperty<File>(this, "gameDir", initialGameDir)
|
val gameDirProperty = ImmediateObjectProperty<File>(this, "gameDir", initialGameDir)
|
||||||
var gameDir: File by gameDirProperty
|
var gameDir: File by gameDirProperty
|
||||||
|
|
||||||
var repository = HMCLGameRepository(initialGameDir)
|
var repository = HMCLGameRepository(this, initialGameDir)
|
||||||
val dependency: DefaultDependencyManager get() = DefaultDependencyManager(repository, Settings.downloadProvider, Settings.proxy)
|
val dependency: DefaultDependencyManager get() = DefaultDependencyManager(repository, Settings.downloadProvider, Settings.proxy)
|
||||||
var modManager = ModManager(repository)
|
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)
|
var vs = repository.getVersionSetting(id)
|
||||||
if (vs == null)
|
if (vs == null)
|
||||||
vs = repository.createVersionSetting(id) ?: return
|
vs = repository.createVersionSetting(id) ?: return null
|
||||||
vs.usesGlobal = false
|
vs.usesGlobal = false
|
||||||
|
return vs
|
||||||
}
|
}
|
||||||
|
|
||||||
fun globalizeVersionSetting(id: String) {
|
fun globalizeVersionSetting(id: String) {
|
||||||
|
@ -28,7 +28,7 @@ import java.net.Proxy
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object Proxies {
|
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)
|
fun getProxyType(index: Int): Proxy.Type? = PROXIES.getOrNull(index)
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ import org.jackhuang.hmcl.i18n
|
|||||||
import org.jackhuang.hmcl.setting.Settings
|
import org.jackhuang.hmcl.setting.Settings
|
||||||
import org.jackhuang.hmcl.task.Scheduler
|
import org.jackhuang.hmcl.task.Scheduler
|
||||||
import org.jackhuang.hmcl.task.Task
|
import org.jackhuang.hmcl.task.Task
|
||||||
|
import org.jackhuang.hmcl.task.task
|
||||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
|
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
|
|||||||
if (newValue != null)
|
if (newValue != null)
|
||||||
Settings.selectedAccount = newValue.properties["account"] as Account
|
Settings.selectedAccount = newValue.properties["account"] as Account
|
||||||
}
|
}
|
||||||
masonryPane.children.setAll(children)
|
masonryPane.resetChildren(children)
|
||||||
Platform.runLater {
|
Platform.runLater {
|
||||||
masonryPane.requestLayout()
|
masonryPane.requestLayout()
|
||||||
scrollPane.requestLayout()
|
scrollPane.requestLayout()
|
||||||
@ -135,7 +136,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
|
|||||||
val username = txtUsername.text
|
val username = txtUsername.text
|
||||||
val password = txtPassword.text
|
val password = txtPassword.text
|
||||||
progressBar.isVisible = true
|
progressBar.isVisible = true
|
||||||
val task = Task.of(Callable {
|
val task = task(Callable {
|
||||||
try {
|
try {
|
||||||
val account = when (type) {
|
val account = when (type) {
|
||||||
0 -> OfflineAccount.fromUsername(username)
|
0 -> OfflineAccount.fromUsername(username)
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui
|
package org.jackhuang.hmcl.ui
|
||||||
|
|
||||||
|
import com.jfoenix.adapters.ReflectionHelper
|
||||||
import com.jfoenix.concurrency.JFXUtilities
|
import com.jfoenix.concurrency.JFXUtilities
|
||||||
import com.jfoenix.controls.*
|
import com.jfoenix.controls.*
|
||||||
import javafx.animation.Animation
|
import javafx.animation.Animation
|
||||||
@ -182,4 +183,10 @@ val SINE: Interpolator = object : Interpolator() {
|
|||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Interpolator.DISCRETE"
|
return "Interpolator.DISCRETE"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun JFXMasonryPane.resetChildren(children: List<Node>) {
|
||||||
|
// Fixes mis-repositioning.
|
||||||
|
ReflectionHelper.setFieldContent(JFXMasonryPane::class.java, this, "oldBoxes", null)
|
||||||
|
this.children.setAll(children)
|
||||||
}
|
}
|
@ -36,6 +36,7 @@ import org.jackhuang.hmcl.i18n
|
|||||||
import org.jackhuang.hmcl.setting.Profile
|
import org.jackhuang.hmcl.setting.Profile
|
||||||
import org.jackhuang.hmcl.setting.Settings
|
import org.jackhuang.hmcl.setting.Settings
|
||||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer
|
import org.jackhuang.hmcl.ui.construct.RipplerContainer
|
||||||
|
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
|
||||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
|
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"))
|
override val titleProperty: StringProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main"))
|
||||||
|
|
||||||
@FXML lateinit var btnLaunch: JFXButton
|
@FXML lateinit var btnLaunch: JFXButton
|
||||||
|
@FXML lateinit var btnRefresh: JFXButton
|
||||||
|
@FXML lateinit var btnAdd: JFXButton
|
||||||
@FXML lateinit var masonryPane: JFXMasonryPane
|
@FXML lateinit var masonryPane: JFXMasonryPane
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -60,8 +63,8 @@ class MainPage : StackPane(), DecoratorPage {
|
|||||||
|
|
||||||
Settings.onProfileLoading()
|
Settings.onProfileLoading()
|
||||||
|
|
||||||
// Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game")
|
btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") }
|
||||||
// Settings.selectedProfile.repository.refreshVersions()
|
btnRefresh.setOnMouseClicked { Settings.selectedProfile.repository.refreshVersions() }
|
||||||
btnLaunch.setOnMouseClicked { LauncherHelper.launch() }
|
btnLaunch.setOnMouseClicked { LauncherHelper.launch() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +109,7 @@ class MainPage : StackPane(), DecoratorPage {
|
|||||||
if (newValue != null)
|
if (newValue != null)
|
||||||
profile.selectedVersion = newValue.properties["version"] as String
|
profile.selectedVersion = newValue.properties["version"] as String
|
||||||
}
|
}
|
||||||
masonryPane.children.setAll(children)
|
masonryPane.resetChildren(children)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@ -25,6 +25,7 @@ import javafx.scene.layout.VBox
|
|||||||
import org.jackhuang.hmcl.mod.ModManager
|
import org.jackhuang.hmcl.mod.ModManager
|
||||||
import org.jackhuang.hmcl.task.Scheduler
|
import org.jackhuang.hmcl.task.Scheduler
|
||||||
import org.jackhuang.hmcl.task.Task
|
import org.jackhuang.hmcl.task.Task
|
||||||
|
import org.jackhuang.hmcl.task.task
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
class ModController {
|
class ModController {
|
||||||
@ -40,9 +41,9 @@ class ModController {
|
|||||||
fun loadMods(modManager: ModManager, versionId: String) {
|
fun loadMods(modManager: ModManager, versionId: String) {
|
||||||
this.modManager = modManager
|
this.modManager = modManager
|
||||||
this.versionId = versionId
|
this.versionId = versionId
|
||||||
Task.of(Callable {
|
task {
|
||||||
modManager.refreshMods(versionId)
|
modManager.refreshMods(versionId)
|
||||||
}).subscribe(Scheduler.JAVAFX) {
|
}.subscribe(Scheduler.JAVAFX) {
|
||||||
rootPane.children.clear()
|
rootPane.children.clear()
|
||||||
for (modInfo in modManager.getMods(versionId)) {
|
for (modInfo in modManager.getMods(versionId)) {
|
||||||
rootPane.children += ModItem(modInfo) {
|
rootPane.children += ModItem(modInfo) {
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,14 +20,29 @@ package org.jackhuang.hmcl.ui.download
|
|||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
import javafx.scene.layout.Pane
|
import javafx.scene.layout.Pane
|
||||||
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider
|
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.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.WizardController
|
||||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider
|
import org.jackhuang.hmcl.ui.wizard.WizardProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class DownloadWizardProvider(): WizardProvider() {
|
class DownloadWizardProvider(): WizardProvider() {
|
||||||
|
lateinit var profile: Profile
|
||||||
|
|
||||||
override fun finish(settings: Map<String, Any>): Any? {
|
override fun start(settings: MutableMap<String, Any>) {
|
||||||
val builder = Settings.selectedProfile.dependency.gameBuilder()
|
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.name(settings["name"] as String)
|
||||||
builder.gameVersion(settings["game"] as String)
|
builder.gameVersion(settings["game"] as String)
|
||||||
@ -44,12 +59,42 @@ class DownloadWizardProvider(): WizardProvider() {
|
|||||||
return builder.buildAsync()
|
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) {
|
return when (step) {
|
||||||
0 -> InstallTypePage(controller)
|
0 -> InstallTypePage(controller)
|
||||||
1 -> when (settings[InstallTypePage.INSTALL_TYPE]) {
|
1 -> when (settings[InstallTypePage.INSTALL_TYPE]) {
|
||||||
0 -> VersionsPage(controller, "", BMCLAPIDownloadProvider, "game", { controller.onNext(InstallersPage(controller, BMCLAPIDownloadProvider)) })
|
0 -> VersionsPage(controller, "", BMCLAPIDownloadProvider, "game", { controller.onNext(InstallersPage(controller, BMCLAPIDownloadProvider)) })
|
||||||
else -> Pane()
|
1 -> ModpackPage(controller)
|
||||||
|
else -> throw Error()
|
||||||
}
|
}
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
@ -59,4 +104,8 @@ class DownloadWizardProvider(): WizardProvider() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PROFILE = "PROFILE"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -126,7 +126,7 @@ interface AbstractWizardDisplayer : WizardDisplayer {
|
|||||||
|
|
||||||
cancelQueue.add(executor)
|
cancelQueue.add(executor)
|
||||||
|
|
||||||
executor.submit(Task.of(Scheduler.JAVAFX) {
|
executor.submit(org.jackhuang.hmcl.task.task(Scheduler.JAVAFX) {
|
||||||
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
|
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
|
||||||
})
|
})
|
||||||
}.start()
|
}.start()
|
||||||
|
@ -31,6 +31,7 @@ class WizardController(protected val displayer: WizardDisplayer) : Navigation {
|
|||||||
pages.clear()
|
pages.clear()
|
||||||
val page = navigatingTo(0)
|
val page = navigatingTo(0)
|
||||||
pages.push(page)
|
pages.push(page)
|
||||||
|
provider.start(settings)
|
||||||
displayer.onStart()
|
displayer.onStart()
|
||||||
displayer.navigateTo(page, Navigation.NavigationDirection.START)
|
displayer.navigateTo(page, Navigation.NavigationDirection.START)
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,8 @@ package org.jackhuang.hmcl.ui.wizard
|
|||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
|
|
||||||
abstract class WizardProvider {
|
abstract class WizardProvider {
|
||||||
|
abstract fun start(settings: MutableMap<String, Any>)
|
||||||
abstract fun finish(settings: Map<String, Any>): Any?
|
abstract fun finish(settings: MutableMap<String, Any>): Any?
|
||||||
abstract fun createPage(controller: WizardController, step: Int, settings: Map<String, Any>): Node
|
abstract fun createPage(controller: WizardController, step: Int, settings: MutableMap<String, Any>): Node
|
||||||
abstract fun cancel(): Boolean
|
abstract fun cancel(): Boolean
|
||||||
}
|
}
|
@ -414,6 +414,12 @@
|
|||||||
-jfx-mask-type: CIRCLE;
|
-jfx-mask-type: CIRCLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jfx-button-raised {
|
||||||
|
-fx-text-fill: white;
|
||||||
|
-fx-background-color: #5264AE;
|
||||||
|
-fx-font-size:14px;
|
||||||
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* *
|
* *
|
||||||
* JFX Check Box *
|
* JFX Check Box *
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<top>
|
<top>
|
||||||
<VBox alignment="CENTER" style="-fx-padding: 40px;" spacing="20">
|
<VBox alignment="CENTER" style="-fx-padding: 40px;" spacing="20">
|
||||||
<Label fx:id="lblGameVersion" alignment="CENTER" />
|
<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>
|
</VBox>
|
||||||
</top>
|
</top>
|
||||||
<center>
|
<center>
|
||||||
@ -48,8 +48,7 @@
|
|||||||
</center>
|
</center>
|
||||||
<bottom>
|
<bottom>
|
||||||
<HBox alignment="CENTER">
|
<HBox alignment="CENTER">
|
||||||
<JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="Install"
|
<JFXButton onMouseClicked="#onInstall" prefWidth="100" prefHeight="40" buttonType="RAISED" text="%ui.button.install" styleClass="jfx-button-raised" />
|
||||||
style="-fx-text-fill:WHITE;-fx-background-color:#5264AE;-fx-font-size:14px;"/>
|
|
||||||
</HBox>
|
</HBox>
|
||||||
</bottom>
|
</bottom>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
31
HMCL/src/main/resources/assets/fxml/download/modpack.fxml
Normal file
31
HMCL/src/main/resources/assets/fxml/download/modpack.fxml
Normal 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>
|
@ -27,7 +27,7 @@
|
|||||||
</items>
|
</items>
|
||||||
</JFXComboBox></right></BorderPane>
|
</JFXComboBox></right></BorderPane>
|
||||||
<BorderPane><left><Label text="%launcher.lang" /></left><right><JFXComboBox fx:id="cboLanguage" /></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">
|
<JFXComboBox fx:id="cboProxyType">
|
||||||
<items>
|
<items>
|
||||||
<FXCollections fx:factory="observableArrayList">
|
<FXCollections fx:factory="observableArrayList">
|
||||||
|
@ -18,9 +18,10 @@
|
|||||||
package org.jackhuang.hmcl.download
|
package org.jackhuang.hmcl.download
|
||||||
|
|
||||||
import org.jackhuang.hmcl.game.*
|
import org.jackhuang.hmcl.game.*
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
abstract class AbstractDependencyManager(repository: GameRepository)
|
abstract class AbstractDependencyManager(repository: GameRepository, proxy: Proxy)
|
||||||
: DependencyManager(repository) {
|
: DependencyManager(repository, proxy) {
|
||||||
abstract val downloadProvider: DownloadProvider
|
abstract val downloadProvider: DownloadProvider
|
||||||
|
|
||||||
fun getVersions(id: String, selfVersion: String) =
|
fun getVersions(id: String, selfVersion: String) =
|
||||||
|
@ -27,8 +27,8 @@ import java.net.Proxy
|
|||||||
/**
|
/**
|
||||||
* This class has no state.
|
* This class has no state.
|
||||||
*/
|
*/
|
||||||
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY)
|
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, proxy: Proxy = Proxy.NO_PROXY)
|
||||||
: AbstractDependencyManager(repository) {
|
: AbstractDependencyManager(repository, proxy) {
|
||||||
|
|
||||||
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)
|
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameB
|
|||||||
val gameVersion = gameVersion
|
val gameVersion = gameVersion
|
||||||
return VersionJSONDownloadTask(gameVersion = gameVersion) then a@{ task ->
|
return VersionJSONDownloadTask(gameVersion = gameVersion) then a@{ task ->
|
||||||
var version = GSON.fromJson<Version>(task.result!!) ?: return@a null
|
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(
|
var result = ParallelTask(
|
||||||
GameAssetDownloadTask(dependencyManager, version),
|
GameAssetDownloadTask(dependencyManager, version),
|
||||||
GameLoggingDownloadTask(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.GameRepository
|
||||||
import org.jackhuang.hmcl.game.Version
|
import org.jackhuang.hmcl.game.Version
|
||||||
import org.jackhuang.hmcl.task.Task
|
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.
|
* Check if the game is complete.
|
||||||
|
@ -53,7 +53,7 @@ class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyMa
|
|||||||
override val dependencies: MutableCollection<Task> = LinkedList()
|
override val dependencies: MutableCollection<Task> = LinkedList()
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
val logging = version.logging?.get(DownloadType.CLIENT) ?: return
|
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())
|
if (!file.exists())
|
||||||
dependencies += FileDownloadTask(logging.file.url.toURL(), file, proxy = dependencyManager.proxy)
|
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 val dependencies: MutableCollection<Task> = LinkedList()
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
val assetIndexInfo = version.actualAssetIndex
|
val assetIndexInfo = version.actualAssetIndex
|
||||||
val assetDir = dependencyManager.repository.getAssetDirectory(assetIndexInfo.id)
|
val assetDir = dependencyManager.repository.getAssetDirectory(version.id, assetIndexInfo.id)
|
||||||
if (!assetDir.makeDirectory())
|
if (!assetDir.makeDirectory())
|
||||||
throw IOException("Cannot create directory: $assetDir")
|
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)
|
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>>>() {
|
class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : TaskResult<Collection<Pair<File, AssetObject>>>() {
|
||||||
private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version)
|
private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version)
|
||||||
private val assetIndexInfo = version.actualAssetIndex
|
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()
|
override val dependents: MutableCollection<Task> = LinkedList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -88,7 +88,7 @@ class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManag
|
|||||||
val res = LinkedList<Pair<File, AssetObject>>()
|
val res = LinkedList<Pair<File, AssetObject>>()
|
||||||
var progress = 0
|
var progress = 0
|
||||||
index?.objects?.entries?.forEach { (_, assetObject) ->
|
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)
|
updateProgress(++progress, index.objects.size)
|
||||||
}
|
}
|
||||||
result = res
|
result = res
|
||||||
|
@ -37,7 +37,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
|||||||
}
|
}
|
||||||
override fun getVersionCount() = versions.size
|
override fun getVersionCount() = versions.size
|
||||||
override fun getVersions() = versions.values
|
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 getRunDirectory(id: String) = baseDirectory
|
||||||
override fun getVersionJar(version: Version): File {
|
override fun getVersionJar(version: Version): File {
|
||||||
val v = version.resolve(this)
|
val v = version.resolve(this)
|
||||||
@ -45,7 +45,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
|||||||
return getVersionRoot(id).resolve("$id.jar")
|
return getVersionRoot(id).resolve("$id.jar")
|
||||||
}
|
}
|
||||||
override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives")
|
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 getVersionJson(id: String) = File(getVersionRoot(id), "$id.json")
|
||||||
open fun readVersionJson(id: String): Version? = readVersionJson(getVersionJson(id))
|
open fun readVersionJson(id: String): Version? = readVersionJson(getVersionJson(id))
|
||||||
@Throws(IOException::class, JsonSyntaxException::class, VersionNotFoundException::class)
|
@Throws(IOException::class, JsonSyntaxException::class, VersionNotFoundException::class)
|
||||||
@ -135,49 +135,49 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
|||||||
EVENT_BUS.fireEvent(RefreshedVersionsEvent(this))
|
EVENT_BUS.fireEvent(RefreshedVersionsEvent(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAssetIndex(assetId: String): AssetIndex {
|
override fun getAssetIndex(version: String, assetId: String): AssetIndex {
|
||||||
return GSON.fromJson(getIndexFile(assetId).readText())!!
|
return GSON.fromJson(getIndexFile(version, assetId).readText())!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActualAssetDirectory(assetId: String): File {
|
override fun getActualAssetDirectory(version: String, assetId: String): File {
|
||||||
try {
|
try {
|
||||||
return reconstructAssets(assetId)
|
return reconstructAssets(version, assetId)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e)
|
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")
|
baseDirectory.resolve("assets")
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun getAssetObject(assetId: String, name: String): File {
|
override fun getAssetObject(version: String, assetId: String, name: String): File {
|
||||||
try {
|
try {
|
||||||
return getAssetObject(assetId, getAssetIndex(assetId).objects["name"]!!)
|
return getAssetObject(version, assetId, getAssetIndex(version, assetId).objects["name"]!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IOException("Asset index file malformed", e)
|
throw IOException("Asset index file malformed", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAssetObject(assetId: String, obj: AssetObject): File =
|
override fun getAssetObject(version: String, assetId: String, obj: AssetObject): File =
|
||||||
getAssetObject(getAssetDirectory(assetId), obj)
|
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}")
|
return assetDir.resolve("objects/${obj.location}")
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getIndexFile(assetId: String): File =
|
open fun getIndexFile(version: String, assetId: String): File =
|
||||||
getAssetDirectory(assetId).resolve("indexes/$assetId.json")
|
getAssetDirectory(version, assetId).resolve("indexes/$assetId.json")
|
||||||
|
|
||||||
override fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File =
|
override fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File =
|
||||||
getAssetDirectory(assetId).resolve("log_configs/${loggingInfo.file.id}")
|
getAssetDirectory(version, assetId).resolve("log_configs/${loggingInfo.file.id}")
|
||||||
|
|
||||||
@Throws(IOException::class, JsonSyntaxException::class)
|
@Throws(IOException::class, JsonSyntaxException::class)
|
||||||
protected open fun reconstructAssets(assetId: String): File {
|
protected open fun reconstructAssets(version: String, assetId: String): File {
|
||||||
val assetsDir = getAssetDirectory(assetId)
|
val assetsDir = getAssetDirectory(version, assetId)
|
||||||
val assetVersion = assetId
|
val assetVersion = assetId
|
||||||
val indexFile: File = getIndexFile(assetVersion)
|
val indexFile: File = getIndexFile(version, assetVersion)
|
||||||
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
|
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
|
||||||
|
|
||||||
if (!indexFile.isFile) {
|
if (!indexFile.isFile) {
|
||||||
@ -193,7 +193,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
|||||||
val tot = index.objects.entries.size
|
val tot = index.objects.entries.size
|
||||||
for ((location, assetObject) in index.objects.entries) {
|
for ((location, assetObject) in index.objects.entries) {
|
||||||
val target = File(virtualRoot, location)
|
val target = File(virtualRoot, location)
|
||||||
val original = getAssetObject(assetsDir, assetObject)
|
val original = getAssetObject(version, assetsDir, assetObject)
|
||||||
if (original.exists()) {
|
if (original.exists()) {
|
||||||
cnt++
|
cnt++
|
||||||
if (!target.isFile)
|
if (!target.isFile)
|
||||||
|
@ -59,6 +59,13 @@ interface GameRepository : VersionProvider {
|
|||||||
*/
|
*/
|
||||||
fun refreshVersions()
|
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.
|
* Gets the current running directory of the given version for game.
|
||||||
* @param id the version id
|
* @param id the version id
|
||||||
@ -69,11 +76,11 @@ interface GameRepository : VersionProvider {
|
|||||||
* Get the library file in disk.
|
* Get the library file in disk.
|
||||||
* This method allows versions and libraries that are not loaded by this game repository.
|
* 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]
|
* @param lib the library, [Version.libraries]
|
||||||
* @return the library file
|
* @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.
|
* 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.
|
* @throws java.io.IOException if I/O operation fails.
|
||||||
* @return the actual asset directory
|
* @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.
|
* 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
|
* 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.
|
* @throws java.io.IOException if I/O operation fails.
|
||||||
* @return the file that given asset object refers to
|
* @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
|
* Get the file that given asset object refers to
|
||||||
@ -147,7 +154,7 @@ interface GameRepository : VersionProvider {
|
|||||||
* @param obj the asset object, [AssetIndex.objects]
|
* @param obj the asset object, [AssetIndex.objects]
|
||||||
* @return the file that given asset object refers to
|
* @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
|
* Get asset index that assetId represents
|
||||||
@ -155,7 +162,7 @@ interface GameRepository : VersionProvider {
|
|||||||
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
|
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
|
||||||
* @return the asset index
|
* @return the asset index
|
||||||
*/
|
*/
|
||||||
fun getAssetIndex(assetId: String): AssetIndex
|
fun getAssetIndex(version: String, assetId: String): AssetIndex
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get logging object
|
* Get logging object
|
||||||
@ -164,5 +171,5 @@ interface GameRepository : VersionProvider {
|
|||||||
* @param loggingInfo the logging info
|
* @param loggingInfo the logging info
|
||||||
* @return the file that loggingInfo refers to
|
* @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) {
|
if (OS.CURRENT_OS == OS.OSX) {
|
||||||
res.add("-Xdock:name=Minecraft ${version.id}")
|
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
|
val logging = version.logging
|
||||||
if (logging != null) {
|
if (logging != null) {
|
||||||
val loggingInfo = logging[DownloadType.CLIENT]
|
val loggingInfo = logging[DownloadType.CLIENT]
|
||||||
if (loggingInfo != null) {
|
if (loggingInfo != null) {
|
||||||
val loggingFile = repository.getLoggingObject(version.actualAssetIndex.id, loggingInfo)
|
val loggingFile = repository.getLoggingObject(version.id, version.actualAssetIndex.id, loggingInfo)
|
||||||
if (loggingFile.exists())
|
if (loggingFile.exists())
|
||||||
res.add(loggingInfo.argument.replace("\${path}", loggingFile.absolutePath))
|
res.add(loggingInfo.argument.replace("\${path}", loggingFile.absolutePath))
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
|
|||||||
res.add(version.mainClass!!)
|
res.add(version.mainClass!!)
|
||||||
|
|
||||||
// Provided Minecraft arguments
|
// Provided Minecraft arguments
|
||||||
val gameAssets = repository.getActualAssetDirectory(version.actualAssetIndex.id)
|
val gameAssets = repository.getActualAssetDirectory(version.id, version.actualAssetIndex.id)
|
||||||
|
|
||||||
version.minecraftArguments!!.tokenize().forEach { line ->
|
version.minecraftArguments!!.tokenize().forEach { line ->
|
||||||
res.add(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)
|
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()
|
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()
|
open val dependencies: Collection<Task> = emptySet()
|
||||||
|
|
||||||
@ -119,14 +119,12 @@ abstract class Task {
|
|||||||
submit(subscriber).start()
|
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 {
|
override fun toString(): String {
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
fun task(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
|
||||||
fun of(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
|
fun <V> task(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
|
||||||
fun <V> of(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.SSLContext
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.util.*
|
||||||
|
import java.util.logging.Level
|
||||||
import kotlin.text.Charsets
|
import kotlin.text.Charsets
|
||||||
|
|
||||||
private val XTM = object : X509TrustManager {
|
private val XTM = object : X509TrustManager {
|
||||||
@ -56,7 +58,7 @@ fun initHttps() {
|
|||||||
HttpsURLConnection.setDefaultHostnameVerifier(HNV);
|
HttpsURLConnection.setDefaultHostnameVerifier(HNV);
|
||||||
}
|
}
|
||||||
|
|
||||||
var DEFAULT_USER_AGENT = "JMCCC"
|
var DEFAULT_USER_AGENT: () -> String = { RandomUserAgent.randomUserAgent }
|
||||||
|
|
||||||
fun String.toURL() = URL(this)
|
fun String.toURL() = URL(this)
|
||||||
|
|
||||||
@ -67,7 +69,7 @@ fun URL.createConnection(proxy: Proxy): HttpURLConnection {
|
|||||||
useCaches = false
|
useCaches = false
|
||||||
connectTimeout = 15000
|
connectTimeout = 15000
|
||||||
readTimeout = 15000
|
readTimeout = 15000
|
||||||
addRequestProperty("User-Agent", DEFAULT_USER_AGENT)
|
addRequestProperty("User-Agent", DEFAULT_USER_AGENT())
|
||||||
} as HttpURLConnection
|
} as HttpURLConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,4 +120,34 @@ fun HttpURLConnection.readData(): String {
|
|||||||
} finally {
|
} finally {
|
||||||
input?.closeQuietly()
|
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.util.makeCommand
|
||||||
import org.jackhuang.hmcl.task.Task
|
import org.jackhuang.hmcl.task.Task
|
||||||
import org.jackhuang.hmcl.task.TaskListener
|
import org.jackhuang.hmcl.task.TaskListener
|
||||||
|
import org.jackhuang.hmcl.util.detectFileName
|
||||||
|
import org.jackhuang.hmcl.util.toURL
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
|
@ -20,7 +20,7 @@ group 'org.jackhuang'
|
|||||||
version '3.0'
|
version '3.0'
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.1.3-2'
|
ext.kotlin_version = '1.1.4'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user