mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-09 03:46:18 -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.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)
|
||||
}
|
||||
|
@ -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 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()
|
||||
|
@ -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") }
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
@ -182,4 +183,10 @@ val SINE: Interpolator = object : Interpolator() {
|
||||
override fun toString(): String {
|
||||
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.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")
|
||||
|
@ -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) {
|
||||
|
@ -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.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"
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
||||
executor.submit(Task.of(Scheduler.JAVAFX) {
|
||||
executor.submit(org.jackhuang.hmcl.task.task(Scheduler.JAVAFX) {
|
||||
navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH)
|
||||
})
|
||||
}.start()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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 *
|
||||
|
@ -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>
|
||||
|
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>
|
||||
</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">
|
||||
|
@ -18,9 +18,10 @@
|
||||
package org.jackhuang.hmcl.download
|
||||
|
||||
import org.jackhuang.hmcl.game.*
|
||||
import java.net.Proxy
|
||||
|
||||
abstract class AbstractDependencyManager(repository: GameRepository)
|
||||
: DependencyManager(repository) {
|
||||
abstract class AbstractDependencyManager(repository: GameRepository, proxy: Proxy)
|
||||
: DependencyManager(repository, proxy) {
|
||||
abstract val downloadProvider: DownloadProvider
|
||||
|
||||
fun getVersions(id: String, selfVersion: String) =
|
||||
|
@ -27,8 +27,8 @@ import java.net.Proxy
|
||||
/**
|
||||
* This class has no state.
|
||||
*/
|
||||
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, val proxy: Proxy = Proxy.NO_PROXY)
|
||||
: AbstractDependencyManager(repository) {
|
||||
class DefaultDependencyManager(override val repository: DefaultGameRepository, override var downloadProvider: DownloadProvider, proxy: Proxy = Proxy.NO_PROXY)
|
||||
: AbstractDependencyManager(repository, proxy) {
|
||||
|
||||
override fun gameBuilder(): GameBuilder = DefaultGameBuilder(this)
|
||||
|
||||
|
@ -30,7 +30,7 @@ class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameB
|
||||
val gameVersion = gameVersion
|
||||
return VersionJSONDownloadTask(gameVersion = gameVersion) then a@{ task ->
|
||||
var version = GSON.fromJson<Version>(task.result!!) ?: return@a null
|
||||
version = version.copy(id = name)
|
||||
version = version.copy(id = name, jar = null)
|
||||
var result = ParallelTask(
|
||||
GameAssetDownloadTask(dependencyManager, version),
|
||||
GameLoggingDownloadTask(dependencyManager, version),
|
||||
|
@ -20,8 +20,9 @@ package org.jackhuang.hmcl.download
|
||||
import org.jackhuang.hmcl.game.GameRepository
|
||||
import org.jackhuang.hmcl.game.Version
|
||||
import org.jackhuang.hmcl.task.Task
|
||||
import java.net.Proxy
|
||||
|
||||
abstract class DependencyManager(open val repository: GameRepository) {
|
||||
abstract class DependencyManager(open val repository: GameRepository, open val proxy: Proxy) {
|
||||
|
||||
/**
|
||||
* Check if the game is complete.
|
||||
|
@ -53,7 +53,7 @@ class GameLoggingDownloadTask(private val dependencyManager: DefaultDependencyMa
|
||||
override val dependencies: MutableCollection<Task> = LinkedList()
|
||||
override fun execute() {
|
||||
val logging = version.logging?.get(DownloadType.CLIENT) ?: return
|
||||
val file = dependencyManager.repository.getLoggingObject(version.actualAssetIndex.id, logging)
|
||||
val file = dependencyManager.repository.getLoggingObject(version.id, version.actualAssetIndex.id, logging)
|
||||
if (!file.exists())
|
||||
dependencies += FileDownloadTask(logging.file.url.toURL(), file, proxy = dependencyManager.proxy)
|
||||
}
|
||||
@ -63,11 +63,11 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
|
||||
override val dependencies: MutableCollection<Task> = LinkedList()
|
||||
override fun execute() {
|
||||
val assetIndexInfo = version.actualAssetIndex
|
||||
val assetDir = dependencyManager.repository.getAssetDirectory(assetIndexInfo.id)
|
||||
val assetDir = dependencyManager.repository.getAssetDirectory(version.id, assetIndexInfo.id)
|
||||
if (!assetDir.makeDirectory())
|
||||
throw IOException("Cannot create directory: $assetDir")
|
||||
|
||||
val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id)
|
||||
val assetIndexFile = dependencyManager.repository.getIndexFile(version.id, assetIndexInfo.id)
|
||||
dependencies += FileDownloadTask(dependencyManager.downloadProvider.injectURL(assetIndexInfo.url).toURL(), assetIndexFile, proxy = dependencyManager.proxy)
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,7 @@ class GameAssetIndexDownloadTask(private val dependencyManager: DefaultDependenc
|
||||
class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManager, private val version: Version) : TaskResult<Collection<Pair<File, AssetObject>>>() {
|
||||
private val assetIndexTask = GameAssetIndexDownloadTask(dependencyManager, version)
|
||||
private val assetIndexInfo = version.actualAssetIndex
|
||||
private val assetIndexFile = dependencyManager.repository.getIndexFile(assetIndexInfo.id)
|
||||
private val assetIndexFile = dependencyManager.repository.getIndexFile(version.id, assetIndexInfo.id)
|
||||
override val dependents: MutableCollection<Task> = LinkedList()
|
||||
|
||||
init {
|
||||
@ -88,7 +88,7 @@ class GameAssetRefreshTask(private val dependencyManager: DefaultDependencyManag
|
||||
val res = LinkedList<Pair<File, AssetObject>>()
|
||||
var progress = 0
|
||||
index?.objects?.entries?.forEach { (_, assetObject) ->
|
||||
res += Pair(dependencyManager.repository.getAssetObject(assetIndexInfo.id, assetObject), assetObject)
|
||||
res += Pair(dependencyManager.repository.getAssetObject(version.id, assetIndexInfo.id, assetObject), assetObject)
|
||||
updateProgress(++progress, index.objects.size)
|
||||
}
|
||||
result = res
|
||||
|
@ -37,7 +37,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
}
|
||||
override fun getVersionCount() = versions.size
|
||||
override fun getVersions() = versions.values
|
||||
override fun getLibraryFile(id: Version, lib: Library) = File(baseDirectory, "libraries/${lib.path}")
|
||||
override fun getLibraryFile(version: Version, lib: Library) = baseDirectory.resolve("libraries/${lib.path}")
|
||||
override fun getRunDirectory(id: String) = baseDirectory
|
||||
override fun getVersionJar(version: Version): File {
|
||||
val v = version.resolve(this)
|
||||
@ -45,7 +45,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
return getVersionRoot(id).resolve("$id.jar")
|
||||
}
|
||||
override fun getNativeDirectory(id: String) = File(getVersionRoot(id), "$id-natives")
|
||||
open fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id")
|
||||
override fun getVersionRoot(id: String) = File(baseDirectory, "versions/$id")
|
||||
open fun getVersionJson(id: String) = File(getVersionRoot(id), "$id.json")
|
||||
open fun readVersionJson(id: String): Version? = readVersionJson(getVersionJson(id))
|
||||
@Throws(IOException::class, JsonSyntaxException::class, VersionNotFoundException::class)
|
||||
@ -135,49 +135,49 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
EVENT_BUS.fireEvent(RefreshedVersionsEvent(this))
|
||||
}
|
||||
|
||||
override fun getAssetIndex(assetId: String): AssetIndex {
|
||||
return GSON.fromJson(getIndexFile(assetId).readText())!!
|
||||
override fun getAssetIndex(version: String, assetId: String): AssetIndex {
|
||||
return GSON.fromJson(getIndexFile(version, assetId).readText())!!
|
||||
}
|
||||
|
||||
override fun getActualAssetDirectory(assetId: String): File {
|
||||
override fun getActualAssetDirectory(version: String, assetId: String): File {
|
||||
try {
|
||||
return reconstructAssets(assetId)
|
||||
return reconstructAssets(version, assetId)
|
||||
} catch (e: IOException) {
|
||||
LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e)
|
||||
return getAssetDirectory(assetId)
|
||||
return getAssetDirectory(version, assetId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAssetDirectory(assetId: String): File =
|
||||
override fun getAssetDirectory(version: String, assetId: String): File =
|
||||
baseDirectory.resolve("assets")
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getAssetObject(assetId: String, name: String): File {
|
||||
override fun getAssetObject(version: String, assetId: String, name: String): File {
|
||||
try {
|
||||
return getAssetObject(assetId, getAssetIndex(assetId).objects["name"]!!)
|
||||
return getAssetObject(version, assetId, getAssetIndex(version, assetId).objects["name"]!!)
|
||||
} catch (e: Exception) {
|
||||
throw IOException("Asset index file malformed", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAssetObject(assetId: String, obj: AssetObject): File =
|
||||
getAssetObject(getAssetDirectory(assetId), obj)
|
||||
override fun getAssetObject(version: String, assetId: String, obj: AssetObject): File =
|
||||
getAssetObject(version, getAssetDirectory(version, assetId), obj)
|
||||
|
||||
open fun getAssetObject(assetDir: File, obj: AssetObject): File {
|
||||
open fun getAssetObject(version: String, assetDir: File, obj: AssetObject): File {
|
||||
return assetDir.resolve("objects/${obj.location}")
|
||||
}
|
||||
|
||||
open fun getIndexFile(assetId: String): File =
|
||||
getAssetDirectory(assetId).resolve("indexes/$assetId.json")
|
||||
open fun getIndexFile(version: String, assetId: String): File =
|
||||
getAssetDirectory(version, assetId).resolve("indexes/$assetId.json")
|
||||
|
||||
override fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File =
|
||||
getAssetDirectory(assetId).resolve("log_configs/${loggingInfo.file.id}")
|
||||
override fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File =
|
||||
getAssetDirectory(version, assetId).resolve("log_configs/${loggingInfo.file.id}")
|
||||
|
||||
@Throws(IOException::class, JsonSyntaxException::class)
|
||||
protected open fun reconstructAssets(assetId: String): File {
|
||||
val assetsDir = getAssetDirectory(assetId)
|
||||
protected open fun reconstructAssets(version: String, assetId: String): File {
|
||||
val assetsDir = getAssetDirectory(version, assetId)
|
||||
val assetVersion = assetId
|
||||
val indexFile: File = getIndexFile(assetVersion)
|
||||
val indexFile: File = getIndexFile(version, assetVersion)
|
||||
val virtualRoot = assetsDir.resolve("virtual").resolve(assetVersion)
|
||||
|
||||
if (!indexFile.isFile) {
|
||||
@ -193,7 +193,7 @@ open class DefaultGameRepository(var baseDirectory: File): GameRepository {
|
||||
val tot = index.objects.entries.size
|
||||
for ((location, assetObject) in index.objects.entries) {
|
||||
val target = File(virtualRoot, location)
|
||||
val original = getAssetObject(assetsDir, assetObject)
|
||||
val original = getAssetObject(version, assetsDir, assetObject)
|
||||
if (original.exists()) {
|
||||
cnt++
|
||||
if (!target.isFile)
|
||||
|
@ -59,6 +59,13 @@ interface GameRepository : VersionProvider {
|
||||
*/
|
||||
fun refreshVersions()
|
||||
|
||||
/**
|
||||
* Gets the root folder of specific version.
|
||||
* The root folders the versions must be unique.
|
||||
* For example, .minecraft/versions/<version name>/.
|
||||
*/
|
||||
fun getVersionRoot(id: String): File
|
||||
|
||||
/**
|
||||
* Gets the current running directory of the given version for game.
|
||||
* @param id the version id
|
||||
@ -69,11 +76,11 @@ interface GameRepository : VersionProvider {
|
||||
* Get the library file in disk.
|
||||
* This method allows versions and libraries that are not loaded by this game repository.
|
||||
*
|
||||
* @param id version id
|
||||
* @param version versionversion
|
||||
* @param lib the library, [Version.libraries]
|
||||
* @return the library file
|
||||
*/
|
||||
fun getLibraryFile(id: Version, lib: Library): File
|
||||
fun getLibraryFile(version: Version, lib: Library): File
|
||||
|
||||
/**
|
||||
* Get the directory that native libraries will be unzipped to.
|
||||
@ -123,12 +130,12 @@ interface GameRepository : VersionProvider {
|
||||
* @throws java.io.IOException if I/O operation fails.
|
||||
* @return the actual asset directory
|
||||
*/
|
||||
fun getActualAssetDirectory(assetId: String): File
|
||||
fun getActualAssetDirectory(version: String, assetId: String): File
|
||||
|
||||
/**
|
||||
* Get the asset directory according to the asset id.
|
||||
*/
|
||||
fun getAssetDirectory(assetId: String): File
|
||||
fun getAssetDirectory(version: String, assetId: String): File
|
||||
|
||||
/**
|
||||
* Get the file that given asset object refers to
|
||||
@ -138,7 +145,7 @@ interface GameRepository : VersionProvider {
|
||||
* @throws java.io.IOException if I/O operation fails.
|
||||
* @return the file that given asset object refers to
|
||||
*/
|
||||
fun getAssetObject(assetId: String, name: String): File
|
||||
fun getAssetObject(version: String, assetId: String, name: String): File
|
||||
|
||||
/**
|
||||
* Get the file that given asset object refers to
|
||||
@ -147,7 +154,7 @@ interface GameRepository : VersionProvider {
|
||||
* @param obj the asset object, [AssetIndex.objects]
|
||||
* @return the file that given asset object refers to
|
||||
*/
|
||||
fun getAssetObject(assetId: String, obj: AssetObject): File
|
||||
fun getAssetObject(version: String, assetId: String, obj: AssetObject): File
|
||||
|
||||
/**
|
||||
* Get asset index that assetId represents
|
||||
@ -155,7 +162,7 @@ interface GameRepository : VersionProvider {
|
||||
* @param assetId the asset id, [AssetIndexInfo.id] [Version.assets]
|
||||
* @return the asset index
|
||||
*/
|
||||
fun getAssetIndex(assetId: String): AssetIndex
|
||||
fun getAssetIndex(version: String, assetId: String): AssetIndex
|
||||
|
||||
/**
|
||||
* Get logging object
|
||||
@ -164,5 +171,5 @@ interface GameRepository : VersionProvider {
|
||||
* @param loggingInfo the logging info
|
||||
* @return the file that loggingInfo refers to
|
||||
*/
|
||||
fun getLoggingObject(assetId: String, loggingInfo: LoggingInfo): File
|
||||
fun getLoggingObject(version: String, assetId: String, loggingInfo: LoggingInfo): File
|
||||
}
|
@ -68,14 +68,14 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
|
||||
|
||||
if (OS.CURRENT_OS == OS.OSX) {
|
||||
res.add("-Xdock:name=Minecraft ${version.id}")
|
||||
res.add("-Xdock:icon=" + repository.getAssetObject(version.actualAssetIndex.id, "icons/minecraft.icns").absolutePath);
|
||||
res.add("-Xdock:icon=" + repository.getAssetObject(version.id, version.actualAssetIndex.id, "icons/minecraft.icns").absolutePath);
|
||||
}
|
||||
|
||||
val logging = version.logging
|
||||
if (logging != null) {
|
||||
val loggingInfo = logging[DownloadType.CLIENT]
|
||||
if (loggingInfo != null) {
|
||||
val loggingFile = repository.getLoggingObject(version.actualAssetIndex.id, loggingInfo)
|
||||
val loggingFile = repository.getLoggingObject(version.id, version.actualAssetIndex.id, loggingInfo)
|
||||
if (loggingFile.exists())
|
||||
res.add(loggingInfo.argument.replace("\${path}", loggingFile.absolutePath))
|
||||
}
|
||||
@ -139,7 +139,7 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
|
||||
res.add(version.mainClass!!)
|
||||
|
||||
// Provided Minecraft arguments
|
||||
val gameAssets = repository.getActualAssetDirectory(version.actualAssetIndex.id)
|
||||
val gameAssets = repository.getActualAssetDirectory(version.id, version.actualAssetIndex.id)
|
||||
|
||||
version.minecraftArguments!!.tokenize().forEach { line ->
|
||||
res.add(line
|
||||
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager
|
||||
import org.jackhuang.hmcl.download.DependencyManager
|
||||
import org.jackhuang.hmcl.game.GameException
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask
|
||||
import org.jackhuang.hmcl.task.Task
|
||||
import org.jackhuang.hmcl.task.task
|
||||
import org.jackhuang.hmcl.util.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.logging.Level
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class CurseForgeModpackManifest @JvmOverloads constructor(
|
||||
@SerializedName("manifestType")
|
||||
val manifestType: String = MINECRAFT_MODPACK,
|
||||
@SerializedName("manifestVersion")
|
||||
val manifestVersion: Int = 1,
|
||||
@SerializedName("name")
|
||||
val name: String = "",
|
||||
@SerializedName("version")
|
||||
val version: String = "1.0",
|
||||
@SerializedName("author")
|
||||
val author: String = "",
|
||||
@SerializedName("overrides")
|
||||
val overrides: String = "overrides",
|
||||
@SerializedName("minecraft")
|
||||
val minecraft: CurseForgeModpackManifestMinecraft = CurseForgeModpackManifestMinecraft(),
|
||||
@SerializedName("files")
|
||||
val files: List<CurseForgeModpackManifestFile> = emptyList()
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(manifestType == MINECRAFT_MODPACK, { "Only support Minecraft modpack" })
|
||||
}
|
||||
|
||||
companion object {
|
||||
val MINECRAFT_MODPACK = "minecraftModpack"
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackManifestMinecraft (
|
||||
@SerializedName("version")
|
||||
val gameVersion: String = "",
|
||||
@SerializedName("modLoaders")
|
||||
val modLoaders: List<CurseForgeModpackManifestModLoader> = emptyList()
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(gameVersion.isNotBlank())
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackManifestModLoader (
|
||||
@SerializedName("id")
|
||||
val id: String = "",
|
||||
@SerializedName("primary")
|
||||
val primary: Boolean = false
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(id.isNotBlank(), { "Curse Forge modpack manifest Mod loader id cannot be blank." })
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackManifestFile (
|
||||
@SerializedName("projectID")
|
||||
val projectID: Int = 0,
|
||||
@SerializedName("fileID")
|
||||
val fileID: Int = 0,
|
||||
@SerializedName("fileName")
|
||||
var fileName: String = "",
|
||||
@SerializedName("required")
|
||||
val required: Boolean = true
|
||||
): Validation {
|
||||
override fun validate() {
|
||||
check(projectID != 0)
|
||||
check(fileID != 0)
|
||||
}
|
||||
|
||||
val url: URL get() = "https://minecraft.curseforge.com/projects/$projectID/files/$fileID/download".toURL()
|
||||
}
|
||||
|
||||
fun readCurseForgeModpackManifest(f: File): CurseForgeModpackManifest {
|
||||
ZipFile(f).use { zipFile ->
|
||||
val entry = zipFile.getEntry("manifest.json") ?: throw IOException("Manifest.json not found. Not a valid CurseForge modpack.")
|
||||
val json = zipFile.getInputStream(entry).readFullyAsString()
|
||||
return GSON.fromJson<CurseForgeModpackManifest>(json) ?: throw JsonParseException("Manifest.json not found. Not a valid CurseForge modpack.")
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackInstallTask(private val dependencyManager: DefaultDependencyManager, private val zipFile: File, private val manifest: CurseForgeModpackManifest, private val name: String): Task() {
|
||||
val repository = dependencyManager.repository
|
||||
init {
|
||||
if (repository.hasVersion(name))
|
||||
throw IllegalStateException("Version $name already exists.")
|
||||
}
|
||||
|
||||
val root = repository.getVersionRoot(name)
|
||||
val run = repository.getRunDirectory(name)
|
||||
override val dependents = mutableListOf<Task>()
|
||||
override val dependencies = mutableListOf<Task>()
|
||||
init {
|
||||
val builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.minecraft.gameVersion)
|
||||
manifest.minecraft.modLoaders.forEach {
|
||||
if (it.id.startsWith("forge-"))
|
||||
builder.version("forge", it.id.substring("forge-".length))
|
||||
}
|
||||
dependents += builder.buildAsync()
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
unzipSubDirectory(zipFile, run, manifest.overrides)
|
||||
|
||||
var finished = 0
|
||||
for (f in manifest.files) {
|
||||
try {
|
||||
f.fileName = f.url.detectFileName(dependencyManager.proxy)
|
||||
dependencies += FileDownloadTask(f.url, run.resolve("mods").resolve(f.fileName), proxy = dependencyManager.proxy)
|
||||
} catch (e: IOException) {
|
||||
// ignore it and retry next time.
|
||||
}
|
||||
++finished
|
||||
updateProgress(1.0 * finished / manifest.files.size)
|
||||
}
|
||||
|
||||
root.resolve("manifest.json").writeText(GSON.toJson(manifest))
|
||||
}
|
||||
}
|
||||
|
||||
class CurseForgeModpackCompletionTask(dependencyManager: DependencyManager, version: String): Task() {
|
||||
val repository = dependencyManager.repository
|
||||
val run = repository.getRunDirectory(version)
|
||||
private var manifest: CurseForgeModpackManifest?
|
||||
private val proxy = dependencyManager.proxy
|
||||
override val dependents = mutableListOf<Task>()
|
||||
override val dependencies = mutableListOf<Task>()
|
||||
|
||||
init {
|
||||
try {
|
||||
val manifestFile = repository.getVersionRoot(version).resolve("manifest.json")
|
||||
if (!manifestFile.exists()) manifest = null
|
||||
else {
|
||||
manifest = GSON.fromJson<CurseForgeModpackManifest>(repository.getVersionRoot(version).resolve("manifest.json").readText())!!
|
||||
|
||||
for (f in manifest!!.files) {
|
||||
if (f.fileName.isBlank())
|
||||
dependents += task { f.fileName = f.url.detectFileName(proxy) }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOG.log(Level.WARNING, "Unable to read CurseForge modpack manifest.json", e)
|
||||
manifest = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
if (manifest == null) return
|
||||
for (f in manifest!!.files) {
|
||||
if (f.fileName.isBlank())
|
||||
throw GameException("Unable to download mod, cannot continue")
|
||||
val file = run.resolve("mods").resolve(f.fileName)
|
||||
if (!file.exists())
|
||||
dependencies += FileDownloadTask(f.url, file, proxy = proxy)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -61,12 +61,12 @@ abstract class Task {
|
||||
infix fun with(b: Task): Task = CoupleTask(this, { b }, false)
|
||||
|
||||
/**
|
||||
* The collection of sub-tasks that should execute before this task running.
|
||||
* The collection of sub-tasks that should execute **before** this task running.
|
||||
*/
|
||||
open val dependents: Collection<Task> = emptySet()
|
||||
|
||||
/**
|
||||
* The collection of sub-tasks that should execute after this task running.
|
||||
* The collection of sub-tasks that should execute **after** this task running.
|
||||
*/
|
||||
open val dependencies: Collection<Task> = emptySet()
|
||||
|
||||
@ -119,14 +119,12 @@ abstract class Task {
|
||||
submit(subscriber).start()
|
||||
}
|
||||
|
||||
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(Task.of(scheduler, closure))
|
||||
fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit) = subscribe(task(scheduler, closure))
|
||||
|
||||
override fun toString(): String {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun of(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
|
||||
fun <V> of(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
|
||||
}
|
||||
}
|
||||
fun task(scheduler: Scheduler = Scheduler.DEFAULT, closure: () -> Unit): Task = SimpleTask(closure, scheduler)
|
||||
fun <V> task(callable: Callable<V>): TaskResult<V> = TaskCallable(callable)
|
@ -30,6 +30,8 @@ import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.X509TrustManager
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
import kotlin.text.Charsets
|
||||
|
||||
private val XTM = object : X509TrustManager {
|
||||
@ -56,7 +58,7 @@ fun initHttps() {
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(HNV);
|
||||
}
|
||||
|
||||
var DEFAULT_USER_AGENT = "JMCCC"
|
||||
var DEFAULT_USER_AGENT: () -> String = { RandomUserAgent.randomUserAgent }
|
||||
|
||||
fun String.toURL() = URL(this)
|
||||
|
||||
@ -67,7 +69,7 @@ fun URL.createConnection(proxy: Proxy): HttpURLConnection {
|
||||
useCaches = false
|
||||
connectTimeout = 15000
|
||||
readTimeout = 15000
|
||||
addRequestProperty("User-Agent", DEFAULT_USER_AGENT)
|
||||
addRequestProperty("User-Agent", DEFAULT_USER_AGENT())
|
||||
} as HttpURLConnection
|
||||
}
|
||||
|
||||
@ -118,4 +120,34 @@ fun HttpURLConnection.readData(): String {
|
||||
} finally {
|
||||
input?.closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
fun URL.detectFileNameQuietly(proxy: Proxy = Proxy.NO_PROXY): String {
|
||||
try {
|
||||
val conn = createConnection(proxy)
|
||||
conn.connect()
|
||||
if (conn.responseCode / 100 != 2)
|
||||
throw IOException("Response code ${conn.responseCode}")
|
||||
return conn.detectFileName()
|
||||
} catch (e: IOException) {
|
||||
LOG.log(Level.WARNING, "Cannot detect the file name of URL $this", e)
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID())
|
||||
}
|
||||
}
|
||||
|
||||
fun URL.detectFileName(proxy: Proxy = Proxy.NO_PROXY): String {
|
||||
val conn = createConnection(proxy)
|
||||
conn.connect()
|
||||
if (conn.responseCode / 100 != 2)
|
||||
throw IOException("Response code ${conn.responseCode}")
|
||||
return conn.detectFileName()
|
||||
}
|
||||
|
||||
fun HttpURLConnection.detectFileName(): String {
|
||||
val disposition = getHeaderField("Content-Disposition")
|
||||
if (disposition == null || disposition.indexOf("filename=") == -1) {
|
||||
val u = url.toString()
|
||||
return u.substringAfterLast('/')
|
||||
} else
|
||||
return disposition.substringAfter("filename=").removeSurrounding("\"")
|
||||
}
|
1683
HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/RandomUserAgent.kt
Normal file
1683
HMCLCore/src/main/kotlin/org/jackhuang/hmcl/util/RandomUserAgent.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -155,4 +155,62 @@ fun unzip(zip: File, dest: File, callback: ((String) -> Boolean)? = null, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件压缩成zip文件
|
||||
|
||||
* @param zip zip文件路径
|
||||
* *
|
||||
* @param dest 待压缩文件根目录
|
||||
* *
|
||||
* @param callback will be called for every entry in the zip file,
|
||||
* * returns false if you dont want this file unzipped.
|
||||
* *
|
||||
* *
|
||||
* @throws java.io.IOException 解压失败或无法写入
|
||||
*/
|
||||
@JvmOverloads
|
||||
@Throws(IOException::class)
|
||||
fun unzipSubDirectory(zip: File, dest: File, subDirectory: String, ignoreExistsFile: Boolean = true) {
|
||||
val buf = ByteArray(1024)
|
||||
dest.mkdirs()
|
||||
ZipInputStream(zip.inputStream()).use { zipFile ->
|
||||
if (zip.exists()) {
|
||||
var gbkPath: String
|
||||
var strtemp: String
|
||||
val strPath = dest.absolutePath
|
||||
var zipEnt: ZipEntry?
|
||||
while (true) {
|
||||
zipEnt = zipFile.nextEntry
|
||||
if (zipEnt == null)
|
||||
break
|
||||
gbkPath = zipEnt.name
|
||||
if (!gbkPath.startsWith(subDirectory))
|
||||
continue
|
||||
gbkPath = gbkPath.substring(subDirectory.length)
|
||||
if (gbkPath.startsWith("/") || gbkPath.startsWith("\\")) gbkPath = gbkPath.substring(1)
|
||||
strtemp = strPath + File.separator + gbkPath
|
||||
if (zipEnt.isDirectory) {
|
||||
val dir = File(strtemp)
|
||||
dir.mkdirs()
|
||||
} else {
|
||||
//建目录
|
||||
val strsubdir = gbkPath
|
||||
for (i in 0..strsubdir.length - 1)
|
||||
if (strsubdir.substring(i, i + 1).equals("/", ignoreCase = true)) {
|
||||
val temp = strPath + File.separator + strsubdir.substring(0, i)
|
||||
val subdir = File(temp)
|
||||
if (!subdir.exists())
|
||||
subdir.mkdir()
|
||||
}
|
||||
if (ignoreExistsFile && File(strtemp).exists())
|
||||
continue
|
||||
File(strtemp).outputStream().use({ fos ->
|
||||
zipFile.copyTo(fos, buf)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,8 @@ import org.jackhuang.hmcl.launch.ProcessListener
|
||||
import org.jackhuang.hmcl.util.makeCommand
|
||||
import org.jackhuang.hmcl.task.Task
|
||||
import org.jackhuang.hmcl.task.TaskListener
|
||||
import org.jackhuang.hmcl.util.detectFileName
|
||||
import org.jackhuang.hmcl.util.toURL
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user